Compare commits

...

282 Commits

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

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

* test: update rendered output css selector

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

* renderer: fold according to configs

* doc: add `output.html.fold`

* refactor: tidy fold config

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

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

* html -> html_config

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

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

* Add some tests for include anchors

* Include parts of Rust files and hide the rest

Fixes #618.

* Increase min supported Rust version to 1.35

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

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

* Ran cargo fmt.

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

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

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

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

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

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

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

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

Links can now be more than only playpen links

* Rename a field to match the enum type it holds

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

* Reformatting sections.

* Update mdbook.md
2019-06-13 07:38:12 +02:00
Eric Huss
07830f7f11 Merge pull request #891 from integer32llc/include-before-test
Write preprocessed content to file before testing with rustdoc
2019-06-12 15:01:58 -07:00
Eric Huss
828b7d05c5 Merge pull request #953 from ehuss/update-changelog
Update changelog.
2019-06-12 09:31:45 -07:00
Eric Huss
379004efcb Update changelog. 2019-06-12 09:30:58 -07:00
Eric Huss
2497e77bf1 Support strikethrough and tasklists. (#952) 2019-06-12 17:02:03 +02:00
Eric Huss
0c2292b9aa Update css to support diff syntax highlighting. (#943)
This adds the rules to highlight diff lines with highlight.js.
2019-06-12 16:59:55 +02:00
lzutao
4386a10e87 Explicitly mark fonts and images files as binary (#951) 2019-06-11 21:44:15 +02:00
Eric Huss
3cfed10098 Update to pulldown-cmark 0.5. (#898)
* Update to pulldown-cmark 0.4.1.

* Update to pulldown-cmark 0.5.2.

* Remove pulldown-cmark-to-cmark dependency.

Since it is not compatible with the new pulldown-cmark. This example isn't
directly usable, anyways, and I think the no-op example sufficiently shows how
to make a preprocessor.

* cargo fmt

* Fix example link.
2019-06-11 18:26:24 +02:00
rnitta
a655d5d241 Header elements wrap links (#948)
* swap hierarchy of header for that of link

* fix comment
2019-06-03 14:31:15 +02:00
Eric Huss
f8c3a2deea Update highlight.js (#942)
Updates to v9.15.8.

My main motivation is to fix a minor issue with TOML highlighting.

This keeps the same language list as before with the addition of a new "common"
language `properties` and added `julia` because someone asked for it and I like
julia. The full list from building:

	:common armasm d go handlebars haskell julia rust scala swift x86asm yaml

- apache
- armasm
- bash
- coffeescript
- cpp
- cs
- css
- d
- diff
- go
- xml
- handlebars
- haskell
- http
- ini
- java
- javascript
- json
- julia
- makefile
- markdown
- nginx
- objectivec
- perl
- php
- properties
- python
- ruby
- rust
- scala
- shell
- sql
- swift
- x86asm
- yaml
2019-06-03 14:22:32 +02:00
Eric Huss
b226d2fc55 cargo fmt 2019-05-31 09:19:46 -07:00
lzutao
53ba0d6655 Remove 'static lifetime from static vars (#947) 2019-05-31 18:01:02 +02:00
Eric Huss
43ead86ecc Update toml. (#945)
Just keeping up-to-date.
2019-05-31 18:00:15 +02:00
Eric Huss
1d3ec7e0c7 Support rust edition in playground. (#946)
The endpoint was recently updated to support the edition param.
2019-05-31 17:59:44 +02:00
Eric Huss
f4017376a9 Merge pull request #944 from lzutao/formatting
Minor formatting fixes
2019-05-30 11:12:57 -07:00
Lzu Tao
672cf456eb Remove unnecessary ::<crate>
Find and replace with `git grep -E '\W::[a-z]'` command.
2019-05-30 23:12:33 +07:00
Lzu Tao
8dce00d54d Cargo.toml: Sort dependencies fields 2019-05-30 22:23:42 +07:00
rnitta
4f7c299de7 update language attribute to configurable 2019-05-30 11:53:49 +09:00
Eric Huss
04e74bfa1b Merge pull request #931 from ehuss/update-deps
cargo update
2019-05-26 11:23:00 -07:00
Eric Huss
4026a586a1 cargo update 2019-05-26 09:23:23 -07:00
lzutao
71281bff10 Update some updatable dependencies (#934)
* Update ammonia dependency

* Update env_logger

* Update itertools

* Update ws dep

* Update pretty_assertions dep
2019-05-26 14:05:42 +02:00
lzutao
8542f7f29d Transition to 2018 edition (#933)
* Transition to 2018 edition

* Update Travis CI badge in README

* Remove non-idiomatic `extern crate` lines
2019-05-25 20:50:41 +02:00
Eric Huss
fe492d1cb9 Merge pull request #937 from ehuss/fix-changelog-typo
Fix changelog typo
2019-05-25 09:02:09 -07:00
Eric Huss
481c2f2194 Fix changelog typo 2019-05-25 09:00:33 -07:00
lzutao
882014860c Update ace editor to v1.4.4 (#935) 2019-05-25 14:39:16 +02:00
Bas Bossink
e3ec751a3f Issue 703 (#929)
* Replace all occurances of altenate backend with alternative backed

Rename test for consistency of the terminology.

* Use better sed command
2019-05-19 22:16:10 +02:00
Eric Huss
fc565df86b Some documentation fixes. (#925) 2019-05-19 00:05:57 +02:00
Eric Huss
ec8e63145c Add a changelog. (#926) 2019-05-18 23:56:01 +02:00
Bas Bossink
2752c88c46 Fix issue #703. (#928) 2019-05-18 23:38:08 +02:00
Eric Huss
e7befd19bc Merge pull request #924 from ehuss/fix-appveyor-token
Fix appveyor github token.
2019-05-16 13:20:42 -07:00
Eric Huss
644b8e132c Fix appveyor github token. 2019-05-16 13:19:39 -07:00
Eric Huss
8e82ae534a Merge pull request #923 from ehuss/new-deploy
Fix automatic deploy.
2019-05-16 09:34:39 -07:00
Eric Huss
6a8a5b7642 Fix automatic deploy.
This should fix deploying release artifacts and gh-pages for the documentation.
Secrets should now be configured in the UI for travis and appveyor.

This also removes some of the appveyor jobs, since they aren't really
necessary, and there are limited jobs on rust-lang-libs.
2019-05-15 15:50:01 -07:00
Roman Proskuryakov
c3284a2ae9 Update mdbook 0.1 -> 0.2 in docs for CI (#918) 2019-05-11 20:30:21 +02:00
Allen
df12cc55c8 Revert "Merge pull request #889 from s3bk/master" (#917)
* Revert "Merge pull request #889 from s3bk/master"

This reverts commit b30b58b565, reversing
changes made to c6220fba83.

* format tests :P
2019-05-09 20:18:28 +02:00
Eric Huss
cb4a3e0711 Fix more print.html links. (#871) 2019-05-08 23:50:59 +02:00
lzutao
9194a40acd CI: Check Rust formatting for each commit (#909) 2019-05-08 21:20:38 +02:00
Bas Bossink
506996808b Fix issue 832 (#841)
* Add if around stub summary creation

Check if an existing SUMMARY.md is present to prevent overwriting it
with the stub SUMMARY.md.

[#832]

* Add test for existing SUMMARY.md
2019-05-08 21:13:20 +02:00
Philipp Hansch
5163c5ab75 Don't let robots index the print.html (#844) 2019-05-08 00:32:43 +02:00
Stefanie Jäger
ecfaed1e02 Change overflow-x to initial (#818)
Change overflow-x from auto to initial.
This resolves weird rendering behavior in Chrome and Safari on macOS.

With help from @bash

Co-authored-by: Ruben Schmidmeister <ruben.schmidmeister@icloud.com>
2019-05-08 00:30:28 +02:00
Eric Huss
8bb5426441 Fix keyboard chapter navigation for file urls. (#915) 2019-05-08 00:29:46 +02:00
Eric Huss
a674c9eff1 Fix "next" navigation on index.html. (#916) 2019-05-08 00:27:48 +02:00
lzutao
7ab939f8f2 CI: Enable sccache build (#913) 2019-05-06 23:41:44 +02:00
lzutao
581187098c Deny 2018 edition idioms globally (#911) 2019-05-06 22:50:34 +02:00
lzutao
ab7802a9a9 Fix most of clippy warnings (#914)
* Fix clippy: cast_lossless

* Fix clippy: match_ref_pats

* Fix clippy: extra_unused_lifetimes

* Fix clippy: needless_lifetimes

* Fix clippy: new_without_default

* Fix clippy: or_fun_call

* Fix clippy: should_implement_trait

* Fix clippy: redundant_closure

* Fix clippy: const_static_lifetime

* Fix clippy: redundant_pattern_matching

* Fix clippy: unused_io_amount

* Fix clippy: string_lit_as_bytes

* Fix clippy: needless_update

* Fix clippy: blacklisted_name

* Fix clippy: collapsible_if

* Fix clippy: match_wild_err_arm

* Fix clippy: single_match

* Fix clippy: useless_vec

* Fix clippy: single_char_pattern

* Fix clippy: float_cmp

* Fix clippy: approx_constant
2019-05-06 20:20:58 +02:00
Dylan DPC
345acb8597 Merge pull request #908 from lzutao/fmt
Formatting whole project
2019-05-06 00:26:50 +02:00
Dylan DPC
6380526e93 Merge pull request #907 from lzutao/lock
Update Cargo.lock
2019-05-05 22:57:41 +02:00
Lzu Tao
4560bdeb47 Update Cargo.lock 2019-05-06 00:12:53 +07:00
Lzu Tao
0aa3a9045a cargo fmt 2019-05-05 22:00:24 +07:00
Dylan DPC
b30b58b565 Merge pull request #889 from s3bk/master
new "Book" theme
2019-05-04 17:45:37 +02:00
Dylan DPC
c6220fba83 Merge pull request #906 from rust-lang-nursery/fix/typo
fixes typo in code
2019-05-04 17:33:05 +02:00
Dylan DPC
652eab6e7e Update custom_preprocessors.rs 2019-05-03 20:32:56 +02:00
Dylan DPC
5726a8afd6 Update build_process.rs 2019-05-03 20:00:43 +02:00
Dylan DPC
7e26a8430d Update mod.rs 2019-05-03 19:59:58 +02:00
Dylan DPC
07a64b110a Merge pull request #905 from ehuss/fix-code-inline-color
Fix color of `code spans` that are links.
2019-05-02 21:14:28 +02:00
Eric Huss
dd69e03ff5 Fix color of code spans that are links. 2019-05-02 11:07:24 -07:00
Dylan DPC
7f3a0ff6a0 Merge pull request #886 from bluejekyll/master
add docs for publishing to github pages manually
2019-04-30 00:50:58 +02:00
Dylan DPC
aea317e173 Update continuous-integration.md 2019-04-30 00:50:35 +02:00
Dylan DPC
f9454615b1 Update book-example/src/continuous-integration.md
Co-Authored-By: bluejekyll <benjaminfry@me.com>
2019-04-29 15:33:31 -07:00
Dylan DPC
39211291d9 Update book-example/src/continuous-integration.md
Co-Authored-By: bluejekyll <benjaminfry@me.com>
2019-04-29 15:33:25 -07:00
Dylan DPC
f01fe854fa Merge pull request #883 from anp/custom_summary
Expose API for building a book with a custom Summary.
2019-04-30 00:25:03 +02:00
Dylan DPC
6eeaaaa44d Merge pull request #819 from StefanieJaeger/css-comment
Update comment in editor.js
2019-04-28 23:56:13 +02:00
Dylan DPC
357ebcf7ce Merge pull request #849 from gentoo90/sidebar-resize
Add sidebar resize
2019-04-28 23:29:07 +02:00
Dylan DPC
1a4f38eace Merge pull request #903 from FreeMasen/master
remove walkdir from lib via handlebars
2019-04-28 22:19:30 +02:00
rfm
1d3f83eede remove walkdir from lib via handlebars 2019-04-28 12:54:01 -05:00
Dylan DPC
9712347b9c Merge pull request #796 from badboy/dont-trim-external-js-path
Don't strip relative path of additional javascript files
2019-04-26 09:01:39 +02:00
Dylan DPC
f73d42d994 Merge pull request #878 from shen390s/master
Cleanup build directory before preprocessors run to keep files genera…
2019-04-26 00:17:09 +02:00
Dylan DPC
a647017e4b Merge pull request #861 from k-nasa/add_colored_help
Add colored help
2019-04-25 23:19:18 +02:00
Dylan DPC
a66d44190e Merge pull request #900 from felixrabe/patch-2
Typo
2019-04-23 22:13:07 +02:00
Felix Rabe
01fd7a76f0 Typo 2019-04-23 22:11:12 +02:00
Dylan DPC
99dc62f9c3 Merge pull request #867 from ffissore/master
Fixed loss of focus when clicking the Copy button
2019-04-23 20:25:58 +02:00
Dylan DPC
b891fd5a12 Merge pull request #834 from donald-pinckney/master
Fixes #826
2019-04-23 11:36:43 +02:00
Federico Fissore
02fa7b0a11 When the Copy button is pressed, focus is lost and scrolling the page
with the arrow keys stops working. Fixed by upgrading ClipboardJS to
the latest version
2019-04-23 09:29:22 +02:00
Dylan DPC
8b2e1c2daa Merge pull request #870 from cauebs/group-events
Group file changes and rebuild book only once
2019-04-23 00:20:54 +02:00
Dylan DPC
88d2f69138 Merge pull request #860 from k-nasa/fix_duplication
Fix duplication with Cargo.toml
2019-04-22 12:31:50 +02:00
Dylan DPC
cb94053779 Merge pull request #892 from integer32llc/fix-warnings
Fix deprecation warnings for trim left/right matches
2019-04-21 21:20:39 +02:00
Dylan DPC
0a8707b1e6 Merge pull request #872 from ehuss/travis-deploy
Fix deploy on Travis.
2019-04-20 19:11:38 +02:00
Donald Pinckney
0dc2728fa9 Merge branch 'master' of github.com:rust-lang-nursery/mdBook 2019-04-18 12:29:32 -04:00
Dylan DPC
9b02cd7496 Merge pull request #897 from ji-zhou/master
Correct grammar: remove the redundancy
2019-04-17 23:47:08 +02:00
ji.zhou
11f86f4511 Correct grammar: remove the redundancy 2019-04-12 22:53:21 +08:00
Carol (Nichols || Goulding)
4abac12c04 Fix deprecation warnings for trim left/right matches 2019-03-23 08:47:10 -04:00
Carol (Nichols || Goulding)
d7c7d91005 Write preprocessed content to file before testing with rustdoc
Fixes #855.
2019-03-23 08:35:44 -04:00
Sebastian Köln
9243cf9d95 well. typo 2019-03-21 07:56:48 +01:00
Sebastian Köln
d2470730fc center book title 2019-03-20 22:13:12 +01:00
Sebastian Köln
6a2e2461fb quote inline code 2019-03-20 17:02:05 +01:00
Sebastian Köln
3faa3e42f0 switch to Source font family 2019-03-20 16:34:19 +01:00
Sebastian Köln
9c8fae4704 fix forgotten rename 2019-03-20 15:36:40 +01:00
Sebastian Köln
9b6f5a9840 add Book theme (Liberation fonts need to be in /fonts/ on the server) 2019-03-20 15:21:36 +01:00
Benjamin Fry
62af2367bb add docs for publishing to github pages manually 2019-03-12 11:22:00 -07:00
Adam Perry
37808b7e08 Expose API for building a book with a custom Summary.
This is useful for situations where you'd like to
supplement or replace the existing Summary parsing with
custom filesystem traversal code or other similar changes.
2019-03-04 11:44:00 -08:00
Rongsong Shen
b37f21a09b Cleanup build directory before preprocessors run to keep files generated by preprocessors 2019-02-16 12:17:26 +08:00
Eric Huss
966632a724 Fix deploy on Travis.
The deploy scripts require TARGET env var to be set, but it wasn't set anywhere, causing it to fail: https://travis-ci.com/rust-lang-nursery/mdBook/builds/97850452.

This also reduces the number of mac builders, since they aren't necessary, and capacity is limited.

The api key will likely need to be updated, too, since the transition to travis.com.

Appveyor also seems to be broken, but I suspect it is also a key issue.
2019-02-03 15:45:02 -08:00
Cauê Baasch de Souza
c7281459f9 Group file changes and rebuild book only once 2019-01-31 16:31:02 -02:00
nasa
ae3f87ad0c fix: Fix Cargo.toml description 2019-01-20 15:12:51 +09:00
Steve Klabnik
c068703028 start next development iteration 0.2.4-alpha.0 2019-01-18 10:43:59 -05:00
Steve Klabnik
6cbc41d413 version 0.2.3 2019-01-18 10:39:51 -05:00
Steve Klabnik
25c1ca1275 Merge pull request #866 from rust-lang-nursery/gh828
Fixing links in print.html
2019-01-18 09:54:17 -05:00
Steve Klabnik
acbb951240 Merge pull request #865 from sderosiaux/patch-1
Fix websocket hostname usage
2019-01-17 15:08:18 -05:00
Steve Klabnik
9e96165d8f Merge pull request #845 from phansch/update_contributing
Update CONTRIBUTING.md (Clippy is on stable now)
2019-01-17 15:07:21 -05:00
Steve Klabnik
5c5ef2f86b Merge pull request #853 from sshplendid/broken-link
Update broken link at the user guide.
2019-01-17 15:06:45 -05:00
Steve Klabnik
23ac06e2eb Fix a bug in link re-writing
In a regex, `.` means `[0-9a-zA-Z]`, not a period character. This means
that this regex worked, unless a link anchor contained `md` inside of
it. This fixes the regex to match a literal period.

Reported by @ehuss in
https://github.com/rust-lang-nursery/mdBook/pull/866#issuecomment-454595806
2019-01-16 15:44:51 -05:00
Steve Klabnik
2ddbb37f49 Fix the bug 2019-01-15 14:10:02 -05:00
Steve Klabnik
a481735fa2 failing test 2019-01-15 14:08:53 -05:00
Stéphane Derosiaux
954cfa86e5 Fix websocket hostname usage
The livereload url was using an unknown property "websocket-address" instead of "websocket-hostname", hence it was always fallback onto the hostname (which can be different).
2019-01-09 17:59:45 +01:00
k-nasa
7e52da3c1b Add colored help 2018-12-25 21:35:37 +09:00
k-nasa
4e8d051bd1 Delete extra space 2018-12-25 21:10:45 +09:00
k-nasa
78ee8e43bb Fix duplication with Cargo.toml 2018-12-25 21:10:07 +09:00
Shawn
b675b91980 Update broken link
https://www.rust-lang.org/downloads.html is the outdated link.
2018-12-15 10:09:23 +09:00
gentoo90
3d8db7f25c Disable text selection and CSS transition while resizing sidebar 2018-12-12 21:55:50 +02:00
gentoo90
3d37e24c14 Add sidebar resizing 2018-12-12 21:55:50 +02:00
Philipp Hansch
eb19d2d654 Update CONTRIBUTING.md (Clippy is on stable now) 2018-12-08 13:48:59 +01:00
Michael Bryan
1052ee92e1 Merge pull request #837 from basbossink/master
Random acts of kindness
2018-12-08 15:44:40 +08:00
Bas Bossink
3598e905aa Make failing_alternate_backend test more platform specific
Use the suggestion from @Michael-F-Bryan to make the passing_ and
failing_alternate_backend test more reliable across platforms.
2018-12-05 22:26:53 +01:00
Bas Bossink
3f002979c4 Suppress dead_code warning in test 2018-12-04 00:57:15 +01:00
Bas Bossink
742dbbc917 Run rustfmt. 2018-12-04 00:11:41 +01:00
Bas Bossink
991a725c26 Solve the simplest clippy warnings and run rustfmt 2018-12-04 00:10:09 +01:00
Donald Pinckney
317c7731da Remove extra comment 2018-11-28 20:07:37 -05:00
Donald Pinckney
4c17b11ed0 Fix #826 2018-11-28 20:05:37 -05:00
Michael Bryan
005dfc55bf Merge pull request #829 from kingdido999/patch-1
Fix invalid url in renderer documentation
2018-11-22 20:12:19 +08:00
Desmond
8c86031384 Fix invalid url in renderer documentation 2018-11-21 08:59:53 +08:00
Stefanie Jäger
5151aae07e Update comment
Comment in editor.js referenced file called "general.styl", changed that
to "general.css".
2018-11-09 17:34:57 +01:00
Michael Bryan
42b87e0fbc Merge pull request #804 from Bassetts/default-theme-option
Default theme option
2018-10-30 21:18:48 +08:00
Matt Ickstadt
33add4b532 Merge pull request #782 from mattico/document-dest-dir-rel
Document dest-dir relative path behavior
2018-10-23 12:16:37 -05:00
Michael Bryan
b0513ee771 Merge pull request #788 from weihanglo/feat/non-ascii-heading-anchor
Allow non alphabetic initial in heading anchor
2018-10-23 19:21:44 +08:00
Michael Bryan
b4538da9c3 Merge pull request #802 from Bassetts/git-button
Implement a git repository button
2018-10-23 19:16:45 +08:00
Michael Bryan
7ac3e50b37 Merge pull request #809 from hikarinotomadoi/fix-documentation-comments-for-HtmlConfig
Improve documentation comments
2018-10-23 19:15:51 +08:00
yoshimura masataka
13a9aab2b2 Improve documentation comments 2018-10-23 10:34:14 +09:00
Jason Liquorish
eccec9bb52 Update documentation for 2018-10-21 13:16:59 +01:00
Michael Bryan
e63f53fe47 (cargo-release) start next development iteration 0.2.3-alpha.0 2018-10-20 14:22:47 +08:00
Michael Bryan
2c20c99d4a (cargo-release) version 0.2.2 2018-10-20 14:21:21 +08:00
Michael Bryan
c6125b184f Merge pull request #807 from rust-lang-nursery/update-readme
Update the readme to mention plugins
2018-10-20 14:17:57 +08:00
Michael Bryan
dfb6e3cb10 Merge pull request #806 from rust-lang-nursery/serialize-checks
Add some round-trip checks for preprocessor input serialization
2018-10-20 14:17:03 +08:00
Michael Bryan
cffc385b0c Updated the user guide's config section to mention specifying plugin commands 2018-10-20 14:16:07 +08:00
Michael Bryan
e73928f933 Mentioned plugins in the README 2018-10-20 14:01:51 +08:00
Michael Bryan
41071a5dd9 Bumped dependencies 2018-10-20 12:51:45 +08:00
Michael Bryan
f6a7432569 Added a round-trip test to make sure parse_input() is always correct 2018-10-20 12:50:35 +08:00
Michael Bryan
89ea60e7a5 Made __non_exhaustive fields #[serde(skip)] 2018-10-20 11:21:24 +08:00
Jason Liquorish
10b69e60c8 Add documentation and tests 2018-10-15 21:40:59 +01:00
Jason Liquorish
336e08fe50 Update FontAwesome version to 4.7.0 2018-10-15 20:01:36 +01:00
Jason Liquorish
5bfdf9fcc8 Added git-repository-icon option
Updated documentation and added tests.
2018-10-15 19:48:54 +01:00
Michael Bryan
29f8b791f1 Merge pull request #792 from rust-lang-nursery/custom-preprocessor
WIP: Custom Preprocessors
2018-10-16 00:02:12 +08:00
xyh
877bf37d18 avoid using cd in example travis-ci script (#803) 2018-10-15 18:52:37 +08:00
Jason Liquorish
d2565af000 Add helper to format theme name for theme changer 2018-10-13 14:44:10 +01:00
Jason Liquorish
599e47f1f1 Initial implementation of a git repository button 2018-10-13 12:17:33 +01:00
Jason Liquorish
0c31ab2953 Initial implementation of default theme option 2018-10-12 19:57:59 +01:00
Michael Bryan
b1c7c54108 Rewrote a large proportion of the Preprocessor docs to be up-to-date 2018-09-25 19:48:20 +08:00
Jan-Erik Rediger
f654c42426 Don't strip relative path of additional javascript files
Previously, additional JavaScript files inside a directory were
correctly copied (with their parent created), but the link to it was
stripped of that parent.
There's no need for that (and it was not done for CSS)
2018-09-19 20:36:32 +02:00
Jan-Erik Rediger
0c926b3e88 Ensure section numbers are correctly incremented after a horizontal separator (#790)
Fixes #779
2018-09-19 23:33:28 +08:00
mwilbur
e4eddb3f26 Fix broken link to API pages (#795) 2018-09-19 23:32:37 +08:00
Michael Bryan
adec78e7f5 Forgot to implement 3rd party preprocessor discovery 2018-09-19 23:16:11 +08:00
Michael Bryan
5cd5e4764c Fleshed out the api docs 2018-09-16 23:44:52 +08:00
Michael Bryan
132f4fd358 Fixed a bug where the tests use the wrong dummy book 2018-09-16 23:33:58 +08:00
Michael Bryan
1d72cea972 The example preprocessor works 2018-09-16 23:28:01 +08:00
Michael Bryan
1aa1194d79 We can shell out to the preprocessor 2018-09-16 23:23:03 +08:00
Michael Bryan
304234c122 The example can now tell mdbook if renderers are supported 2018-09-16 23:00:19 +08:00
Michael Bryan
729c94a7e4 Started working on a custom preprocessor 2018-09-16 22:49:52 +08:00
Ramon Buckland
df874cdbdb Resolves #270. details for Clippy and Rustmft usage (#776)
* Resolves #270. details for Clippy and Rustmft usage

* Fix Typo
2018-09-16 14:39:18 +08:00
Weihang Lo
d729a762fe Remove insertion on non alphabetic initial headings 2018-09-09 12:00:28 +08:00
Weihang Lo
43b3d157d9 (test) validate id from non ascii headings 2018-09-09 12:00:25 +08:00
Matt Ickstadt
a9f3be6f44 Make serve command note more prominent 2018-09-06 10:24:56 -05:00
Matt Ickstadt
34356b87a0 Document dest-dir relative path behavior 2018-09-06 10:24:42 -05:00
104 changed files with 11462 additions and 2937 deletions

8
.gitattributes vendored
View File

@@ -2,7 +2,7 @@
* text=auto eol=lf
*.rs rust
*.woff -text
*.ttf -text
*.otf -text
*.png -text
*.woff binary
*.ttf binary
*.otf binary
*.png binary

45
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Deploy
on:
release:
types: [created]
jobs:
release:
name: Deploy Release
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@master
- name: Install hub
run: ci/install-hub.sh ${{ matrix.os }}
shell: bash
- name: Install 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
steps:
- uses: actions/checkout@master
- name: Install Rust (rustup)
run: rustup update stable --no-self-update && rustup default stable
- name: Build book
run: cargo run -- build book-example
- name: Deploy to GitHub
env:
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
run: |
touch book-example/book/.nojekyll
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
cd book-example/book
/tmp/deploy

53
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: CI
on:
# Only run when merging to master, or open/synchronize/reopen a PR.
push:
branches:
- master
pull_request:
jobs:
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
build: [stable, beta, nightly, macos, windows, msrv]
include:
- build: stable
os: ubuntu-latest
rust: stable
- build: beta
os: ubuntu-latest
rust: beta
- build: nightly
os: ubuntu-latest
rust: nightly
- build: macos
os: macos-latest
rust: stable
- build: windows
os: windows-latest
rust: stable
- build: msrv
os: ubuntu-latest
rust: 1.35.0
steps:
- uses: actions/checkout@master
- name: Install Rustup
run: bash ci/install-rustup.sh ${{ matrix.rust }}
- name: Install Rust
run: bash ci/install-rust.sh ${{ matrix.rust }}
- name: Build and run tests
run: cargo test
- name: Test no default
run: cargo test --no-default-features
rustfmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Install Rust
run: rustup update stable && rustup default stable && rustup component add rustfmt
- run: cargo fmt -- --check

2
.gitignore vendored
View File

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

View File

@@ -1,48 +0,0 @@
language: rust
rust:
- stable
- beta
- nightly
os:
- linux
- osx
cache:
timeout: 360
cargo: true
before_cache:
- chmod -R a+r $HOME/.cargo
env:
global:
- CRATE_NAME=mdbook
script:
- cargo test --all
- cargo test --all --no-default-features
before_deploy:
- sh ci/before_deploy.sh
deploy:
provider: releases
api_key:
- secure: cURRWBr034iqBz/ifD7uOunBfNR30YxIXfgLX0osWz+iafkVbhDGYYz9sBmRraqO2P7L2koEXMADVb/md1kI2+ykiq/ml+l9zuEAZPVmvSGUN7ZD+7s+lu3l5OBPG5z175T+b2q2q2m8XVR7TW20ra4QbE0bq06KAoOyjSgQVBTSCYsL9uTsGwiVRMEqqJT/BmKhKJNkpGsTKyBSKkOXvfeAAbE260vXUDEN9TYdJ3fvteRrpwLX56ee64gIZUq0RjDc4SKIEqilM6iUtNMvurqaewYNGkiXKRruV6BPCHxEHo6NNT46kOJLBJTf7gZw//dWhSoWpg9P0gdAnPWm407kSa3F7aJ1eRShAFQ4BLyfz9efTqm+jP3fOp7Mm7igSh9w6caSRuOnSsUf5+raRQ8E5Y9HsWGzzpZQk24Fx9EGZ04EeDSdpZAFz+jcbMpHf8t2p4CEx0CCNwYvKx6EydMKbMF5QteQ8SQkXNLhv7Rz2OgtXWYZPRVCMfQfOplsi2InsLCrQxTgwh+6u654SqVSgaHG+IncEAxBrdWy4rHcg7qereUcKfcY3k96vaDxdn/T2c00Ig0aNFR91YnixGMd6J6tQgDcRK9jh6fUm1CCBE9hT+pNUmtgYKuWBoLZexUZFFnfuBed0WciBot1bGDDamndqKq0jJiAzg+GMHk=
file_glob: true
file: "$CRATE_NAME-$TRAVIS_TAG-$TARGET.*"
on:
condition: "$TRAVIS_RUST_VERSION = stable"
tags: true
skip_cleanup: true
branches:
only:
- master
- /^v\d+\.\d+\.\d+.*$/
notifications:
email:
on_success: never

299
CHANGELOG.md Normal file
View File

@@ -0,0 +1,299 @@
# Changelog
## mdBook 0.3.3
[2b649fe...e5f77aa](https://github.com/rust-lang-nursery/mdBook/compare/2b649fe...e5f77aa)
### Changed
- Improvements to the automatic dark theme selection.
[#1069](https://github.com/rust-lang-nursery/mdBook/pull/1069)
- Fragment links now prevent scrolling the header behind the menu bar.
[#1077](https://github.com/rust-lang-nursery/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-nursery/mdBook/pull/1075)
## mdBook 0.3.2
[9cd47eb...2b649fe](https://github.com/rust-lang-nursery/mdBook/compare/9cd47eb...2b649fe)
### Added
- Added a markdown renderer, which is off by default. This may be useful for
debugging preprocessors.
[#1018](https://github.com/rust-lang-nursery/mdBook/pull/1018)
- Code samples may now include line numbers with the
`output.html.playpen.line-numbers` configuration value.
[#1035](https://github.com/rust-lang-nursery/mdBook/pull/1035)
- The `watch` and `serve` commands will now ignore files listed in
`.gitignore`.
[#1044](https://github.com/rust-lang-nursery/mdBook/pull/1044)
- Added automatic dark-theme detection based on the CSS `prefers-color-scheme`
feature. This may be enabled by setting `output.html.preferred-dark-theme`
to your preferred dark theme.
[#1037](https://github.com/rust-lang-nursery/mdBook/pull/1037)
- Added `rustdoc_include` preprocessor. This makes it easier to include
portions of an external Rust source file. The rest of the file is hidden,
but the user may expand it to see the entire file, and will continue to work
with `mdbook test`.
[#1003](https://github.com/rust-lang-nursery/mdBook/pull/1003)
- Added Ctrl-Enter shortcut to the playpen editor to automatically run the
sample.
[#1066](https://github.com/rust-lang-nursery/mdBook/pull/1066)
- Added `output.html.playpen.copyable` configuration option to disable
the copy button.
[#1050](https://github.com/rust-lang-nursery/mdBook/pull/1050)
- Added ability to dynamically expand and fold sections within the sidebar.
See the `output.html.fold` configuration to enable this feature.
[#1027](https://github.com/rust-lang-nursery/mdBook/pull/1027)
### Changed
- Use standard `scrollbar-color` CSS along with webkit extension
[#816](https://github.com/rust-lang-nursery/mdBook/pull/816)
- The renderer build directory is no longer deleted before the renderer is
run. This allows a backend to cache results between runs.
[#985](https://github.com/rust-lang-nursery/mdBook/pull/985)
- Next/prev links now highlight on hover to indicate it is clickable.
[#994](https://github.com/rust-lang-nursery/mdBook/pull/994)
- Increase padding of table headers.
[#824](https://github.com/rust-lang-nursery/mdBook/pull/824)
- Errors in `[output.html]` config are no longer ignored.
[#1033](https://github.com/rust-lang-nursery/mdBook/pull/1033)
- Updated highlight.js for syntax highlighting updates (primarily to add
async/await to Rust highlighting).
[#1041](https://github.com/rust-lang-nursery/mdBook/pull/1041)
- Raised minimum supported rust version to 1.35.
[#1003](https://github.com/rust-lang-nursery/mdBook/pull/1003)
- Hidden code lines are no longer dynamically removed via JavaScript, but
instead managed with CSS.
[#846](https://github.com/rust-lang-nursery/mdBook/pull/846)
[#1065](https://github.com/rust-lang-nursery/mdBook/pull/1065)
- Changed the default font set for the ACE editor, giving preference to
"Source Code Pro".
[#1062](https://github.com/rust-lang-nursery/mdBook/pull/1062)
- Windows 32-bit releases are no longer published.
[#1071](https://github.com/rust-lang-nursery/mdBook/pull/1071)
### Fixed
- Fixed sidebar auto-scrolling.
[#1052](https://github.com/rust-lang-nursery/mdBook/pull/1052)
- Fixed error message when running `clean` multiple times.
[#1055](https://github.com/rust-lang-nursery/mdBook/pull/1055)
- Actually fix the "next" link on index.html. The previous fix didn't work.
[#1005](https://github.com/rust-lang-nursery/mdBook/pull/1005)
- Stop using `inline-block` for `inline code`, fixing selection highlighting
and some rendering issues.
[#1058](https://github.com/rust-lang-nursery/mdBook/pull/1058)
- Fix header auto-hide on browsers with momentum scrolling that allows
negative `scrollTop`.
[#1070](https://github.com/rust-lang-nursery/mdBook/pull/1070)
## mdBook 0.3.1
[69a08ef...9cd47eb](https://github.com/rust-lang-nursery/mdBook/compare/69a08ef...9cd47eb)
### Added
- 🔥 Added ability to include files using anchor points instead of line numbers.
[#851](https://github.com/rust-lang-nursery/mdBook/pull/851)
- Added `language` configuration value to set the language of the book, which
will affect things like the `<html lang="en">` tag.
[#941](https://github.com/rust-lang-nursery/mdBook/pull/941)
### Changed
- Updated to handlebars 2.0.
[#977](https://github.com/rust-lang-nursery/mdBook/pull/977)
### Fixed
- Fixed memory leak warning.
[#967](https://github.com/rust-lang-nursery/mdBook/pull/967)
- Fix more print.html links.
[#963](https://github.com/rust-lang-nursery/mdBook/pull/963)
- Fixed crash on some unicode input.
[#978](https://github.com/rust-lang-nursery/mdBook/pull/978)
## mdBook 0.3.0
[6cbc41d...69a08ef](https://github.com/rust-lang-nursery/mdBook/compare/6cbc41d...69a08ef)
### Added
- Added ability to resize the sidebar.
[#849](https://github.com/rust-lang-nursery/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)
- Set `noindex` on `print.html` page to prevent robots from indexing it.
[#844](https://github.com/rust-lang-nursery/mdBook/pull/844)
- Added support for ~~strikethrough~~ and GitHub-style tasklists.
[#952](https://github.com/rust-lang-nursery/mdBook/pull/952)
### Changed
- Command-line help output is now colored.
[#861](https://github.com/rust-lang-nursery/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)
- Removed dependency on `same-file` crate.
[#903](https://github.com/rust-lang-nursery/mdBook/pull/903)
- 💥 Renamed `with_preprecessor` to `with_preprocessor`.
[#906](https://github.com/rust-lang-nursery/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)
- Dependencies have been updated.
[#934](https://github.com/rust-lang-nursery/mdBook/pull/934)
[#945](https://github.com/rust-lang-nursery/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)
- 🔥 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)
- The `diff` language should now highlight correctly.
[#943](https://github.com/rust-lang-nursery/mdBook/pull/943)
- Make the blank region of a header not clickable.
[#948](https://github.com/rust-lang-nursery/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)
### 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)
- Fixed on-hover color highlighting for links in sidebar.
[#834](https://github.com/rust-lang-nursery/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)
- Fixed incorrectly stripping the path for `additional-js` files.
[#796](https://github.com/rust-lang-nursery/mdBook/pull/796)
- Fixed color of `code spans` that are links.
[#905](https://github.com/rust-lang-nursery/mdBook/pull/905)
- Fixed "next" navigation on index.html.
[#916](https://github.com/rust-lang-nursery/mdBook/pull/916)
- Fixed keyboard chapter navigation for `file` urls.
[#915](https://github.com/rust-lang-nursery/mdBook/pull/915)
- Fixed bad wrapping for inline code on some browsers.
[#818](https://github.com/rust-lang-nursery/mdBook/pull/818)
- Properly load an existing `SUMMARY.md` in `mdbook init`.
[#841](https://github.com/rust-lang-nursery/mdBook/pull/841)
- Fixed some broken links in `print.html`.
[#871](https://github.com/rust-lang-nursery/mdBook/pull/871)
- The Rust Playground link now supports the 2018 edition.
[#946](https://github.com/rust-lang-nursery/mdBook/pull/946)
## mdBook 0.2.3 (2018-01-18)
[2c20c99...6cbc41d](https://github.com/rust-lang-nursery/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)
- Added a `default-theme` option to the `[output.html]` section.
[#804](https://github.com/rust-lang-nursery/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)
### Fixed
- Fix websocket hostname usage
[#865](https://github.com/rust-lang-nursery/mdBook/pull/865)
- Fixing links in print.html
[#866](https://github.com/rust-lang-nursery/mdBook/pull/866)
## mdBook 0.2.2 (2018-10-19)
[7e2e095...2c20c99](https://github.com/rust-lang-nursery/mdBook/compare/7e2e095...2c20c99)
### Added
- 🎉 Process-based custom preprocessors. See [the
docs](https://rust-lang-nursery.github.io/mdBook/for_developers/preprocessors.html)
for more.
[#792](https://github.com/rust-lang-nursery/mdBook/pull/792)
- 🎉 Configurable preprocessors.
Added `build.use-default-preprocessors` boolean TOML key to allow disabling
the built-in `links` and `index` preprocessors.
Added `[preprocessor]` TOML tables to configure each preprocessor.
Specifying `[preprocessor.links]` or `[preprocessor.index]` will enable the
respective built-in preprocessor if `build.use-default-preprocessors` is
`false`.
Added `fn supports_renderer(&self, renderer: &str) -> bool` to the
`Preprocessor` trait to specify if the preprocessor supports the given
renderer. The default implementation always returns `true`.
`Preprocessor::run` now takes a book by value instead of a mutable
reference. It should return a `Book` value with the intended modifications.
Added `PreprocessorContext::renderer` to indicate the renderer being used.
[#658](https://github.com/rust-lang-nursery/mdBook/pull/658)
[#787](https://github.com/rust-lang-nursery/mdBook/pull/787)
### Fixed
- Fix paths to additional CSS and JavaScript files
[#777](https://github.com/rust-lang-nursery/mdBook/pull/777)
- Ensure section numbers are correctly incremented after a horizontal
separator
[#790](https://github.com/rust-lang-nursery/mdBook/pull/790)
## mdBook 0.2.1 (2018-08-22)
[91ffca1...7e2e095](https://github.com/rust-lang-nursery/mdBook/compare/91ffca1...7e2e095)
### Changed
- Update to handlebars-rs 1.0
[#761](https://github.com/rust-lang-nursery/mdBook/pull/761)
### Fixed
- Fix table colors, broken by Stylus -> CSS transition
[#765](https://github.com/rust-lang-nursery/mdBook/pull/765)
## mdBook 0.2.0 (2018-08-02)
### Changed
- 💥 This release changes how links are handled in mdBook. Previously, relative
links were interpreted relative to the book's root. In `0.2.0`+ links are
relative to the page they are in, and use the `.md` extension. This has [several
advantages](https://github.com/rust-lang-nursery/mdBook/pull/603#issue-166701447),
such as making links work in other markdown viewers like GitHub. You will
likely have to change links in your book to accommodate this change. For
example, a book with this layout:
```
chapter_1/
section_1.md
section_2.md
SUMMARY.md
```
Previously a link in `section_1.md` to `section_2.md` would look like this:
```markdown
[section_2](chapter_1/section_2.html)
```
Now it must be changed to this:
```markdown
[section_2](section_2.md)
```
- 💥 `mdbook test --library-path` now accepts a comma-delimited list of
arguments rather than taking all following arguments. This makes it easier
to handle the trailing book directory argument without always needing to put
` -- ` before it. Multiple instances of the option continue to be accepted:
`mdbook test -L foo -L bar`.
- 💥 `mdbook serve` has some of its options renamed for clarity. See `mdbook
help serve` for details.
- Embedded rust playpens now use the "stable" playground API.
[#754](https://github.com/rust-lang-nursery/mdBook/pull/754)
### Fixed
- Escaped includes (`\{{#include file.rs}}`) will now render correctly.
[f30ce01](https://github.com/rust-lang-nursery/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)

View File

@@ -48,6 +48,54 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
### Code Quality
We love code quality and Rust has some excellent tools to assist you with contributions.
#### Formatting Code with rustfmt
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
This will ensure we have good quality source code that is better for us all to maintain.
[rustfmt](https://github.com/rust-lang-nursery/rustfmt) has a lot more information on the project.
The quick guide is
1. Install it
```
rustup component add rustfmt
```
1. You can now run `rustfmt` on a single file simply by...
```
rustfmt src/path/to/your/file.rs
```
... or you can format the entire project with
```
cargo fmt
```
When run through `cargo` it will format all bin and lib files in the current crate.
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang-nursery/rustfmt)
#### Finding Issues with Clippy
Clippy is a code analyser/linter detecting mistakes, and therfore helps to improve your code.
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
help us maintain awesome code.
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang-nursery/rust-clippy)
1. To install
```
rustup component add clippy
```
2. Running clippy
```
cargo clippy
```
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang-nursery.github.io/rust-clippy/master/index.html).
### Making a pull-request
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.

1596
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +1,64 @@
[package]
name = "mdbook"
version = "0.2.2-alpha.0"
version = "0.3.3"
authors = [
"Mathieu David <mathieudavid@mathieudavid.org>",
"Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>",
"Matt Ickstadt <mattico8@gmail.com>"
]
description = "Create books from markdown files"
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
repository = "https://github.com/rust-lang-nursery/mdBook"
edition = "2018"
exclude = ["/book-example/*"]
keywords = ["book", "gitbook", "rustbook", "markdown"]
license = "MPL-2.0"
readme = "README.md"
exclude = ["book-example/*"]
repository = "https://github.com/rust-lang-nursery/mdBook"
description = "Creates a book from markdown files"
[dependencies]
clap = "2.24"
chrono = "0.4"
handlebars = "1.0"
serde = "1.0"
serde_derive = "1.0"
clap = "2.24"
env_logger = "0.6"
error-chain = "0.12"
serde_json = "1.0"
pulldown-cmark = "0.1.2"
handlebars = { version = "2.0", default-features = false, features = ["no_dir_source"] }
itertools = "0.8"
lazy_static = "1.0"
log = "0.4"
env_logger = "0.5"
toml = "0.4"
memchr = "2.0"
open = "1.1"
pulldown-cmark = "0.5"
regex = "1.0.0"
tempfile = "3.0"
itertools = "0.7"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
shlex = "0.1"
toml-query = "0.7"
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.7", optional = true}
ws = { version = "0.9", optional = true}
# Search feature
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
ammonia = { version = "1.1", optional = true }
ammonia = { version = "3", optional = true }
[dev-dependencies]
select = "0.4"
pretty_assertions = "0.5"
pretty_assertions = "0.6"
walkdir = "2.0"
pulldown-cmark-to-cmark = "1.1.0"
[features]
default = ["output", "watch", "serve", "search"]
debug = []
output = []
watch = ["notify"]
watch = ["notify", "gitignore"]
serve = ["iron", "staticfile", "ws"]
search = ["elasticlunr-rs", "ammonia"]

View File

@@ -1,25 +1,8 @@
# mdBook
<table>
<tr>
<td><strong>Linux / OS X</strong></td>
<td>
<a href="https://travis-ci.org/rust-lang-nursery/mdBook"><img src="https://travis-ci.org/rust-lang-nursery/mdBook.svg?branch=master"></a>
</td>
</tr>
<tr>
<td><strong>Windows</strong></td>
<td>
<a href="https://ci.appveyor.com/project/rust-lang-libs/mdbook"><img src="https://ci.appveyor.com/api/projects/status/ysyke2rvo85sni55?svg=true"></a>
</td>
</tr>
<tr>
<td colspan="2">
<a href="https://crates.io/crates/mdbook"><img src="https://img.shields.io/crates/v/mdbook.svg"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg"></a>
</td>
</tr>
</table>
[![Build Status](https://github.com/rust-lang-nursery/mdBook/workflows/CI/badge.svg)](https://github.com/rust-lang-nursery/mdBook/actions?workflow=CI)
[![crates.io](https://img.shields.io/crates/v/mdbook.svg)](https://crates.io/crates/mdbook)
[![LICENSE](https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg)](LICENSE)
mdBook is a utility to create modern online books from Markdown files.
@@ -41,7 +24,7 @@ There are multiple ways to install mdBook.
2. **From Crates.io**
This requires at least [Rust] 1.20 and Cargo to be installed. Once you have installed
This requires at least [Rust] 1.35 and Cargo to be installed. Once you have installed
Rust, type the following in the terminal:
```
@@ -57,7 +40,7 @@ There are multiple ways to install mdBook.
another CI server, we recommend that you specify a semver version range for
mdBook when you install it through your script!
This will constrain the server to install the latests **non-breaking**
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:
@@ -65,7 +48,7 @@ There are multiple ways to install mdBook.
cargo install mdbook --vers "^0.1.0"
```
3. **From Git**
3. **From Git**
The version published to crates.io will ever so slightly be behind the
version hosted here on GitHub. If you need the latest version you can build
@@ -77,7 +60,7 @@ There are multiple ways to install mdBook.
Again, make sure to add the Cargo bin directory to your `PATH`.
4. **For Contributions**
4. **For Contributions**
If you want to contribute to mdBook you will have to clone the repository on
your local machine:
@@ -145,6 +128,60 @@ explanation, check out the [User Guide].
Delete directory in which generated book is located.
### 3rd Party Plugins
The way a book is loaded and rendered can be configured by the user via third
party plugins. These plugins are just programs which will be invoked during the
build process and are split into roughly two categories, *preprocessors* and
*renderers*.
Preprocessors are used to transform a book before it is sent to a renderer.
One example would be to replace all occurrences of
`{{#include some_file.ext}}` with the contents of that file. Some existing
preprocessors are:
- `index` - a built-in preprocessor (enabled by default) which will transform
all `README.md` chapters to `index.md` so `foo/README.md` can be accessed via
the url `foo/` when published to a browser
- `links` - a built-in preprocessor (enabled by default) for expanding the
`{{# playpen}}` and `{{# include}}` helpers in a chapter.
Renderers are given the final book so they can do something with it. This is
typically used for, as the name suggests, rendering the document in a particular
format, however there's nothing stopping a renderer from doing static analysis
of a book in order to validate links or run tests. Some existing renderers are:
- `html` - the built-in renderer which will generate a HTML version of the book
- `markdown` - the built-in renderer (disabled by default) which will run
preprocessors then output the resulting Markdown. Useful for debugging
preprocessors.
- [`linkcheck`] - a backend which will check that all links are valid
- [`epub`] - an experimental EPUB generator
> **Note for Developers:** Feel free to send us a PR if you've developed your
> own plugin and want it mentioned here.
A preprocessor or renderer is enabled by installing the appropriate program and
then mentioning it in the book's `book.toml` file.
```console
$ cargo install mdbook-linkcheck
$ edit book.toml && cat book.toml
[book]
title = "My Awesome Book"
authors = ["Michael-F-Bryan"]
[output.html]
[output.linkcheck] # enable the "mdbook-linkcheck" renderer
$ mdbook build
2018-10-20 13:57:51 [INFO] (mdbook::book): Book building has started
2018-10-20 13:57:51 [INFO] (mdbook::book): Running the html backend
2018-10-20 13:57:53 [INFO] (mdbook::book): Running the linkcheck backend
```
For more information on the plugin system, consult the [User Guide].
### As a library
@@ -188,4 +225,6 @@ All the code in this repository is released under the ***Mozilla Public License
[releases]: https://github.com/rust-lang-nursery/mdBook/releases
[Rust]: https://www.rust-lang.org/
[CLI docs]: http://rust-lang-nursery.github.io/mdBook/cli/init.html
[master-docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
[master-docs]: http://rust-lang-nursery.github.io/mdBook/
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
[`epub`]: https://crates.io/crates/mdbook-epub

View File

@@ -1,64 +0,0 @@
environment:
global:
PROJECT_NAME: mdBook
matrix:
# Stable channel
- TARGET: i686-pc-windows-msvc
RUST_CHANNEL: stable
- TARGET: x86_64-pc-windows-msvc
RUST_CHANNEL: stable
# Beta channel
- TARGET: i686-pc-windows-msvc
RUST_CHANNEL: beta
- TARGET: x86_64-pc-windows-msvc
RUST_CHANNEL: beta
# Nightly channel
- TARGET: i686-pc-windows-msvc
RUST_CHANNEL: nightly
- TARGET: x86_64-pc-windows-msvc
RUST_CHANNEL: nightly
# Install Rust and Cargo
install:
- ps: >-
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
$Env:PATH += ';C:\msys64\mingw64\bin'
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
$Env:PATH += ';C:\msys64\mingw32\bin'
}
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
- rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_CHANNEL%
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustc -Vv
- cargo -V
build: false
# Equivalent to Travis' `script` phase
test_script:
- cargo test --all
- cargo test --all --no-default-features
before_deploy:
# Generate artifacts for release
- cargo rustc --bin mdbook --release -- -C lto
- mkdir staging
- copy target\release\mdbook.exe staging
- cd staging
- 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip *
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
deploy:
description: 'Windows release'
artifact: /.*\.zip/
auth_token:
secure: QQhjKVyz7mpjlyGhlXytbFQQfKFQWTahHkD+B0NzIUoEVqO7ZLWjnoWasvLqW4nE
provider: GitHub
on:
RUST_CHANNEL: stable
appveyor_repo_tag: true
branches:
only:
- master
- /^v\d+\.\d+\.\d+.*$/

View File

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

View File

@@ -20,7 +20,7 @@
- [Continuous Integration](continuous-integration.md)
- [For Developers](for_developers/README.md)
- [Preprocessors](for_developers/preprocessors.md)
- [Alternate Backends](for_developers/backends.md)
- [Alternative Backends](for_developers/backends.md)
-----------

View File

@@ -18,7 +18,7 @@ mdBook can also be installed from source
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs
to be compiled with **Cargo**. If you haven't already installed Rust, please go
ahead and [install it](https://www.rust-lang.org/downloads.html) now.
ahead and [install it](https://www.rust-lang.org/tools/install) now.
### Install Crates.io version

View File

@@ -29,8 +29,9 @@ your default web browser after building it.
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. If not specified it will default to the value of the `build.build-dir` key
in `book.toml`, or to `./book` relative to the book's root directory.
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`.
-------------------

View File

@@ -19,9 +19,9 @@ mdbook clean path/to/book
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to override the book's output
directory, which will be deleted by this command. If not specified it will
default to the value of the `build.build-dir` key in `book.toml`, or to `./book`
relative to the book's root directory.
directory, which will be deleted by this command. Relative paths are interpreted
relative to the book's root directory. If not specified it will default to the
value of the `build.build-dir` key in `book.toml`, or to `./book`.
```bash
mdbook clean --dest-dir=path/to/book

View File

@@ -5,6 +5,9 @@ The serve command is used to preview a book by serving it over HTTP at
changes, rebuilding the book and refreshing clients for each change. A websocket
connection is used to trigger the client-side refresh.
***Note:*** *The `serve` command is for testing a book's HTML output, and is not
intended to be a complete HTTP server for a website.*
#### Specify a directory
The `serve` command can take a directory as an argument to use as the book's
@@ -34,16 +37,12 @@ configured.
#### --open
When you use the `--open` (`-o`) flag, mdbook will open the book in your your
When you use the `--open` (`-o`) flag, mdbook will open the book in your
default web browser after starting the server.
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. If not specified it will default to the value of the `build.build-dir` key
in `book.toml`, or to `./book` relative to the book's root directory.
-----
***Note:*** *The `serve` command is for testing, and is not intended to be a
complete HTTP server for a website.*
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`.

View File

@@ -48,5 +48,6 @@ comma-delimited list (`-L foo,bar`).
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. If not specified it will default to the value of the `build.build-dir` key
in `book.toml`, or to `./book` relative to the book's root directory.
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`.

View File

@@ -22,5 +22,6 @@ your default web browser.
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. If not specified it will default to the value of the `build.build-dir` key
in `book.toml`, or to `./book` relative to the book's root directory.
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`.

View File

@@ -22,11 +22,11 @@ rust:
before_script:
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.1" mdbook)
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.3" mdbook)
- cargo install-update -a
script:
- cd path/to/mybook && mdbook build && mdbook test
- mdbook build path/to/mybook && mdbook test path/to/mybook
```
## Deploying Your Book to GitHub Pages
@@ -54,3 +54,36 @@ deploy:
```
That's it!
### Deploying to GitHub Pages manually
If your CI doesn't support GitHub pages, or you're deploying somewhere else
with integrations such as Github Pages:
*note: you may want to use different tmp dirs*:
```console
$> git worktree add /tmp/book gh-pages
$> mdbook build
$> rm -rf /tmp/book/* # this won't delete the .git directory
$> cp -rp book/* /tmp/book/
$> cd /tmp/book
$> git add -A
$> git commit 'new book message'
$> git push origin gh-pages
$> cd -
```
Or put this into a Makefile rule:
```makefile
.PHONY: deploy
deploy: book
@echo "====> deploying to github"
git worktree add /tmp/book gh-pages
rm -rf /tmp/book/*
cp -rp book/* /tmp/book/
cd /tmp/book && \
git add -A && \
git commit -m "deployed on $(shell date) by ${USER}" && \
git push origin gh-pages
```

View File

@@ -12,14 +12,14 @@ The *For Developers* chapters are here to show you the more advanced usage of
The two main ways a developer can hook into the book's build process is via,
- [Preprocessors](preprocessors.md)
- [Alternate Backends](backends.md)
- [Alternative Backends](backends.md)
## The Build Process
The process of rendering a book project goes through several steps.
1. Load the book
1. Load the book
- Parse the `book.toml`, falling back to the default `Config` if it doesn't
exist
- Load the book chapters into memory
@@ -41,6 +41,6 @@ The easiest way to find out how to use the `mdbook` crate is by looking at the
explanation on the configuration system.
[`MDBook`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.MDBook.html
[API Docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
[config]: file:///home/michael/Documents/forks/mdBook/target/doc/mdbook/config/index.html
[`MDBook`]: https://docs.rs/mdbook/*/mdbook/book/struct.MDBook.html
[API Docs]: https://docs.rs/mdbook/*/mdbook/
[config]: https://docs.rs/mdbook/*/mdbook/config/index.html

View File

@@ -1,11 +1,11 @@
# Alternate Backends
# Alternative Backends
A "backend" is simply a program which `mdbook` will invoke during the book
rendering process. This program is passed a JSON representation of the book and
configuration information via `stdin`. Once the backend receives this
information it is free to do whatever it wants.
There are already several alternate backends on GitHub which can be used as a
There are already several alternative backends on GitHub which can be used as a
rough example of how this is accomplished in practice.
- [mdbook-linkcheck] - a simple program for verifying the book doesn't contain
@@ -14,7 +14,7 @@ rough example of how this is accomplished in practice.
- [mdbook-test] - a program to run the book's contents through [rust-skeptic] to
verify everything compiles and runs correctly (similar to `rustdoc --test`)
This page will step you through creating your own alternate backend in the form
This page will step you through creating your own alternative backend in the form
of a simple word counting program. Although it will be written in Rust, there's
no reason why it couldn't be accomplished using something like Python or Ruby.
@@ -24,9 +24,9 @@ no reason why it couldn't be accomplished using something like Python or Ruby.
First you'll want to create a new binary program and add `mdbook` as a
dependency.
```
```shell
$ cargo new --bin mdbook-wordcount
$ cd mdbook-wordcount
$ cd mdbook-wordcount
$ cargo add mdbook
```
@@ -52,8 +52,8 @@ fn main() {
> **Note:** The `RenderContext` contains a `version` field. This lets backends
figure out whether they are compatible with the version of `mdbook` it's being
called by. This `version` comes directly from the corresponding field in
`mdbook`'s `Cargo.toml`.
`mdbook`'s `Cargo.toml`.
It is recommended that backends use the [`semver`] crate to inspect this field
and emit a warning if there may be a compatibility issue.
@@ -92,12 +92,12 @@ fn count_words(ch: &Chapter) -> usize {
Now we've got the basics running, we want to actually use it. First, install the
program.
```
$ cargo install
```shell
$ cargo install --path .
```
Then `cd` to the particular book you'd like to count the words of and update its
`book.toml` file.
`book.toml` file.
```diff
[book]
@@ -112,7 +112,7 @@ Then `cd` to the particular book you'd like to count the words of and update its
When it loads a book into memory, `mdbook` will inspect your `book.toml` file to
try and figure out which backends to use by looking for all `output.*` tables.
If none are provided it'll fall back to using the default HTML renderer.
If none are provided it'll fall back to using the default HTML renderer.
Notably, this means if you want to add your own custom backend you'll also need
to make sure to add the HTML backend, even if its table just stays empty.
@@ -120,7 +120,7 @@ to make sure to add the HTML backend, even if its table just stays empty.
Now you just need to build your book like normal, and everything should *Just
Work*.
```
```shell
$ mdbook build
...
2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
@@ -140,7 +140,7 @@ Syntax highlighting: 314
MathJax Support: 153
Rust code specific features: 148
For Developers: 788
Alternate Backends: 710
Alternative Backends: 710
Contributors: 85
```
@@ -169,7 +169,7 @@ arguments or be an interpreted script), you can use the `command` field.
Now imagine you don't want to count the number of words on a particular chapter
(it might be generated text/code, etc). The canonical way to do this is via the
usual `book.toml` configuration file by adding items to your `[output.foo]`
table.
table.
The `Config` can be treated roughly as a nested hashmap which lets you call
methods like `get()` to access the config's contents, with a
@@ -211,13 +211,13 @@ and then add a check to make sure we skip ignored chapters.
+ let cfg: WordcountConfig = ctx.config
+ .get_deserialized("output.wordcount")
+ .unwrap_or_default();
for item in ctx.book.iter() {
if let BookItem::Chapter(ref ch) = *item {
+ if cfg.ignores.contains(&ch.name) {
+ continue;
+ }
+
+
let num_words = count_words(ch);
println!("{}: {}", ch.name, num_words);
}
@@ -239,17 +239,17 @@ in [`RenderContext`].
- use std::io;
use mdbook::renderer::RenderContext;
use mdbook::book::{BookItem, Chapter};
fn main() {
...
+ let _ = fs::create_dir_all(&ctx.destination);
+ let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
+
+
for item in ctx.book.iter() {
if let BookItem::Chapter(ref ch) = *item {
...
let num_words = count_words(ch);
println!("{}: {}", ch.name, num_words);
+ writeln!(f, "{}: {}", ch.name, num_words).unwrap();
@@ -261,6 +261,10 @@ in [`RenderContext`].
> **Note:** There is no guarantee that the destination directory exists or is
> empty (`mdbook` may leave the previous contents to let backends do caching),
> so it's always a good idea to create it with `fs::create_dir_all()`.
>
> If the destination directory already exists, don't assume it will be empty.
> To allow backends to cache the results from previous runs, `mdbook` may leave
> old content in the directory.
There's always the possibility that an error will occur while processing a book
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
@@ -276,11 +280,11 @@ like this:
fn main() {
...
for item in ctx.book.iter() {
if let BookItem::Chapter(ref ch) = *item {
...
let num_words = count_words(ch);
println!("{}: {}", ch.name, num_words);
writeln!(f, "{}: {}", ch.name, num_words).unwrap();
@@ -303,8 +307,8 @@ like this:
Now, if we reinstall the backend and build a book,
```
$ cargo install --force
```shell
$ cargo install --path . --force
$ mdbook build /path/to/book
...
2018-01-16 21:21:39 [INFO] (mdbook::renderer): Invoking the "wordcount" renderer
@@ -329,7 +333,7 @@ the usual `RUST_LOG` to control logging verbosity.
## Wrapping Up
Although contrived, hopefully this example was enough to show how you'd create
an alternate backend for `mdbook`. If you feel it's missing something, don't
an alternative backend for `mdbook`. If you feel it's missing something, don't
hesitate to create an issue in the [issue tracker] so we can improve the user
guide.
@@ -342,10 +346,10 @@ the source code or ask questions.
[mdbook-epub]: https://github.com/Michael-F-Bryan/mdbook-epub
[mdbook-test]: https://github.com/Michael-F-Bryan/mdbook-test
[rust-skeptic]: https://github.com/budziq/rust-skeptic
[`RenderContext`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html
[`RenderContext::from_json()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html#method.from_json
[`RenderContext`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
[`RenderContext::from_json()`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json
[`semver`]: https://crates.io/crates/semver
[`Book`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html
[`Book::iter()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html#method.iter
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
[`Book`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html
[`Book::iter()`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html#method.iter
[`Config`]: https://docs.rs/mdbook/*/mdbook/config/struct.Config.html
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues

View File

@@ -11,68 +11,71 @@ the book. Possible use cases are:
mathjax equivalents
## Implementing a Preprocessor
## Hooking Into MDBook
A preprocessor is represented by the `Preprocessor` trait.
MDBook uses a fairly simple mechanism for discovering third party plugins.
A new table is added to `book.toml` (e.g. `preprocessor.foo` for the `foo`
preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
part of the build process.
```rust
pub trait Preprocessor {
fn name(&self) -> &str;
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
fn supports_renderer(&self, _renderer: &str) -> bool {
true
}
}
While preprocessors can be hard-coded to specify which backend it should be run
for (e.g. it doesn't make sense for MathJax to be used for non-HTML renderers)
with the `preprocessor.foo.renderer` key.
```toml
[book]
title = "My Book"
authors = ["Michael-F-Bryan"]
[preprocessor.foo]
# The command can also be specified manually
command = "python3 /path/to/foo.py"
# Only run the `foo` preprocessor for the HTML and EPUB renderer
renderer = ["html", "epub"]
```
Where the `PreprocessorContext` is defined as
In typical unix style, all inputs to the plugin will be written to `stdin` as
JSON and `mdbook` will read from `stdout` if it is expecting output.
The easiest way to get started is by creating your own implementation of the
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
translates inputs to the correct `Preprocessor` method. For convenience, there
is [an example no-op preprocessor] in the `examples/` directory which can easily
be adapted for other preprocessors.
<details>
<summary>Example no-op preprocessor</summary>
```rust
pub struct PreprocessorContext {
pub root: PathBuf,
pub config: Config,
/// The `Renderer` this preprocessor is being used with.
pub renderer: String,
}
// nop-preprocessors.rs
{{#include ../../../examples/nop-preprocessor.rs}}
```
</details>
The `renderer` value allows you react accordingly, for example, PDF or HTML.
## Hints For Implementing A Preprocessor
## A complete Example
By pulling in `mdbook` as a library, preprocessors can have access to the
existing infrastructure for dealing with books.
The magic happens within the `run(...)` method of the
[`Preprocessor`][preprocessor-docs] trait implementation.
For example, a custom preprocessor could use the
[`CmdPreprocessor::parse_input()`] function to deserialize the JSON written to
`stdin`. Then each chapter of the `Book` can be mutated in-place via
[`Book::for_each_mut()`], and then written to `stdout` with the `serde_json`
crate.
As direct access to the chapters is not possible, you will probably end up
iterating them using `for_each_mut(...)`:
Chapters can be accessed either directly (by recursively iterating over
chapters) or via the `Book::for_each_mut()` convenience method.
```rust
book.for_each_mut(|item: &mut BookItem| {
if let BookItem::Chapter(ref mut chapter) = *item {
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
res = Some(
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
Ok(md) => {
chapter.content = md;
Ok(())
}
Err(err) => Err(err),
},
);
}
});
```
The `chapter.content` is just a string which happens to be markdown. While it's
entirely possible to use regular expressions or do a manual find & replace,
you'll probably want to process the input into something more computer-friendly.
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to
translate events back into markdown text.
The `chapter.content` is just a markdown formatted string, and you will have to
process it in some way. Even though it's entirely possible to implement some
sort of manual find & replace operation, if that feels too unsafe you can use
[`pulldown-cmark`][pc] to parse the string into events and work on them instead.
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events
back to a string.
The following code block shows how to remove all emphasis from markdown, and do
so safely.
The following code block shows how to remove all emphasis from markdown,
without accidentally breaking the document.
```rust
fn remove_emphasis(
@@ -106,4 +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/de-emphasize.rs
[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
[`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

@@ -14,9 +14,9 @@ description = "The example book covers examples."
build-dir = "my-example-book"
create-missing = false
[preprocess.index]
[preprocessor.index]
[preprocess.links]
[preprocessor.links]
[output.html]
additional-css = ["custom.css"]
@@ -27,7 +27,7 @@ limit-results = 15
## Supported configuration options
It is important to note that **any** relative path specified in the in the
It is important to note that **any** relative path specified in the
configuration will always be taken relative from the root of the book where the
configuration file is located.
@@ -42,6 +42,7 @@ This is general information about your book.
- **src:** By default, the source directory is found in the directory named
`src` directly under the root folder. But this is configurable with the `src`
key in the configuration file.
- **language:** The main language of the book, which is used as a language attribute `<html lang="en">` for example.
**book.toml**
```toml
@@ -50,6 +51,7 @@ title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
language = "en"
```
### Build options
@@ -68,19 +70,19 @@ This controls the build process of your book.
If you have the same, and/or other preprocessors declared via their table
of configuration, they will run instead.
- For clarity, with no preprocessor configuration, the default `links` and
- For clarity, with no preprocessor configuration, the default `links` and
`index` will run.
- Setting `use-default-preprocessors = false` will disable these
default preprocessors from running.
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
`use-default-preprocessors` that `links` it will run.
## Configuring Preprocessors
The following preprocessors are available and included by default:
- `links`: Expand the `{{ #playpen }}` and `{{ #include }}` handlebars helpers in
a chapter to include the contents of a file.
- `links`: Expand the `{{ #playpen }}`, `{{ #include }}`, and `{{ #rustdoc_include }}` handlebars
helpers in a chapter to include the contents of a file.
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
to say, all `README.md` would be rendered to an index file `index.html` in the
rendered book.
@@ -92,20 +94,21 @@ The following preprocessors are available and included by default:
build-dir = "build"
create-missing = false
[preprocess.links]
[preprocessor.links]
[preprocess.index]
[preprocessor.index]
```
### Custom Preprocessor Configuration
Like renderers, preprocessor will need to be given its own table (e.g. `[preprocessor.mathjax]`).
In the section, you may then pass extra configuration to the preprocessor by adding key-value pairs to the table.
Like renderers, preprocessor will need to be given its own table (e.g.
`[preprocessor.mathjax]`). In the section, you may then pass extra
configuration to the preprocessor by adding key-value pairs to the table.
For example
```
[preprocess.links]
```toml
[preprocessor.links]
# set the renderers this preprocessor will run for
renderers = ["html"]
some_extra_feature = true
@@ -113,13 +116,26 @@ some_extra_feature = true
#### Locking a Preprocessor dependency to a renderer
You can explicitly specify that a preprocessor should run for a renderer by binding the two together.
You can explicitly specify that a preprocessor should run for a renderer by
binding the two together.
```
```toml
[preprocessor.mathjax]
renderers = ["html"] # mathjax only makes sense with the HTML renderer
```
### Provide Your Own Command
By default when you add a `[preprocessor.foo]` table to your `book.toml` file,
`mdbook` will try to invoke the `mdbook-foo` executable. If you want to use a
different program name or pass in command-line arguments, this behaviour can
be overridden by adding a `command` field.
```toml
[preprocessor.random]
command = "python random.py"
```
## Configuring Renderers
### HTML renderer options
@@ -132,8 +148,16 @@ The following configuration options are available:
- **theme:** mdBook comes with a default theme and all the resource files needed
for it. But if this option is set, mdBook will selectively overwrite the theme
files with the ones found in the specified folder.
- **default-theme:** The theme color scheme to select by default in the
'Change Theme' dropdown. Defaults to `light`.
- **preferred-dark-theme:** The default dark theme. This theme will be used if
the browser requests the dark version of the site via the
['prefers-color-scheme'](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
CSS media query. Defaults to the same theme as `default-theme`.
- **curly-quotes:** Convert straight quotes to curly quotes, except for those
that occur in code blocks and code spans. Defaults to `false`.
- **mathjax-support:** Adds support for [MathJax](mathjax.md). Defaults to
`false`.
- **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
@@ -146,15 +170,29 @@ The following configuration options are available:
- **no-section-label:** mdBook by defaults adds section label in table of
contents column. For example, "1.", "2.1". Set this option to true to disable
those labels. Defaults to `false`.
- **fold:** A subtable for configuring sidebar section-folding behavior.
- **playpen:** A subtable for configuring various playpen settings.
- **search:** A subtable for configuring the in-browser search functionality.
mdBook must be compiled with the `search` feature enabled (on by default).
- **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`.
Available configuration options for the `[output.html.fold]` table:
- **enable:** Enable section-folding. When off, all folds are open.
Defaults to `false`.
- **level:** The higher the more folded regions are open. When level is 0, all
folds are closed. Defaults to `0`.
Available configuration options for the `[output.html.playpen]` table:
- **editable:** Allow editing the source code. Defaults to `false`.
- **copyable:** Display the copy button on code snippets. Defaults to `true`.
- **copy-js:** Copy JavaScript files for the editor to the output directory.
Defaults to `true`.
- **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`.
[Ace]: https://ace.c9.io/
@@ -165,7 +203,7 @@ Available configuration options for the `[output.html.search]` table:
- **teaser-word-count:** The number of words used for a search result teaser.
Defaults to `30`.
- **use-boolean-and:** Define the logical link between multiple search words. If
true, all search words must appear in each result. Defaults to `true`.
true, all search words must appear in each result. Defaults to `false`.
- **boost-title:** Boost factor for the search result score if a search word
appears in the header. Defaults to `2`.
- **boost-hierarchy:** Boost factor for the search result score if a search word
@@ -181,32 +219,38 @@ Available configuration options for the `[output.html.search]` table:
- **copy-js:** Copy JavaScript files for the search implementation to the output
directory. Defaults to `true`.
This shows all available options in the **book.toml**:
This shows all available HTML output options in the **book.toml**:
```toml
[book]
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
[build]
build-dir = "book"
create-missing = true
preprocess = ["links", "index"]
[output.html]
theme = "my-theme"
default-theme = "light"
preferred-dark-theme = "navy"
curly-quotes = true
mathjax-support = false
google-analytics = "123456"
additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"]
no-section-label = false
git-repository-url = "https://github.com/rust-lang-nursery/mdBook"
git-repository-icon = "fa-github"
[output.html.fold]
enable = false
level = 0
[output.html.playpen]
editor = "./path/to/editor"
editable = false
copy-js = true
line-numbers = false
[output.html.search]
enable = true
searcher = "./path/to/searcher"
limit-results = 30
teaser-word-count = 30
use-boolean-and = true
@@ -218,6 +262,36 @@ heading-split-level = 3
copy-js = true
```
### Markdown Renderer
The Markdown renderer will run preprocessors and then output the resulting
Markdown. This is mostly useful for debugging preprocessors, especially in
conjunction with `mdbook test` to see the Markdown that `mdbook` is passing
to `rustdoc`.
The Markdown renderer is included with `mdbook` but disabled by default.
Enable it by adding an emtpy table to your `book.toml` as follows:
```toml
[output.markdown]
```
There are no configuration options for the Markdown renderer at this time;
only whether it is enabled or disabled.
See [the preprocessors documentation](#configuring-preprocessors) for how to
specify which preprocessors should run before the Markdown renderer.
### Custom Renderers
A custom renderer can be enabled by adding a `[output.foo]` table to your
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
rendering.
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.
## Environment Variables
@@ -249,11 +323,11 @@ book's title without needing to touch your `book.toml`.
> This means, if you so desired, you could override all book metadata when
> building the book with something like
>
> ```text
> ```shell
> $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
> $ mdbook build
> ```
The latter case may be useful in situations where `mdbook` is invoked from a
script or CI, where it sometimes isn't possible to update the `book.toml` before
building.
building.

View File

@@ -3,7 +3,9 @@
## Hiding code lines
There is a feature in mdBook that lets you hide code lines by prepending them
with a `#`.
with a `#` [in the same way that Rustdoc does][rustdoc-hide].
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example
```bash
# fn main() {
@@ -35,10 +37,20 @@ With the following syntax, you can include files into your book:
The path to the file has to be relative from the current source file.
Usually, this command is used for including code snippets and examples. In this
case, oftens one would include a specific part of the file e.g. which only
contains the relevant lines for the example. We support four different modes of
partial includes:
mdBook will interpret included files as markdown. Since the include command
is usually used for inserting code snippets and examples, you will often
wrap the command with ```` ``` ```` to display the file contents without
interpretting them.
````hbs
```
\{{#include file.rs}}
```
````
## Including portions of a file
Often you only need a specific part of the file e.g. relevant lines for an
example. We support four different modes of partial includes:
```hbs
\{{#include file.rs:2}}
@@ -53,6 +65,114 @@ the file are omitted. The third command includes all lines from line 2, i.e. the
first line is omitted. The last command includes the excerpt of `file.rs`
consisting of lines 2 to 10.
To avoid breaking your book when modifying included files, you can also
include a specific section using anchors instead of line numbers.
An anchor is a pair of matching lines. The line beginning an anchor must
match the regex "ANCHOR:\s*[\w_-]+" and similarly the ending line must match
the regex "ANCHOR_END:\s*[\w_-]+". This allows you to put anchors in
any kind of commented line.
Consider the following file to include:
```rs
/* ANCHOR: all */
// ANCHOR: component
struct Paddle {
hello: f32,
}
// ANCHOR_END: component
////////// ANCHOR: system
impl System for MySystem { ... }
////////// ANCHOR_END: system
/* ANCHOR_END: all */
```
Then in the book, all you have to do is:
````hbs
Here is a component:
```rust,no_run,noplaypen
\{{#include file.rs:component}}
```
Here is a system:
```rust,no_run,noplaypen
\{{#include file.rs:system}}
```
This is the full file.
```rust,no_run,noplaypen
\{{#include file.rs:all}}
```
````
Lines containing anchor patterns inside the included anchor are ignored.
## Including a file but initially hiding all except specified lines
The `rustdoc_include` helper is for including code from external Rust files that contain complete
examples, but only initially showing particular lines specified with line numbers or anchors in the
same way as with `include`.
The lines not in the line number range or between the anchors will still be included, but they will
be prefaced with `#`. This way, a reader can expand the snippet to see the complete example, and
Rustdoc will use the complete example when you run `mdbook test`.
For example, consider a file named `file.rs` that contains this Rust program:
```rust
fn main() {
let x = add_one(2);
assert_eq!(x, 3);
}
fn add_one(num: i32) -> i32 {
num + 1
}
```
We can include a snippet that initially shows only line 2 by using this syntax:
````hbs
To call the `add_one` function, we pass it an `i32` and bind the returned value to `x`:
```rust
\{{#rustdoc_include file.rs:2}}
```
````
This would have the same effect as if we had manually inserted the code and hidden all but line 2
using `#`:
````hbs
To call the `add_one` function, we pass it an `i32` and bind the returned value to `x`:
```rust
# fn main() {
let x = add_one(2);
# assert_eq!(x, 3);
# }
#
# fn add_one(num: i32) -> i32 {
# num + 1
#}
```
````
That is, it looks like this (click the "expand" icon to see the rest of the file):
```rust
# fn main() {
let x = add_one(2);
# assert_eq!(x, 3);
# }
#
# fn add_one(num: i32) -> i32 {
# num + 1
#}
```
## Inserting runnable Rust files
With the following syntax, you can insert runnable Rust files into your book:

View File

@@ -17,9 +17,8 @@ handlebars template you can access this information by using
Here is a list of the properties that are exposed:
- ***language*** Language of the book in the form `en`. To use in <code
class="language-html">\<html lang="{{ language }}"></code> for example. At the
moment it is hardcoded.
- ***language*** Language of the book in the form `en`, as specified in `book.toml` (if not specified, defaults to `en`). To use in <code
class="language-html">\<html lang="{{ language }}"></code> for example.
- ***title*** Title of the book, as specified in `book.toml`
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
@@ -45,51 +44,55 @@ at your disposal.
### 1. toc
The toc helper is used like this
The toc helper is used like this
```handlebars
{{#toc}}{{/toc}}
```
```handlebars
{{#toc}}{{/toc}}
```
and outputs something that looks like this, depending on the structure of your book
and outputs something that looks like this, depending on the structure of your
book
```html
<ul class="chapter">
<li><a href="link/to/file.html">Some chapter</a></li>
<li>
<ul class="section">
<li><a href="link/to/other_file.html">Some other Chapter</a></li>
</ul>
</li>
</ul>
```
```html
<ul class="chapter">
<li><a href="link/to/file.html">Some chapter</a></li>
<li>
<ul class="section">
<li><a href="link/to/other_file.html">Some other Chapter</a></li>
</ul>
</li>
</ul>
```
If you would like to make a toc with another structure, you have access to the chapters property containing all the data.
The only limitation at the moment is that you would have to do it with JavaScript instead of with a handlebars helper.
If you would like to make a toc with another structure, you have access to the
chapters property containing all the data. The only limitation at the moment
is that you would have to do it with JavaScript instead of with a handlebars
helper.
```html
<script>
var chapters = {{chapters}};
// Processing here
</script>
```
```html
<script>
var chapters = {{chapters}};
// Processing here
</script>
```
### 2. previous / next
The previous and next helpers expose a `link` and `name` property to the previous and next chapters.
The previous and next helpers expose a `link` and `name` property to the
previous and next chapters.
They are used like this
They are used like this
```handlebars
{{#previous}}
<a href="{{link}}" class="nav-chapters previous">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
```
```handlebars
{{#previous}}
<a href="{{link}}" class="nav-chapters previous">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
```
The inner html will only be rendered if the previous / next chapter exists.
Of course the inner html can be changed to your liking.
The inner html will only be rendered if the previous / next chapter exists.
Of course the inner html can be changed to your liking.
------

View File

@@ -1,32 +0,0 @@
# This script takes care of building your crate and packaging it for release
set -ex
main() {
local src=$(pwd) \
stage=
case $TRAVIS_OS_NAME in
linux)
stage=$(mktemp -d)
;;
osx)
stage=$(mktemp -d -t tmp)
;;
esac
# This will slow down the build, but is necessary to not run out of disk space
cargo clean
cargo rustc --bin mdbook --target $TARGET --release -- -C lto
cp target/$TARGET/release/mdbook $stage/
cd $stage
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
cd $src
rm -rf $stage
}
main

24
ci/install-hub.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Installs the `hub` executable into hub/bin
set -ex
case $1 in
ubuntu*)
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-linux-amd64-2.12.8.tgz -o hub.tgz
mkdir hub
tar -xzvf hub.tgz --strip=1 -C hub
;;
macos*)
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-darwin-amd64-2.12.8.tgz -o hub.tgz
mkdir hub
tar -xzvf hub.tgz --strip=1 -C hub
;;
windows*)
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-windows-amd64-2.12.8.zip -o hub.zip
7z x hub.zip -ohub
;;
*)
echo "OS should be first parameter, was: $1"
;;
esac
echo "##[add-path]$PWD/hub/bin"

19
ci/install-rust.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Install/update rust.
# The first argument should be the toolchain to install.
set -ex
if [ -z "$1" ]
then
echo "First parameter must be toolchain to install."
exit 1
fi
TOOLCHAIN="$1"
rustup set profile minimal
rustup component remove --toolchain=$TOOLCHAIN rust-docs || echo "already removed"
rustup update $TOOLCHAIN
rustup default $TOOLCHAIN
rustup -V
rustc -Vv
cargo -V

26
ci/install-rustup.sh Executable file
View File

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

36
ci/make-release.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Builds the release and creates an archive and optionally deploys to GitHub.
set -ex
if [[ -z "$GITHUB_REF" ]]
then
echo "GITHUB_REF must be set"
exit 1
fi
# Strip mdbook-refs/tags/ from the start of the ref.
TAG=${GITHUB_REF#*/tags/}
host=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
cargo rustc --bin mdbook --release -- -C lto
cd target/release
case $1 in
ubuntu* | macos*)
asset="mdbook-$TAG-$host.tar.gz"
tar czf ../../$asset mdbook
;;
windows*)
asset="mdbook-$TAG-$host.zip"
7z a ../../$asset mdbook.exe
;;
*)
echo "OS should be first parameter, was: $1"
;;
esac
cd ../..
if [[ -z "$GITHUB_TOKEN" ]]
then
echo "$GITHUB_TOKEN not set, skipping deploy."
else
hub release edit -m "" --attach $asset $TAG
fi

View File

@@ -1,98 +0,0 @@
//! This program removes all forms of emphasis from the markdown of the book.
extern crate mdbook;
extern crate pulldown_cmark;
extern crate pulldown_cmark_to_cmark;
use mdbook::book::{Book, BookItem, Chapter};
use mdbook::errors::{Error, Result};
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::MDBook;
use pulldown_cmark::{Event, Parser, Tag};
use pulldown_cmark_to_cmark::fmt::cmark;
use std::env::{args, args_os};
use std::ffi::OsString;
use std::process;
const NAME: &str = "md-links-to-html-links";
fn do_it(book: OsString) -> Result<()> {
let mut book = MDBook::load(book)?;
book.with_preprecessor(Deemphasize);
book.build()
}
fn main() {
if args_os().count() != 2 {
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
return;
}
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
eprintln!("{}", e);
process::exit(1);
}
}
struct Deemphasize;
impl Preprocessor for Deemphasize {
fn name(&self) -> &str {
NAME
}
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
eprintln!("Running '{}' preprocessor", self.name());
let mut num_removed_items = 0;
process(&mut book.sections, &mut num_removed_items)?;
eprintln!(
"{}: removed {} events from markdown stream.",
self.name(),
num_removed_items
);
Ok(book)
}
}
fn process<'a, I>(items: I, num_removed_items: &mut usize) -> Result<()>
where
I: IntoIterator<Item = &'a mut BookItem> + 'a,
{
for item in items {
if let BookItem::Chapter(ref mut chapter) = *item {
eprintln!("{}: processing chapter '{}'", NAME, chapter.name);
let md = remove_emphasis(num_removed_items, chapter)?;
chapter.content = md;
}
}
Ok(())
}
fn remove_emphasis(
num_removed_items: &mut usize,
chapter: &mut Chapter,
) -> Result<String> {
let mut buf = String::with_capacity(chapter.content.len());
let events = Parser::new(&chapter.content).filter(|e| {
let should_keep = match *e {
Event::Start(Tag::Emphasis)
| Event::Start(Tag::Strong)
| Event::End(Tag::Emphasis)
| Event::End(Tag::Strong) => false,
_ => true,
};
if !should_keep {
*num_removed_items += 1;
}
should_keep
});
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
Error::from(format!("Markdown serialization failed: {}", err))
})
}

View File

@@ -0,0 +1,102 @@
use crate::nop_lib::Nop;
use clap::{App, Arg, ArgMatches, SubCommand};
use mdbook::book::Book;
use mdbook::errors::Error;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
use std::io;
use std::process;
pub fn make_app() -> App<'static, 'static> {
App::new("nop-preprocessor")
.about("A mdbook preprocessor which does precisely nothing")
.subcommand(
SubCommand::with_name("supports")
.arg(Arg::with_name("renderer").required(true))
.about("Check whether a renderer is supported by this preprocessor"),
)
}
fn main() {
let matches = make_app().get_matches();
// Users will want to construct their own preprocessor here
let preprocessor = Nop::new();
if let Some(sub_args) = matches.subcommand_matches("supports") {
handle_supports(&preprocessor, sub_args);
} else if let Err(e) = handle_preprocessing(&preprocessor) {
eprintln!("{}", e);
process::exit(1);
}
}
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
// We should probably use the `semver` crate to check compatibility
// here...
eprintln!(
"Warning: The {} plugin was built against version {} of mdbook, \
but we're being called from version {}",
pre.name(),
mdbook::MDBOOK_VERSION,
ctx.mdbook_version
);
}
let processed_book = pre.run(&ctx, book)?;
serde_json::to_writer(io::stdout(), &processed_book)?;
Ok(())
}
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
let renderer = sub_args.value_of("renderer").expect("Required argument");
let supported = pre.supports_renderer(&renderer);
// Signal whether the renderer is supported by exiting with 1 or 0.
if supported {
process::exit(0);
} else {
process::exit(1);
}
}
/// The actual implementation of the `Nop` preprocessor. This would usually go
/// in your main `lib.rs` file.
mod nop_lib {
use super::*;
/// A no-op preprocessor.
pub struct Nop;
impl Nop {
pub fn new() -> Nop {
Nop
}
}
impl Preprocessor for Nop {
fn name(&self) -> &str {
"nop-preprocessor"
}
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book, Error> {
// In testing we want to tell the preprocessor to blow up by setting a
// particular config value
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
if nop_cfg.contains_key("blow-up") {
return Err("Boom!!1!".into());
}
}
// we *are* a no-op preprocessor after all
Ok(book)
}
fn supports_renderer(&self, renderer: &str) -> bool {
renderer != "not-supported"
}
}
}

View File

@@ -5,8 +5,8 @@ use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use config::BuildConfig;
use errors::*;
use crate::config::BuildConfig;
use crate::errors::*;
/// Load a book into memory from its `src/` directory.
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
@@ -82,7 +82,7 @@ impl Book {
}
/// Get a depth-first iterator over the items in the book.
pub fn iter(&self) -> BookItems {
pub fn iter(&self) -> BookItems<'_> {
BookItems {
items: self.sections.iter().collect(),
}
@@ -116,7 +116,7 @@ where
I: IntoIterator<Item = &'a mut BookItem>,
{
for item in items {
if let &mut BookItem::Chapter(ref mut ch) = item {
if let BookItem::Chapter(ch) = item {
for_each_mut(func, &mut ch.sub_items);
}
@@ -167,9 +167,9 @@ impl Chapter {
) -> Chapter {
Chapter {
name: name.to_string(),
content: content,
content,
path: path.into(),
parent_names: parent_names,
parent_names,
..Default::default()
}
}
@@ -179,7 +179,7 @@ impl Chapter {
///
/// You need to pass in the book's source directory because all the links in
/// `SUMMARY.md` give the chapter locations relative to it.
fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
debug!("Loading the book from disk");
let src_dir = src_dir.as_ref();
@@ -210,7 +210,7 @@ fn load_summary_item<P: AsRef<Path>>(
match *item {
SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(ref link) => {
load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c))
load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
}
}
}
@@ -286,7 +286,7 @@ impl<'a> Iterator for BookItems<'a> {
}
impl Display for Chapter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(ref section_number) = self.number {
write!(f, "{} ", section_number)?;
}
@@ -301,7 +301,7 @@ mod tests {
use std::io::Write;
use tempfile::{Builder as TempFileBuilder, TempDir};
const DUMMY_SRC: &'static str = "
const DUMMY_SRC: &str = "
# Dummy Chapter
this is some dummy text.
@@ -317,7 +317,7 @@ And here is some \
let chapter_path = temp.path().join("chapter_1.md");
File::create(&chapter_path)
.unwrap()
.write(DUMMY_SRC.as_bytes())
.write_all(DUMMY_SRC.as_bytes())
.unwrap();
let link = Link::new("Chapter 1", chapter_path);
@@ -333,7 +333,7 @@ And here is some \
File::create(&second_path)
.unwrap()
.write_all("Hello World!".as_bytes())
.write_all(b"Hello World!")
.unwrap();
let mut second = Link::new("Nested Chapter 1", &second_path);
@@ -481,7 +481,8 @@ And here is some \
.filter_map(|i| match *i {
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
_ => None,
}).collect();
})
.collect();
let should_be: Vec<_> = vec![
String::from("Chapter 1"),
String::from("Hello World"),

View File

@@ -1,12 +1,11 @@
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use toml;
use super::MDBook;
use config::Config;
use errors::*;
use theme;
use crate::config::Config;
use crate::errors::*;
use crate::theme;
/// A helper for setting up a new book and its directory structure.
#[derive(Debug, Clone, PartialEq)]
@@ -173,15 +172,19 @@ impl BookBuilder {
let src_dir = self.root.join(&self.config.book.src);
let summary = src_dir.join("SUMMARY.md");
let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?;
writeln!(f, "# Summary")?;
writeln!(f, "")?;
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
let chapter_1 = src_dir.join("chapter_1.md");
let mut f = File::create(&chapter_1).chain_err(|| "Unable to create chapter_1.md")?;
writeln!(f, "# Chapter 1")?;
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")?;
writeln!(f, "# Summary")?;
writeln!(f)?;
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
let chapter_1 = src_dir.join("chapter_1.md");
let mut f = File::create(&chapter_1).chain_err(|| "Unable to create chapter_1.md")?;
writeln!(f, "# Chapter 1")?;
} else {
trace!("Existing summary found, no need to create stub files.");
}
Ok(())
}

View File

@@ -16,15 +16,18 @@ pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::string::ToString;
use tempfile::Builder as TempFileBuilder;
use toml::Value;
use errors::*;
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext};
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
use utils;
use crate::errors::*;
use crate::preprocess::{
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
};
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
use crate::utils;
use config::Config;
use crate::config::Config;
/// The object used to manage and build a book.
pub struct MDBook {
@@ -34,10 +37,10 @@ pub struct MDBook {
pub config: Config,
/// A representation of the book's contents in memory.
pub book: Book,
renderers: Vec<Box<Renderer>>,
renderers: Vec<Box<dyn Renderer>>,
/// List of pre-processors to be run on the book
preprocessors: Vec<Box<Preprocessor>>,
preprocessors: Vec<Box<dyn Preprocessor>>,
}
impl MDBook {
@@ -65,7 +68,7 @@ impl MDBook {
config.update_from_env();
if log_enabled!(::log::Level::Trace) {
if log_enabled!(log::Level::Trace) {
for line in format!("Config: {:#?}", config).lines() {
trace!("{}", line);
}
@@ -93,12 +96,34 @@ impl MDBook {
})
}
/// Load a book from its root directory using a custom config and a custom summary.
pub fn load_with_config_and_summary<P: Into<PathBuf>>(
book_root: P,
config: Config,
summary: Summary,
) -> Result<MDBook> {
let root = book_root.into();
let src_dir = root.join(&config.book.src);
let book = book::load_book_from_disk(&summary, &src_dir)?;
let renderers = determine_renderers(&config);
let preprocessors = determine_preprocessors(&config)?;
Ok(MDBook {
root,
config,
book,
renderers,
preprocessors,
})
}
/// Returns a flat depth-first iterator over the elements of the book,
/// it returns an [BookItem enum](bookitem.html):
/// `(section: String, bookitem: &BookItem)`
///
/// ```no_run
/// # extern crate mdbook;
/// # use mdbook::MDBook;
/// # use mdbook::book::BookItem;
/// # #[allow(unused_variables)]
@@ -120,7 +145,7 @@ impl MDBook {
/// // etc.
/// # }
/// ```
pub fn iter(&self) -> BookItems {
pub fn iter(&self) -> BookItems<'_> {
self.book.iter()
}
@@ -157,17 +182,18 @@ impl MDBook {
}
/// Run the entire build process for a particular `Renderer`.
fn execute_build_process(&self, renderer: &Renderer) -> Result<()> {
fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
let mut preprocessed_book = self.book.clone();
let preprocess_ctx = PreprocessorContext::new(self.root.clone(),
self.config.clone(),
renderer.name().to_string());
let preprocess_ctx = PreprocessorContext::new(
self.root.clone(),
self.config.clone(),
renderer.name().to_string(),
);
for preprocessor in &self.preprocessors {
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
debug!("Running the {} preprocessor.", preprocessor.name());
preprocessed_book =
preprocessor.run(&preprocess_ctx, preprocessed_book)?;
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
}
}
@@ -177,23 +203,9 @@ impl MDBook {
Ok(())
}
fn render(
&self,
preprocessed_book: &Book,
renderer: &Renderer,
) -> Result<()> {
fn render(&self, preprocessed_book: &Book, renderer: &dyn Renderer) -> Result<()> {
let name = renderer.name();
let build_dir = self.build_dir_for(name);
if build_dir.exists() {
debug!(
"Cleaning build dir for the \"{}\" renderer ({})",
name,
build_dir.display()
);
utils::fs::remove_dir_content(&build_dir)
.chain_err(|| "Unable to clear output directory")?;
}
let render_context = RenderContext::new(
self.root.clone(),
@@ -216,7 +228,7 @@ impl MDBook {
}
/// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book.
pub fn with_preprecessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
self.preprocessors.push(Box::new(preprocessor));
self
}
@@ -232,9 +244,8 @@ impl MDBook {
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
// FIXME: Is "test" the proper renderer name to use here?
let preprocess_context = PreprocessorContext::new(self.root.clone(),
self.config.clone(),
"test".to_string());
let preprocess_context =
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
// Index Preprocessor is disabled so that chapter paths continue to point to the
@@ -244,13 +255,12 @@ impl MDBook {
if let BookItem::Chapter(ref ch) = *item {
if !ch.path.as_os_str().is_empty() {
let path = self.source_dir().join(&ch.path);
let content = utils::fs::file_to_string(&path)?;
info!("Testing file: {:?}", path);
// write preprocessed file to tempdir
let path = temp_dir.path().join(&ch.path);
let mut tmpf = utils::fs::create_file(&path)?;
tmpf.write_all(content.as_bytes())?;
tmpf.write_all(ch.content.as_bytes())?;
let output = Command::new("rustdoc")
.arg(&path)
@@ -319,19 +329,19 @@ impl MDBook {
}
/// Look at the `Config` and try to figure out what renderers to use.
fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
let mut renderers: Vec<Box<Renderer>> = Vec::new();
fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
let mut renderers = Vec::new();
if let Some(output_table) = config.get("output").and_then(|o| o.as_table()) {
for (key, table) in output_table.iter() {
// the "html" backend has its own Renderer
if let Some(output_table) = config.get("output").and_then(Value::as_table) {
renderers.extend(output_table.iter().map(|(key, table)| {
if key == "html" {
renderers.push(Box::new(HtmlHandlebars::new()));
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
} else if key == "markdown" {
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
} else {
let renderer = interpret_custom_renderer(key, table);
renderers.push(renderer);
interpret_custom_renderer(key, table)
}
}
}));
}
// if we couldn't find anything, add the HTML renderer as a default
@@ -342,56 +352,59 @@ fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
renderers
}
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> {
vec![
Box::new(LinkPreprocessor::new()),
Box::new(IndexPreprocessor::new()),
]
}
fn is_default_preprocessor(pre: &Preprocessor) -> bool {
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
let name = pre.name();
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
}
/// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
let preprocessor_keys = config.get("preprocessor")
.and_then(|value| value.as_table())
.map(|table| table.keys());
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
let mut preprocessors = Vec::new();
let mut preprocessors = if config.build.use_default_preprocessors {
default_preprocessors()
} else {
Vec::new()
};
if config.build.use_default_preprocessors {
preprocessors.extend(default_preprocessors());
}
let preprocessor_keys = match preprocessor_keys {
Some(keys) => keys,
// If no preprocessor field is set, default to the LinkPreprocessor and
// IndexPreprocessor. This allows you to disable default preprocessors
// by setting "preprocess" to an empty list.
None => return Ok(preprocessors),
};
for key in preprocessor_keys {
match key.as_ref() {
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
_ => bail!("{:?} is not a recognised preprocessor", key),
if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
for key in preprocessor_table.keys() {
match key.as_ref() {
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
name => preprocessors.push(interpret_custom_preprocessor(
name,
&preprocessor_table[name],
)),
}
}
}
Ok(preprocessors)
}
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> {
let command = table
.get("command")
.and_then(Value::as_str)
.map(ToString::to_string)
.unwrap_or_else(|| format!("mdbook-{}", key));
Box::new(CmdPreprocessor::new(key.to_string(), command.to_string()))
}
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
// look for the `command` field, falling back to using the key
// prepended by "mdbook-"
let table_dot_command = table
.get("command")
.and_then(|c| c.as_str())
.map(|s| s.to_string());
.and_then(Value::as_str)
.map(ToString::to_string);
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
@@ -404,7 +417,11 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
///
/// The `build.use-default-preprocessors` config option can be used to ensure
/// default preprocessors always run if they support the renderer.
fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg: &Config) -> bool {
fn preprocessor_should_run(
preprocessor: &dyn Preprocessor,
renderer: &dyn Renderer,
cfg: &Config,
) -> bool {
// default preprocessors should be run by default (if supported)
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
return preprocessor.supports_renderer(renderer.name());
@@ -414,18 +431,19 @@ fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg
let renderer_name = renderer.name();
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
return explicit_renderers.into_iter()
.filter_map(|val| val.as_str())
return explicit_renderers
.iter()
.filter_map(Value::as_str)
.any(|name| name == renderer_name);
}
preprocessor.supports_renderer(renderer_name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use toml::value::{Table, Value};
#[test]
@@ -492,8 +510,8 @@ mod tests {
}
#[test]
fn config_complains_if_unimplemented_preprocessor() {
let cfg_str: &'static str = r#"
fn can_determine_third_party_preprocessors() {
let cfg_str = r#"
[book]
title = "Some Book"
@@ -509,14 +527,30 @@ mod tests {
// make sure the `preprocessor.random` table exists
assert!(cfg.get_preprocessor("random").is_some());
let got = determine_preprocessors(&cfg);
let got = determine_preprocessors(&cfg).unwrap();
assert!(got.is_err());
assert!(got.into_iter().any(|p| p.name() == "random"));
}
#[test]
fn preprocessors_can_provide_their_own_commands() {
let cfg_str = r#"
[preprocessor.random]
command = "python random.py"
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure the `preprocessor.random` table exists
let random = cfg.get_preprocessor("random").unwrap();
let random = interpret_custom_preprocessor("random", &Value::Table(random.clone()));
assert_eq!(random.cmd(), "python random.py");
}
#[test]
fn config_respects_preprocessor_selection() {
let cfg_str: &'static str = r#"
let cfg_str = r#"
[preprocessor.links]
renderers = ["html"]
"#;
@@ -524,11 +558,12 @@ mod tests {
let cfg = Config::from_str(cfg_str).unwrap();
// double-check that we can access preprocessor.links.renderers[0]
let html = cfg.get_preprocessor("links")
let html = cfg
.get_preprocessor("links")
.and_then(|links| links.get("renderers"))
.and_then(|renderers| renderers.as_array())
.and_then(Value::as_array)
.and_then(|renderers| renderers.get(0))
.and_then(|renderer| renderer.as_str())
.and_then(Value::as_str)
.unwrap();
assert_eq!(html, "html");
let html_renderer = HtmlHandlebars::default();

View File

@@ -1,4 +1,4 @@
use errors::*;
use crate::errors::*;
use memchr::{self, Memchr};
use pulldown_cmark::{self, Event, Tag};
use std::fmt::{self, Display, Formatter};
@@ -195,7 +195,7 @@ macro_rules! collect_events {
}
impl<'a> SummaryParser<'a> {
fn new(text: &str) -> SummaryParser {
fn new(text: &str) -> SummaryParser<'_> {
let pulldown_parser = pulldown_cmark::Parser::new(text);
SummaryParser {
@@ -259,7 +259,7 @@ impl<'a> SummaryParser<'a> {
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
}
}
Some(Event::Start(Tag::Link(href, _))) => {
Some(Event::Start(Tag::Link(_type, href, _title))) => {
let link = self.parse_link(href.to_string())?;
items.push(SummaryItem::Link(link));
}
@@ -280,7 +280,7 @@ impl<'a> SummaryParser<'a> {
Err(self.parse_error("You can't have an empty link."))
} else {
Ok(Link {
name: name,
name,
location: PathBuf::from(href.to_string()),
number: None,
nested_items: Vec::new(),
@@ -292,6 +292,7 @@ impl<'a> SummaryParser<'a> {
/// already been consumed by a previous parser.
fn parse_numbered(&mut self) -> 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
@@ -308,7 +309,8 @@ impl<'a> SummaryParser<'a> {
// if we've resumed after something like a rule the root sections
// will be numbered from 1. We need to manually go back and update
// them
update_section_numbers(&mut bunch_of_items, 0, items.len() as u32);
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() {
@@ -395,7 +397,7 @@ impl<'a> SummaryParser<'a> {
loop {
match self.next_event() {
Some(Event::Start(Tag::Paragraph)) => continue,
Some(Event::Start(Tag::Link(href, _))) => {
Some(Event::Start(Tag::Link(_type, href, _title))) => {
let mut link = self.parse_link(href.to_string())?;
let mut number = parent.clone();
@@ -469,13 +471,14 @@ fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
/// Removes the styling from a list of Markdown events and returns just the
/// plain text.
fn stringify_events(events: Vec<Event>) -> String {
fn stringify_events(events: Vec<Event<'_>>) -> String {
events
.into_iter()
.filter_map(|t| match t {
Event::Text(text) => Some(text.into_owned()),
Event::Text(text) | Event::Code(text) => Some(text.into_string()),
_ => None,
}).collect()
})
.collect()
}
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
@@ -484,7 +487,7 @@ fn stringify_events(events: Vec<Event>) -> String {
pub struct SectionNumber(pub Vec<u32>);
impl Display for SectionNumber {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
write!(f, "0")
} else {
@@ -626,7 +629,7 @@ mod tests {
let _ = parser.stream.next(); // skip past start of paragraph
let href = match parser.stream.next() {
Some(Event::Start(Tag::Link(href, _))) => href.to_string(),
Some(Event::Start(Tag::Link(_type, href, _title))) => href.to_string(),
other => panic!("Unreachable, {:?}", other),
};
@@ -724,4 +727,41 @@ mod tests {
let got = parser.parse_numbered();
assert!(got.is_err());
}
/// Regression test for https://github.com/rust-lang-nursery/mdBook/issues/779
/// Ensure section numbers are correctly incremented after a horizontal separator.
#[test]
fn keep_numbering_after_separator() {
let src =
"- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: 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"),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
SummaryItem::Separator,
SummaryItem::Link(Link {
name: String::from("Third"),
location: 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();
assert_eq!(got, should_be);
}
}

View File

@@ -1,7 +1,7 @@
use crate::{get_book_dir, open};
use clap::{App, ArgMatches, SubCommand};
use mdbook::errors::Result;
use mdbook::MDBook;
use {get_book_dir, open};
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
@@ -9,11 +9,14 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Builds a book from its markdown files")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
).arg_from_usage(
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
).arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
)
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
}
// Build command implementation

View File

@@ -1,5 +1,5 @@
use crate::get_book_dir;
use clap::{App, ArgMatches, SubCommand};
use get_book_dir;
use mdbook::errors::*;
use mdbook::MDBook;
use std::fs;
@@ -10,15 +10,18 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Deletes a built book")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
).arg_from_usage(
Relative paths are interpreted relative to the book's root directory.{n}\
Running this command deletes this directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
)
}
// Clean command implementation
pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
let book_dir = get_book_dir(args);
let book = MDBook::load(&book_dir)?;
@@ -26,7 +29,10 @@ pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
Some(dest_dir) => dest_dir.into(),
None => book.root.join(&book.config.build.build_dir),
};
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
if dir_to_remove.exists() {
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
}
Ok(())
}

View File

@@ -1,5 +1,5 @@
use crate::get_book_dir;
use clap::{App, ArgMatches, SubCommand};
use get_book_dir;
use mdbook::config;
use mdbook::errors::Result;
use mdbook::MDBook;
@@ -12,8 +12,10 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("init")
.about("Creates the boilerplate structure and files for a new book")
// the {n} denotes a newline which will properly aligned in all help messages
.arg_from_usage("[dir] 'Directory to create the book in{n}\
(Defaults to the Current Directory when omitted)'")
.arg_from_usage(
"[dir] 'Directory to create the book in{n}\
(Defaults to the Current Directory when omitted)'",
)
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
.arg_from_usage("--force 'Skips confirmation prompts'")
}

View File

@@ -1,18 +1,11 @@
extern crate iron;
extern crate staticfile;
extern crate ws;
use self::iron::{
status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set,
};
#[cfg(feature = "watch")]
use super::watch;
use crate::{get_book_dir, open};
use clap::{App, Arg, ArgMatches, SubCommand};
use iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set};
use mdbook::errors::*;
use mdbook::utils;
use mdbook::MDBook;
use std;
use {get_book_dir, open};
struct ErrorRecover;
@@ -22,7 +15,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
@@ -75,7 +69,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let port = args.value_of("port").unwrap();
let ws_port = args.value_of("websocket-port").unwrap();
let hostname = args.value_of("hostname").unwrap();
let public_address = args.value_of("websocket-address").unwrap_or(hostname);
let public_address = args.value_of("websocket-hostname").unwrap_or(hostname);
let open_browser = args.is_present("open");
let address = format!("{}:{}", hostname, port);
@@ -114,8 +108,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
}
#[cfg(feature = "watch")]
watch::trigger_on_change(&mut book, move |path, book_dir| {
info!("File changed: {:?}", path);
watch::trigger_on_change(&book, move |paths, book_dir| {
info!("Files changed: {:?}", paths);
info!("Building book...");
// FIXME: This area is really ugly because we need to re-set livereload :(
@@ -125,7 +119,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
b.config
.set("output.html.livereload-url", &livereload_url)?;
Ok(b)
}).and_then(|b| b.build());
})
.and_then(|b| b.build());
if let Err(e) = result {
error!("Unable to load the book");

View File

@@ -1,5 +1,5 @@
use crate::get_book_dir;
use clap::{App, Arg, ArgMatches, SubCommand};
use get_book_dir;
use mdbook::errors::Result;
use mdbook::MDBook;
@@ -9,7 +9,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Tests that a book's Rust code samples compile")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
@@ -30,7 +31,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
pub fn execute(args: &ArgMatches) -> Result<()> {
let library_paths: Vec<&str> = args
.values_of("library-path")
.map(|v| v.collect())
.map(std::iter::Iterator::collect)
.unwrap_or_default();
let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?;

View File

@@ -1,14 +1,13 @@
extern crate notify;
use self::notify::Watcher;
use crate::{get_book_dir, open};
use clap::{App, ArgMatches, SubCommand};
use mdbook::errors::Result;
use mdbook::utils;
use mdbook::MDBook;
use std::path::Path;
use notify::Watcher;
use std::path::{Path, PathBuf};
use std::sync::mpsc::channel;
use std::thread::sleep;
use std::time::Duration;
use {get_book_dir, open};
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
@@ -16,11 +15,14 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Watches a book's files and rebuilds it on changes")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
).arg_from_usage(
Relative paths are interpreted relative to the book's root directory.{n}\
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
).arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
)
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
}
// Watch command implementation
@@ -33,8 +35,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
open(book.build_dir_for("html").join("index.html"));
}
trigger_on_change(&book, |path, book_dir| {
info!("File changed: {:?}\nBuilding book...\n", path);
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());
if let Err(e) = result {
@@ -46,13 +48,60 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
Ok(())
}
fn remove_ignored_files(book_root: &PathBuf, paths: &[PathBuf]) -> Vec<PathBuf> {
if paths.is_empty() {
return vec![];
}
match find_gitignore(book_root) {
Some(gitignore_path) => {
match gitignore::File::new(gitignore_path.as_path()) {
Ok(exclusion_checker) => filter_ignored_files(exclusion_checker, paths),
Err(_) => {
// We're unable to read the .gitignore file, so we'll silently allow everything.
// Please see discussion: https://github.com/rust-lang-nursery/mdBook/pull/1051
paths.iter().map(|path| path.to_path_buf()).collect()
}
}
}
None => {
// There is no .gitignore file.
paths.iter().map(|path| path.to_path_buf()).collect()
}
}
}
fn find_gitignore(book_root: &PathBuf) -> Option<PathBuf> {
book_root
.ancestors()
.map(|p| p.join(".gitignore"))
.find(|p| p.exists())
}
fn filter_ignored_files(exclusion_checker: gitignore::File, paths: &[PathBuf]) -> Vec<PathBuf> {
paths
.iter()
.filter(|path| match exclusion_checker.is_excluded(path) {
Ok(exclude) => !exclude,
Err(error) => {
warn!(
"Unable to determine if {:?} is excluded: {:?}. Including it.",
&path, error
);
true
}
})
.map(|path| path.to_path_buf())
.collect()
}
/// Calls the closure when a book source file is changed, blocking indefinitely.
pub fn trigger_on_change<F>(book: &MDBook, closure: F)
where
F: Fn(&Path, &Path),
F: Fn(Vec<PathBuf>, &Path),
{
use self::notify::DebouncedEvent::*;
use self::notify::RecursiveMode::*;
use notify::DebouncedEvent::*;
use notify::RecursiveMode::*;
// Create a channel to receive the events.
let (tx, rx) = channel();
@@ -61,14 +110,14 @@ where
Ok(w) => w,
Err(e) => {
error!("Error while trying to watch the files:\n\n\t{:?}", e);
::std::process::exit(1)
std::process::exit(1)
}
};
// Add the source directory to the watcher
if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
::std::process::exit(1);
std::process::exit(1);
};
let _ = watcher.watch(book.theme_dir(), Recursive);
@@ -78,13 +127,28 @@ where
info!("Listening for changes...");
for event in rx.iter() {
debug!("Received filesystem event: {:?}", event);
match event {
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
closure(&path, &book.root);
}
_ => {}
loop {
let first_event = rx.recv().unwrap();
sleep(Duration::from_millis(50));
let other_events = rx.try_iter();
let all_events = std::iter::once(first_event).chain(other_events);
let paths = all_events
.filter_map(|event| {
debug!("Received filesystem event: {:?}", event);
match event {
Create(path) | Write(path) | Remove(path) | Rename(_, path) => Some(path),
_ => None,
}
})
.collect::<Vec<_>>();
let paths = remove_ignored_files(&book.root, &paths[..]);
if !paths.is_empty() {
closure(paths, &book.root);
}
}
}

View File

@@ -3,16 +3,15 @@
//! The main entrypoint of the `config` module is the `Config` struct. This acts
//! essentially as a bag of configuration information, with a couple
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
//! for arbitrary data which is exposed to plugins and alternate backends.
//! for arbitrary data which is exposed to plugins and alternative backends.
//!
//!
//! # Examples
//!
//! ```rust
//! # extern crate mdbook;
//! # use mdbook::errors::*;
//! # extern crate toml;
//! use std::path::PathBuf;
//! use std::str::FromStr;
//! use mdbook::Config;
//! use toml::Value;
//!
@@ -41,8 +40,8 @@
//! cfg.set("output.html.theme", "./themes");
//!
//! // then load it again, automatically deserializing to a `PathBuf`.
//! let got: PathBuf = cfg.get_deserialized("output.html.theme")?;
//! assert_eq!(got, PathBuf::from("./themes"));
//! let got: Option<PathBuf> = cfg.get_deserialized_opt("output.html.theme")?;
//! assert_eq!(got, Some(PathBuf::from("./themes")));
//! # Ok(())
//! # }
//! # fn main() { run().unwrap() }
@@ -51,18 +50,19 @@
#![deny(missing_docs)]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json;
use std::env;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use toml::value::Table;
use toml::{self, Value};
use toml_query::delete::TomlValueDeleteExt;
use toml_query::insert::TomlValueInsertExt;
use toml_query::read::TomlValueReadExt;
use errors::*;
use crate::errors::*;
use crate::utils;
/// The overall configuration object for MDBook, essentially an in-memory
/// representation of `book.toml`.
@@ -75,12 +75,16 @@ pub struct Config {
rest: Value,
}
impl Config {
impl FromStr for Config {
type Err = Error;
/// Load a `Config` from some string.
pub fn from_str(src: &str) -> Result<Config> {
fn from_str(src: &str) -> Result<Self> {
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
}
}
impl Config {
/// Load the configuration file from disk.
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
let mut buffer = String::new();
@@ -128,10 +132,8 @@ impl Config {
pub fn update_from_env(&mut self) {
debug!("Updating the config from environment variables");
let overrides = env::vars().filter_map(|(key, value)| match parse_env(&key) {
Some(index) => Some((index, value)),
None => None,
});
let overrides =
env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value)));
for (key, value) in overrides {
trace!("{} => {}", key, value);
@@ -148,14 +150,11 @@ impl Config {
/// `output.html.playpen` will fetch the "playpen" out of the html output
/// table).
pub fn get(&self, key: &str) -> Option<&Value> {
match self.rest.read(key) {
Ok(inner) => inner,
Err(_) => None,
}
self.rest.read(key).unwrap_or(None)
}
/// Fetch a value from the `Config` so you can mutate it.
pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
match self.rest.read_mut(key) {
Ok(inner) => inner,
Err(_) => None,
@@ -170,22 +169,41 @@ impl Config {
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
#[doc(hidden)]
pub fn html_config(&self) -> Option<HtmlConfig> {
self.get_deserialized("output.html").ok()
match self.get_deserialized_opt("output.html") {
Ok(Some(config)) => Some(config),
Ok(None) => None,
Err(e) => {
utils::log_backtrace(&e.chain_err(|| "Parsing configuration [output.html]"));
None
}
}
}
/// Deprecated, use get_deserialized_opt instead.
#[deprecated = "use get_deserialized_opt instead"]
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
let name = name.as_ref();
match self.get_deserialized_opt(name)? {
Some(value) => Ok(value),
None => bail!("Key not found, {:?}", name),
}
}
/// Convenience function to fetch a value from the config and deserialize it
/// into some arbitrary type.
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
pub fn get_deserialized_opt<'de, T: Deserialize<'de>, S: AsRef<str>>(
&self,
name: S,
) -> Result<Option<T>> {
let name = name.as_ref();
if let Some(value) = self.get(name) {
value
.clone()
.try_into()
.chain_err(|| "Couldn't deserialize the value")
} else {
bail!("Key not found, {:?}", name)
}
self.get(name)
.map(|value| {
value
.clone()
.try_into()
.chain_err(|| "Couldn't deserialize the value")
})
.transpose()
}
/// Set a config key, clobbering any existing values along the way.
@@ -203,7 +221,9 @@ impl Config {
} else if index.starts_with("build.") {
self.build.update_value(&index[6..], value);
} else {
self.rest.insert(index, value)?;
self.rest
.insert(index, value)
.map_err(ErrorKind::TomlQueryError)?;
}
Ok(())
@@ -212,13 +232,13 @@ impl Config {
/// Get the table associated with a particular renderer.
pub fn get_renderer<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
let key = format!("output.{}", index.as_ref());
self.get(&key).and_then(|v| v.as_table())
self.get(&key).and_then(Value::as_table)
}
/// Get the table associated with a particular preprocessor.
pub fn get_preprocessor<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
let key = format!("preprocessor.{}", index.as_ref());
self.get(&key).and_then(|v| v.as_table())
self.get(&key).and_then(Value::as_table)
}
fn from_legacy(mut table: Value) -> Config {
@@ -265,7 +285,7 @@ impl Default for Config {
}
}
impl<'de> Deserialize<'de> for Config {
fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
let raw = Value::deserialize(de)?;
if is_legacy_format(&raw) {
@@ -301,15 +321,15 @@ impl<'de> Deserialize<'de> for Config {
.unwrap_or_default();
Ok(Config {
book: book,
build: build,
book,
build,
rest: Value::Table(table),
})
}
}
impl Serialize for Config {
fn serialize<S: Serializer>(&self, s: S) -> ::std::result::Result<S::Ok, S::Error> {
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::Error;
let mut table = self.rest.clone();
@@ -371,6 +391,8 @@ pub struct BookConfig {
pub src: PathBuf,
/// Does this book support more than one language?
pub multilingual: bool,
/// The main language of the book.
pub language: Option<String>,
}
impl Default for BookConfig {
@@ -381,6 +403,7 @@ impl Default for BookConfig {
description: None,
src: PathBuf::from("src"),
multilingual: false,
language: Some(String::from("en")),
}
}
}
@@ -415,6 +438,11 @@ impl Default for BuildConfig {
pub struct HtmlConfig {
/// The theme directory, if specified.
pub theme: Option<PathBuf>,
/// The default theme to use, defaults to 'light'
pub default_theme: Option<String>,
/// The theme to use if the browser requests the dark version of the site.
/// Defaults to the same as 'default_theme'
pub preferred_dark_theme: Option<String>,
/// Use "smart quotes" instead of the usual `"` character.
pub curly_quotes: bool,
/// Should mathjax be enabled?
@@ -426,8 +454,19 @@ pub struct HtmlConfig {
/// Additional JS scripts to include at the bottom of the rendered page's
/// `<body>`.
pub additional_js: Vec<PathBuf>,
/// Fold settings.
pub fold: Fold,
/// Playpen settings.
pub playpen: Playpen,
/// Don't render section labels.
pub no_section_label: bool,
/// Search settings. If `None`, the default will be used.
pub search: Option<Search>,
/// Git repository url. If `None`, the git button will not be shown.
pub git_repository_url: Option<String>,
/// FontAwesome icon class to use for the Git repository link.
/// Defaults to `fa-github` if `None`.
pub git_repository_icon: Option<String>,
/// 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
@@ -436,10 +475,6 @@ pub struct HtmlConfig {
/// This config item *should not be edited* by the end user.
#[doc(hidden)]
pub livereload_url: Option<String>,
/// Should section labels be rendered?
pub no_section_label: bool,
/// Search settings. If `None`, the default will be used.
pub search: Option<Search>,
}
impl HtmlConfig {
@@ -453,22 +488,40 @@ impl HtmlConfig {
}
}
/// Configuration for how to fold chapters of sidebar.
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Fold {
/// When off, all folds are open. Default: `false`.
pub enable: bool,
/// The higher the more folded regions are open. When level is 0, all folds
/// are closed.
/// Default: `0`.
pub level: u8,
}
/// Configuration for tweaking how the the HTML renderer handles the playpen.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Playpen {
/// Should playpen snippets be editable? Default: `false`.
pub editable: bool,
/// Display the copy button. Default: `true`.
pub copyable: bool,
/// Copy JavaScript files for the editor to the output directory?
/// Default: `true`.
pub copy_js: bool,
/// Display line numbers on playpen snippets. Default: `false`.
pub line_numbers: bool,
}
impl Default for Playpen {
fn default() -> Playpen {
Playpen {
editable: false,
copyable: true,
copy_js: true,
line_numbers: false,
}
}
}
@@ -484,7 +537,7 @@ pub struct Search {
/// The number of words used for a search result teaser. Default: `30`.
pub teaser_word_count: u32,
/// Define the logical link between multiple search words.
/// If true, all search words must appear in each result. Default: `true`.
/// If true, all search words must appear in each result. Default: `false`.
pub use_boolean_and: bool,
/// Boost factor for the search result score if a search word appears in the header.
/// Default: `2`.
@@ -533,12 +586,10 @@ trait Updateable<'de>: Serialize + Deserialize<'de> {
fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
let mut raw = Value::try_from(&self).expect("unreachable");
{
if let Ok(value) = Value::try_from(value) {
let _ = raw.insert(key, value);
} else {
return;
}
if let Ok(value) = Value::try_from(value) {
let _ = raw.insert(key, value);
} else {
return;
}
if let Ok(updated) = raw.try_into() {
@@ -553,13 +604,14 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
mod tests {
use super::*;
const COMPLEX_CONFIG: &'static str = r#"
const COMPLEX_CONFIG: &str = r#"
[book]
title = "Some Book"
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
description = "A completely useless book"
multilingual = true
src = "source"
language = "ja"
[build]
build-dir = "outputs"
@@ -568,17 +620,20 @@ mod tests {
[output.html]
theme = "./themedir"
default-theme = "rust"
curly-quotes = true
google-analytics = "123456"
additional-css = ["./foo/bar/baz.css"]
git-repository-url = "https://foo.com/"
git-repository-icon = "fa-code-fork"
[output.html.playpen]
editable = true
editor = "ace"
[preprocess.first]
[preprocessor.first]
[preprocess.second]
[preprocessor.second]
"#;
#[test]
@@ -591,7 +646,7 @@ mod tests {
description: Some(String::from("A completely useless book")),
multilingual: true,
src: PathBuf::from("source"),
..Default::default()
language: Some(String::from("ja")),
};
let build_should_be = BuildConfig {
build_dir: PathBuf::from("outputs"),
@@ -600,14 +655,19 @@ mod tests {
};
let playpen_should_be = Playpen {
editable: true,
copyable: true,
copy_js: true,
line_numbers: false,
};
let html_should_be = HtmlConfig {
curly_quotes: true,
google_analytics: Some(String::from("123456")),
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
theme: Some(PathBuf::from("./themedir")),
default_theme: Some(String::from("rust")),
playpen: playpen_should_be,
git_repository_url: Some(String::from("https://foo.com/")),
git_repository_icon: Some(String::from("fa-code-fork")),
..Default::default()
};
@@ -641,14 +701,17 @@ mod tests {
};
let cfg = Config::from_str(src).unwrap();
let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
let got: RandomOutput = cfg.get_deserialized_opt("output.random").unwrap().unwrap();
assert_eq!(got, should_be);
let baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
let got_baz: Vec<bool> = cfg
.get_deserialized_opt("output.random.baz")
.unwrap()
.unwrap();
let baz_should_be = vec![true, true, false];
assert_eq!(baz, baz_should_be);
assert_eq!(got_baz, baz_should_be);
}
#[test]
@@ -725,7 +788,7 @@ mod tests {
assert!(cfg.get(key).is_none());
cfg.set(key, value).unwrap();
let got: String = cfg.get_deserialized(key).unwrap();
let got: String = cfg.get_deserialized_opt(key).unwrap().unwrap();
assert_eq!(got, value);
}
@@ -740,7 +803,7 @@ mod tests {
for (src, should_be) in inputs {
let got = parse_env(src);
let should_be = should_be.map(|s| s.to_string());
let should_be = should_be.map(ToString::to_string);
assert_eq!(got, should_be);
}
@@ -766,10 +829,14 @@ mod tests {
cfg.update_from_env();
assert_eq!(cfg.get_deserialized::<String, _>(key).unwrap(), value);
assert_eq!(
cfg.get_deserialized_opt::<String, _>(key).unwrap().unwrap(),
value
);
}
#[test]
#[allow(clippy::approx_constant)]
fn update_config_using_env_var_and_complex_value() {
let mut cfg = Config::default();
let key = "foo-bar.baz";
@@ -784,7 +851,9 @@ mod tests {
cfg.update_from_env();
assert_eq!(
cfg.get_deserialized::<serde_json::Value, _>(key).unwrap(),
cfg.get_deserialized_opt::<serde_json::Value, _>(key)
.unwrap()
.unwrap(),
value
);
}

View File

@@ -81,27 +81,18 @@
//! [`Config`]: config/struct.Config.html
#![deny(missing_docs)]
#![deny(rust_2018_idioms)]
#[macro_use]
extern crate error_chain;
extern crate handlebars;
extern crate itertools;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate memchr;
extern crate pulldown_cmark;
extern crate regex;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
extern crate shlex;
extern crate tempfile;
extern crate toml;
extern crate toml_query;
#[cfg(test)]
#[macro_use]
@@ -120,31 +111,27 @@ pub mod utils;
/// compatibility checks.
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
pub use book::BookItem;
pub use book::MDBook;
pub use config::Config;
pub use renderer::Renderer;
pub use crate::book::BookItem;
pub use crate::book::MDBook;
pub use crate::config::Config;
pub use crate::renderer::Renderer;
/// The error types used through out this crate.
pub mod errors {
use std::path::PathBuf;
error_chain!{
error_chain! {
foreign_links {
Io(::std::io::Error) #[doc = "A wrapper around `std::io::Error`"];
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
SerdeJson(::serde_json::Error) #[doc = "JSON conversion failed"];
}
links {
TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind) #[doc = "A TomlQuery error"];
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) {
Subprocess(message: String, output: std::process::Output) {
description("A subprocess failed")
display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
}
@@ -160,12 +147,18 @@ pub mod errors {
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 {
impl From<handlebars::TemplateError> for Error {
fn from(e: handlebars::TemplateError) -> Error {
From::from(Box::new(e))
}
}

View File

@@ -1,12 +1,7 @@
extern crate chrono;
#[macro_use]
extern crate clap;
extern crate env_logger;
extern crate error_chain;
#[macro_use]
extern crate log;
extern crate mdbook;
extern crate open;
use chrono::Local;
use clap::{App, AppSettings, ArgMatches};
@@ -20,19 +15,19 @@ use std::path::{Path, PathBuf};
mod cmd;
const NAME: &'static str = "mdBook";
const VERSION: &'static str = concat!("v", crate_version!());
const VERSION: &str = concat!("v", crate_version!());
fn main() {
init_logger();
// Create a list of valid arguments and sub-commands
let app = App::new(NAME)
.about("Creates a book from markdown files")
let app = App::new(crate_name!())
.about(crate_description!())
.author("Mathieu David <mathieudavid@mathieudavid.org>")
.version(VERSION)
.setting(AppSettings::GlobalVersion)
.setting(AppSettings::ArgRequiredElseHelp)
.setting(AppSettings::ColoredHelp)
.after_help(
"For more information about a specific command, try `mdbook <command> --help`\n\
The source code for mdBook is available at: https://github.com/rust-lang-nursery/mdBook",
@@ -63,7 +58,7 @@ fn main() {
if let Err(e) = res {
utils::log_backtrace(&e);
::std::process::exit(101);
std::process::exit(101);
}
}
@@ -82,7 +77,7 @@ fn init_logger() {
});
if let Ok(var) = env::var("RUST_LOG") {
builder.parse(&var);
builder.parse_filters(&var);
} else {
// if no RUST_LOG provided, default to logging at the Info level
builder.filter(None, LevelFilter::Info);

196
src/preprocess/cmd.rs Normal file
View File

@@ -0,0 +1,196 @@
use super::{Preprocessor, PreprocessorContext};
use crate::book::Book;
use crate::errors::*;
use shlex::Shlex;
use std::io::{self, Read, Write};
use std::process::{Child, Command, Stdio};
/// A custom preprocessor which will shell out to a 3rd-party program.
///
/// # Preprocessing Protocol
///
/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
/// execute the shell command `$cmd supports $renderer`. If the renderer is
/// supported, custom preprocessors should exit with a exit code of `0`,
/// any other exit code be considered as unsupported.
///
/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
/// should then "return" a processed book by printing it to `stdout` as JSON.
/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
/// to parse the input provided by `mdbook`.
///
/// Exiting with a non-zero exit code while preprocessing is considered an
/// error. `stderr` is passed directly through to the user, so it can be used
/// for logging or emitting warnings if desired.
///
/// # Examples
///
/// An example preprocessor is available in this project's `examples/`
/// directory.
#[derive(Debug, Clone, PartialEq)]
pub struct CmdPreprocessor {
name: String,
cmd: String,
}
impl CmdPreprocessor {
/// Create a new `CmdPreprocessor`.
pub fn new(name: String, cmd: String) -> CmdPreprocessor {
CmdPreprocessor { name, cmd }
}
/// A convenience function custom preprocessors can use to parse the input
/// written to `stdin` by a `CmdRenderer`.
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
serde_json::from_reader(reader).chain_err(|| "Unable to parse the input")
}
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
let stdin = child.stdin.take().expect("Child has stdin");
if let Err(e) = self.write_input(stdin, &book, &ctx) {
// Looks like the backend hung up before we could finish
// sending it the render context. Log the error and keep going
warn!("Error writing the RenderContext to the backend, {}", e);
}
}
fn write_input<W: Write>(
&self,
writer: W,
book: &Book,
ctx: &PreprocessorContext,
) -> Result<()> {
serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
}
/// The command this `Preprocessor` will invoke.
pub fn cmd(&self) -> &str {
&self.cmd
}
fn command(&self) -> Result<Command> {
let mut words = Shlex::new(&self.cmd);
let executable = match words.next() {
Some(e) => e,
None => bail!("Command string was empty"),
};
let mut cmd = Command::new(executable);
for arg in words {
cmd.arg(arg);
}
Ok(cmd)
}
}
impl Preprocessor for CmdPreprocessor {
fn name(&self) -> &str {
&self.name
}
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
let mut cmd = self.command()?;
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.chain_err(|| {
format!(
"Unable to start the \"{}\" preprocessor. Is it installed?",
self.name()
)
})?;
self.write_input_to_child(&mut child, &book, ctx);
let output = child
.wait_with_output()
.chain_err(|| "Error waiting for the preprocessor to complete")?;
trace!("{} exited with output: {:?}", self.cmd, output);
ensure!(
output.status.success(),
"The preprocessor exited unsuccessfully"
);
serde_json::from_slice(&output.stdout).chain_err(|| "Unable to parse the preprocessed book")
}
fn supports_renderer(&self, renderer: &str) -> bool {
debug!(
"Checking if the \"{}\" preprocessor supports \"{}\"",
self.name(),
renderer
);
let mut cmd = match self.command() {
Ok(c) => c,
Err(e) => {
warn!(
"Unable to create the command for the \"{}\" preprocessor, {}",
self.name(),
e
);
return false;
}
};
let outcome = cmd
.arg("supports")
.arg(renderer)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map(|status| status.code() == Some(0));
if let Err(ref e) = outcome {
if e.kind() == io::ErrorKind::NotFound {
warn!(
"The command wasn't found, is the \"{}\" preprocessor installed?",
self.name
);
warn!("\tCommand: {}", self.cmd);
}
}
outcome.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MDBook;
use std::path::Path;
fn book_example() -> MDBook {
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
MDBook::load(example).unwrap()
}
#[test]
fn round_trip_write_and_parse_input() {
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
let md = book_example();
let ctx = PreprocessorContext::new(
md.root.clone(),
md.config.clone(),
"some-renderer".to_string(),
);
let mut buffer = Vec::new();
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
assert_eq!(got_book, md.book);
assert_eq!(got_ctx, ctx);
}
}

View File

@@ -1,13 +1,14 @@
use regex::Regex;
use std::path::Path;
use errors::*;
use crate::errors::*;
use super::{Preprocessor, PreprocessorContext};
use book::{Book, BookItem};
use crate::book::{Book, BookItem};
/// A preprocessor for converting file name `README.md` to `index.md` since
/// `README.md` is the de facto index file in a markdown-based documentation.
/// `README.md` is the de facto index file in markdown-based documentation.
#[derive(Default)]
pub struct IndexPreprocessor;
impl IndexPreprocessor {
@@ -45,7 +46,10 @@ impl Preprocessor for IndexPreprocessor {
fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
let file_name = readme_path.as_ref().file_name().unwrap_or_default();
let parent_dir = index_path.as_ref().parent().unwrap_or(index_path.as_ref());
let parent_dir = index_path
.as_ref()
.parent()
.unwrap_or_else(|| index_path.as_ref());
warn!(
"It seems that there are both {:?} and index.md under \"{}\".",
file_name,
@@ -67,7 +71,7 @@ fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
RE.is_match(
path.as_ref()
.file_stem()
.and_then(|s| s.to_str())
.and_then(std::ffi::OsStr::to_str)
.unwrap_or_default(),
)
}

View File

@@ -1,18 +1,29 @@
use errors::*;
use crate::errors::*;
use crate::utils::{
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
take_rustdoc_include_lines,
};
use regex::{CaptureMatches, Captures, Regex};
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
use std::fs;
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
use std::path::{Path, PathBuf};
use utils::fs::file_to_string;
use utils::take_lines;
use super::{Preprocessor, PreprocessorContext};
use book::{Book, BookItem};
use crate::book::{Book, BookItem};
const ESCAPE_CHAR: char = '\\';
const MAX_LINK_NESTED_DEPTH: usize = 10;
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
/// helpers in a chapter.
/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
///
/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
///. lines, or only between the specified anchors.
/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
/// This hides the lines from initial display but shows them when the reader expands the code
/// block and provides them to Rustdoc for testing.
/// - `{{# playpen}}` - Insert runnable Rust files
#[derive(Default)]
pub struct LinkPreprocessor;
impl LinkPreprocessor {
@@ -62,13 +73,13 @@ where
let mut previous_end_index = 0;
let mut replaced = String::new();
for playpen in find_links(s) {
replaced.push_str(&s[previous_end_index..playpen.start_index]);
for link in find_links(s) {
replaced.push_str(&s[previous_end_index..link.start_index]);
match playpen.render_with_path(&path) {
match link.render_with_path(&path) {
Ok(new_content) => {
if depth < MAX_LINK_NESTED_DEPTH {
if let Some(rel_path) = playpen.link.relative_path(path) {
if let Some(rel_path) = link.link_type.relative_path(path) {
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
} else {
replaced.push_str(&new_content);
@@ -79,13 +90,17 @@ where
source.display()
);
}
previous_end_index = playpen.end_index;
previous_end_index = link.end_index;
}
Err(e) => {
error!("Error updating \"{}\", {}", playpen.link_text, e);
error!("Error updating \"{}\", {}", link.link_text, e);
for cause in e.iter().skip(1) {
warn!("Caused By: {}", cause);
}
// This should make sure we include the raw `{{# ... }}` snippet
// in the page content if there are any errors.
previous_end_index = playpen.start_index;
previous_end_index = link.start_index;
}
}
}
@@ -97,11 +112,68 @@ where
#[derive(PartialEq, Debug, Clone)]
enum LinkType<'a> {
Escaped,
IncludeRange(PathBuf, Range<usize>),
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
IncludeRangeTo(PathBuf, RangeTo<usize>),
IncludeRangeFull(PathBuf, RangeFull),
Include(PathBuf, RangeOrAnchor),
Playpen(PathBuf, Vec<&'a str>),
RustdocInclude(PathBuf, RangeOrAnchor),
}
#[derive(PartialEq, Debug, Clone)]
enum RangeOrAnchor {
Range(LineRange),
Anchor(String),
}
// A range of lines specified with some include directive.
#[derive(PartialEq, Debug, Clone)]
enum LineRange {
Range(Range<usize>),
RangeFrom(RangeFrom<usize>),
RangeTo(RangeTo<usize>),
RangeFull(RangeFull),
}
impl RangeBounds<usize> for LineRange {
fn start_bound(&self) -> Bound<&usize> {
match self {
LineRange::Range(r) => r.start_bound(),
LineRange::RangeFrom(r) => r.start_bound(),
LineRange::RangeTo(r) => r.start_bound(),
LineRange::RangeFull(r) => r.start_bound(),
}
}
fn end_bound(&self) -> Bound<&usize> {
match self {
LineRange::Range(r) => r.end_bound(),
LineRange::RangeFrom(r) => r.end_bound(),
LineRange::RangeTo(r) => r.end_bound(),
LineRange::RangeFull(r) => r.end_bound(),
}
}
}
impl From<Range<usize>> for LineRange {
fn from(r: Range<usize>) -> LineRange {
LineRange::Range(r)
}
}
impl From<RangeFrom<usize>> for LineRange {
fn from(r: RangeFrom<usize>) -> LineRange {
LineRange::RangeFrom(r)
}
}
impl From<RangeTo<usize>> for LineRange {
fn from(r: RangeTo<usize>) -> LineRange {
LineRange::RangeTo(r)
}
}
impl From<RangeFull> for LineRange {
fn from(r: RangeFull) -> LineRange {
LineRange::RangeFull(r)
}
}
impl<'a> LinkType<'a> {
@@ -109,11 +181,9 @@ impl<'a> LinkType<'a> {
let base = base.as_ref();
match self {
LinkType::Escaped => None,
LinkType::IncludeRange(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
}
}
}
@@ -125,50 +195,59 @@ fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
.to_path_buf()
}
fn parse_include_path(path: &str) -> LinkType<'static> {
let mut parts = path.split(':');
let path = parts.next().unwrap().into();
// subtract 1 since line numbers usually begin with 1
let start = parts
.next()
.and_then(|s| s.parse::<usize>().ok())
.map(|val| val.saturating_sub(1));
fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor {
let mut parts = parts.unwrap_or("").splitn(3, ':').fuse();
let next_element = parts.next();
let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
// subtract 1 since line numbers usually begin with 1
Some(value.saturating_sub(1))
} else if let Some("") = next_element {
None
} else if let Some(anchor) = next_element {
return RangeOrAnchor::Anchor(String::from(anchor));
} else {
None
};
let end = parts.next();
let has_end = end.is_some();
let end = end.and_then(|s| s.parse::<usize>().ok());
match start {
Some(start) => match end {
Some(end) => LinkType::IncludeRange(
path,
Range {
start: start,
end: end,
},
),
None => if has_end {
LinkType::IncludeRangeFrom(path, RangeFrom { start: start })
} else {
LinkType::IncludeRange(
path,
Range {
start: start,
end: start + 1,
},
)
},
},
None => match end {
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end: end }),
None => LinkType::IncludeRangeFull(path, RangeFull),
},
// If `end` is empty string or any other value that can't be parsed as a usize, treat this
// include as a range with only a start bound. However, if end isn't specified, include only
// the single line specified by `start`.
let end = end.map(|s| s.parse::<usize>());
match (start, end) {
(Some(start), Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(start..end)),
(Some(start), Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(start..)),
(Some(start), None) => RangeOrAnchor::Range(LineRange::from(start..start + 1)),
(None, Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(..end)),
(None, None) | (None, Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(RangeFull)),
}
}
fn parse_include_path(path: &str) -> LinkType<'static> {
let mut parts = path.splitn(2, ':');
let path = parts.next().unwrap().into();
let range_or_anchor = parse_range_or_anchor(parts.next());
LinkType::Include(path, range_or_anchor)
}
fn parse_rustdoc_include_path(path: &str) -> LinkType<'static> {
let mut parts = path.splitn(2, ':');
let path = parts.next().unwrap().into();
let range_or_anchor = parse_range_or_anchor(parts.next());
LinkType::RustdocInclude(path, range_or_anchor)
}
#[derive(PartialEq, Debug, Clone)]
struct Link<'a> {
start_index: usize,
end_index: usize,
link: LinkType<'a>,
link_type: LinkType<'a>,
link_text: &'a str,
}
@@ -183,6 +262,7 @@ impl<'a> Link<'a> {
match (typ.as_str(), file_arg) {
("include", Some(pth)) => Some(parse_include_path(pth)),
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
_ => None,
}
}
@@ -192,11 +272,11 @@ impl<'a> Link<'a> {
_ => None,
};
link_type.and_then(|lnk| {
link_type.and_then(|lnk_type| {
cap.get(0).map(|mat| Link {
start_index: mat.start(),
end_index: mat.end(),
link: lnk,
link_type: lnk_type,
link_text: mat.as_str(),
})
})
@@ -204,23 +284,55 @@ impl<'a> Link<'a> {
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
let base = base.as_ref();
match self.link {
match self.link_type {
// omit the escape char
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
LinkType::IncludeRange(ref pat, ref range) => file_to_string(base.join(pat))
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(base.join(pat))
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::IncludeRangeTo(ref pat, ref range) => file_to_string(base.join(pat))
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::IncludeRangeFull(ref pat, _) => file_to_string(base.join(pat))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::Include(ref pat, ref range_or_anchor) => {
let target = base.join(pat);
fs::read_to_string(&target)
.map(|s| match range_or_anchor {
RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
})
.chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
target.display(),
)
})
}
LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
let target = base.join(pat);
fs::read_to_string(&target)
.map(|s| match range_or_anchor {
RangeOrAnchor::Range(range) => {
take_rustdoc_include_lines(&s, range.clone())
}
RangeOrAnchor::Anchor(anchor) => {
take_rustdoc_include_anchored_lines(&s, anchor)
}
})
.chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
target.display(),
)
})
}
LinkType::Playpen(ref pat, ref attrs) => {
let contents = file_to_string(base.join(pat))
.chain_err(|| format!("Could not read file for link {}", self.link_text))?;
let target = base.join(pat);
let contents = fs::read_to_string(&target).chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
target.display()
)
})?;
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
Ok(format!(
"```{}{}\n{}\n```\n",
@@ -247,7 +359,7 @@ impl<'a> Iterator for LinkIter<'a> {
}
}
fn find_links(contents: &str) -> LinkIter {
fn find_links(contents: &str) -> LinkIter<'_> {
// lazily compute following regex
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
lazy_static! {
@@ -256,11 +368,12 @@ fn find_links(contents: &str) -> LinkIter {
\\\{\{\#.*\}\} # match escaped link
| # or
\{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9]+) # link type
\#([a-zA-Z0-9_]+) # link type
\s+ # separating whitespace
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
\s*\}\} # whitespace and link closing parens"
).unwrap();
)
.unwrap();
}
LinkIter(RE.captures_iter(contents))
}
@@ -325,13 +438,13 @@ mod tests {
Link {
start_index: 22,
end_index: 42,
link: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
link_type: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
link_text: "{{#playpen file.rs}}",
},
Link {
start_index: 47,
end_index: 68,
link: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
link_type: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
link_text: "{{#playpen test.rs }}",
},
]
@@ -348,7 +461,10 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 48,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(9..20))
),
link_text: "{{#include file.rs:10:20}}",
}]
);
@@ -364,7 +480,10 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 45,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(9..10))
),
link_text: "{{#include file.rs:10}}",
}]
);
@@ -380,7 +499,10 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(9..))
),
link_text: "{{#include file.rs:10:}}",
}]
);
@@ -396,7 +518,10 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(..20))
),
link_text: "{{#include file.rs::20}}",
}]
);
@@ -412,7 +537,10 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 44,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(..))
),
link_text: "{{#include file.rs::}}",
}]
);
@@ -428,12 +556,34 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 42,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(..))
),
link_text: "{{#include file.rs}}",
}]
);
}
#[test]
fn test_find_links_with_anchor() {
let s = "Some random text with {{#include file.rs:anchor}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![Link {
start_index: 22,
end_index: 49,
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Anchor(String::from("anchor"))
),
link_text: "{{#include file.rs:anchor}}",
}]
);
}
#[test]
fn test_find_links_escaped_link() {
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
@@ -446,7 +596,7 @@ mod tests {
vec![Link {
start_index: 38,
end_index: 68,
link: LinkType::Escaped,
link_type: LinkType::Escaped,
link_text: "\\{{#playpen file.rs editable}}",
}]
);
@@ -465,13 +615,13 @@ mod tests {
Link {
start_index: 38,
end_index: 68,
link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
link_type: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
link_text: "{{#playpen file.rs editable }}",
},
Link {
start_index: 89,
end_index: 136,
link: LinkType::Playpen(
link_type: LinkType::Playpen(
PathBuf::from("my.rs"),
vec!["editable", "no_run", "should_panic"],
),
@@ -495,7 +645,10 @@ mod tests {
Link {
start_index: 38,
end_index: 58,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(..))
),
link_text: "{{#include file.rs}}",
}
);
@@ -504,7 +657,7 @@ mod tests {
Link {
start_index: 63,
end_index: 112,
link: LinkType::Escaped,
link_type: LinkType::Escaped,
link_text: "\\{{#contents are insignifficant in escaped link}}",
}
);
@@ -513,7 +666,7 @@ mod tests {
Link {
start_index: 130,
end_index: 177,
link: LinkType::Playpen(
link_type: LinkType::Playpen(
PathBuf::from("my.rs"),
vec!["editable", "no_run", "should_panic"]
),
@@ -522,4 +675,183 @@ mod tests {
);
}
#[test]
fn parse_without_colon_includes_all() {
let link_type = parse_include_path("arbitrary");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(RangeFull))
)
);
}
#[test]
fn parse_with_nothing_after_colon_includes_all() {
let link_type = parse_include_path("arbitrary:");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(RangeFull))
)
);
}
#[test]
fn parse_with_two_colons_includes_all() {
let link_type = parse_include_path("arbitrary::");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(RangeFull))
)
);
}
#[test]
fn parse_with_garbage_after_two_colons_includes_all() {
let link_type = parse_include_path("arbitrary::NaN");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(RangeFull))
)
);
}
#[test]
fn parse_with_one_number_after_colon_only_that_line() {
let link_type = parse_include_path("arbitrary:5");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(4..5))
)
);
}
#[test]
fn parse_with_one_based_start_becomes_zero_based() {
let link_type = parse_include_path("arbitrary:1");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(0..1))
)
);
}
#[test]
fn parse_with_zero_based_start_stays_zero_based_but_is_probably_an_error() {
let link_type = parse_include_path("arbitrary:0");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(0..1))
)
);
}
#[test]
fn parse_start_only_range() {
let link_type = parse_include_path("arbitrary:5:");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(4..))
)
);
}
#[test]
fn parse_start_with_garbage_interpreted_as_start_only_range() {
let link_type = parse_include_path("arbitrary:5:NaN");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(4..))
)
);
}
#[test]
fn parse_end_only_range() {
let link_type = parse_include_path("arbitrary::5");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(..5))
)
);
}
#[test]
fn parse_start_and_end_range() {
let link_type = parse_include_path("arbitrary:5:10");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(4..10))
)
);
}
#[test]
fn parse_with_negative_interpreted_as_anchor() {
let link_type = parse_include_path("arbitrary:-5");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Anchor("-5".to_string())
)
);
}
#[test]
fn parse_with_floating_point_interpreted_as_anchor() {
let link_type = parse_include_path("arbitrary:-5.7");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Anchor("-5.7".to_string())
)
);
}
#[test]
fn parse_with_anchor_followed_by_colon() {
let link_type = parse_include_path("arbitrary:some-anchor:this-gets-ignored");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Anchor("some-anchor".to_string())
)
);
}
#[test]
fn parse_with_more_than_three_colons_ignores_everything_after_third_colon() {
let link_type = parse_include_path("arbitrary:5:10:17:anything:");
assert_eq!(
link_type,
LinkType::Include(
PathBuf::from("arbitrary"),
RangeOrAnchor::Range(LineRange::from(4..10))
)
);
}
}

View File

@@ -1,19 +1,22 @@
//! Book preprocessing.
pub use self::cmd::CmdPreprocessor;
pub use self::index::IndexPreprocessor;
pub use self::links::LinkPreprocessor;
mod cmd;
mod index;
mod links;
use book::Book;
use config::Config;
use errors::*;
use crate::book::Book;
use crate::config::Config;
use crate::errors::*;
use std::path::PathBuf;
/// Extra information for a `Preprocessor` to give them more context when
/// processing a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PreprocessorContext {
/// The location of the book directory on disk.
pub root: PathBuf,
@@ -23,6 +26,7 @@ pub struct PreprocessorContext {
pub renderer: String,
/// The calling `mdbook` version.
pub mdbook_version: String,
#[serde(skip)]
__non_exhaustive: (),
}
@@ -33,7 +37,7 @@ impl PreprocessorContext {
root,
config,
renderer,
mdbook_version: ::MDBOOK_VERSION.to_string(),
mdbook_version: crate::MDBOOK_VERSION.to_string(),
__non_exhaustive: (),
}
}

View File

@@ -1,11 +1,12 @@
use book::{Book, BookItem};
use config::{Config, HtmlConfig, Playpen};
use errors::*;
use renderer::html_handlebars::helpers;
use renderer::{RenderContext, Renderer};
use theme::{self, playpen_editor, Theme};
use utils;
use crate::book::{Book, BookItem};
use crate::config::{Config, HtmlConfig, Playpen};
use crate::errors::*;
use crate::renderer::html_handlebars::helpers;
use crate::renderer::{RenderContext, Renderer};
use crate::theme::{self, playpen_editor, Theme};
use crate::utils;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fs;
@@ -13,7 +14,6 @@ use std::path::{Path, PathBuf};
use handlebars::Handlebars;
use regex::{Captures, Regex};
use serde_json;
#[derive(Default)]
pub struct HtmlHandlebars;
@@ -26,79 +26,82 @@ impl HtmlHandlebars {
fn render_item(
&self,
item: &BookItem,
mut ctx: RenderItemContext,
mut ctx: RenderItemContext<'_>,
print_content: &mut String,
) -> Result<()> {
// FIXME: This should be made DRY-er and rely less on mutable state
match *item {
BookItem::Chapter(ref ch) => {
let content = ch.content.clone();
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
print_content.push_str(&content);
if let BookItem::Chapter(ref ch) = *item {
let content = ch.content.clone();
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
// 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 fixed_content = utils::render_markdown_with_path(
&ch.content,
ctx.html_config.curly_quotes,
Some(&ch.path),
);
print_content.push_str(&fixed_content);
// "print.html" is used for the print page.
if ch.path == Path::new("print.md") {
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
};
// 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");
// 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;
}
// "print.html" is used for the print page.
if ch.path == Path::new("print.md") {
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
};
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)),
);
// 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.playpen);
// 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.html"));
ctx.data.insert("path_to_root".to_owned(), json!(""));
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
let rendered_index =
self.post_process(rendered_index, &ctx.html_config.playpen);
debug!("Creating index.html from {}", path);
utils::fs::write_file(
&ctx.destination,
"index.html",
rendered_index.as_bytes(),
)?;
}
// 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;
}
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()));
}
// 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.playpen);
// 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.playpen);
debug!("Creating index.html from {}", path);
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
}
_ => {}
}
Ok(())
}
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
let rendered = build_header_links(&rendered);
let rendered = fix_code_blocks(&rendered);
@@ -113,7 +116,7 @@ impl HtmlHandlebars {
theme: &Theme,
html_config: &HtmlConfig,
) -> Result<()> {
use utils::fs::write_file;
use crate::utils::fs::write_file;
write_file(
destination,
@@ -214,6 +217,7 @@ impl HtmlHandlebars {
);
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
handlebars.register_helper("next", Box::new(helpers::navigation::next));
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
}
/// Copy across any additional CSS and JavaScript files which the book
@@ -284,6 +288,11 @@ impl Renderer for HtmlHandlebars {
let destination = &ctx.destination;
let book = &ctx.book;
if destination.exists() {
utils::fs::remove_dir_content(destination)
.chain_err(|| "Unable to remove stale HTML output")?;
}
trace!("render");
let mut handlebars = Handlebars::new();
@@ -327,7 +336,7 @@ impl Renderer for HtmlHandlebars {
handlebars: &handlebars,
destination: destination.to_path_buf(),
data: data.clone(),
is_index: is_index,
is_index,
html_config: html_config.clone(),
};
self.render_item(item, ctx, &mut print_content)?;
@@ -378,10 +387,12 @@ fn make_data(
html_config: &HtmlConfig,
) -> Result<serde_json::Map<String, serde_json::Value>> {
trace!("make_data");
let html = config.html_config().unwrap_or_default();
let mut data = serde_json::Map::new();
data.insert("language".to_owned(), json!("en"));
data.insert(
"language".to_owned(),
json!(config.book.language.clone().unwrap_or_default()),
);
data.insert(
"book_title".to_owned(),
json!(config.book.title.clone().unwrap_or_default()),
@@ -395,19 +406,34 @@ fn make_data(
data.insert("livereload".to_owned(), json!(livereload));
}
let default_theme = match html_config.default_theme {
Some(ref theme) => theme,
None => "light",
};
data.insert("default_theme".to_owned(), json!(default_theme));
let preferred_dark_theme = match html_config.preferred_dark_theme {
Some(ref theme) => theme,
None => default_theme,
};
data.insert(
"preferred_dark_theme".to_owned(),
json!(preferred_dark_theme),
);
// Add google analytics tag
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
if let Some(ref ga) = html_config.google_analytics {
data.insert("google_analytics".to_owned(), json!(ga));
}
if html.mathjax_support {
if html_config.mathjax_support {
data.insert("mathjax_support".to_owned(), json!(true));
}
// Add check to see if there is an additional style
if !html.additional_css.is_empty() {
if !html_config.additional_css.is_empty() {
let mut css = Vec::new();
for style in &html.additional_css {
for style in &html_config.additional_css {
match style.strip_prefix(root) {
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
@@ -417,26 +443,29 @@ fn make_data(
}
// Add check to see if there is an additional script
if !html.additional_js.is_empty() {
if !html_config.additional_js.is_empty() {
let mut js = Vec::new();
for script in &html.additional_js {
for script in &html_config.additional_js {
match script.strip_prefix(root) {
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
Err(_) => js.push(
script
.file_name()
.expect("File has a file name")
.to_str()
.expect("Could not convert to str"),
),
Err(_) => js.push(script.to_str().expect("Could not convert to str")),
}
}
data.insert("additional_js".to_owned(), json!(js));
}
if html.playpen.editable && html.playpen.copy_js {
if html_config.playpen.editable && html_config.playpen.copy_js {
data.insert("playpen_js".to_owned(), json!(true));
if html_config.playpen.line_numbers {
data.insert("playpen_line_numbers".to_owned(), json!(true));
}
}
if html_config.playpen.copyable {
data.insert("playpen_copyable".to_owned(), json!(true));
}
data.insert("fold_enable".to_owned(), json!((html_config.fold.enable)));
data.insert("fold_level".to_owned(), json!((html_config.fold.level)));
let search = html_config.search.clone();
if cfg!(feature = "search") {
@@ -454,6 +483,16 @@ fn make_data(
)
}
if let Some(ref git_repository_url) = html_config.git_repository_url {
data.insert("git_repository_url".to_owned(), json!(git_repository_url));
}
let git_repository_icon = match html_config.git_repository_icon {
Some(ref git_repository_icon) => git_repository_icon,
None => "fa-github",
};
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
let mut chapters = vec![];
for item in book.iter() {
@@ -466,6 +505,11 @@ fn make_data(
chapter.insert("section".to_owned(), json!(section.to_string()));
}
chapter.insert(
"has_sub_items".to_owned(),
json!((!ch.sub_items.is_empty()).to_string()),
);
chapter.insert("name".to_owned(), json!(ch.name));
let path = ch
.path
@@ -487,25 +531,26 @@ fn make_data(
Ok(data)
}
/// Goes through the rendered HTML, making sure all header tags are wrapped in
/// an anchor so people can link to sections directly.
/// Goes through the rendered HTML, making sure all header tags have
/// an anchor respectively so people can link to sections directly.
fn build_header_links(html: &str) -> String {
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
let mut id_counter = HashMap::new();
regex
.replace_all(html, |caps: &Captures| {
.replace_all(html, |caps: &Captures<'_>| {
let level = caps[1]
.parse()
.expect("Regex should ensure we only ever get numbers here");
wrap_header_with_link(level, &caps[2], &mut id_counter)
}).into_owned()
insert_link_into_header(level, &caps[2], &mut id_counter)
})
.into_owned()
}
/// Wraps a single header tag with a link, making sure each tag gets its own
/// Insert a sinle link into a header, making sure each link gets its own
/// unique ID by appending an auto-incremented number (if necessary).
fn wrap_header_with_link(
fn insert_link_into_header(
level: usize,
content: &str,
id_counter: &mut HashMap<String, usize>,
@@ -522,7 +567,7 @@ fn wrap_header_with_link(
*id_count += 1;
format!(
r##"<a class="header" href="#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
r##"<h{level}><a class="header" href="#{id}" id="{id}">{text}</a></h{level}>"##,
level = level,
id = id,
text = content
@@ -540,7 +585,7 @@ fn wrap_header_with_link(
fn fix_code_blocks(html: &str) -> String {
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
regex
.replace_all(html, |caps: &Captures| {
.replace_all(html, |caps: &Captures<'_>| {
let before = &caps[1];
let classes = &caps[2].replace(",", " ");
let after = &caps[3];
@@ -551,13 +596,15 @@ fn fix_code_blocks(html: &str) -> String {
classes = classes,
after = after
)
}).into_owned()
})
.into_owned()
}
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
let boring_line_regex = Regex::new(r"^(\s*)#(#|.)(.*)$").unwrap();
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
regex
.replace_all(html, |caps: &Captures| {
.replace_all(html, |caps: &Captures<'_>| {
let text = &caps[1];
let classes = &caps[2];
let code = &caps[3];
@@ -568,26 +615,57 @@ fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|| classes.contains("mdbook-runnable")
{
// wrap the contents in an external pre block
if playpen_config.editable && classes.contains("editable")
|| text.contains("fn main")
|| text.contains("quick_main!")
{
format!("<pre class=\"playpen\">{}</pre>", text)
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!(
"<pre class=\"playpen\"><code class=\"{}\">{}</code></pre>",
classes,
{
let content: Cow<'_, str> = if playpen_config.editable
&& classes.contains("editable")
|| text.contains("fn main")
|| text.contains("quick_main!")
{
code.into()
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!(
"<pre class=\"playpen\"><code class=\"{}\">\n# \
#![allow(unused_variables)]\n{}#fn main() {{\n{}#}}</code></pre>",
classes, attrs, code
)
}
format!(
"\n# #![allow(unused_variables)]\n{}#fn main() {{\n{}#}}",
attrs, code
)
.into()
};
let mut prev_line_hidden = false;
let mut result = String::with_capacity(content.len());
for line in content.lines() {
if let Some(caps) = boring_line_regex.captures(line) {
if !prev_line_hidden && &caps[2] != "#" {
result += "<span class=\"boring\">";
prev_line_hidden = true;
}
result += &caps[1];
if &caps[2] != " " {
result += &caps[2];
}
result += &caps[3];
} else {
if prev_line_hidden {
result += "</span>";
prev_line_hidden = false;
}
result += line;
}
result += "\n";
}
result
}
)
} else {
// not language-rust, so no-op
text.to_owned()
}
}).into_owned()
})
.into_owned()
}
fn partition_source(s: &str) -> (String, String) {
@@ -597,7 +675,7 @@ fn partition_source(s: &str) -> (String, String) {
for line in s.lines() {
let trimline = line.trim();
let header = trimline.chars().all(|c| c.is_whitespace()) || trimline.starts_with("#![");
let header = trimline.chars().all(char::is_whitespace) || trimline.starts_with("#![");
if !header || after_header {
after_header = true;
after.push_str(line);
@@ -628,27 +706,27 @@ mod tests {
let inputs = vec![
(
"blah blah <h1>Foo</h1>",
r##"blah blah <a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
r##"blah blah <h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
),
(
"<h1>Foo</h1>",
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
),
(
"<h3>Foo^bar</h3>",
r##"<a class="header" href="#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
r##"<h3><a class="header" href="#foobar" id="foobar">Foo^bar</a></h3>"##,
),
(
"<h4></h4>",
r##"<a class="header" href="#" id=""><h4></h4></a>"##,
r##"<h4><a class="header" href="#" id=""></a></h4>"##,
),
(
"<h4><em>Hï</em></h4>",
r##"<a class="header" href="#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
r##"<h4><a class="header" href="#hï" id="hï"><em>Hï</em></a></h4>"##,
),
(
"<h1>Foo</h1><h3>Foo</h3>",
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a><a class="header" href="#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1><h3><a class="header" href="#foo-1" id="foo-1">Foo</a></h3>"##,
),
];
@@ -657,4 +735,28 @@ mod tests {
assert_eq!(got, should_be);
}
}
#[test]
fn add_playpen() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playpen\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused_variables)]\nfn main() {\n</span>x()\n<span class=\"boring\">}\n</code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playpen\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
"<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
"<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
];
for (src, should_be) in &inputs {
let got = add_playpen_pre(
src,
&Playpen {
editable: true,
..Playpen::default()
},
);
assert_eq!(&*got, *should_be);
}
}
}

View File

@@ -1,2 +1,3 @@
pub mod navigation;
pub mod theme;
pub mod toc;

View File

@@ -2,9 +2,8 @@ use std::collections::BTreeMap;
use std::path::Path;
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
use serde_json;
use utils;
use crate::utils;
type StringMap = BTreeMap<String, String>;
@@ -18,13 +17,13 @@ impl Target {
/// Returns target if found.
fn find(
&self,
base_path: &String,
current_path: &String,
base_path: &str,
current_path: &str,
current_item: &StringMap,
previous_item: &StringMap,
) -> Result<Option<StringMap>, RenderError> {
match self {
&Target::Next => {
match *self {
Target::Next => {
let previous_path = previous_item
.get("path")
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
@@ -34,7 +33,7 @@ impl Target {
}
}
&Target::Previous => {
Target::Previous => {
if current_path == base_path {
return Ok(Some(previous_item.clone()));
}
@@ -47,22 +46,44 @@ impl Target {
fn find_chapter(
ctx: &Context,
rc: &mut RenderContext,
rc: &mut RenderContext<'_>,
target: Target,
) -> Result<Option<StringMap>, RenderError> {
debug!("Get data from context");
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let base_path = rc
.evaluate_absolute(ctx, "path", true)?
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
// Special case for index.md which may be a synthetic page.
// Target::find won't match because there is no page with the path
// "index.md" (unless there really is an index.md in SUMMARY.md).
match target {
Target::Previous => return Ok(None),
Target::Next => match chapters
.iter()
.filter(|chapter| {
// Skip things like "spacer"
chapter.contains_key("path")
})
.skip(1)
.next()
{
Some(chapter) => return Ok(Some(chapter.clone())),
None => return Ok(None),
},
}
}
let mut previous: Option<StringMap> = None;
debug!("Search for chapter");
@@ -86,18 +107,19 @@ fn find_chapter(
}
fn render(
_h: &Helper,
_h: &Helper<'_, '_>,
r: &Handlebars,
ctx: &Context,
rc: &mut RenderContext,
out: &mut Output,
rc: &mut RenderContext<'_>,
out: &mut dyn Output,
chapter: &StringMap,
) -> Result<(), RenderError> {
trace!("Creating BTreeMap to inject in context");
let mut context = BTreeMap::new();
let base_path = rc
.evaluate_absolute(ctx, "path", false)?
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
@@ -137,11 +159,11 @@ fn render(
}
pub fn previous(
_h: &Helper,
_h: &Helper<'_, '_>,
r: &Handlebars,
ctx: &Context,
rc: &mut RenderContext,
out: &mut Output,
rc: &mut RenderContext<'_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
trace!("previous (handlebars helper)");
@@ -153,11 +175,11 @@ pub fn previous(
}
pub fn next(
_h: &Helper,
_h: &Helper<'_, '_>,
r: &Handlebars,
ctx: &Context,
rc: &mut RenderContext,
out: &mut Output,
rc: &mut RenderContext<'_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
trace!("next (handlebars helper)");
@@ -172,29 +194,29 @@ pub fn next(
mod tests {
use super::*;
static TEMPLATE: &'static str =
static TEMPLATE: &str =
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
#[test]
fn test_next_previous() {
let data = json!({
"name": "two",
"path": "two.path",
"chapters": [
{
"name": "one",
"path": "one.path"
},
{
"name": "two",
"path": "two.path",
},
{
"name": "three",
"path": "three.path"
}
]
});
"name": "two",
"path": "two.path",
"chapters": [
{
"name": "one",
"path": "one.path"
},
{
"name": "two",
"path": "two.path",
},
{
"name": "three",
"path": "three.path"
}
]
});
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
@@ -209,23 +231,23 @@ mod tests {
#[test]
fn test_first() {
let data = json!({
"name": "one",
"path": "one.path",
"chapters": [
{
"name": "one",
"path": "one.path"
},
{
"name": "two",
"path": "two.path",
},
{
"name": "three",
"path": "three.path"
}
]
});
"name": "one",
"path": "one.path",
"chapters": [
{
"name": "one",
"path": "one.path"
},
{
"name": "two",
"path": "two.path",
},
{
"name": "three",
"path": "three.path"
}
]
});
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
@@ -239,23 +261,23 @@ mod tests {
#[test]
fn test_last() {
let data = json!({
"name": "three",
"path": "three.path",
"chapters": [
{
"name": "one",
"path": "one.path"
},
{
"name": "two",
"path": "two.path",
},
{
"name": "three",
"path": "three.path"
}
]
});
"name": "three",
"path": "three.path",
"chapters": [
{
"name": "one",
"path": "one.path"
},
{
"name": "two",
"path": "two.path",
},
{
"name": "three",
"path": "three.path"
}
]
});
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));

View File

@@ -0,0 +1,28 @@
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
pub fn theme_option(
h: &Helper<'_, '_>,
_r: &Handlebars,
ctx: &Context,
rc: &mut RenderContext<'_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
trace!("theme_option (handlebars helper)");
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
RenderError::new("Param 0 with String type is required for theme_option helper.")
})?;
let default_theme = rc.evaluate(ctx, "@root/default_theme")?;
let default_theme_name = default_theme
.as_json()
.as_str()
.ok_or_else(|| RenderError::new("Type error for `default_theme`, string expected"))?;
out.write(param)?;
if param.to_lowercase() == default_theme_name.to_lowercase() {
out.write(" (default)")?;
}
Ok(())
}

View File

@@ -1,11 +1,10 @@
use std::collections::BTreeMap;
use std::path::Path;
use utils;
use crate::utils;
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
use pulldown_cmark::{html, Event, Parser, Tag};
use serde_json;
use pulldown_cmark::{html, Event, Parser};
// Handlebars helper to construct TOC
#[derive(Clone, Copy)]
@@ -16,25 +15,49 @@ pub struct RenderToc {
impl HelperDef for RenderToc {
fn call<'reg: 'rc, 'rc>(
&self,
_h: &Helper,
_: &Handlebars,
ctx: &Context,
rc: &mut RenderContext,
out: &mut Output,
_h: &Helper<'reg, 'rc>,
_r: &'reg Handlebars,
ctx: &'rc Context,
rc: &mut RenderContext<'reg>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
// get value from context data
// rc.get_path() is current json parent path, you should always use it like this
// param is the key of value you want to display
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let current = rc
.evaluate_absolute(ctx, "path", true)?
let current_path = rc
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.ok_or(RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
let current_section = rc
.evaluate(ctx, "@root/section")?
.as_json()
.as_str()
.map(str::to_owned)
.unwrap_or_default();
let fold_enable = rc
.evaluate(ctx, "@root/fold_enable")?
.as_json()
.as_bool()
.ok_or(RenderError::new(
"Type error for `fold_enable`, bool expected",
))?;
let fold_level = rc
.evaluate(ctx, "@root/fold_level")?
.as_json()
.as_u64()
.ok_or(RenderError::new(
"Type error for `fold_level`, u64 expected",
))?;
out.write("<ol class=\"chapter\">")?;
let mut current_level = 1;
@@ -46,10 +69,23 @@ impl HelperDef for RenderToc {
continue;
}
let level = if let Some(s) = item.get("section") {
s.matches('.').count()
let (section, level) = if let Some(s) = item.get("section") {
(s.as_str(), s.matches('.').count())
} else {
1
("", 1)
};
let is_expanded = {
if !fold_enable {
// Disable fold. Expand all chapters.
true
} else if !section.is_empty() && current_section.starts_with(section) {
// The section is ancestor or the current section itself.
true
} else {
// Levels that are larger than this would be folded.
level - 1 < fold_level as usize
}
};
if level > current_level {
@@ -58,20 +94,16 @@ impl HelperDef for RenderToc {
out.write("<ol class=\"section\">")?;
current_level += 1;
}
out.write("<li>")?;
write_li_open_tag(out, is_expanded, false)?;
} else if level < current_level {
while level < current_level {
out.write("</ol>")?;
out.write("</li>")?;
current_level -= 1;
}
out.write("<li>")?;
write_li_open_tag(out, is_expanded, false)?;
} else {
out.write("<li")?;
if item.get("section").is_none() {
out.write(" class=\"affix\"")?;
}
out.write(">")?;
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
}
// Link
@@ -87,11 +119,11 @@ impl HelperDef for RenderToc {
.replace("\\", "/");
// Add link
out.write(&utils::fs::path_to_root(&current))?;
out.write(&utils::fs::path_to_root(&current_path))?;
out.write(&tmp)?;
out.write("\"")?;
if path == &current {
if path == &current_path {
out.write(" class=\"active\"")?;
}
@@ -118,10 +150,7 @@ impl HelperDef for RenderToc {
// filter all events that are not inline code blocks
let parser = Parser::new(name).filter(|event| match *event {
Event::Start(Tag::Code)
| Event::End(Tag::Code)
| Event::InlineHtml(_)
| Event::Text(_) => true,
Event::Code(_) | Event::InlineHtml(_) | Event::Text(_) => true,
_ => false,
});
@@ -137,6 +166,13 @@ impl HelperDef for RenderToc {
out.write("</a>")?;
}
// Render expand/collapse toggle
if let Some(flag) = item.get("has_sub_items") {
let has_sub_items = flag.parse::<bool>().unwrap_or_default();
if fold_enable && has_sub_items {
out.write("<a class=\"toggle\"><div>❱</div></a>")?;
}
}
out.write("</li>")?;
}
while current_level > 1 {
@@ -149,3 +185,19 @@ impl HelperDef for RenderToc {
Ok(())
}
}
fn write_li_open_tag(
out: &mut dyn Output,
is_expanded: bool,
is_affix: bool,
) -> Result<(), std::io::Error> {
let mut li = String::from("<li class=\"");
if is_expanded {
li.push_str("expanded ");
}
if is_affix {
li.push_str("affix ");
}
li.push_str("\">");
out.write(&li)
}

View File

@@ -1,19 +1,15 @@
extern crate ammonia;
extern crate elasticlunr;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use self::elasticlunr::Index;
use elasticlunr::Index;
use pulldown_cmark::*;
use serde_json;
use book::{Book, BookItem};
use config::Search;
use errors::*;
use theme::searcher;
use utils;
use crate::book::{Book, BookItem};
use crate::config::Search;
use crate::errors::*;
use crate::theme::searcher;
use crate::utils;
/// Creates all files required for search.
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
@@ -35,7 +31,7 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
utils::fs::write_file(
destination,
"searchindex.js",
format!("window.search = {};", index).as_bytes(),
format!("Object.assign(window.search, {});", index).as_bytes(),
)?;
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
@@ -54,7 +50,7 @@ fn add_doc(
section_id: &Option<String>,
items: &[&str],
) {
let url = if let &Some(ref id) = section_id {
let url = if let Some(ref id) = *section_id {
Cow::Owned(format!("{}#{}", anchor_base, id))
} else {
Cow::Borrowed(anchor_base)
@@ -74,8 +70,8 @@ fn render_item(
doc_urls: &mut Vec<String>,
item: &BookItem,
) -> Result<()> {
let chapter = match item {
&BookItem::Chapter(ref ch) => ch,
let chapter = match *item {
BookItem::Chapter(ref ch) => ch,
_ => return Ok(()),
};
@@ -85,23 +81,21 @@ fn render_item(
.chain_err(|| "Could not convert HTML path to str")?;
let anchor_base = utils::fs::normalize_path(filepath);
let mut opts = Options::empty();
opts.insert(OPTION_ENABLE_TABLES);
opts.insert(OPTION_ENABLE_FOOTNOTES);
let p = Parser::new_ext(&chapter.content, opts);
let p = utils::new_cmark_parser(&chapter.content);
let mut in_header = false;
let max_section_depth = search_config.heading_split_level as i32;
let max_section_depth = i32::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 {
match event {
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
if heading.len() > 0 {
if !heading.is_empty() {
// Section finished, the next header is following now
// Write the data to the index, and clear it for the next section
add_doc(
@@ -128,6 +122,13 @@ fn render_item(
let number = footnote_numbers.len() + 1;
footnote_numbers.entry(name).or_insert(number);
}
Event::Html(html) => {
html_block.push_str(&html);
}
Event::End(Tag::HtmlBlock) => {
body.push_str(&clean_html(&html_block));
html_block.clear();
}
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
// Insert spaces where HTML output would usually seperate text
// to ensure words don't get merged together
@@ -137,14 +138,14 @@ fn render_item(
body.push(' ');
}
}
Event::Text(text) => {
Event::Text(text) | Event::Code(text) => {
if in_header {
heading.push_str(&text);
} else {
body.push_str(&text);
}
}
Event::Html(html) | Event::InlineHtml(html) => {
Event::InlineHtml(html) => {
body.push_str(&clean_html(&html));
}
Event::FootnoteReference(name) => {
@@ -152,10 +153,11 @@ fn render_item(
let number = footnote_numbers.entry(name).or_insert(len);
body.push_str(&format!(" [{}] ", number));
}
Event::TaskListMarker(_checked) => {}
}
}
if heading.len() > 0 {
if !heading.is_empty() {
// Make sure the last section is added to the index
add_doc(
index,
@@ -170,7 +172,7 @@ fn render_item(
}
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
use elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
use std::collections::BTreeMap;
#[derive(Serialize)]

View File

@@ -0,0 +1,46 @@
use crate::book::BookItem;
use crate::errors::*;
use crate::renderer::{RenderContext, Renderer};
use crate::utils;
use std::fs;
#[derive(Default)]
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
/// when debugging preprocessors.
pub struct MarkdownRenderer;
impl MarkdownRenderer {
/// Create a new `MarkdownRenderer` instance.
pub fn new() -> Self {
MarkdownRenderer
}
}
impl Renderer for MarkdownRenderer {
fn name(&self) -> &str {
"markdown"
}
fn render(&self, ctx: &RenderContext) -> Result<()> {
let destination = &ctx.destination;
let book = &ctx.book;
if destination.exists() {
utils::fs::remove_dir_content(destination)
.chain_err(|| "Unable to remove stale Markdown output")?;
}
trace!("markdown render");
for item in book.iter() {
if let BookItem::Chapter(ref ch) = *item {
utils::fs::write_file(&ctx.destination, &ch.path, ch.content.as_bytes())?;
}
}
fs::create_dir_all(&destination)
.chain_err(|| "Unexpected error when constructing destination path")?;
Ok(())
}
}

View File

@@ -8,23 +8,24 @@
//!
//! The definition for [RenderContext] may be useful though.
//!
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/lib/index.html
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/for_developers/index.html
//! [RenderContext]: struct.RenderContext.html
pub use self::html_handlebars::HtmlHandlebars;
pub use self::markdown_renderer::MarkdownRenderer;
mod html_handlebars;
mod markdown_renderer;
use serde_json;
use shlex::Shlex;
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use book::Book;
use config::Config;
use errors::*;
use crate::book::Book;
use crate::config::Config;
use crate::errors::*;
/// An arbitrary `mdbook` backend.
///
@@ -64,6 +65,7 @@ pub struct RenderContext {
/// renderers to cache intermediate results, this directory is not
/// guaranteed to be empty or even exist.
pub destination: PathBuf,
#[serde(skip)]
__non_exhaustive: (),
}
@@ -75,9 +77,9 @@ impl RenderContext {
Q: Into<PathBuf>,
{
RenderContext {
book: book,
config: config,
version: ::MDBOOK_VERSION.to_string(),
book,
config,
version: crate::MDBOOK_VERSION.to_string(),
root: root.into(),
destination: destination.into(),
__non_exhaustive: (),

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 434 KiB

View File

@@ -69,3 +69,11 @@ Original by Dempfi (https://github.com/dempfi/ayu)
.hljs-strong {
font-weight: bold;
}
.hljs-addition {
color: #91b362;
}
.hljs-deletion {
color: #d96c75;
}

View File

@@ -55,6 +55,15 @@ function playpen_text(playpen) {
editor.addEventListener("change", function (e) {
update_play_button(playpen_block, playground_crates);
});
// add Ctrl-Enter command to execute rust code
editor.commands.addCommand({
name: "run",
bindKey: {
win: "Ctrl-Enter",
mac: "Ctrl-Enter"
},
exec: _editor => run_rust_code(playpen_block)
});
}
}
}
@@ -101,11 +110,15 @@ function playpen_text(playpen) {
}
let text = playpen_text(code_block);
let classes = code_block.querySelector('code').classList;
let has_2018 = classes.contains("edition2018");
let edition = has_2018 ? "2018" : "2015";
var params = {
version: "stable",
optimize: "0",
code: text
code: text,
edition: edition
};
if (text.indexOf("#![feature") !== -1) {
@@ -157,95 +170,59 @@ function playpen_text(playpen) {
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
var code_block = block;
var pre_block = block.parentNode;
// hide lines
var lines = code_block.innerHTML.split("\n");
var first_non_hidden_line = false;
var lines_hidden = false;
var trimmed_line = "";
for (var n = 0; n < lines.length; n++) {
trimmed_line = lines[n].trim();
if (trimmed_line[0] == hiding_character && trimmed_line[1] != hiding_character) {
if (first_non_hidden_line) {
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)# ?/, "$1") + "</span>";
}
else {
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)# ?/, "$1") + "\n" + "</span>";
}
lines_hidden = true;
}
else if (first_non_hidden_line) {
lines[n] = "\n" + lines[n];
}
else {
first_non_hidden_line = true;
}
if (trimmed_line[0] == hiding_character && trimmed_line[1] == hiding_character) {
lines[n] = lines[n].replace("##", "#")
}
}
code_block.innerHTML = lines.join("");
var lines = Array.from(block.querySelectorAll('.boring'));
// If no lines were hidden, return
if (!lines_hidden) { return; }
if (!lines.length) { return; }
block.classList.add("hide-boring");
var buttons = document.createElement('div');
buttons.className = 'buttons';
buttons.innerHTML = "<button class=\"fa fa-expand\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
// add expand button
var pre_block = block.parentNode;
pre_block.insertBefore(buttons, pre_block.firstChild);
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
if (e.target.classList.contains('fa-expand')) {
var lines = pre_block.querySelectorAll('span.hidden');
e.target.classList.remove('fa-expand');
e.target.classList.add('fa-compress');
e.target.title = 'Hide lines';
e.target.setAttribute('aria-label', e.target.title);
Array.from(lines).forEach(function (line) {
line.classList.remove('hidden');
line.classList.add('unhidden');
});
block.classList.remove('hide-boring');
} else if (e.target.classList.contains('fa-compress')) {
var lines = pre_block.querySelectorAll('span.unhidden');
e.target.classList.remove('fa-compress');
e.target.classList.add('fa-expand');
e.target.title = 'Show hidden lines';
e.target.setAttribute('aria-label', e.target.title);
Array.from(lines).forEach(function (line) {
line.classList.remove('unhidden');
line.classList.add('hidden');
});
block.classList.add('hide-boring');
}
});
});
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
var pre_block = block.parentNode;
if (!pre_block.classList.contains('playpen')) {
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
if (window.playpen_copyable) {
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
var pre_block = block.parentNode;
if (!pre_block.classList.contains('playpen')) {
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
var clipButton = document.createElement('button');
clipButton.className = 'fa fa-copy clip-button';
clipButton.title = 'Copy to clipboard';
clipButton.setAttribute('aria-label', clipButton.title);
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
buttons.insertBefore(clipButton, buttons.firstChild);
}
var clipButton = document.createElement('button');
clipButton.className = 'fa fa-copy clip-button';
clipButton.title = 'Copy to clipboard';
clipButton.setAttribute('aria-label', clipButton.title);
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
buttons.insertBefore(clipButton, buttons.firstChild);
}
});
});
}
// Process playpen code blocks
Array.from(document.querySelectorAll(".playpen")).forEach(function (pre_block) {
@@ -263,19 +240,21 @@ function playpen_text(playpen) {
runCodeButton.title = 'Run this code';
runCodeButton.setAttribute('aria-label', runCodeButton.title);
var copyCodeClipboardButton = document.createElement('button');
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
copyCodeClipboardButton.title = 'Copy to clipboard';
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
buttons.insertBefore(runCodeButton, buttons.firstChild);
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
runCodeButton.addEventListener('click', function (e) {
run_rust_code(pre_block);
});
if (window.playpen_copyable) {
var copyCodeClipboardButton = document.createElement('button');
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
copyCodeClipboardButton.title = 'Copy to clipboard';
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
}
let code_block = pre_block.querySelector("code");
if (window.ace && code_block.classList.contains("editable")) {
var undoChangesButton = document.createElement('button');
@@ -317,7 +296,7 @@ function playpen_text(playpen) {
themeToggleButton.focus();
}
function set_theme(theme) {
function set_theme(theme, store = true) {
let ace_theme;
if (theme == 'coal' || theme == 'navy') {
@@ -330,13 +309,11 @@ function playpen_text(playpen) {
stylesheets.ayuHighlight.disabled = false;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = true;
ace_theme = "ace/theme/tomorrow_night";
} else {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = false;
ace_theme = "ace/theme/dawn";
}
@@ -352,11 +329,12 @@ function playpen_text(playpen) {
var previousTheme;
try { previousTheme = localStorage.getItem('mdbook-theme'); } catch (e) { }
if (previousTheme === null || previousTheme === undefined) { previousTheme = 'light'; }
if (previousTheme === null || previousTheme === undefined) { previousTheme = default_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);
}
@@ -364,9 +342,9 @@ function playpen_text(playpen) {
// Set theme
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
if (theme === null || theme === undefined) { theme = default_theme; }
set_theme(theme);
set_theme(theme, false);
themeToggleButton.addEventListener('click', function () {
if (themePopup.style.display === 'block') {
@@ -433,8 +411,10 @@ 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");
var firstContact = null;
function showSidebar() {
@@ -448,6 +428,17 @@ function playpen_text(playpen) {
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
}
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
function toggleSection(ev) {
ev.currentTarget.parentElement.classList.toggle('expanded');
}
Array.from(sidebarAnchorToggles).forEach(function (el) {
el.addEventListener('click', toggleSection);
});
function hideSidebar() {
html.classList.remove('sidebar-visible')
html.classList.add('sidebar-hidden');
@@ -474,6 +465,23 @@ function playpen_text(playpen) {
}
});
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
function initResize(e) {
window.addEventListener('mousemove', resize, false);
window.addEventListener('mouseup', stopResize, false);
html.classList.add('sidebar-resizing');
}
function resize(e) {
document.documentElement.style.setProperty('--sidebar-width', (e.clientX - sidebar.offsetLeft) + 'px');
}
//on mouseup remove windows functions mousemove & mouseup
function stopResize(e) {
html.classList.remove('sidebar-resizing');
window.removeEventListener('mousemove', resize, false);
window.removeEventListener('mouseup', stopResize, false);
}
document.addEventListener('touchstart', function (e) {
firstContact = {
x: e.touches[0].clientX,
@@ -502,7 +510,7 @@ function playpen_text(playpen) {
// Scroll sidebar to current active section
var activeSection = sidebar.querySelector(".active");
if (activeSection) {
sidebar.scrollTop = activeSection.offsetTop;
sidebarScrollBox.scrollTop = activeSection.offsetTop;
}
})();
@@ -543,7 +551,7 @@ function playpen_text(playpen) {
elem.className = 'fa fa-copy tooltipped';
}
var clipboardSnippets = new Clipboard('.clip-button', {
var clipboardSnippets = new ClipboardJS('.clip-button', {
text: function (trigger) {
hideTooltip(trigger);
let playpen = trigger.closest("pre");
@@ -595,6 +603,6 @@ function playpen_text(playpen) {
menu.classList.remove('bordered');
}
previousScrollTop = document.scrollingElement.scrollTop;
previousScrollTop = Math.max(document.scrollingElement.scrollTop, 0);
}, { passive: true });
})();

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,9 @@
::-webkit-scrollbar-thumb {
background: var(--scrollbar);
}
html {
scrollbar-color: var(--scrollbar) var(--bg);
}
#searchresults a,
.content a:link,
a:visited,
@@ -43,7 +45,7 @@ a > .hljs {
position: relative;
padding: 0 8px;
z-index: 10;
line-height: 50px;
line-height: var(--menu-bar-height);
cursor: pointer;
transition: color 0.5s;
}
@@ -63,19 +65,22 @@ a > .hljs {
margin: 0;
}
#print-button {
.right-buttons {
margin: 0 15px;
}
.right-buttons a {
text-decoration: none;
}
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
transform: translateY(-60px);
transform: translateY(calc(-10px - var(--menu-bar-height)));
}
.left-buttons {
display: flex;
margin: 0 5px;
}
.no-js .left-buttons {
.no-js .left-buttons {
display: none;
}
@@ -83,7 +88,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
display: inline-block;
font-weight: 200;
font-size: 20px;
line-height: 50px;
line-height: var(--menu-bar-height);
text-align: center;
margin: 0;
flex: 1;
@@ -121,7 +126,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
text-decoration: none;
position: fixed;
top: 50px; /* Height of menu-bar */
top: 0;
bottom: 0;
margin: 0;
max-width: 150px;
@@ -132,17 +137,21 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
align-content: center;
flex-direction: column;
transition: color 0.5s;
transition: color 0.5s, background-color 0.5s;
}
.nav-chapters:hover { text-decoration: none; }
.nav-chapters:hover {
text-decoration: none;
background-color: var(--theme-hover);
transition: background-color 0.15s, color 0.15s;
}
.nav-wrapper {
margin-top: 50px;
display: none;
}
.mobile-nav-chapters {
.mobile-nav-chapters {
font-size: 2.5em;
text-align: center;
text-decoration: none;
@@ -173,11 +182,14 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
/* Inline code */
:not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
display: inline;
padding: 0.1em 0.3em;
border-radius: 3px;
}
:not(pre):not(a) > .hljs {
color: var(--inline-code-color);
overflow-x: initial;
}
a:hover > .hljs {
@@ -298,8 +310,6 @@ ul#searchresults span.teaser em {
top: 0;
bottom: 0;
width: var(--sidebar-width);
overflow-y: auto;
padding: 10px 10px;
font-size: 0.875em;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
@@ -307,12 +317,39 @@ ul#searchresults span.teaser em {
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.js .sidebar {
.sidebar-resizing {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.js:not(.sidebar-resizing) .sidebar {
transition: transform 0.3s; /* Animation: slide away */
}
.sidebar code {
line-height: 2em;
}
.sidebar .sidebar-scrollbox {
overflow-y: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: 10px 10px;
}
.sidebar .sidebar-resize-handle {
position: absolute;
cursor: col-resize;
width: 0;
right: 0;
top: 0;
bottom: 0;
}
.js .sidebar .sidebar-resize-handle {
cursor: col-resize;
width: 5px;
}
.sidebar-hidden .sidebar {
transform: translateX(calc(0px - var(--sidebar-width)));
}
@@ -338,22 +375,52 @@ ul#searchresults span.teaser em {
padding-left: 0;
line-height: 2.2em;
}
.chapter ol {
width: 100%;
}
.chapter li {
display: flex;
color: var(--sidebar-non-existant);
}
.chapter li a {
color: var(--sidebar-fg);
display: block;
padding: 0;
text-decoration: none;
color: var(--sidebar-fg);
}
.chapter li a:hover { text-decoration: none }
.chapter li .active,
a:hover {
/* Animate color change */
.chapter li a:hover {
color: var(--sidebar-active);
}
.chapter li a.active {
color: var(--sidebar-active);
}
.chapter li > a.toggle {
cursor: pointer;
display: block;
margin-left: auto;
padding: 0 10px;
user-select: none;
opacity: 0.68;
}
.chapter li > a.toggle div {
transition: transform 0.5s;
}
/* collapse the section */
.chapter li:not(.expanded) + li > ol {
display: none;
}
.chapter li.expanded > a.toggle div {
transform: rotate(90deg);
}
.spacer {
width: 100%;
height: 3px;
@@ -363,7 +430,7 @@ a:hover {
background-color: var(--sidebar-spacer);
}
@media (-moz-touch-enabled: 1), (pointer: coarse) {
@media (-moz-touch-enabled: 1), (pointer: coarse) {
.chapter li a { padding: 5px 0; }
.spacer { margin: 10px 0; }
}
@@ -379,7 +446,7 @@ a:hover {
.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

@@ -16,34 +16,42 @@ body {
}
code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important;
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
}
.left { float: left; }
.right { float: right; }
.boring { opacity: 0.6; }
.hide-boring .boring { display: none; }
.hidden { display: none; }
.play-button.hidden { display: none; }
h2, h3 { margin-top: 2.5em; }
h4, h5 { margin-top: 2em; }
.header + .header h3,
.header + .header h4,
.header + .header h5 {
.header + .header h5 {
margin-top: 1em;
}
a.header:target h1:before,
a.header:target h2:before,
a.header:target h3:before,
a.header:target h4:before {
h1 a.header:target::before,
h2 a.header:target::before,
h3 a.header:target::before,
h4 a.header:target::before {
display: inline-block;
content: "»";
margin-left: -30px;
width: 30px;
}
h1 a.header:target,
h2 a.header:target,
h3 a.header:target,
h4 a.header:target {
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
}
.page {
outline: 0;
padding: 0 var(--page-padding);
@@ -51,7 +59,7 @@ a.header:target h4:before {
.page-wrapper {
box-sizing: border-box;
}
.js .page-wrapper {
.js:not(.sidebar-resizing) .page-wrapper {
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
}
@@ -92,6 +100,9 @@ table thead td {
font-weight: 700;
border: none;
}
table thead th {
padding: 3px 20px;
}
table thead tr {
border: 1px var(--table-header-bg) solid;
}
@@ -141,4 +152,3 @@ blockquote {
.tooltipped .tooltiptext {
visibility: visible;
}

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

View File

@@ -67,3 +67,13 @@
.hljs-strong {
font-weight: bold;
}
.hljs-addition {
color: #22863a;
background-color: #f0fff4;
}
.hljs-deletion {
color: #b31d28;
background-color: #ffeef0;
}

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +1,13 @@
<!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">
<title>{{ title }}</title>
{{#if is_print }}
<meta name="robots" content="noindex" />
{{/if}}
<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">
@@ -35,9 +39,12 @@
<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="light">
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">var path_to_root = "{{ path_to_root }}";</script>
<script type="text/javascript">
var path_to_root = "{{ path_to_root }}";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
@@ -58,10 +65,13 @@
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
document.body.className = theme;
document.querySelector('html').className = theme + ' js';
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('{{ default_theme }}')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
@@ -77,7 +87,10 @@
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
{{#toc}}{{/toc}}
<div id="sidebar-scrollbox" class="sidebar-scrollbox">
{{#toc}}{{/toc}}
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
@@ -94,11 +107,11 @@
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
<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">
@@ -107,12 +120,17 @@
{{/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>
</div>
@@ -215,6 +233,18 @@
</script>
{{/if}}
{{#if playpen_line_numbers}}
<script type="text/javascript">
window.playpen_line_numbers = true;
</script>
{{/if}}
{{#if playpen_copyable}}
<script type="text/javascript">
window.playpen_copyable = true;
</script>
{{/if}}
{{#if playpen_js}}
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>

View File

@@ -9,33 +9,29 @@ use std::fs::File;
use std::io::Read;
use std::path::Path;
use errors::*;
use crate::errors::*;
pub static INDEX: &'static [u8] = include_bytes!("index.hbs");
pub static HEADER: &'static [u8] = include_bytes!("header.hbs");
pub static CHROME_CSS: &'static [u8] = include_bytes!("css/chrome.css");
pub static GENERAL_CSS: &'static [u8] = include_bytes!("css/general.css");
pub static PRINT_CSS: &'static [u8] = include_bytes!("css/print.css");
pub static VARIABLES_CSS: &'static [u8] = include_bytes!("css/variables.css");
pub static FAVICON: &'static [u8] = include_bytes!("favicon.png");
pub static JS: &'static [u8] = include_bytes!("book.js");
pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js");
pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.css");
pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css");
pub static AYU_HIGHLIGHT_CSS: &'static [u8] = include_bytes!("ayu-highlight.css");
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.js");
pub static FONT_AWESOME: &'static [u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
pub static FONT_AWESOME_EOT: &'static [u8] =
include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
pub static FONT_AWESOME_SVG: &'static [u8] =
include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
pub static FONT_AWESOME_TTF: &'static [u8] =
include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
pub static FONT_AWESOME_WOFF: &'static [u8] =
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
pub static FONT_AWESOME_WOFF2: &'static [u8] =
pub static INDEX: &[u8] = include_bytes!("index.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 JS: &[u8] = include_bytes!("book.js");
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("highlight.js");
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("tomorrow-night.css");
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("highlight.css");
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("ayu-highlight.css");
pub static CLIPBOARD_JS: &[u8] = include_bytes!("clipboard.min.js");
pub static FONT_AWESOME: &[u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
pub static FONT_AWESOME_WOFF2: &[u8] =
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2");
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
/// The `Theme` struct should be used instead of the static variables because
/// the `new()` method will look if the user has a theme directory in their

File diff suppressed because one or more lines are too long

View File

@@ -6,14 +6,16 @@ window.editors = [];
}
Array.from(document.querySelectorAll('.editable')).forEach(function(editable) {
let display_line_numbers = window.playpen_line_numbers || false;
let editor = ace.edit(editable);
editor.setOptions({
highlightActiveLine: false,
showPrintMargin: false,
showLineNumbers: false,
showGutter: false,
showLineNumbers: display_line_numbers,
showGutter: display_line_numbers,
maxLines: Infinity,
fontSize: "0.875em" // please adjust the font size of the code in general.styl
fontSize: "0.875em" // please adjust the font size of the code in general.css
});
editor.$blockScrolling = Infinity;

View File

@@ -1,7 +1,7 @@
//! Theme dependencies for the playpen editor.
pub static JS: &'static [u8] = include_bytes!("editor.js");
pub static ACE_JS: &'static [u8] = include_bytes!("ace.js");
pub static MODE_RUST_JS: &'static [u8] = include_bytes!("mode-rust.js");
pub static THEME_DAWN_JS: &'static [u8] = include_bytes!("theme-dawn.js");
pub static THEME_TOMORROW_NIGHT_JS: &'static [u8] = include_bytes!("theme-tomorrow_night.js");
pub static JS: &[u8] = include_bytes!("editor.js");
pub static ACE_JS: &[u8] = include_bytes!("ace.js");
pub static MODE_RUST_JS: &[u8] = include_bytes!("mode-rust.js");
pub static THEME_DAWN_JS: &[u8] = include_bytes!("theme-dawn.js");
pub static THEME_TOMORROW_NIGHT_JS: &[u8] = include_bytes!("theme-tomorrow_night.js");

File diff suppressed because one or more lines are too long

View File

@@ -1 +1,7 @@
ace.define("ace/theme/dawn",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-dawn",t.cssText=".ace-dawn .ace_gutter {background: #ebebeb;color: #333}.ace-dawn .ace_print-margin {width: 1px;background: #e8e8e8}.ace-dawn {background-color: #F9F9F9;color: #080808}.ace-dawn .ace_cursor {color: #000000}.ace-dawn .ace_marker-layer .ace_selection {background: rgba(39, 95, 255, 0.30)}.ace-dawn.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #F9F9F9;}.ace-dawn .ace_marker-layer .ace_step {background: rgb(255, 255, 0)}.ace-dawn .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(75, 75, 126, 0.50)}.ace-dawn .ace_marker-layer .ace_active-line {background: rgba(36, 99, 180, 0.12)}.ace-dawn .ace_gutter-active-line {background-color : #dcdcdc}.ace-dawn .ace_marker-layer .ace_selected-word {border: 1px solid rgba(39, 95, 255, 0.30)}.ace-dawn .ace_invisible {color: rgba(75, 75, 126, 0.50)}.ace-dawn .ace_keyword,.ace-dawn .ace_meta {color: #794938}.ace-dawn .ace_constant,.ace-dawn .ace_constant.ace_character,.ace-dawn .ace_constant.ace_character.ace_escape,.ace-dawn .ace_constant.ace_other {color: #811F24}.ace-dawn .ace_invalid.ace_illegal {text-decoration: underline;font-style: italic;color: #F8F8F8;background-color: #B52A1D}.ace-dawn .ace_invalid.ace_deprecated {text-decoration: underline;font-style: italic;color: #B52A1D}.ace-dawn .ace_support {color: #691C97}.ace-dawn .ace_support.ace_constant {color: #B4371F}.ace-dawn .ace_fold {background-color: #794938;border-color: #080808}.ace-dawn .ace_list,.ace-dawn .ace_markup.ace_list,.ace-dawn .ace_support.ace_function {color: #693A17}.ace-dawn .ace_storage {font-style: italic;color: #A71D5D}.ace-dawn .ace_string {color: #0B6125}.ace-dawn .ace_string.ace_regexp {color: #CF5628}.ace-dawn .ace_comment {font-style: italic;color: #5A525F}.ace-dawn .ace_heading,.ace-dawn .ace_markup.ace_heading {color: #19356D}.ace-dawn .ace_variable {color: #234A97}.ace-dawn .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYLh/5+x/AAizA4hxNNsZAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})
ace.define("ace/theme/dawn",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-dawn",t.cssText=".ace-dawn .ace_gutter {background: #ebebeb;color: #333}.ace-dawn .ace_print-margin {width: 1px;background: #e8e8e8}.ace-dawn {background-color: #F9F9F9;color: #080808}.ace-dawn .ace_cursor {color: #000000}.ace-dawn .ace_marker-layer .ace_selection {background: rgba(39, 95, 255, 0.30)}.ace-dawn.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #F9F9F9;}.ace-dawn .ace_marker-layer .ace_step {background: rgb(255, 255, 0)}.ace-dawn .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgba(75, 75, 126, 0.50)}.ace-dawn .ace_marker-layer .ace_active-line {background: rgba(36, 99, 180, 0.12)}.ace-dawn .ace_gutter-active-line {background-color : #dcdcdc}.ace-dawn .ace_marker-layer .ace_selected-word {border: 1px solid rgba(39, 95, 255, 0.30)}.ace-dawn .ace_invisible {color: rgba(75, 75, 126, 0.50)}.ace-dawn .ace_keyword,.ace-dawn .ace_meta {color: #794938}.ace-dawn .ace_constant,.ace-dawn .ace_constant.ace_character,.ace-dawn .ace_constant.ace_character.ace_escape,.ace-dawn .ace_constant.ace_other {color: #811F24}.ace-dawn .ace_invalid.ace_illegal {text-decoration: underline;font-style: italic;color: #F8F8F8;background-color: #B52A1D}.ace-dawn .ace_invalid.ace_deprecated {text-decoration: underline;font-style: italic;color: #B52A1D}.ace-dawn .ace_support {color: #691C97}.ace-dawn .ace_support.ace_constant {color: #B4371F}.ace-dawn .ace_fold {background-color: #794938;border-color: #080808}.ace-dawn .ace_list,.ace-dawn .ace_markup.ace_list,.ace-dawn .ace_support.ace_function {color: #693A17}.ace-dawn .ace_storage {font-style: italic;color: #A71D5D}.ace-dawn .ace_string {color: #0B6125}.ace-dawn .ace_string.ace_regexp {color: #CF5628}.ace-dawn .ace_comment {font-style: italic;color: #5A525F}.ace-dawn .ace_heading,.ace-dawn .ace_markup.ace_heading {color: #19356D}.ace-dawn .ace_variable {color: #234A97}.ace-dawn .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYLh/5+x/AAizA4hxNNsZAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}); (function() {
ace.require(["ace/theme/dawn"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

View File

@@ -1 +1,7 @@
ace.define("ace/theme/tomorrow_night",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-tomorrow-night",t.cssText=".ace-tomorrow-night .ace_gutter {background: #25282c;color: #C5C8C6}.ace-tomorrow-night .ace_print-margin {width: 1px;background: #25282c}.ace-tomorrow-night {background-color: #1D1F21;color: #C5C8C6}.ace-tomorrow-night .ace_cursor {color: #AEAFAD}.ace-tomorrow-night .ace_marker-layer .ace_selection {background: #373B41}.ace-tomorrow-night.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #1D1F21;}.ace-tomorrow-night .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-tomorrow-night .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #4B4E55}.ace-tomorrow-night .ace_marker-layer .ace_active-line {background: #282A2E}.ace-tomorrow-night .ace_gutter-active-line {background-color: #282A2E}.ace-tomorrow-night .ace_marker-layer .ace_selected-word {border: 1px solid #373B41}.ace-tomorrow-night .ace_invisible {color: #4B4E55}.ace-tomorrow-night .ace_keyword,.ace-tomorrow-night .ace_meta,.ace-tomorrow-night .ace_storage,.ace-tomorrow-night .ace_storage.ace_type,.ace-tomorrow-night .ace_support.ace_type {color: #B294BB}.ace-tomorrow-night .ace_keyword.ace_operator {color: #8ABEB7}.ace-tomorrow-night .ace_constant.ace_character,.ace-tomorrow-night .ace_constant.ace_language,.ace-tomorrow-night .ace_constant.ace_numeric,.ace-tomorrow-night .ace_keyword.ace_other.ace_unit,.ace-tomorrow-night .ace_support.ace_constant,.ace-tomorrow-night .ace_variable.ace_parameter {color: #DE935F}.ace-tomorrow-night .ace_constant.ace_other {color: #CED1CF}.ace-tomorrow-night .ace_invalid {color: #CED2CF;background-color: #DF5F5F}.ace-tomorrow-night .ace_invalid.ace_deprecated {color: #CED2CF;background-color: #B798BF}.ace-tomorrow-night .ace_fold {background-color: #81A2BE;border-color: #C5C8C6}.ace-tomorrow-night .ace_entity.ace_name.ace_function,.ace-tomorrow-night .ace_support.ace_function,.ace-tomorrow-night .ace_variable {color: #81A2BE}.ace-tomorrow-night .ace_support.ace_class,.ace-tomorrow-night .ace_support.ace_type {color: #F0C674}.ace-tomorrow-night .ace_heading,.ace-tomorrow-night .ace_markup.ace_heading,.ace-tomorrow-night .ace_string {color: #B5BD68}.ace-tomorrow-night .ace_entity.ace_name.ace_tag,.ace-tomorrow-night .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow-night .ace_meta.ace_tag,.ace-tomorrow-night .ace_string.ace_regexp,.ace-tomorrow-night .ace_variable {color: #CC6666}.ace-tomorrow-night .ace_comment {color: #969896}.ace-tomorrow-night .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHB3d/8PAAOIAdULw8qMAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})
ace.define("ace/theme/tomorrow_night",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!0,t.cssClass="ace-tomorrow-night",t.cssText=".ace-tomorrow-night .ace_gutter {background: #25282c;color: #C5C8C6}.ace-tomorrow-night .ace_print-margin {width: 1px;background: #25282c}.ace-tomorrow-night {background-color: #1D1F21;color: #C5C8C6}.ace-tomorrow-night .ace_cursor {color: #AEAFAD}.ace-tomorrow-night .ace_marker-layer .ace_selection {background: #373B41}.ace-tomorrow-night.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #1D1F21;}.ace-tomorrow-night .ace_marker-layer .ace_step {background: rgb(102, 82, 0)}.ace-tomorrow-night .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #4B4E55}.ace-tomorrow-night .ace_marker-layer .ace_active-line {background: #282A2E}.ace-tomorrow-night .ace_gutter-active-line {background-color: #282A2E}.ace-tomorrow-night .ace_marker-layer .ace_selected-word {border: 1px solid #373B41}.ace-tomorrow-night .ace_invisible {color: #4B4E55}.ace-tomorrow-night .ace_keyword,.ace-tomorrow-night .ace_meta,.ace-tomorrow-night .ace_storage,.ace-tomorrow-night .ace_storage.ace_type,.ace-tomorrow-night .ace_support.ace_type {color: #B294BB}.ace-tomorrow-night .ace_keyword.ace_operator {color: #8ABEB7}.ace-tomorrow-night .ace_constant.ace_character,.ace-tomorrow-night .ace_constant.ace_language,.ace-tomorrow-night .ace_constant.ace_numeric,.ace-tomorrow-night .ace_keyword.ace_other.ace_unit,.ace-tomorrow-night .ace_support.ace_constant,.ace-tomorrow-night .ace_variable.ace_parameter {color: #DE935F}.ace-tomorrow-night .ace_constant.ace_other {color: #CED1CF}.ace-tomorrow-night .ace_invalid {color: #CED2CF;background-color: #DF5F5F}.ace-tomorrow-night .ace_invalid.ace_deprecated {color: #CED2CF;background-color: #B798BF}.ace-tomorrow-night .ace_fold {background-color: #81A2BE;border-color: #C5C8C6}.ace-tomorrow-night .ace_entity.ace_name.ace_function,.ace-tomorrow-night .ace_support.ace_function,.ace-tomorrow-night .ace_variable {color: #81A2BE}.ace-tomorrow-night .ace_support.ace_class,.ace-tomorrow-night .ace_support.ace_type {color: #F0C674}.ace-tomorrow-night .ace_heading,.ace-tomorrow-night .ace_markup.ace_heading,.ace-tomorrow-night .ace_string {color: #B5BD68}.ace-tomorrow-night .ace_entity.ace_name.ace_tag,.ace-tomorrow-night .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow-night .ace_meta.ace_tag,.ace-tomorrow-night .ace_string.ace_regexp,.ace-tomorrow-night .ace_variable {color: #CC6666}.ace-tomorrow-night .ace_comment {color: #969896}.ace-tomorrow-night .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWNgYGBgYHB3d/8PAAOIAdULw8qMAAAAAElFTkSuQmCC) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}); (function() {
ace.require(["ace/theme/tomorrow_night"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

View File

@@ -1,6 +1,6 @@
//! Theme dependencies for in-browser search. Not included in mdbook when
//! the "search" cargo feature is disabled.
pub static JS: &'static [u8] = include_bytes!("searcher.js");
pub static MARK_JS: &'static [u8] = include_bytes!("mark.min.js");
pub static ELASTICLUNR_JS: &'static [u8] = include_bytes!("elasticlunr.min.js");
pub static JS: &[u8] = include_bytes!("searcher.js");
pub static MARK_JS: &[u8] = include_bytes!("mark.min.js");
pub static ELASTICLUNR_JS: &[u8] = include_bytes!("elasticlunr.min.js");

View File

@@ -94,3 +94,11 @@
.xml .hljs-cdata {
opacity: 0.5;
}
.hljs-addition {
color: #718c00;
}
.hljs-deletion {
color: #c82829;
}

View File

@@ -1,21 +1,9 @@
use errors::*;
use crate::errors::*;
use std::convert::Into;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::io::Write;
use std::path::{Component, Path, PathBuf};
/// Takes a path to a file and try to read the file into a String
pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
let path = path.as_ref();
let mut content = String::new();
File::open(path)
.chain_err(|| "Unable to open the file")?
.read_to_string(&mut content)
.chain_err(|| "Unable to read the file")?;
Ok(content)
}
/// Naively replaces any path seperator with a forward-slash '/'
pub fn normalize_path(path: &str) -> String {
use std::path::is_separator;
@@ -28,7 +16,7 @@ pub fn normalize_path(path: &str) -> String {
pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8]) -> Result<()> {
let path = build_dir.join(filename);
create_file(&path)?.write_all(content).map_err(|e| e.into())
create_file(&path)?.write_all(content).map_err(Into::into)
}
/// Takes a path and returns a path containing just enough `../` to point to
@@ -38,8 +26,6 @@ pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8])
/// directory from where the path starts.
///
/// ```rust
/// # extern crate mdbook;
/// #
/// # use std::path::Path;
/// # use mdbook::utils::fs::path_to_root;
/// #
@@ -85,7 +71,7 @@ pub fn create_file(path: &Path) -> Result<File> {
fs::create_dir_all(p)?;
}
File::create(path).map_err(|e| e.into())
File::create(path).map_err(Into::into)
}
/// Removes all the content of a directory but not the directory itself
@@ -187,8 +173,6 @@ pub fn copy_files_except_ext(
#[cfg(test)]
mod tests {
extern crate tempfile;
use super::copy_files_except_ext;
use std::fs;
@@ -196,43 +180,44 @@ mod tests {
fn copy_files_except_ext_test() {
let tmp = match tempfile::TempDir::new() {
Ok(t) => t,
Err(_) => panic!("Could not create a temp dir"),
Err(e) => panic!("Could not create a temp dir: {}", e),
};
// Create a couple of files
if let Err(_) = fs::File::create(&tmp.path().join("file.txt")) {
panic!("Could not create file.txt")
if let Err(err) = fs::File::create(&tmp.path().join("file.txt")) {
panic!("Could not create file.txt: {}", err);
}
if let Err(_) = fs::File::create(&tmp.path().join("file.md")) {
panic!("Could not create file.md")
if let Err(err) = fs::File::create(&tmp.path().join("file.md")) {
panic!("Could not create file.md: {}", err);
}
if let Err(_) = fs::File::create(&tmp.path().join("file.png")) {
panic!("Could not create file.png")
if let Err(err) = fs::File::create(&tmp.path().join("file.png")) {
panic!("Could not create file.png: {}", err);
}
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir")) {
panic!("Could not create sub_dir")
if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir")) {
panic!("Could not create sub_dir: {}", err);
}
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
panic!("Could not create sub_dir/file.png")
if let Err(err) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
panic!("Could not create sub_dir/file.png: {}", err);
}
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
panic!("Could not create sub_dir_exists")
if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
panic!("Could not create sub_dir_exists: {}", err);
}
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
panic!("Could not create sub_dir_exists/file.txt")
if let Err(err) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
panic!("Could not create sub_dir_exists/file.txt: {}", err);
}
// Create output dir
if let Err(_) = fs::create_dir(&tmp.path().join("output")) {
panic!("Could not create output")
if let Err(err) = fs::create_dir(&tmp.path().join("output")) {
panic!("Could not create output: {}", err);
}
if let Err(_) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
panic!("Could not create output/sub_dir_exists")
if let Err(err) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
panic!("Could not create output/sub_dir_exists: {}", err);
}
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
Err(e) => panic!("Error while executing the function:\n{:?}", e),
Ok(_) => {}
if let Err(e) =
copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"])
{
panic!("Error while executing the function:\n{:?}", e);
}
// Check if the correct files where created

View File

@@ -2,28 +2,32 @@
pub mod fs;
mod string;
use errors::Error;
use crate::errors::Error;
use regex::Regex;
use pulldown_cmark::{
html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES, OPTION_ENABLE_TABLES,
};
use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag};
use std::borrow::Cow;
use std::fmt::Write;
use std::path::Path;
pub use self::string::{take_lines, RangeArgument};
pub use self::string::{
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
take_rustdoc_include_lines,
};
/// Replaces multiple consecutive whitespace characters with a single space character.
pub fn collapse_whitespace<'a>(text: &'a str) -> Cow<'a, str> {
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
lazy_static! {
static ref RE: Regex = Regex::new(r"\s\s+").unwrap();
}
RE.replace_all(text, " ")
}
/// Convert the given string to a valid HTML element ID
/// Convert the given string to a valid HTML element ID.
/// The only restriction is that the ID must not contain any ASCII whitespace.
pub fn normalize_id(content: &str) -> String {
let mut ret = content
content
.chars()
.filter_map(|ch| {
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
@@ -33,16 +37,8 @@ pub fn normalize_id(content: &str) -> String {
} else {
None
}
}).collect::<String>();
// Ensure that the first character is [A-Za-z]
if ret
.chars()
.next()
.map_or(false, |c| !c.is_ascii_alphabetic())
{
ret.insert(0, 'a');
}
ret
})
.collect::<String>()
}
/// Generate an ID for use with anchors which is derived from a "normalised"
@@ -69,50 +65,125 @@ pub fn id_from_content(content: &str) -> String {
}
// Remove spaces and hashes indicating a header
let trimmed = content.trim().trim_left_matches('#').trim();
let trimmed = content.trim().trim_start_matches('#').trim();
normalize_id(trimmed)
}
fn adjust_links(event: Event) -> Event {
/// Fix links to the correct location.
///
/// This adjusts links, such as turning `.md` extensions to `.html`.
///
/// `path` is the path to the page being rendered relative to the root of the
/// book. This is used for the `print.html` page so that links on the print
/// page go to the original location. Normal page rendering sets `path` to
/// None. Ideally, print page links would link to anchors on the print page,
/// but that is very difficult.
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
lazy_static! {
static ref HTTP_LINK: Regex = Regex::new("^https?://").unwrap();
static ref MD_LINK: Regex = Regex::new("(?P<link>.*).md(?P<anchor>#.*)?").unwrap();
static ref SCHEME_LINK: Regex = Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap();
static ref MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap();
}
match event {
Event::Start(Tag::Link(dest, title)) => {
if !HTTP_LINK.is_match(&dest) {
if let Some(caps) = MD_LINK.captures(&dest) {
let mut html_link = [&caps["link"], ".html"].concat();
if let Some(anchor) = caps.name("anchor") {
html_link.push_str(anchor.as_str());
}
return Event::Start(Tag::Link(Cow::from(html_link), title));
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
if dest.starts_with('#') {
// Fragment-only link.
if let Some(path) = path {
let mut base = path.display().to_string();
if base.ends_with(".md") {
base.replace_range(base.len() - 3.., ".html");
}
return format!("{}{}", base, dest).into();
} else {
return dest;
}
}
// Don't modify links with schemes like `https`.
if !SCHEME_LINK.is_match(&dest) {
// This is a relative link, adjust it as necessary.
let mut fixed_link = String::new();
if let Some(path) = path {
let base = path
.parent()
.expect("path can't be empty")
.to_str()
.expect("utf-8 paths only");
if !base.is_empty() {
write!(fixed_link, "{}/", base).unwrap();
}
}
Event::Start(Tag::Link(dest, title))
if let Some(caps) = MD_LINK.captures(&dest) {
fixed_link.push_str(&caps["link"]);
fixed_link.push_str(".html");
if let Some(anchor) = caps.name("anchor") {
fixed_link.push_str(anchor.as_str());
}
} else {
fixed_link.push_str(&dest);
};
return CowStr::from(fixed_link);
}
dest
}
fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
// This is a terrible hack, but should be reasonably reliable. Nobody
// should ever parse a tag with a regex. However, there isn't anything
// in Rust that I know of that is suitable for handling partial html
// fragments like those generated by pulldown_cmark.
//
// There are dozens of HTML tags/attributes that contain paths, so
// feel free to add more tags if desired; these are the only ones I
// care about right now.
lazy_static! {
static ref HTML_LINK: Regex =
Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap();
}
HTML_LINK
.replace_all(&html, |caps: &regex::Captures<'_>| {
let fixed = fix(caps[2].into(), path);
format!("{}{}\"", &caps[1], fixed)
})
.into_owned()
.into()
}
match event {
Event::Start(Tag::Link(link_type, dest, title)) => {
Event::Start(Tag::Link(link_type, fix(dest, path), title))
}
Event::Start(Tag::Image(link_type, dest, title)) => {
Event::Start(Tag::Image(link_type, fix(dest, path), title))
}
Event::Html(html) => Event::Html(fix_html(html, path)),
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
_ => event,
}
}
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
render_markdown_with_path(text, curly_quotes, None)
}
pub fn new_cmark_parser(text: &str) -> Parser<'_> {
let mut opts = Options::empty();
opts.insert(OPTION_ENABLE_TABLES);
opts.insert(OPTION_ENABLE_FOOTNOTES);
opts.insert(Options::ENABLE_TABLES);
opts.insert(Options::ENABLE_FOOTNOTES);
opts.insert(Options::ENABLE_STRIKETHROUGH);
opts.insert(Options::ENABLE_TASKLISTS);
Parser::new_ext(text, opts)
}
let p = Parser::new_ext(text, opts);
pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
let p = new_cmark_parser(text);
let mut converter = EventQuoteConverter::new(curly_quotes);
let events = p
.map(clean_codeblock_headers)
.map(adjust_links)
.map(|event| adjust_links(event, path))
.map(|event| converter.convert(event));
html::push_html(&mut s, events);
@@ -127,7 +198,7 @@ struct EventQuoteConverter {
impl EventQuoteConverter {
fn new(enabled: bool) -> Self {
EventQuoteConverter {
enabled: enabled,
enabled,
convert_text: true,
}
}
@@ -138,28 +209,28 @@ impl EventQuoteConverter {
}
match event {
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
Event::Start(Tag::CodeBlock(_)) => {
self.convert_text = false;
event
}
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
Event::End(Tag::CodeBlock(_)) => {
self.convert_text = true;
event
}
Event::Text(ref text) if self.convert_text => {
Event::Text(Cow::from(convert_quotes_to_curly(text)))
Event::Text(CowStr::from(convert_quotes_to_curly(text)))
}
_ => event,
}
}
}
fn clean_codeblock_headers(event: Event) -> Event {
fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
match event {
Event::Start(Tag::CodeBlock(ref info)) => {
let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect();
Event::Start(Tag::CodeBlock(Cow::from(info)))
Event::Start(Tag::CodeBlock(CowStr::from(info)))
}
_ => event,
}
@@ -193,7 +264,8 @@ fn convert_quotes_to_curly(original_text: &str) -> String {
preceded_by_whitespace = original_char.is_whitespace();
converted_char
}).collect()
})
.collect()
}
/// Prints a "backtrace" of some `Error`.
@@ -228,6 +300,12 @@ mod tests {
render_markdown("[example_anchor](example.md#anchor)", false),
"<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
);
// this anchor contains 'md' inside of it
assert_eq!(
render_markdown("[phantom data](foo.html#phantomdata)", false),
"<p><a href=\"foo.html#phantomdata\">phantom data</a></p>\n"
);
}
#[test]
@@ -328,28 +406,42 @@ more text with spaces
#[test]
fn it_generates_anchors() {
assert_eq!(
id_from_content("## `--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes"
);
assert_eq!(
id_from_content("## Method-call expressions"),
"method-call-expressions"
);
assert_eq!(id_from_content("## **Bold** title"), "bold-title");
assert_eq!(id_from_content("## `Code` title"), "code-title");
}
#[test]
fn it_generates_anchors_from_non_ascii_initial() {
assert_eq!(
id_from_content("## `--passes`: add more rustdoc passes"),
"--passes-add-more-rustdoc-passes"
);
assert_eq!(
id_from_content("## 中文標題 CJK title"),
"中文標題-cjk-title"
);
assert_eq!(id_from_content("## Über"), "Über");
}
#[test]
fn it_normalizes_ids() {
assert_eq!(
normalize_id("`--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes"
"--passes-add-more-rustdoc-passes"
);
assert_eq!(
normalize_id("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-"
);
assert_eq!(normalize_id("_-_12345"), "a_-_12345");
assert_eq!(normalize_id("12345"), "a12345");
assert_eq!(normalize_id("_-_12345"), "_-_12345");
assert_eq!(normalize_id("12345"), "12345");
assert_eq!(normalize_id("中文"), "中文");
assert_eq!(normalize_id("にほんご"), "にほんご");
assert_eq!(normalize_id("한국어"), "한국어");
assert_eq!(normalize_id(""), "");
}
}
@@ -359,18 +451,12 @@ more text with spaces
#[test]
fn it_converts_single_quotes() {
assert_eq!(
convert_quotes_to_curly("'one', 'two'"),
"one, two"
);
assert_eq!(convert_quotes_to_curly("'one', 'two'"), "one, two");
}
#[test]
fn it_converts_double_quotes() {
assert_eq!(
convert_quotes_to_curly(r#""one", "two""#),
"“one”, “two”"
);
assert_eq!(convert_quotes_to_curly(r#""one", "two""#), "“one”, “two”");
}
#[test]

View File

@@ -1,63 +1,124 @@
use itertools::Itertools;
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
// This trait is already contained in the standard lib, however it is unstable.
// TODO: Remove when the `collections_range` feature stabilises
// (https://github.com/rust-lang/rust/issues/30877)
pub trait RangeArgument<T: ?Sized> {
fn start(&self) -> Option<&T>;
fn end(&self) -> Option<&T>;
}
impl<T: ?Sized> RangeArgument<T> for RangeFull {
fn start(&self) -> Option<&T> {
None
}
fn end(&self) -> Option<&T> {
None
}
}
impl<T> RangeArgument<T> for RangeFrom<T> {
fn start(&self) -> Option<&T> {
Some(&self.start)
}
fn end(&self) -> Option<&T> {
None
}
}
impl<T> RangeArgument<T> for RangeTo<T> {
fn start(&self) -> Option<&T> {
None
}
fn end(&self) -> Option<&T> {
Some(&self.end)
}
}
impl<T> RangeArgument<T> for Range<T> {
fn start(&self) -> Option<&T> {
Some(&self.start)
}
fn end(&self) -> Option<&T> {
Some(&self.end)
}
}
use regex::Regex;
use std::ops::Bound::{Excluded, Included, Unbounded};
use std::ops::RangeBounds;
/// Take a range of lines from a string.
pub fn take_lines<R: RangeArgument<usize>>(s: &str, range: R) -> String {
let start = *range.start().unwrap_or(&0);
pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
let start = match range.start_bound() {
Excluded(&n) => n + 1,
Included(&n) => n,
Unbounded => 0,
};
let mut lines = s.lines().skip(start);
match range.end() {
Some(&end) => lines.take(end.saturating_sub(start)).join("\n"),
None => lines.join("\n"),
match range.end_bound() {
Excluded(end) => lines.take(end.saturating_sub(start)).join("\n"),
Included(end) => lines.take((end + 1).saturating_sub(start)).join("\n"),
Unbounded => lines.join("\n"),
}
}
lazy_static! {
static ref ANCHOR_START: Regex = Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap();
static ref ANCHOR_END: Regex = Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap();
}
/// Take anchored lines from a string.
/// Lines containing anchor are ignored.
pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
let mut retained = Vec::<&str>::new();
let mut anchor_found = false;
for l in s.lines() {
if anchor_found {
match ANCHOR_END.captures(l) {
Some(cap) => {
if &cap["anchor_name"] == anchor {
break;
}
}
None => {
if !ANCHOR_START.is_match(l) {
retained.push(l);
}
}
}
} else {
if let Some(cap) = ANCHOR_START.captures(l) {
if &cap["anchor_name"] == anchor {
anchor_found = true;
}
}
}
}
retained.join("\n")
}
/// Keep lines contained within the range specified as-is.
/// For any lines not in the range, include them but use `#` at the beginning. This will hide the
/// lines from initial display but include them when expanding the code snippet or testing with
/// rustdoc.
pub fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
let mut output = String::with_capacity(s.len());
for (index, line) in s.lines().enumerate() {
if !range.contains(&index) {
output.push_str("# ");
}
output.push_str(line);
output.push_str("\n");
}
output.pop();
output
}
/// Keep lines between the anchor comments specified as-is.
/// For any lines not between the anchors, include them but use `#` at the beginning. This will
/// hide the lines from initial display but include them when expanding the code snippet or testing
/// with rustdoc.
pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
let mut output = String::with_capacity(s.len());
let mut within_anchored_section = false;
for l in s.lines() {
if within_anchored_section {
match ANCHOR_END.captures(l) {
Some(cap) => {
if &cap["anchor_name"] == anchor {
within_anchored_section = false;
}
}
None => {
if !ANCHOR_START.is_match(l) {
output.push_str(l);
output.push_str("\n");
}
}
}
} else {
if let Some(cap) = ANCHOR_START.captures(l) {
if &cap["anchor_name"] == anchor {
within_anchored_section = true;
}
} else if !ANCHOR_END.is_match(l) {
output.push_str("# ");
output.push_str(l);
output.push_str("\n");
}
}
}
output.pop();
output
}
#[cfg(test)]
mod tests {
use super::take_lines;
use super::{
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
take_rustdoc_include_lines,
};
#[test]
fn take_lines_test() {
@@ -70,4 +131,122 @@ mod tests {
assert_eq!(take_lines(s, 4..3), "");
assert_eq!(take_lines(s, ..100), s);
}
#[test]
fn take_anchored_lines_test() {
let s = "Lorem\nipsum\ndolor\nsit\namet";
assert_eq!(take_anchored_lines(s, "test"), "");
let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet";
assert_eq!(take_anchored_lines(s, "test"), "");
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet";
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");
let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
assert_eq!(take_anchored_lines(s, "test"), "ipsum\ndolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");
let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum";
assert_eq!(
take_anchored_lines(s, "test2"),
"ipsum\ndolor\nsit\namet\nlorem"
);
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");
}
#[test]
fn take_rustdoc_include_lines_test() {
let s = "Lorem\nipsum\ndolor\nsit\namet";
assert_eq!(
take_rustdoc_include_lines(s, 1..3),
"# Lorem\nipsum\ndolor\n# sit\n# amet"
);
assert_eq!(
take_rustdoc_include_lines(s, 3..),
"# Lorem\n# ipsum\n# dolor\nsit\namet"
);
assert_eq!(
take_rustdoc_include_lines(s, ..3),
"Lorem\nipsum\ndolor\n# sit\n# amet"
);
assert_eq!(take_rustdoc_include_lines(s, ..), s);
// corner cases
assert_eq!(
take_rustdoc_include_lines(s, 4..3),
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
);
assert_eq!(take_rustdoc_include_lines(s, ..100), s);
}
#[test]
fn take_rustdoc_include_anchored_lines_test() {
let s = "Lorem\nipsum\ndolor\nsit\namet";
assert_eq!(
take_rustdoc_include_anchored_lines(s, "test"),
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
);
let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet";
assert_eq!(
take_rustdoc_include_anchored_lines(s, "test"),
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
);
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet";
assert_eq!(
take_rustdoc_include_anchored_lines(s, "test"),
"# Lorem\n# ipsum\ndolor\nsit\namet"
);
assert_eq!(
take_rustdoc_include_anchored_lines(s, "something"),
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
);
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
assert_eq!(
take_rustdoc_include_anchored_lines(s, "test"),
"# Lorem\n# ipsum\ndolor\nsit\namet\n# lorem\n# ipsum"
);
assert_eq!(
take_rustdoc_include_anchored_lines(s, "something"),
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum"
);
let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
assert_eq!(
take_rustdoc_include_anchored_lines(s, "test"),
"# Lorem\nipsum\ndolor\nsit\namet\n# lorem\n# ipsum"
);
assert_eq!(
take_rustdoc_include_anchored_lines(s, "something"),
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum"
);
let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum";
assert_eq!(
take_rustdoc_include_anchored_lines(s, "test2"),
"# Lorem\nipsum\ndolor\nsit\namet\nlorem\n# ipsum"
);
assert_eq!(
take_rustdoc_include_anchored_lines(s, "test"),
"# Lorem\n# ipsum\ndolor\nsit\namet\n# lorem\n# ipsum"
);
assert_eq!(
take_rustdoc_include_anchored_lines(s, "something"),
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum"
);
let s = "Lorem\nANCHOR: test\nipsum\nANCHOR_END: test\ndolor\nANCHOR: test\nsit\nANCHOR_END: test\namet";
assert_eq!(
take_rustdoc_include_anchored_lines(s, "test"),
"# Lorem\nipsum\n# dolor\nsit\n# amet"
);
}
}

View File

@@ -1,7 +1,4 @@
//! Integration tests to make sure alternate backends work.
extern crate mdbook;
extern crate tempfile;
//! Integration tests to make sure alternative backends work.
use mdbook::config::Config;
use mdbook::MDBook;
@@ -11,14 +8,14 @@ use tempfile::{Builder as TempFileBuilder, TempDir};
#[test]
fn passing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("passing", "true");
let (md, _temp) = dummy_book_with_backend("passing", success_cmd());
md.build().unwrap();
}
#[test]
fn failing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("failing", "false");
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd());
md.build().unwrap_err();
}
@@ -84,3 +81,19 @@ fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) {
(md, temp)
}
fn fail_cmd() -> &'static str {
if cfg!(windows) {
r#"cmd.exe /c "exit 1""#
} else {
"false"
}
}
fn success_cmd() -> &'static str {
if cfg!(windows) {
r#"cmd.exe /c "exit 0""#
} else {
"true"
}
}

View File

@@ -1,8 +1,6 @@
extern crate mdbook;
mod dummy_book;
use dummy_book::DummyBook;
use crate::dummy_book::DummyBook;
use mdbook::book::Book;
use mdbook::config::Config;
use mdbook::errors::*;
@@ -52,7 +50,7 @@ fn mdbook_runs_preprocessors() {
let cfg = Config::default();
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
book.with_preprecessor(Spy(Arc::clone(&spy)));
book.with_preprocessor(Spy(Arc::clone(&spy)));
book.build().unwrap();
let inner = spy.lock().unwrap();

View File

@@ -0,0 +1,56 @@
mod dummy_book;
use crate::dummy_book::DummyBook;
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
use mdbook::MDBook;
fn example() -> CmdPreprocessor {
CmdPreprocessor::new(
"nop-preprocessor".to_string(),
"cargo run --example nop-preprocessor --".to_string(),
)
}
#[test]
fn example_supports_whatever() {
let cmd = example();
let got = cmd.supports_renderer("whatever");
assert_eq!(got, true);
}
#[test]
fn example_doesnt_support_not_supported() {
let cmd = example();
let got = cmd.supports_renderer("not-supported");
assert_eq!(got, false);
}
#[test]
fn ask_the_preprocessor_to_blow_up() {
let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.with_preprocessor(example());
md.config
.set("preprocessor.nop-preprocessor.blow-up", true)
.unwrap();
let got = md.build();
assert!(got.is_err());
}
#[test]
fn process_the_dummy_book() {
let dummy_book = DummyBook::new();
let temp = dummy_book.build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.with_preprocessor(example());
md.build().unwrap();
}

View File

@@ -3,21 +3,15 @@
// Not all features are used in all test crates, so...
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
extern crate mdbook;
extern crate tempfile;
extern crate walkdir;
use mdbook::errors::*;
use mdbook::utils::fs::file_to_string;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
// The funny `self::` here is because we've got an `extern crate ...` and are
// in a submodule
use self::mdbook::MDBook;
use self::tempfile::{Builder as TempFileBuilder, TempDir};
use self::walkdir::WalkDir;
use mdbook::MDBook;
use tempfile::{Builder as TempFileBuilder, TempDir};
use walkdir::WalkDir;
/// Create a dummy book in a temporary directory, using the contents of
/// `SUMMARY_MD` as a guide.
@@ -58,15 +52,24 @@ impl DummyBook {
})?;
let sub_pattern = if self.passing_test { "true" } else { "false" };
let file_containing_test = temp.path().join("src/first/nested.md");
replace_pattern_in_file(&file_containing_test, "$TEST_STATUS", sub_pattern)?;
let files_containing_tests = [
"src/first/nested.md",
"src/first/nested-test.rs",
"src/first/nested-test-with-anchors.rs",
"src/first/partially-included-test.rs",
"src/first/partially-included-test-with-anchors.rs",
];
for file in &files_containing_tests {
let path_containing_tests = temp.path().join(file);
replace_pattern_in_file(&path_containing_tests, "$TEST_STATUS", sub_pattern)?;
}
Ok(temp)
}
}
fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()> {
let contents = file_to_string(filename)?;
let contents = fs::read_to_string(filename)?;
File::create(filename)?.write_all(contents.replace(from, to).as_bytes())?;
Ok(())
@@ -76,7 +79,7 @@ fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()>
/// the list of strings asserting that the file contains all of them.
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let filename = filename.as_ref();
let content = file_to_string(filename).expect("Couldn't read the file's contents");
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(
@@ -91,7 +94,7 @@ pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
pub fn assert_doesnt_contain_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let filename = filename.as_ref();
let content = file_to_string(filename).expect("Couldn't read the file's contents");
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(

View File

@@ -1,13 +1,19 @@
# Summary
[Dummy Book](README.md)
---
[Introduction](intro.md)
- [First Chapter](first/index.md)
- [Nested Chapter](first/nested.md)
- [Includes](first/includes.md)
- [Recursive](first/recursive.md)
- [Markdown](first/markdown.md)
- [Unicode](first/unicode.md)
- [Second Chapter](second.md)
- [Nested Chapter](second/nested.md)
---

View File

@@ -0,0 +1,29 @@
# Markdown tests
Tests for some markdown output.
## Tables
| foo | bar |
| --- | --- |
| baz | bim |
## Footnotes
Footnote example[^1], or with a word[^word].
[^1]: This is a footnote.
[^word]: A longer footnote.
With multiple lines.
Third line.
## Strikethrough
~~strikethrough example~~
## Tasklisks
- [X] Apples
- [X] Broccoli
- [ ] Carrots

View File

@@ -0,0 +1,11 @@
// The next line will cause a `testing` test to fail if the anchor feature is broken in such a way
// that the whole file gets mistakenly included.
assert!(!$TEST_STATUS);
// ANCHOR: myanchor
// ANCHOR: unendinganchor
// The next line will cause a `rendered_output` test to fail if the anchor feature is broken in
// such a way that the content between anchors isn't included.
// unique-string-for-anchor-test
assert!($TEST_STATUS);
// ANCHOR_END: myanchor

View File

@@ -0,0 +1 @@
assert!($TEST_STATUS);

View File

@@ -7,3 +7,25 @@ assert!($TEST_STATUS);
```
## Some Section
```rust
{{#include nested-test.rs}}
```
## Anchors include the part of a file between special comments
```rust
{{#include nested-test-with-anchors.rs:myanchor}}
```
## Rustdoc include adds the rest of the file as hidden
```rust
{{#rustdoc_include partially-included-test.rs:5:7}}
```
## Rustdoc include works with anchors too
```rust
{{#rustdoc_include partially-included-test-with-anchors.rs:rustdoc-include-anchor}}
```

View File

@@ -0,0 +1,11 @@
fn some_other_function() {
// ANCHOR: unused-anchor-that-should-be-stripped
assert!($TEST_STATUS);
// ANCHOR_END: unused-anchor-that-should-be-stripped
}
// ANCHOR: rustdoc-include-anchor
fn main() {
some_other_function();
}
// ANCHOR_END: rustdoc-include-anchor

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
extern crate mdbook;
extern crate tempfile;
use mdbook::config::Config;
use mdbook::MDBook;
use std::fs;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use tempfile::Builder as TempFileBuilder;
@@ -27,6 +26,36 @@ fn base_mdbook_init_should_create_default_content() {
}
}
/// Run `mdbook init` in a directory containing a SUMMARY.md should create the
/// files listed in the summary.
#[test]
fn run_mdbook_init_should_create_content_from_summary() {
let created_files = vec!["intro.md", "first.md", "outro.md"];
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let src_dir = temp.path().join("src");
fs::create_dir_all(src_dir.clone()).unwrap();
static SUMMARY: &str = r#"# Summary
[intro](intro.md)
- [First chapter](first.md)
[outro](outro.md)
"#;
let mut summary = File::create(src_dir.join("SUMMARY.md")).unwrap();
summary.write_all(SUMMARY.as_bytes()).unwrap();
MDBook::init(temp.path()).build().unwrap();
for file in &created_files {
let target = src_dir.join(file);
println!("{}", target.display());
assert!(target.exists(), "{} doesn't exist", file);
}
}
/// Set some custom arguments for where to place the source and destination
/// files, then call `mdbook init`.
#[test]

Some files were not shown because too many files have changed in this diff Show More