Compare commits

...

249 Commits

Author SHA1 Message Date
Michael Bryan
fe3f2ee4b1 Removed a deprecation warning 2018-04-07 06:27:58 +08:00
Michael Bryan
9784d7a23b Updated dependencies 2018-04-07 06:15:52 +08:00
Michael Bryan
38c06f3c39 Removed all the unnecessary CI jobs 2018-04-07 06:15:26 +08:00
Bastien Orivel
55f7ed1c37 Replace tempdir by tempfile (#650)
The former has been deprecated in favor of the latter
2018-03-27 07:47:37 +08:00
Anders Rasmussen
eb0f7179ab Use git config to get author name in mdbook init (#649)
* Use `git config` to get author name in `mdbook init`

* Return `None` if `git` command fails

* Use `.ok()?` to convert from Result to Option and return early if `None`
2018-03-26 22:37:11 +08:00
Matt Ickstadt
5fb3675151 Update elasticlunr-rs (#646)
* Update dependencies

* Use config structs from elasticlunr-rs

* Update searchindex fixture
2018-03-20 20:22:35 +08:00
Michael Bryan
77b4f6a940 (cargo-release) start next development iteration 0.1.6-alpha.0 2018-03-16 07:38:05 +08:00
Michael Bryan
6308da699a (cargo-release) version 0.1.5 2018-03-16 07:36:40 +08:00
Guillaume Gomez
62a727c041 Fix search (#645) 2018-03-16 07:37:08 +08:00
Michael Bryan
3bc5d907f4 Merge pull request #641 from rust-lang-nursery/pre-release
Pre release
2018-03-15 09:24:26 +08:00
Michael Bryan
3cd12e7092 (cargo-release) start next development iteration 0.1.5-alpha.0 2018-03-15 09:22:59 +08:00
Michael Bryan
c8bbfd4bc1 (cargo-release) version 0.1.4 2018-03-15 09:21:49 +08:00
Michael Bryan
d48a27f94f Updated the appveyor CI image 2018-03-15 09:19:58 +08:00
Michael Bryan
8c456666ff Added a stability warning to the API docs 2018-03-14 23:48:57 +08:00
Michael Bryan
48b0f547c5 Updated serde dependencies 2018-03-14 23:48:57 +08:00
Michael Bryan
867fbfec05 Updated the call site for handlebars rendering 2018-03-14 23:48:56 +08:00
Michael Bryan
951c873df6 Updated deps 2018-03-14 23:48:56 +08:00
Michael Bryan
4af155e963 Exposed the sections inside a book (#642) 2018-03-14 23:47:17 +08:00
Dylan Maccora
07719a8e0e Adding for content to book.toml on init (#627)
* Obtaining author name from gitconfig

* Writing theme to config on init

* Addressing a FIXME came across

* Add request for book title.
2018-03-14 23:27:56 +08:00
Guillaume Gomez
cc92d665ca Improve css so anchor don't go under sidebar (#638) 2018-03-14 23:23:55 +08:00
Pawel Duzinkiewicz
b86533b2a1 pulldown-cmark updated to 0.1.2, fixmes removed, new cargo.lock generated. (#639) 2018-03-11 22:17:38 +08:00
Matt Ickstadt
b2ad669c61 Search with Elasticlunr, updated (#604)
* Add search with elasticlunr.js

This commit adds search functionality to mdBook, based on work done by @phaiax. The in-browser search code uses elasticlunr.js to execute the search, using an index generated at book build time by elasticlunr-rs.

* Add generator comment
Someone on Reddit was wondering how the rust book was generated and said they checked the source. Thought I'd put this here. Might be a good idea to have a little footer "made with mdBook", but this'll do for now.

* Remove search/editor file override behavior

* Use for loop for book iterator

* Improve HTML regex

* Fix search CORS in file URIs

* Use ammonia to sanitize HTML

* Filter html5ever log messages
2018-03-07 21:02:06 +08:00
Sebastian Thiel
bb043ef660 Add complete preprocessor example (#629)
* First version of preprocessor example, with quicli

It seems it's not worth it right now.

* Remove quicli, just to simplify everything

* Finish de-emphasise example

* Finish preprocessor example in book

* Rename preprocessor type

* Apply changes requested in review

* Update preprocessor docs with latest code

[skip CI]
2018-02-24 18:14:52 +08:00
Sorin Davidoi
82aef1bc3f fix(theme/book): Workaround focusout bug in macOS and iOS (#630) 2018-02-24 17:23:45 +08:00
Dylan Maccora
38c883e1ef Changing clap settings (#624) 2018-02-18 15:10:47 +08:00
Sorin Davidoi
8a00a004d8 Handle some cases when JavaScript is disabled (#614)
* feat(theme/index): Assume the sidebar is initially visible

In case the inline script does not execute, the fallback is to show the sidebar.

* feat(theme/index): Hide sidebar toggle and theme selector buttons when JavaScript is disabled

Makes no sense to show them in this case since they do not work.
2018-02-18 15:05:15 +08:00
Bulat Musin
6af77a7792 Update documentation to reflect addition of clean subcommand. (#607)
* update documentation

Update README.md and User Guide to reflect addition of `clean`
subcommand. Do minor spelling fixes too.

* fix grammar in `clean` documentation
2018-02-18 15:04:04 +08:00
Michael Bryan
b5ca820345 (cargo-release) start next development iteration 0.1.4-alpha.0 2018-02-16 07:47:35 +08:00
Michael Bryan
b765023da3 (cargo-release) version 0.1.3 2018-02-16 07:45:52 +08:00
Sorin Davidoi
d306aed587 Accessibility improvements (#611)
* fix(theme/book/themes): Check for control keys in event listener

* fix(theme/index): Menu role for theme selector

* fix(theme/book/themes): Handle focus when toggling theme list

* feat(theme/book/themes): Handle ArrowUp, ArrowDown, Home and End
2018-02-15 07:37:19 +08:00
Sorin Davidoi
89a5dbaf9a fix(theme/stylus/sidebar): Contain scrolling to the sidebar (#612)
> A position fixed left navigation bar does not want to hand off scrolling to the document because a scroll gesture performed on the navigation bar is almost never meant to scroll the document. In this case, the author can use contain on the sidebar to prevent scrolling from being chained to the parent document element.

https://wicg.github.io/overscroll-behavior/#motivating-examples
2018-02-15 07:24:39 +08:00
Ryan Scheel
6961247f56 Include Cargo.lock (#620) 2018-02-15 07:03:10 +08:00
Sorin Davidoi
07551760c9 feat(theme/stylus/sidebar): Reduce padding on non-touch devices (#615)
Closes #594.
2018-02-15 06:59:55 +08:00
Sorin Davidoi
990daceed5 feat(theme/book): Scroll to top when clicking the page title (#613)
Common pattern, especially on mobile devices where the page can be quite long.
2018-02-09 18:34:18 +08:00
Mathieu David
2989096188 Merge pull request #609 from bchatard/typo
Fix typo in format configuration
2018-02-05 15:17:24 +01:00
bchatard
03c6c44e5b Fix typo in format configuration 2018-02-05 13:01:17 +01:00
Ofek Lev
31a370d149 fix readme (#606) 2018-02-05 07:11:55 +08:00
Bulat Musin
0bc1030a02 implement clean subcommand (#583) 2018-02-04 21:00:29 +08:00
boxdot
43fcd00cd5 Inline footnotes. (#600) 2018-02-02 20:15:48 +08:00
Mathieu David
3d83b784b3 Merge pull request #601 from tshepang/patch-1
doc: small fixes
2018-02-01 11:31:55 +01:00
Tshepang Lekhonkhobe
5d42738a79 doc: small fixes 2018-02-01 03:29:39 +02:00
Michael Bryan
1f4dab3e5c (cargo-release) start next development iteration 0.1.3-alpha.0 2018-01-31 19:19:50 +08:00
Michael Bryan
7181993b43 (cargo-release) version 0.1.2 2018-01-31 19:18:02 +08:00
boxdot
bf9f58e11b Add docs for mdBook specific include feature (#593)
* Add docs for mdBook specific include feature.

Also:
* Fix bug in take_lines taking `end`-many lines instead of
  `end-start` many.
* Handle special case `include:number` as including a single line.
* Start counting lines at 1 and not 0.

* Merge mdBook and rust specific features into one chapter.
2018-01-31 18:57:47 +08:00
Steve Klabnik
3ba71c570c Handle input path with regards to custom css (#598)
* Handle input path with regards to custom css

Before, when someone like the Reference set their extra css as
"theme/reference.css" in their book.toml, this path would be treated as
relative to the invocation of mdbook, and not respect the input path. This
PR modifies these relative paths to do so.

Fixes the build of https://github.com/rust-lang/rust/pull/47753 which
blocks updating rustc to mdbook 0.1

* don't use file-name

the style name is theme/reference.css, this results in a Err(StripPrefixError(())), which means that we push only the file_name, losing the theme bit
2018-01-30 12:29:09 +08:00
Sorin Davidoi
674e58e747 fix(theme): Use aria-label alonside title (#568)
Tested this on macOS with VoiceOver, and it does not pick up the title as the text of the button. Kind of makes sense, since title and aria-label are not the same. This will make sure that the buttons and links are labeled properly.
2018-01-27 18:52:47 +08:00
Michael Bryan
348c5d07c5 (cargo-release) start next development iteration 0.1.2-alpha.0 2018-01-27 11:56:22 +08:00
Michael Bryan
1790b04e03 (cargo-release) version 0.1.1 2018-01-27 11:54:27 +08:00
Michael Bryan
50ee15472b Updated the light theme to have a lighter scrollbar (#590) 2018-01-27 11:52:43 +08:00
Michael Bryan
ffb90bb9e2 Made sure we create the themes directory (#586) 2018-01-26 14:38:53 +08:00
Sorin Davidoi
186e649530 feat(src/theme): Scrollbar theme (#563) 2018-01-26 01:17:02 +08:00
Michael Bryan
adc1f4ade7 Reverted #549 (#565) 2018-01-26 01:12:10 +08:00
Michael Bryan
b777a318f7 Expose functionality for creating core types (#578)
* You can now add chapters to a Book

* Made the RenderContext::new() constructor public
2018-01-26 01:11:48 +08:00
Michael Bryan
30e3b83167 Updated Cargo.toml metadata to make releases easier (#584) 2018-01-26 01:11:32 +08:00
Sorin Davidoi
f082187844 fix(theme/book): Use passive listeners for touchstart, touchmove (#575) 2018-01-25 18:44:22 +08:00
Michael Bryan
6119972fa7 Merge pull request #581 from bmusin/patch-3
fix typo
2018-01-25 17:49:40 +08:00
Michael Bryan
a910435fd9 Merge pull request #577 from Michael-F-Bryan/missing-backends-arent-fatal
Missing backends shouldn't be fatal
2018-01-25 07:33:54 +08:00
Bulat Musin
53b902b479 remove dot for consistency (#580) 2018-01-25 07:32:52 +08:00
Bulat Musin
2e9d8671a0 fix typo (insert dash) (#582) 2018-01-25 07:32:33 +08:00
Bulat Musin
50cdfc9623 fix typo (#579) 2018-01-25 07:25:56 +08:00
Bulat Musin
d47f4dce7f fix typo 2018-01-24 21:52:51 +03:00
Michael Bryan
bda23f0183 Missing backends are no longer fatal 2018-01-25 01:15:29 +08:00
Michael Bryan
1fbad982d8 (cargo-release) start next development iteration 0.1.1-alpha.0 2018-01-23 22:06:55 +08:00
Michael Bryan
fada73eb74 (cargo-release) version 0.1.0 2018-01-23 22:05:07 +08:00
Michael Bryan
dee91b146b Added Michael-F-Bryan to the authors list 2018-01-23 22:04:45 +08:00
Michael Bryan
492f393c0b Escaped the book's example {{#include ...}} tag 2018-01-23 21:13:33 +08:00
Sorin Davidoi
bcfb37d964 fix(theme): Sticky header support in Safari (#572)
Safari scrolls on body, not on the html tag. It also needs sticky to be prefixed.
2018-01-23 21:13:11 +08:00
Markus Kohlhase
b1b8ba4b98 fix(doc): make installation from git work again (#570)
cargo install --git https://github.com/rust-lang-nursery/mdBook.git
leads to error: multiple packages with binaries found: mdbook, mdbook-wordcount
Fix: append name of binary (`mdbook`).
2018-01-23 21:07:14 +08:00
Sorin Davidoi
0531b585e4 fix(theme/index): Use localStorage, not store (#574)
* fix(theme/index): Use localStorage, not store

* fix(theme/index): Handle quotes values in localStorage
2018-01-23 20:30:50 +08:00
Michael Bryan
5379a0bdf8 Made the logging a lot quieter by default (#569) 2018-01-23 01:28:37 +08:00
Michael Bryan
0d146ffa82 Improve SUMMARY parser error messages (fixes #566) (#567) 2018-01-22 20:47:29 +08:00
Sorin Davidoi
0bc3544c81 refactor: Remove store.js (use localStorage) (#550) 2018-01-22 07:04:16 +08:00
Michael Bryan
c89245b45b Recursively apply replace_all() when running the links preprocessor (#564)
* Looks like we forgot to recursively apply replace_all() in #532

* Removed some print statements

* Made sure we ignore the rendered dummy_book
2018-01-22 06:44:28 +08:00
Sorin Davidoi
05e4157c2e refactor(theme/book): Split into modules (#556)
Advantages:
 - Easier to reason about
 - Can easily disable some modules when debugging
 - Shared dependencies are explicit (playpen_text)
 - Enables some fancier things later one (e.g. run `codeSnippets` slightly later, to avoid blocking the page)

I'm aware that `codeSnippets` should be split into the highlighter and the editor, but I'm not sure I understand exactly how they interact so I've left it as it is for now.
2018-01-21 23:01:57 +08:00
Michael Bryan
9fe19d8f31 Increase Documentation Coverage (#543)
* Added documentation to the `config` module

* Added an example to the `config` module

* Updated the docs in lib.rs regarding implementing backends

* Started writing an alternate backends walkthrough

* Mentioned the output.foo.command key

* Added example output

* Added a config section to the backends tutorial

* Finished off the backends tutorial

* Made sure travis checks mdbook-wordcount

* Fixed the broken link at in the user guide

* Changed how travis builds the project

* Added a conclusion

* Went through and documented a lot of stuff

* Added a preprocessors chapter and updated For Developers
2018-01-21 22:35:11 +08:00
Michael Bryan
232a923676 Added a windows-specific tee equivalent (#561)
* Added a windows-specific `tee` equivalent

* Changed how the windows "tee" command gets quoted

* Temporarily disabled the backends_receive_render_context_via_stdin test
2018-01-20 21:46:44 +08:00
Sorin Davidoi
3d1a311638 refactor: Remove jQuery file (#562) 2018-01-20 15:56:32 +08:00
Sorin Davidoi
80f42675d6 feat(theme/stylus/menu): Make sticky (#551)
* feat(theme/stylus/menu): Make sticky

* feat(theme/stylus/menu): Faint bottom border

* feat(theme/book): Auto hide menu when scrolling down

* feat(theme/stylus/menu): Don't hide title

* feat(theme/stylus/menu): Only show bottom border when sticky

* fix(theme/stylus/menu): Don't hide when sidebar is expanded

* feat(theme/book): Show menu bar on hover
2018-01-19 23:41:50 +08:00
Michael Bryan
fa84da0856 Stop pulling pulldown-cmark from master (#555)
* Manually implemented PartialEq for pulldown_cmark types

* Fixed an issue where we wouldn't skip a tag properly
2018-01-19 01:21:04 +08:00
Michael Bryan
947d0312c6 Merge pull request #554 from sorin-davidoi/fix-disable-stylesheet
fix(theme/book): Disable stylesheets by property, not attribute
2018-01-19 00:40:10 +08:00
Sorin Davidoi
05a0d7e625 fix(theme/book): Disable stylesheets by property, not attribute
Closes #553.
2018-01-18 17:12:41 +01:00
Michael Bryan
7b356b7530 Merge pull request #538 from sorin-davidoi/remove-jquery
Remove jQuery
2018-01-18 07:26:18 +08:00
Michael Bryan
d44b67bbcc Merge pull request #552 from projektir/editor-docs
Documenting the editor #521
2018-01-18 07:19:52 +08:00
Michael Bryan
7b4b70a49d Merge pull request #532 from JaimeValdemoros/implementing-preprocessors
implementing preprocessors
2018-01-18 07:18:54 +08:00
Jaime Valdemoros
1136f671a0 Log as each preprocessor is run 2018-01-17 19:05:15 +00:00
Jaime Valdemoros
80a20eb730 Use Arc instead of lazy_static for dummy preprocessor 2018-01-17 19:02:25 +00:00
Jaime Valdemoros
0d62578c7b Make Preprocessor context store config and root 2018-01-17 09:44:52 +00:00
projektir
57f960a03b Documenting the editor #521 2018-01-16 21:26:13 -08:00
Jaime Valdemoros
47cc57177d Update comment with rustdoc link 2018-01-16 23:02:50 +00:00
Jaime Valdemoros
90fa1b4909 Turn chained if's into match 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
f2d7b705af Pull out default preprocessors into function 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
4177288b11 Add test to make sure pre-processors are being run 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
b599956516 Move preprocess field location and add tests 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
08027b86cc Revert reordering 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
b98ed3f794 Clean up LinkPreprocessor exports and use explicit PreprocessorContext struct 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
9c922cf26b Add LinkPreprocessor::new constructor 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
144358bec6 Change name of link preprocessor 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
4cc708e00f Preprocess links by default 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
12815fe399 Add pre-processing step to build method of MDBook 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
f282a553fd Remove unnecessary mem::replace 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
966811061b Start determining preprocessors 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
cad76a9f6c Factor out replace_all preprocessor 2018-01-16 22:39:11 +00:00
Jaime Valdemoros
01df904bb3 Initial Preprocessor trait implementation 2018-01-16 22:39:11 +00:00
Sorin Davidoi
5a4adcce53 refactor(theme): Remove jQuery 2018-01-16 16:31:08 +01:00
Michael Bryan
bf093e2f5f Temporarily ignore OSX jobs on travis (#549)
* Made sure the CmdRenderer always creates the destination dir

* Commented out osx jobs temporarily
2018-01-16 23:29:46 +08:00
Michael Bryan
be9a524eeb Made sure the CmdRenderer always creates the destination dir 2018-01-16 21:39:59 +08:00
Sorin Davidoi
b0e91193e9 Meta theme color (#547)
* feat(theme/index): Add meta tag for theme-color

* feat(theme/book): Sync theme-color meta tag with the current theme
2018-01-16 20:29:20 +08:00
Sorin Davidoi
61fad2786b Improve accessibility (#535)
* fix(theme/index): Use nav element for Table of Content

* fix(renderer/html_handlebars/helpers/toc): Use ol instead of ul

Chapters and sections are ordered, so we should use the appropriate HTML tag.

* fix(renderer/html_handlebars/helpers/toc): Hide section number from screen readers

Screen readers have this functionality build-in, no need to present this. Ideally, this should not even be in the DOM tree, since the numbers can be shown by using CSS.

* fix(theme/index): Remove tabIndex="-1" from .page

Divs are not focusable by default

* fix(theme): Make sidebar accessible

Using aria-hidden (together with tabIndex) takes the links out of the tab order.
http://heydonworks.com/practical_aria_examples/#progressive-collapsibles

* fix(theme/index): Wrap content inside main tag

The main tag helps users skip additional content on the page.

* fix(theme/book): Don't focus .page on page load

The main content is identified by the main tag, not by auto-focusing it on page load.

* fix(theme/index): Make page controls accessible

* fix: Make theme selector accessible

- Use ul and li (since it is a list)
- Add aria-expanded and aria-haspopup to the toggle button
- Use button instead of div (buttons are accessible by default)
- Handle Esc key (close popup)
- Adjust CSS to keep same visual style

* fix(theme/stylus/sidebar): Make link clickable area wider

Links now expand to fill the entire row.

* fix(theme): Wrap header buttons and improve animation performance

Previously, the header had a fixed height, which meant that sometimes the print button was not visible. Animating the left property is expensive, which lead to laggy animations - transform is much cheaper and has the same effect.

* fix(theme/stylus/theme-popup): Theme button inherits color

Bug introduced while making the popup accessible

* fix(theme/book): Handle edge case when toggling sidebar

Bug introduced when switching from animating left to using transform.
2018-01-15 21:26:53 +08:00
Michael Bryan
9ab54412ea Made it so the CmdRenderer writes directly to the child's stdin (#544) 2018-01-14 19:14:27 +08:00
Michael Bryan
be949ceae8 Cleaned up the is_legacy_format() function 2018-01-14 04:54:11 +08:00
Michael Bryan
a1b6ccc29a Override configuration using environment variables (#541)
* Added the ability to update config settings from env vars

* Added tests

* Documented that you can override configuration with environment
variables

* Refactored the config get() methods to use toml-query

* Made the `Updateable` trait more generic
2018-01-14 02:38:43 +08:00
vabc3
e825357848 Add option to disable section label in html (#533) 2018-01-08 00:31:46 +08:00
vabc3
fd8f3bb415 Remove unneeded mut modifier in test (#534) 2018-01-08 00:25:45 +08:00
Michael Bryan
fd7e8d1b7b Add support for alternative backends (#507)
* Added a mechanism for creating alternate backends

* Added a CmdRenderer and the ability to have multiple renderers

* Made MDBook::load() autodetect renderers

* Added a couple methods to RenderContext

* Converted RenderContext.version to a String

* Made sure all alternate renderers are invoked as `mdbook-*`

* Factored out the logic for determining which renderer to use

* Added tests for renderer detection

* Made it so `mdbook test` works on the book-example again

* Updated the "For Developers" docs

* Removed `[output.epub]` from the example book's book.toml

* Added a bit more info on how backends should work

* Added a `destination` key to the RenderContext

* Altered how we wait for an alternate backend to finish

* Refactored the Renderer trait to not use MDBook and moved livereload to the template

* Moved info for developers out of the book.toml format chapter

* MOAR docs

* MDBook::build() no longer takes &mut self

* Replaced a bunch of println!()'s with proper log macros

* Cleaned up the build() method and backend discovery

* Added a couple notes and doc-comments

* Found a race condition when backends exit really quickly

* Added support for backends with arguments

* Fixed a funny doc-comment
2018-01-07 22:10:48 +08:00
Jan Likar
dedc208a6a Prohibit 'print.md' files (#524)
* Prohibit 'print.md' files

Fix #258 by emmiting an error whenever an mdBook contains a
"print.md" file in its root.

* Apply suggested changes

* Add tests
2018-01-07 00:02:23 +08:00
boxdot
e461610dab Implement partial include of source files (#520)
* Implement partial include of source files.

The macro `{{include some_file}}` accepts now optional line number
arguments, s.t. the specified line range is included. The following
forms are supported:

* `{{include some_file::}}` is equivalent to `{{include some_file}}`
* `{{include some_file:from:}}` includes lines [from, infinity)
* `{{include some_file::to}}` includes lines [0, to]
* `{{include some_file:from:to}}` includes lines [from, to]

* Remove the special case IncludeFull which is IncludeFrom(0).

* Use Range, RangeFrom, RangeTo and RangeFull to represent include-ranges.

Also:
* Move out introduced methods as free functions.
* Introduce RangeArgument trait as long it is unstable in stdlib.
* Use itertools for joining of lines on the fly.
* Split tests.
* Simplify include file argument parsing.

* Make utils::string private and link collections_range feature issue.
2018-01-06 05:03:30 +08:00
Michael Bryan
e791f250fa Workaround for travis-ci/travis-ci#4704 (#529)
large writes to stdout error with errno 11 (resource unavailable)
2018-01-06 04:47:59 +08:00
Michael Bryan
cd1abf8ec2 Revert "Added dummy df statements to debug the travis failures (#526)" (#528)
This reverts commit 2b943d703f.
2018-01-06 04:41:26 +08:00
Michael Bryan
2b943d703f Added dummy df statements to debug the travis failures (#526)
https://internals.rust-lang.org/t/rustdoc-ice-in-ci-build/6480/2
2018-01-05 18:39:23 +08:00
Michael Bryan
b9ab71b231 Reformatted the README and added a link to the master docs (#525) 2018-01-05 11:32:54 +08:00
Mario Nebl
7c81335c9a Use <button> for menu bar (#523) 2018-01-04 17:19:20 +08:00
Michael Bryan
e74c376833 You need to call rx.iter() to iterate over events from a channel (#522) 2018-01-03 19:32:49 +08:00
Michael Bryan
6ba0162ff7 Merge pull request #519 from Michael-F-Bryan/rollback-iron-version
Roll back iron version in Cargo.toml
2018-01-02 23:27:21 +08:00
Michael Bryan
b5382f49c6 Merge pull request #474 from stgn/sidebar-swipe
Basic swipe gesture functionality for opening/closing sidebar
2018-01-02 22:10:30 +08:00
Michael Bryan
daade63c27 Rolls back the Iron version due to issues with staticfile 2018-01-02 22:02:34 +08:00
Michael Bryan
9480ac0ea6 All git commands in github_pages.sh are now --quiet 2017-12-30 23:31:32 +08:00
Michael Bryan
65900b87e6 Use cargo run for github pages instead of hard-coding the binary path 2017-12-30 21:47:53 +08:00
Michael Bryan
d4a5176f26 Forgot to install npm deps globally in #516 2017-12-30 20:23:06 +08:00
Michael Bryan
c721ba82b2 Update travis CI (#516)
* Use japaric/trust for travis configuration

* Moved the github pages logic (not running the script breaks the build)

* Removed the x86 linux musl build
2017-12-30 19:15:17 +08:00
Michael Bryan
bd134fbaa4 Merge pull request #517 from Michael-F-Bryan/update-deps
Update dependency versions and made logging more readable
2017-12-30 19:05:46 +08:00
Michael Bryan
cba16a0083 Updated dependencies and made logging more readable 2017-12-30 18:43:46 +08:00
Michael Bryan
549a9ffd54 Merge pull request #470 from stgn/sidebar-page-ux
Improve experience on smaller screens
2017-12-30 16:15:50 +08:00
Michael Bryan
06c9d67e75 Merge pull request #515 from jridgewell/playpen-default
Implement playpen defaults
2017-12-21 14:36:09 +08:00
Justin Ridgewell
61356ce5fe Implement playpen defaults
Avoids issues where we enable `editable` but forget to specify `editor`
(eg, https://github.com/rust-lang/rust-by-example/issues/963).  Since we
already include Ace editor, seems like we can just treat it as the
default.
2017-12-21 00:18:12 -05:00
Shane Nelson
31fb443562 Cleanup and various fixes 2017-12-18 00:14:25 -05:00
Shane Nelson
a2759b7bd2 Prevent horizontal scroll on mobile browsers if page wrapper is pushed off-screen 2017-12-17 23:04:30 -05:00
Shane Nelson
71689da6b1 Force hidden sidebar on load for screen widths smaller than sidebar plus content and simplify styles 2017-12-17 23:04:30 -05:00
Shane Nelson
6e90e520d6 Avoid resizing page/content on sidebar toggle 2017-12-17 23:04:30 -05:00
Michael Bryan
65acb355d7 Merge pull request #513 from Michael-F-Bryan/error-messages
Added better error messages for missing CSS/JS
2017-12-14 20:28:28 +11:00
Michael Bryan
89eff2d824 Added better error messages for missing CSS/JS 2017-12-14 20:03:17 +11:00
Michael Bryan
a280a3003b Merge pull request #447 from Listwon/clipboard
Fix copy to clipboard is available only for rust snippets
2017-12-13 14:32:41 +11:00
Michael Bryan
cb6f2289cf Merge pull request #510 from Michael-F-Bryan/book-json-warning
Added a warning if people use the book.json config file
2017-12-13 13:18:51 +11:00
Michael Bryan
396426662d Made mdbook watch and mdbook serve rebuild the book again (#508)
Made `mdbook watch` and `mdbook serve` rebuild the book again
2017-12-13 12:04:24 +11:00
Michael Bryan
718d251c7f Added a warning if people use the book.json config file 2017-12-13 11:55:37 +11:00
Michael Bryan
d69bc9c7c3 Merge pull request #491 from Michael-F-Bryan/book-representation-3
WIP: Book representation - Attempt 3
2017-12-12 19:24:13 +11:00
Michael Bryan
a46e2e2b27 Merge branch 'master' into book-representation-3 2017-12-11 19:06:09 +11:00
Michael Bryan
75dac15f09 Fixed a couple issues with the docs 2017-12-11 18:50:31 +11:00
Michael Bryan
5041359817 Updated the pulldown-cmark patch in Cargo.toml 2017-12-11 17:36:13 +11:00
Michael Bryan
ff9e0b0add Made sure create_missing also creates the parent directory 2017-12-11 17:29:32 +11:00
Michael Bryan
148511eceb Able to parse all existing SUMMARY.md files 2017-12-11 17:20:05 +11:00
Michael Bryan
f5e9b857de Rewrote summary parser from a state machine to use recursive descent 2017-12-11 15:17:20 +11:00
Michael Bryan
4f4e86db3a Added tests to make sure we parse existing SUMMARY.md's 2017-12-11 12:26:11 +11:00
Michael Bryan
ebcf41c25b Improved error messages using error_chain::ChainedError::display_chain() 2017-12-11 11:42:36 +11:00
Michael Bryan
1b51cd244e Moved the book examples to the top level lib.rs 2017-12-11 11:29:30 +11:00
Michael Bryan
be4654c9c2 Fleshed out the docs for the book module 2017-12-11 11:24:43 +11:00
Michael Bryan
ace0b51fb6 Put the create_missing feature back in 2017-12-11 10:32:35 +11:00
Michael Bryan
9950f69c48 Removed the MDBook::read_config() method because it's redundant now 2017-12-10 23:13:46 +11:00
Michael Bryan
12d1ed5558 The example book renders correctly 2017-12-10 22:59:35 +11:00
Michael Bryan
751da4f05f Added a test to make sure you can include rust files in chapters 2017-12-10 22:59:35 +11:00
Michael Bryan
527fc5cf79 Completely removed the create_missing option from MDBook 2017-12-10 22:52:20 +11:00
Michael Bryan
f993677626 All tests finally pass! 2017-12-10 22:52:00 +11:00
Michael Bryan
21498631b3 Made sure the dummy book can build 2017-12-10 22:50:57 +11:00
Michael Bryan
8b21da9950 Fleshed out book creation 2017-12-10 22:50:57 +11:00
Michael Bryan
47eb4788cb Introduced the BookBuilder.
- You now use a `BookBuilder` for creating a book directory tree

- This also removes the `--no-create` argument
2017-12-10 22:49:40 +11:00
Michael Bryan
cafb8b75e7 The library not compiles (probably completely broken) 2017-12-10 22:44:20 +11:00
Michael Bryan
4c6c696c87 Copied across the summary parser and Book structure (doesn't compile) 2017-12-10 22:43:12 +11:00
Michael Bryan
3838fa0e68 Merge pull request #501 from lifta42/master
Fix misplaced "previous" icon
2017-12-10 22:34:09 +11:00
Michael Bryan
4619ab60b0 (cargo-release) start next development iteration 0.0.29-alpha.0 2017-12-09 21:26:12 +11:00
Michael Bryan
2a5409db20 (cargo-release) version 0.0.28 2017-12-09 21:23:44 +11:00
Michael Bryan
dc89a82329 Merge pull request #506 from Michael-F-Bryan/quickfix
Added a quick fix so if the config isn't found we use a default
2017-12-09 21:18:34 +11:00
Michael Bryan
42ff5a895c Added a test to make sure book.toml isn't required 2017-12-09 20:46:39 +11:00
Michael Bryan
8ee795045a Added a quick fix so if the config isn't found we use a default 2017-12-09 20:36:23 +11:00
Michael Bryan
f22835f7bc (cargo-release) start next development iteration 0.0.28-alpha.0 2017-12-07 21:40:45 +11:00
Michael Bryan
1f84f66041 Bumped the version number in preparation for the next release 2017-12-07 21:35:50 +11:00
lifta42
9143110a43 Optimized the stylesheet for better performance 2017-12-06 16:20:22 +08:00
Michael Bryan
e735bc6d3e Merge pull request #500 from cspiegel/create-missing
WIP: Add a create-missing option to book.toml.
2017-12-06 00:30:49 +08:00
Chris Spiegel
803df90efa Add tests to check create-missing. 2017-12-02 22:53:19 -08:00
lifta42
3b136689ee Fix a CSS bug to show arrow properly
The arrow for "previous chapter" was on the incorrect position when screen width is between 1060px and 1250px.
2017-12-02 14:25:24 +08:00
Michael Bryan
b614b0fd65 Merge pull request #454 from projektir/header_partial
Adding a header partial integration #453
2017-12-02 13:36:09 +08:00
projektir
32df76d077 Adding a header partial integration #453 2017-11-30 21:48:29 -08:00
Chris Spiegel
dacc274e0d Add myself to the contributors list. 2017-11-30 07:54:43 -08:00
Chris Spiegel
b0b09bad3f Clean up build configuration.
This rolls all "create missing" handling into BuildConfig, and moves the
build-dir option from the "book" table to the "build" table. Some
documentation cleanup surrounding the build table is also updated.
2017-11-30 07:39:58 -08:00
Chris Spiegel
93874edebf Add a create-missing option to book.toml. 2017-11-29 20:02:58 -08:00
shan1024
1aa9c92ac1 Fix typo 2017-11-27 13:18:51 +01:00
Anna Liao
5ce05a79be updated links from azerupi to rust-lang-nursery (#489) 2017-11-22 11:35:18 +01:00
Aaron Turon
c51e080783 Update broken doc link 2017-11-21 20:32:08 -08:00
Michael Bryan
dd5d94393d Fixed the OSX/beta travis builds (#492)
Fixed the OSX/beta travis builds
2017-11-19 01:12:25 +08:00
Jacob Wahlgren
3d5eb48e32 Refactor navigation helpers (#465)
* Refactor navigation helpers

* Target::find: take previous_item by reference

This makes more sense for find as an interface, though it causes a
second clone in some cases. Maybe rustc is smart here?

* Test next and previous navigation helpers

* Add more next/previous tests
2017-11-18 19:17:26 +08:00
Michael Bryan
d56ff94ce6 Regression tests (#422)
* Created regression tests for the table of contents

* Refactoring to make the test more readable

* Fixed some bitrot and removed the (now redundant) tests/helper module

* Removed the include_str!() stuff and use just the dummy book for testing

* Regression tests now pass again!

* Pinned a `*` dependency to use a particular version

* Made sure test mocks return errors instead of panicking

* Addressed the rest of @budziq's review

* Replaced a file open/read with file_to_string
2017-11-16 15:51:12 +08:00
Michael Bryan
fb99276f52 Merge pull request #457 from Michael-F-Bryan/config
Making configuration more flexible
2017-11-12 21:47:10 +08:00
Michael Bryan
5eff572dbb Updated the warning to give some basic migration instructions 2017-11-12 21:37:39 +08:00
Michael Bryan
238dfb7d1d Added in legacy config format support 2017-11-12 21:37:38 +08:00
Michael Bryan
c777913136 Updated the configuration chapter in the book-example 2017-11-12 21:37:38 +08:00
Michael Bryan
c25c5d72c8 Went back and simplified Config to be a smart wrapper around toml::Table 2017-11-12 21:37:38 +08:00
Michael Bryan
3aa6436679 Added in things from @Phaiax's review 2017-11-12 21:37:38 +08:00
Michael Bryan
d37821c194 Rebased after #438 2017-11-12 21:37:38 +08:00
Michael Bryan
1b5137c84e All tests pass again :) 2017-11-12 21:37:37 +08:00
Michael Bryan
18c725ee12 Integration tests pass again 2017-11-12 21:37:37 +08:00
Michael Bryan
1743f2a39f Removed the now redundant config files 2017-11-12 21:37:37 +08:00
Michael Bryan
cee3296a32 main library tests pass 2017-11-12 21:37:37 +08:00
Michael Bryan
ddb0834da8 Upgraded binaries to new configuration API 2017-11-12 21:37:36 +08:00
Michael Bryan
b74c2c18ef Removed all references to old the configuration from the html renderer 2017-11-12 21:37:36 +08:00
Michael Bryan
c056b5cbd0 Removed old configs from MDBook 2017-11-12 21:37:36 +08:00
Michael Bryan
8d7970b32d Changed to the new config types 2017-11-12 21:37:36 +08:00
Michael Bryan
1d22a9a040 Added some basic deserializing tests and helpers 2017-11-12 21:37:36 +08:00
Michael Bryan
6059883229 Added some basic configuration objects 2017-11-12 21:37:35 +08:00
Bartłomiej T. Listwon
8960013322 Fix copy to clipboard is available only for rust snippets
Fixes #432
2017-11-10 21:18:14 +01:00
Michael Bryan
79dd03e8e9 Merge pull request #471 from stgn/theme-popup-absolute
Use absolute positioning for theme popup
2017-11-10 13:31:12 +08:00
François
aecc403fb8 add tooltip to icons (#477)
add tooltip to icons and previous/next chapter links
2017-10-27 14:46:55 +02:00
Shane Nelson
6e4d2485c3 Basic swipe gesture functionality for opening/closing sidebar 2017-10-23 20:51:43 -04:00
Mathieu David
cd711bfb1c Merge pull request #456 from Michael-F-Bryan/conditional-ga
Conditional Google Analytics
2017-10-18 14:10:53 +02:00
Mathieu David
afd9ccb7b1 Merge pull request #461 from Michael-F-Bryan/move-custom-js
Custom JS belongs at the bottom
2017-10-18 14:09:14 +02:00
Mathieu David
cb5ae21b89 Merge pull request #469 from jacwah/ignore
Fix last clippy warnings
2017-10-18 14:07:10 +02:00
Shane Nelson
dd3bef8000 Use absolute positioning for theme popup 2017-10-16 21:40:32 -04:00
Jacob Wahlgren
7e5892bd35 Ignore unhelpful clippy warning
As discussed in https://github.com/azerupi/mdBook/pull/466
2017-10-12 22:14:48 +02:00
Jacob Wahlgren
56cee872e8 Box Handlebars template error
See https://github.com/azerupi/mdBook/pull/466#issuecomment-335450110
2017-10-12 21:50:33 +02:00
Michael Bryan
a554390aa2 Slightly cleaned up the google analytics tag (skip ci) 2017-10-09 09:53:02 +08:00
Mathieu David
c64384abc3 Merge pull request #462 from Michael-F-Bryan/update-contributors
Updated Contributors
2017-10-08 16:07:19 +02:00
Mathieu David
ba7d40284b Merge pull request #466 from jacwah/lints
Fix some clippy warnings
2017-10-08 16:06:39 +02:00
Jacob Wahlgren
8f6523a94c Fix some clippy warnings 2017-10-07 18:11:05 +02:00
Michael Bryan
8fbc59720d Added myself to the contributors list 2017-10-04 20:04:26 +08:00
Michael Bryan
ac9c150902 Moved custom JS to the bottom 2017-10-04 19:59:10 +08:00
Michael Bryan
f2e56c887b Got the logic around the wrong way 2017-10-04 19:57:06 +08:00
Michael Bryan
b4a12fa723 Made sure google analytics isn't included when inspecting locally 2017-10-04 19:57:06 +08:00
Pratik Karki
382fc4139b run rustfmt on the repository #398(Updated) (#438)
rustfmt the repository #398
2017-10-03 13:40:23 +02:00
steveklabnik
b45e5e4420 Release 0.0.26
Fixes #460
2017-10-02 09:10:17 -04:00
Mathieu David
a6d4881e00 Merge pull request #450 from Zengor/master
Call playground with /execute
2017-09-23 21:35:41 +02:00
Mathieu David
a0515bd104 Merge pull request #449 from steveklabnik/fix-regression
Change key for theme to not clobber old books
2017-09-23 20:42:31 +02:00
steveklabnik
9b64db908f prefix sidebar too 2017-09-22 13:58:45 -04:00
steveklabnik
f562878131 I forgot one theme, thanks budziq 2017-09-22 13:56:58 -04:00
Zengor
3823fc0e74 Call playground with /execute and not the legacy /evaluate.json
This commit changes the url used to call the playground, and the
request parameter format to go with it. The older evaluate is
available in the playground as a form of backwards compatibility
and swithcing now opens way for using newer features.
2017-09-21 00:24:47 -03:00
steveklabnik
793fb8f654 Change key for theme to not clobber old books
Fixes https://github.com/azerupi/mdBook/issues/448
2017-09-19 16:59:16 -04:00
Bartłomiej T. Listwon
911683d2cf Fix styling regression on print media in chromium
Forces 0px left padding on print view even if sidebar is visible
2017-09-18 22:10:31 +02:00
Mathieu David
2ae6e6a6e3 Merge pull request #445 from Listwon/master
Fix code snippet font size a little smaller in FF
2017-09-18 14:32:57 +02:00
Bartłomiej T. Listwon
91fd8a2865 Fix code snippet font size a little smaller in FF 2017-09-18 11:18:21 +02:00
Steve Klabnik
a3b6e549e2 Merge pull request #440 from budziq/force_runnable
added `mdbook-runnable` infostring support
2017-09-14 12:48:29 -04:00
Steve Klabnik
d450518292 Merge pull request #439 from azerupi/fix-print
Fix the issue with pages named print not at the root
2017-09-14 11:44:38 -04:00
Michal Budzynski
c056df597a added mdbook-runnable infostring support
makes `ignore`'d playpens runnable
2017-09-13 22:54:01 +02:00
Mathieu David
0d6adc5fc9 Fix the issue with pages named print not at the root 2017-09-13 22:17:23 +02:00
Mathieu David
0226da91e4 Fix shield in README 2017-09-11 19:47:39 +02:00
Mathieu David
ef5895fa78 Update all dependencies 2017-09-11 19:38:10 +02:00
Mathieu David
8e0abfb22f Fix test condition in Travis config 2017-09-10 09:06:46 +02:00
Mathieu David
c9bc13d786 Merge pull request #429 from azerupi/ci-infra
Revive Travis
2017-09-10 00:09:08 +02:00
Mathieu David
7ce78cbfea Fix missing && in travis config 2017-09-08 20:24:29 +02:00
Mathieu David
26544fa531 Fix Travis script 2017-09-08 20:24:29 +02:00
Mathieu David
0c93770f4a Update and simplify Travis CI 2017-09-08 20:24:29 +02:00
Mathieu David
743713ad3a Merge pull request #425 from dvberkel/correct-documentation-of-inline-mathematics
Correct inline mathematics delimiters
2017-09-08 20:23:08 +02:00
Mathieu David
e3f4bb5101 (cargo-release) start next development iteration 0.0.26-alpha.0 2017-09-08 20:21:49 +02:00
Daan van Berkel
bd30cae17e Correct inline mathematics delimiters
This fixes #424
2017-09-08 09:21:20 +02:00
127 changed files with 12897 additions and 4015 deletions

4
.gitattributes vendored
View File

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

5
.gitignore vendored
View File

@@ -1,4 +1,3 @@
Cargo.lock
target
# MacOS temp file
@@ -7,4 +6,6 @@ target
book-test
book-example/book
.vscode
.vscode
tests/dummy_book/book/

View File

@@ -1,111 +1,46 @@
sudo: false
language: rust
language: generic
cache:
- cargo
before_cache:
- chmod -R a+r $HOME/.cargo
env:
global:
- PROJECT_NAME=mdBook
matrix:
include:
# Stable channel
- os: osx
env: TARGET=i686-apple-darwin CHANNEL=stable
- os: linux
env: TARGET=i686-unknown-linux-gnu CHANNEL=stable
addons:
apt:
packages: &i686_unknown_linux_gnu
- gcc-multilib
- os: osx
env: TARGET=x86_64-apple-darwin CHANNEL=stable
- os: linux
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=stable
addons:
apt:
packages:
- nodejs
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=stable
dist: trusty
addons:
apt:
packages: &musl_packages
- musl
- musl-dev
- musl-tools
# Beta channel
- os: osx
env: TARGET=i686-apple-darwin CHANNEL=beta
- os: linux
env: TARGET=i686-unknown-linux-gnu CHANNEL=beta
addons:
apt:
packages: *i686_unknown_linux_gnu
- os: osx
env: TARGET=x86_64-apple-darwin CHANNEL=beta
- os: linux
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=beta
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=beta
dist: trusty
addons:
apt:
packages: &musl_packages
- musl
- musl-dev
- musl-tools
# Nightly channel
- os: osx
env: TARGET=i686-apple-darwin CHANNEL=nightly
- os: linux
env: TARGET=i686-unknown-linux-gnu CHANNEL=nightly
addons:
apt:
packages: *i686_unknown_linux_gnu
- os: osx
env: TARGET=x86_64-apple-darwin CHANNEL=nightly
- os: linux
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=nightly
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
dist: trusty
addons:
apt:
packages: &musl_packages
- musl
- musl-dev
- musl-tools
- CRATE_NAME=mdbook
- TARGET=x86_64-unknown-linux-gnu
install:
- export PATH="$PATH:$HOME/.cargo/bin"
- bash ci/install.sh
- sh ci/install.sh
- export PATH=$PATH:$HOME/.cargo/bin
script:
- bash ci/script.sh
- cargo build --all --no-default-features
- cargo build --verbose
- cargo test --verbose
after_success:
- test "$TRAVIS_PULL_REQUEST" == "false" &&
test "$TRAVIS_BRANCH" == "master" &&
test "$TARGET" == "x86_64-unknown-linux-gnu" &&
test "$CHANNEL" = "stable" &&
npm install stylus nib &&
bash deploy.sh
- bash ci/github_pages.sh
before_deploy:
- bash ci/before_deploy.sh
- sh ci/before_deploy.sh
deploy:
provider: releases
api_key:
secure: Z1k7WqX7z+tT4+SzTh4tBBzf11VaADB4AWuEczHtylaEb/0hRs8gaiHCNSVHm/QTp0QPWQR2Vw7uKMhVuxG7I8X7h31j3A7ulYBh/iVk0DVIrtrn2Q4WOED9CpoXLuLtk2nxo9MBViFW7mw4nJe9H2Tn9o/9oEYBuwzekvW5mh4muqUuCVTr8eQVYbs3jbC9pQy5oYjOLeUnlL9Cey5VN/nAhzAtyFP+6lIMri0PKit4JtkFou/O1MEpFYlP3VGC2lFiWuByocPKBT/L45FecS9qoHq+i6+ZCPDH2eu46nuYsDbLKAkPdGvf1MdPBPwoj0vSnZbgaTisQ4hIoBngQQQPZlPaGtcdd6g6asxSfnbA9cQhClI5oZJmg+ksxQE+peE8pnbmZ10Ix0PpIkkfWdQeMdUUCQarOTkTK54Munw+X+kp1lH19j6+krQPLBYr95fPRd4b5tWsJD2+pb/UOYFEEJxMNoUHyLCrtdCO7imOwrSUcv51+Z8UudqfPpKQeszrJcntL4owip35r3sF5TsE9YfW5qssLC164IylvP32y1AcfL1jqg8b+zrqLZKanjvDOJ1dtHHuwKqxcwf7PhAf0YjAtVSH9OIYcDzmDa0EMLrq7EK0fs6NAeb5qt6CML7pZrRS3fmOxN53Fbmj81qm6TmjQjDe4dmZlELgNow=
file: ${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.tar.gz
# don't delete the artifacts from previous phases
skip_cleanup: true
# deploy when a new tag is pushed
- 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: $CHANNEL = stable
condition: "$TRAVIS_RUST_VERSION = stable"
tags: true
skip_cleanup: true
branches:
only:
- "/^v\\d+\\.\\d+\\.\\d+.*$/"
- master
notifications:
email:

View File

@@ -5,22 +5,22 @@ Welcome stranger!
If you have come here to learn how to contribute to mdBook, we have some tips for you!
First of all, don't hesitate to ask questions!
Use the [issue tracker](https://github.com/azerupi/mdBook/issues), no question is too simple.
If we don't respond in a couple of days, ping us @azerupi, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
Use the [issue tracker](https://github.com/rust-lang-nursery/mdBook/issues), no question is too simple.
If we don't respond in a couple of days, ping us @Michael-F-Bryan, @budziq, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
### Issues to work on
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
[E-Easy issues](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
[E-Easy issues](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
include documentation improvements, new tests, examples, updating dependencies, etc.
If you come from a web development background, you might be interested in issues related to web technologies tagged
[A-JavaScript](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
[A-Style](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
[A-HTML](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
[A-Mobile](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
[A-JavaScript](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
[A-Style](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
[A-HTML](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
[A-Mobile](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
When you decide you want to work on a specific issue, ping us on that issue so that we can assign it to you.
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
@@ -41,7 +41,7 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
0. Clone this repository with git.
```
git clone https://github.com/azerupi/mdBook.git
git clone https://github.com/rust-lang-nursery/mdBook.git
```
0. Navigate into the newly created `mdBook` directory
0. Run `cargo build`
@@ -55,7 +55,7 @@ mdBook doesn't use CSS directly but uses [Stylus](http://stylus-lang.com/), a CS
When you want to change the style, it is important to not change the CSS directly because any manual modification to
the CSS files will be overwritten when compiling the stylus files. Instead, you should make your changes directly in the
[stylus files](https://github.com/azerupi/mdBook/tree/master/src/theme/stylus) and regenerate the CSS.
[stylus files](https://github.com/rust-lang-nursery/mdBook/tree/master/src/theme/stylus) and regenerate the CSS.
For this to work, you first need [Node and NPM](https://nodejs.org/en/) installed on your machine.
Then run the following command to install both [stylus](http://stylus-lang.com/) and [nib](https://tj.github.io/nib/), you might need `sudo` to install successfully.

1541
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
[package]
name = "mdbook"
version = "0.0.25"
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
description = "create books from markdown files (like Gitbook)"
documentation = "http://azerupi.github.io/mdBook/index.html"
repository = "https://github.com/azerupi/mdBook"
version = "0.1.6"
authors = ["Mathieu David <mathieudavid@mathieudavid.org>", "Michael-F-Bryan <michaelfbryan@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"
keywords = ["book", "gitbook", "rustbook", "markdown"]
license = "MPL-2.0"
readme = "README.md"
@@ -14,42 +14,63 @@ exclude = [
"src/theme/stylus/**",
]
[package.metadata.release]
sign-commit = true
push-remote = "origin"
tag-prefix = "v"
[dependencies]
clap = "2.24"
handlebars = "0.27"
chrono = "0.4"
handlebars = "0.32"
serde = "1.0"
serde_derive = "1.0"
error-chain = "0.10.0"
error-chain = "0.11"
serde_json = "1.0"
pulldown-cmark = "0.0.14"
lazy_static = "0.2"
log = "0.3"
env_logger = "0.4.0"
pulldown-cmark = "0.1.2"
lazy_static = "1.0"
log = "0.4"
env_logger = "0.5.0-rc.1"
toml = "0.4"
memchr = "2.0"
open = "1.1"
regex = "0.2.1"
tempdir = "0.3.4"
tempfile = "3.0"
itertools = "0.7"
shlex = "0.1"
toml-query = "0.6"
# Watch feature
notify = { version = "4.0", optional = true }
time = { version = "0.1.34", optional = true }
crossbeam = { version = "0.2.8", optional = true }
crossbeam = { version = "0.3", optional = true }
# Serve feature
iron = { version = "0.5", optional = true }
staticfile = { version = "0.4", optional = true }
iron = { version = "0.6", optional = true }
staticfile = { version = "0.5", optional = true }
ws = { version = "0.7", optional = true}
# Search feature
elasticlunr-rs = { version = "2.0", optional = true }
ammonia = { version = "1.1", optional = true }
[build-dependencies]
error-chain = "0.10"
error-chain = "0.11"
[dev-dependencies]
select = "0.4"
pretty_assertions = "0.5"
walkdir = "2.0"
pulldown-cmark-to-cmark = "1.1.0"
[features]
default = ["output", "watch", "serve"]
default = ["output", "watch", "serve", "search"]
debug = []
output = []
regenerate-css = []
watch = ["notify", "time", "crossbeam"]
serve = ["iron", "staticfile", "ws"]
search = ["elasticlunr-rs", "ammonia"]
[[bin]]
doc = false

126
README.md
View File

@@ -4,19 +4,19 @@
<tr>
<td><strong>Linux / OS X</strong></td>
<td>
<a href="https://travis-ci.org/azerupi/mdBook"><img src="https://travis-ci.org/azerupi/mdBook.svg?branch=master"></a>
<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/azerupi/mdbook/"><img src="https://ci.appveyor.com/api/projects/status/o38racsnbcospyc8/branch/master?svg=true"></a>
<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/crates/l/mdbook.svg"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg"></a>
</td>
</tr>
</table>
@@ -26,64 +26,88 @@ mdBook is a utility to create modern online books from Markdown files.
## What does it look like?
The [**Documentation**](http://azerupi.github.io/mdBook/) for mdBook has been written in Markdown and is using mdBook to generate the online book-like website you can read. The documentation uses the latest version on GitHub and showcases the available features.
The [User Guide] for mdBook has been written in Markdown and is using mdBook to
generate the online book-like website you can read. The documentation uses the
latest version on GitHub and showcases the available features.
## Installation
There are multiple ways to install mdBook.
1. **Binaries**
Binaries are available for download [here](https://github.com/azerupi/mdBook/releases). Make sure to put the path to the binary into your `PATH`.
1. **Binaries**
Binaries are available for download [here][releases]. Make sure to put the
path to the binary into your `PATH`.
2. **From Crates.io**
This requires [Rust] and Cargo to be installed. Once you have installed
Rust, type the following in the terminal:
2. **From Crates.io**
This requires [Rust and Cargo](https://www.rust-lang.org/) to be installed. Once you have installed Rust, type the following in the terminal:
```
cargo install mdbook
```
This will download and compile mdBook for you, the only thing left to do is to add the Cargo bin directory to your `PATH`.
This will download and compile mdBook for you, the only thing left to do is
to add the Cargo bin directory to your `PATH`.
**Note for automatic deployment**
If you are using a script to do automatic deployments using Travis or 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** version of mdBook and will prevent your books from failing to build because we released a new version. For example:
**Note for automatic deployment**
If you are using a script to do automatic deployments using Travis or
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**
version of mdBook and will prevent your books from failing to build because
we released a new version. For example:
```
cargo install mdbook --vers "^0.1.0"
```
3. **From Git**
The version published to crates.io will ever so slightly be behind the version hosted here on GitHub. If you need the latest version you can build the git version of mdBook yourself. Cargo makes this ***super easy***!
The version published to crates.io will ever so slightly be behind the
version hosted here on GitHub. If you need the latest version you can build
the git version of mdBook yourself. Cargo makes this ***super easy***!
```
cargo install --git https://github.com/azerupi/mdBook.git
cargo install --git https://github.com/rust-lang-nursery/mdBook.git mdbook
```
Again, make sure to add the Cargo bin directory to your `PATH`.
4. **For Contributions**
If you want to contribute to mdBook you will have to clone the repository on your local machine:
If you want to contribute to mdBook you will have to clone the repository on
your local machine:
```
git clone https://github.com/azerupi/mdBook.git
git clone https://github.com/rust-lang-nursery/mdBook.git
```
`cd` into `mdBook/` and run
```
cargo build
```
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
The resulting binary can be found in `mdBook/target/debug/` under the name
`mdBook` or `mdBook.exe`.
## Usage
mdBook will primarily be used as a command line tool, even though it exposes all its functionality as a Rust crate for integration in other projects.
mdBook will primarily be used as a command line tool, even though it exposes
all its functionality as a Rust crate for integration in other projects.
Here are the main commands you will want to run. For a more exhaustive explanation, check out the [documentation](http://azerupi.github.io/mdBook/).
Here are the main commands you will want to run. For a more exhaustive
explanation, check out the [User Guide].
- `mdbook init`
The init command will create a directory with the minimal boilerplate to start with.
The init command will create a directory with the minimal boilerplate to
start with.
```
book-test/
@@ -93,37 +117,75 @@ Here are the main commands you will want to run. For a more exhaustive explanati
└── SUMMARY.md
```
`book` and `src` are both directories. `src` contains the markdown files that will be used to render the output to the `book` directory.
`book` and `src` are both directories. `src` contains the markdown files
that will be used to render the output to the `book` directory.
Please, take a look at the [**Documentation**](http://azerupi.github.io/mdBook/cli/init.html) for more information and some neat tricks.
Please, take a look at the [CLI docs] for more information and some neat tricks.
- `mdbook build`
This is the command you will run to render your book, it reads the `SUMMARY.md` file to understand the structure of your book, takes the markdown files in the source directory as input and outputs static html pages that you can upload to a server.
This is the command you will run to render your book, it reads the
`SUMMARY.md` file to understand the structure of your book, takes the
markdown files in the source directory as input and outputs static html
pages that you can upload to a server.
- `mdbook watch`
When you run this command, mdbook will watch your markdown files to rebuild the book on every change. This avoids having to come back to the terminal to type `mdbook build` over and over again.
When you run this command, mdbook will watch your markdown files to rebuild
the book on every change. This avoids having to come back to the terminal
to type `mdbook build` over and over again.
- `mdbook serve`
Does the same thing as `mdbook watch` but additionally serves the book at `http://localhost:3000` (port is changeable) and reloads the browser when a change occurs.
Does the same thing as `mdbook watch` but additionally serves the book at
`http://localhost:3000` (port is changeable) and reloads the browser when a
change occurs.
- `mdbook clean`
Delete directory in which generated book is located.
### As a library
Aside from the command line interface, this crate can also be used as a library. This means that you could integrate it in an existing project, like a web-app for example. Since the command line interface is just a wrapper around the library functionality, when you use this crate as a library you have full access to all the functionality of the command line interface with an easy to use API and more!
Aside from the command line interface, this crate can also be used as a
library. This means that you could integrate it in an existing project, like a
web-app for example. Since the command line interface is just a wrapper around
the library functionality, when you use this crate as a library you have full
access to all the functionality of the command line interface with an easy to
use API and more!
See the [Documentation](http://azerupi.github.io/mdBook/lib/lib.html) and the [API docs](http://azerupi.github.io/mdBook/mdbook/index.html) for more information.
See the [User Guide] and the [API docs] for more information.
## Contributions
Contributions are highly appreciated and encouraged! Don't hesitate to participate to discussions in the issues, propose new features and ask for help.
Contributions are highly appreciated and encouraged! Don't hesitate to
participate to discussions in the issues, propose new features and ask for
help.
If you are just starting out with Rust, there are a series of issus that are tagged [E-Easy](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy) and **we will gladly mentor you** so that you can successfully go through the process of fixing a bug or adding a new feature! Let us know if you need any help.
If you are just starting out with Rust, there are a series of issus that are
tagged [E-Easy] and **we will gladly mentor you** so that you can successfully
go through the process of fixing a bug or adding a new feature! Let us know if
you need any help.
For more info about contributing, check out our [contribution guide](CONTRIBUTING.md) who helps you go through the build and contribution process!
For more info about contributing, check out our [contribution guide] who helps
you go through the build and contribution process!
There is also a [rendered version][master-docs] of the latest API docs
available, for those hacking on `master`.
## License
All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE](LICENSE) file.
All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE] file.
[User Guide]: https://rust-lang-nursery.github.io/mdBook/
[API docs]: https://docs.rs/mdbook/*/mdbook/
[E-Easy]: https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy
[contribution guide]: https://github.com/rust-lang-nursery/mdBook/blob/master/CONTRIBUTING.md
[LICENSE]: https://github.com/rust-lang-nursery/mdBook/blob/master/LICENSE
[releases]: https://github.com/rust-lang-nursery/mdBook/releases
[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/

View File

@@ -1,6 +1,19 @@
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
author = "Mathieu David"
authors = ["Mathieu David", "Michael-F-Bryan"]
[output.html]
mathjax-support = true
mathjax-support = true
[output.html.playpen]
editable = true
[output.html.search]
limit-results = 20
use-boolean-and = true
boost-title = 2
boost-hierarchy = 2
boost-paragraph = 1
expand = true
heading-split-level = 2

View File

@@ -4,12 +4,11 @@
What you are reading serves as an example of the output of mdBook and at the same time as a high-level documentation.
mdBook is free and open source, you can find the source code on [Github](https://github.com/azerupi/mdBook). Issues and feature requests can be posted on the [Github Issue tracker](https://github.com/azerupi/mdBook/issues).
mdBook is free and open source, you can find the source code on [Github](https://github.com/rust-lang-nursery/mdBook). Issues and feature requests can be posted on the [Github Issue tracker](https://github.com/rust-lang-nursery/mdBook/issues).
## API docs
Alongside this book you can also read the [API docs](mdbook/index.html) generated by Rustdoc if you would like
to use mdBook as a crate or write a new renderer and need a more low-level overview.
Alongside this book you can also read the [API docs](https://docs.rs/mdbook/*/mdbook/) generated by Rustdoc if you would like to use mdBook as a crate or write a new renderer and need a more low-level overview.
## License

View File

@@ -7,14 +7,18 @@
- [watch](cli/watch.md)
- [serve](cli/serve.md)
- [test](cli/test.md)
- [clean](cli/clean.md)
- [Format](format/format.md)
- [SUMMARY.md](format/summary.md)
- [Configuration](format/config.md)
- [Theme](format/theme/theme.md)
- [index.hbs](format/theme/index-hbs.md)
- [Syntax highlighting](format/theme/syntax-highlighting.md)
- [Editor](format/theme/editor.md)
- [MathJax Support](format/mathjax.md)
- [Rust code specific features](format/rust.md)
- [Rust Library](lib/lib.md)
- [mdBook specific features](format/mdbook.md)
- [For Developers](for_developers/index.md)
- [Preprocessors](for_developers/preprocessors.md)
- [Alternate Backends](for_developers/backends.md)
-----------
[Contributors](misc/contributors.md)

View File

@@ -14,8 +14,8 @@ convenience. Large books will therefore remain structured when rendered.
#### Specify a directory
Like `init`, the `build` command can take a directory as argument to use instead of the
current working directory.
Like `init`, the `build` command can take a directory as an argument to use
instead of the current working directory.
```bash
mdbook build path/to/book

View File

@@ -0,0 +1,21 @@
# The clean command
The clean command is used to delete the generated book and any other build
artifacts.
```bash
mdbook clean
```
It will try to delete the built book. If a path is provided, it will be used.
#### Specify a directory
Like `init`, the `clean` command can take a directory as an argument to use
instead of the normal build directory.
```bash
mdbook clean --dest-dir=path/to/book
```
`path/to/book` could be absolute or relative.

View File

@@ -24,10 +24,10 @@ Run `mdbook help` in your terminal to verify if it works. Congratulations, you h
### Install Git version
The **[git version](https://github.com/azerupi/mdBook)** contains all the latest bug-fixes and features, that will be released in the next version on **Crates.io**, if you can't wait until the next release. You can build the git version yourself. Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all the latest bug-fixes and features, that will be released in the next version on **Crates.io**, if you can't wait until the next release. You can build the git version yourself. Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.
```bash
git clone --depth=1 https://github.com/azerupi/mdBook.git
git clone --depth=1 https://github.com/rust-lang-nursery/mdBook.git
cd mdBook
cargo build --release
```

View File

@@ -6,8 +6,8 @@ This preferred by many for writing books with mdbook because it allows for you t
#### Specify a directory
Like `watch`, `serve` can take a directory as argument to use instead of the
current working directory.
Like `watch`, `serve` can take a directory as an argument to use instead of
the current working directory.
```bash
mdbook serve path/to/book
@@ -21,7 +21,7 @@ mdbook serve path/to/book
For example: suppose you had an nginx server for SSL termination which has a public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on port 8000. To run use the nginx proxy do:
```bash
mdbook server path/to/book -p 8000 -i 127.0.0.1 -a 192.168.1.100
mdbook serve path/to/book -p 8000 -i 127.0.0.1 -a 192.168.1.100
```
If you were to want live reloading for this you would need to proxy the websocket calls through nginx as well from `192.168.1.100:<WS_PORT>` to `127.0.0.1:<WS_PORT>`. The `-w` flag allows for the websocket port to be configured.
@@ -37,4 +37,4 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for you
-----
***note:*** *the `serve` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/azerupi/mdBook/issues)*
***note:*** *the `serve` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/rust-lang-nursery/mdBook/issues)*

View File

@@ -5,8 +5,8 @@ You could repeatedly issue `mdbook build` every time a file is changed. But usin
#### Specify a directory
Like `init` and `build`, `watch` can take a directory as argument to use instead of the
current working directory.
Like `init` and `build`, `watch` can take a directory as an argument to use
instead of the current working directory.
```bash
mdbook watch path/to/book
@@ -23,4 +23,4 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for you
-----
***note:*** *the `watch` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/azerupi/mdBook/issues)*
***note:*** *the `watch` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/rust-lang-nursery/mdBook/issues)*

View File

@@ -0,0 +1,352 @@
# Alternate Backends
A "backend" is simply a program which `mdbook` will invoke during the book
rendering process. This program is passed a JSON representation of the book and
configuration information via `stdin`. Once the backend receives this
information it is free to do whatever it wants.
There are already several alternate backends on GitHub which can be used as a
rough example of how this is accomplished in practice.
- [mdbook-linkcheck] - a simple program for verifying the book doesn't contain
any broken links
- [mdbook-epub] - an EPUB renderer
- [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
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.
## Setting Up
First you'll want to create a new binary program and add `mdbook` as a
dependency.
```
$ cargo new --bin mdbook-wordcount
$ cd mdbook-wordcount
$ cargo add mdbook
```
When our `mdbook-wordcount` plugin is invoked, `mdbook` will send it a JSON
version of [`RenderContext`] via our plugin's `stdin`. For convenience, there's
a [`RenderContext::from_json()`] constructor which will load a `RenderContext`.
This is all the boilerplate necessary for our backend to load the book.
```rust
// src/main.rs
extern crate mdbook;
use std::io;
use mdbook::renderer::RenderContext;
fn main() {
let mut stdin = io::stdin();
let ctx = RenderContext::from_json(&mut stdin).unwrap();
}
```
> **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`.
It is recommended that backends use the [`semver`] crate to inspect this field
and emit a warning if there may be a compatibility issue.
## Inspecting the Book
Now our backend has a copy of the book, lets count how many words are in each
chapter!
Because the `RenderContext` contains a [`Book`] field (`book`), and a `Book` has
the [`Book::iter()`] method for iterating over all items in a `Book`, this step
turns out to be just as easy as the first.
```rust
fn main() {
let mut stdin = io::stdin();
let ctx = RenderContext::from_json(&mut stdin).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);
}
}
}
fn count_words(ch: &Chapter) -> usize {
ch.content.split_whitespace().count()
}
```
## Enabling the Backend
Now we've got the basics running, we want to actually use it. First, install
the program.
```
$ cargo install
```
Then `cd` to the particular book you'd like to count the words of and update its
`book.toml` file.
```diff
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David", "Michael-F-Bryan"]
+ [output.html]
+ [output.wordcount]
```
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.
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.
Now you just need to build your book like normal, and everything should *Just
Work*.
```
$ mdbook build
...
2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
mdBook: 126
Command Line Tool: 224
init: 283
build: 145
watch: 146
serve: 292
test: 139
Format: 30
SUMMARY.md: 259
Configuration: 784
Theme: 304
index.hbs: 447
Syntax highlighting: 314
MathJax Support: 153
Rust code specific features: 148
For Developers: 788
Alternate Backends: 710
Contributors: 85
```
The reason we didn't need to specify the full name/path of our `wordcount`
backend is because `mdbook` will try to *infer* the program's name via
convention. The executable for the `foo` backend is typically called
`mdbook-foo`, with an associated `[output.foo]` entry in the `book.toml`. To
explicitly tell `mdbook` what command to invoke (it may require command-line
arguments or be an interpreted script), you can use the `command` field.
```diff
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David", "Michael-F-Bryan"]
[output.html]
[output.wordcount]
+ command = "python /path/to/wordcount.py"
```
## Configuration
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.
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
`get_deserialized()` convenience method for retrieving a value and
automatically deserializing to some arbitrary type `T`.
To implement this, we'll create our own serializable `WordcountConfig` struct
which will encapsulate all configuration for this backend.
First add `serde` and `serde_derive` to your `Cargo.toml`,
```
$ cargo add serde serde_derive
```
And then you can create the config struct,
```rust
extern crate serde;
#[macro_use]
extern crate serde_derive;
...
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct WordcountConfig {
pub ignores: Vec<String>,
}
```
Now we just need to deserialize the `WordcountConfig` from our `RenderContext`
and then add a check to make sure we skip ignored chapters.
```diff
fn main() {
let mut stdin = io::stdin();
let ctx = RenderContext::from_json(&mut stdin).unwrap();
+ 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);
}
}
}
```
## Output and Signalling Failure
While it's nice to print word counts to the terminal when a book is built, it
might also be a good idea to output them to a file somewhere. `mdbook` tells a
backend where it should place any generated output via the `destination` field
in [`RenderContext`].
```diff
+ use std::fs::{self, File};
+ use std::io::{self, Write};
- 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();
}
}
}
```
> **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()`.
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
interpret a non-zero exit code as a rendering failure.
For example, if we wanted to make sure all chapters have an *even* number of
words, erroring out if an odd number is encountered, then you may do something
like this:
```diff
+ use std::process;
...
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();
+ if cfg.deny_odds && num_words % 2 == 1 {
+ eprintln!("{} has an odd number of words!", ch.name);
+ process::exit(1);
}
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct WordcountConfig {
pub ignores: Vec<String>,
+ pub deny_odds: bool,
}
```
Now, if we reinstall the backend and build a book,
```
$ cargo install --force
$ mdbook build /path/to/book
...
2018-01-16 21:21:39 [INFO] (mdbook::renderer): Invoking the "wordcount" renderer
mdBook: 126
Command Line Tool: 224
init: 283
init has an odd number of words!
2018-01-16 21:21:39 [ERROR] (mdbook::renderer): Renderer exited with non-zero return code.
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Error: Rendering failed
2018-01-16 21:21:39 [ERROR] (mdbook::utils): Caused By: The "mdbook-wordcount" renderer failed
```
As you've probably already noticed, output from the plugin's subprocess is
immediately passed through to the user. It is encouraged for plugins to
follow the "rule of silence" and only generate output when necessary (e.g. an
error in generation or a warning).
All environment variables are passed through to the backend, allowing you to
use the usual `RUST_LOG` to control logging verbosity.
## 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
hesitate to create an issue in the [issue tracker] so we can improve the user
guide.
The existing backends mentioned towards the start of this chapter should serve
as a good example of how it's done in real life, so feel free to skim through
the source code or ask questions.
[mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck
[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
[`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
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues

View File

@@ -0,0 +1,46 @@
# For Developers
While `mdbook` is mainly used as a command line tool, you can also import the
underlying library directly and use that to manage a book. It also has a fairly
flexible plugin mechanism, allowing you to create your own custom tooling and
consumers (often referred to as *backends*) if you need to do some analysis of
the book or render it in a different format.
The *For Developers* chapters are here to show you the more advanced usage of
`mdbook`.
The two main ways a developer can hook into the book's build process is via,
- [Preprocessors](for_developers/preprocessors.html)
- [Alternate Backends](for_developers/backends.html)
## The Build Process
The process of rendering a book project goes through several steps.
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
- Discover which preprocessors/backends should be used
2. Run the preprocessors
3. Call each backend in turn
## Using `mdbook` as a Library
The `mdbook` binary is just a wrapper around the `mdbook` crate, exposing its
functionality as a command-line program. As such it is quite easy to create your
own programs which use `mdbook` internally, adding your own functionality (e.g.
a custom preprocessor) or tweaking the build process.
The easiest way to find out how to use the `mdbook` crate is by looking at the
[API Docs]. The top level documentation explains how one would use the
[`MDBook`] type to load and build a book, while the [config] module gives a good
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

View File

@@ -0,0 +1,9 @@
[package]
name = "mdbook-wordcount"
version = "0.1.0"
authors = ["Michael Bryan <michaelfbryan@gmail.com>"]
[dependencies]
mdbook = { path = "../../../..", version = "*" }
serde = "1.0"
serde_derive = "1.0"

View File

@@ -0,0 +1,49 @@
extern crate mdbook;
extern crate serde;
#[macro_use]
extern crate serde_derive;
use std::process;
use std::fs::{self, File};
use std::io::{self, Write};
use mdbook::renderer::RenderContext;
use mdbook::book::{BookItem, Chapter};
fn main() {
let mut stdin = io::stdin();
let ctx = RenderContext::from_json(&mut stdin).unwrap();
let cfg: WordcountConfig = ctx.config
.get_deserialized("output.wordcount")
.unwrap_or_default();
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 {
if cfg.ignores.contains(&ch.name) {
continue;
}
let num_words = count_words(ch);
println!("{}: {}", ch.name, num_words);
writeln!(f, "{}: {}", ch.name, num_words).unwrap();
if cfg.deny_odds && num_words % 2 == 1 {
eprintln!("{} has an odd number of words!", ch.name);
process::exit(1);
}
}
}
}
fn count_words(ch: &Chapter) -> usize {
ch.content.split_whitespace().count()
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct WordcountConfig {
pub ignores: Vec<String>,
pub deny_odds: bool,
}

View File

@@ -0,0 +1,96 @@
# Preprocessors
A *preprocessor* is simply a bit of code which gets run immediately after the
book is loaded and before it gets rendered, allowing you to update and mutate
the book. Possible use cases are:
- Creating custom helpers like `\{{#include /path/to/file.md}}`
- Updating links so `[some chapter](some_chapter.md)` is automatically changed
to `[some chapter](some_chapter.html)` for the HTML renderer
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
mathjax equivalents
## Implementing a Preprocessor
A preprocessor is represented by the `Preprocessor` trait.
```rust
pub trait Preprocessor {
fn name(&self) -> &str;
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
}
```
Where the `PreprocessorContext` is defined as
```rust
pub struct PreprocessorContext {
pub root: PathBuf,
pub config: Config,
}
```
## A complete Example
The magic happens within the `run(...)` method of the [`Preprocessor`][preprocessor-docs] trait implementation.
As direct access to the chapters is not possible, you will probably end up iterating
them using `for_each_mut(...)`:
```rust
book.for_each_mut(|item: &mut BookItem| {
if let BookItem::Chapter(ref mut chapter) = *item {
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
res = Some(
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
Ok(md) => {
chapter.content = md;
Ok(())
}
Err(err) => Err(err),
},
);
}
});
```
The `chapter.content` is just a markdown formatted string, and you will have to
process it in some way. Even though it's entirely possible to implement some sort of
manual find & replace operation, if that feels too unsafe you can use [`pulldown-cmark`][pc]
to parse the string into events and work on them instead.
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events back to
a string.
The following code block shows how to remove all emphasis from markdown, and do so
safely.
```rust
fn remove_emphasis(num_removed_items: &mut i32, 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)))
}
```
For everything else, have a look [at the complete example][example].
[preprocessor-docs]: https://docs.rs/mdbook/0.1.3/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

View File

@@ -2,21 +2,23 @@
You can configure the parameters for your book in the ***book.toml*** file.
>**Note:**
JSON configuration files were previously supported but have been deprecated in favor of
the TOML configuration file. If you are still using JSON we strongly encourage you to migrate to
the TOML configuration because JSON support will be removed in the future.
Here is an example of what a ***book.toml*** file might look like:
```toml
[book]
title = "Example book"
author = "John Doe"
description = "The example book covers examples."
[build]
build-dir = "my-example-book"
create-missing = false
[output.html]
destination = "my-example-book"
additional-css = ["custom.css"]
[output.html.search]
limit-results = 15
```
## Supported configuration options
@@ -24,66 +26,172 @@ additional-css = ["custom.css"]
It is important to note that **any** relative path specified in the in the configuration will
always be taken relative from the root of the book where the configuration file is located.
### General metadata
This is general information about your book.
- **title:** The title of the book
- **author:** The author of the book
- **description:** A description for the book, which is added as meta information in the html `<head>` of each page
**book.toml**
```toml
title = "Example book"
author = "John Doe"
description = "The example book covers examples."
```
Some books may have multiple authors, there is an alternative key called `authors` plural that lets you specify an array
of authors.
- **authors:** The author(s) of the book
- **description:** A description for the book, which is added as meta
information in the html `<head>` of each page
- **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.
**book.toml**
```toml
[book]
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`
```
### Source directory
By default, the source directory is found in the directory named `src` directly under the root folder. But this is configurable
with the `source` key in the configuration file.
### Build options
This controls the build process of your book.
- **build-dir:** The directory to put the rendered book in. By default this is
`book/` in the book's root directory.
- **create-missing:** By default, any missing files specified in `SUMMARY.md`
will be created when the book is built (i.e. `create-missing = true`). If this
is `false` then the build process will instead exit with an error if any files
do not exist.
**book.toml**
```toml
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
source = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
[build]
build-dir = "build"
create-missing = false
```
### HTML renderer options
The HTML renderer has a couple of options aswell. All the options for the renderer need to be specified under the TOML table `[output.html]`.
The HTML renderer has a couple of options as well. All the options for the
renderer need to be specified under the TOML table `[output.html]`.
The following configuration options are available:
- **`destination`:** By default, the HTML book will be rendered in the `root/book` directory, but this option lets you specify another
destination fodler.
- **`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.
- **`curly-quotes`:** Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans. 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 without overwriting the whole style, you can specify a set of stylesheets that will be loaded after the default ones where you can surgically change the style.
- **`additional-js`:** If you need to add some behaviour to your book without removing the current behaviour, you can specify a set of javascript files that will be loaded alongside the default one.
- **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.
- **curly-quotes:** Convert straight quotes to curly quotes, except for
those that occur in code blocks and code spans. 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 without overwriting the whole style, you can specify a set of
stylesheets that will be loaded after the default ones where you can
surgically change the style.
- **additional-js:** If you need to add some behaviour to your book without
removing the current behaviour, you can specify a set of JavaScript files
that will be loaded alongside the default one.
- **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`.
- **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).
**book.toml**
Available configuration options for the `[output.html.playpen]` table:
- **editable:** Allow editing the source code. Defaults to `false`.
- **copy-js:** Copy JavaScript files for the editor to the output directory.
Defaults to `true`.
[Ace]: https://ace.c9.io/
Available configuration options for the `[output.html.search]` table:
- **limit-results:** The maximum number of search results. Defaults to `30`.
- **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`.
- **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 appears in the hierarchy. The hierarchy contains all titles of the
parent documents and all parent headings. Defaults to `1`.
- **boost-paragraph:** Boost factor for the search result score if a search
word appears in the text. Defaults to `1`.
- **expand:** True if search should match longer results e.g. search `micro`
should match `microwave`. Defaults to `true`.
- **heading-split-level:** Search results will link to a section of the document
which contains the result. Documents are split into sections by headings
this level or less.
Defaults to `3`. (`### This is a level 3 heading`)
- **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**:
```toml
[book]
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
[output.html]
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
theme = "my-theme"
curly-quotes = true
google-analytics = "123456"
additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"]
[output.html.playpen]
editor = "./path/to/editor"
editable = false
[output.html.search]
enable = true
searcher = "./path/to/searcher"
limit-results = 30
teaser-word-count = 30
use-boolean-and = true
boost-title = 2
boost-hierarchy = 1
boost-paragraph = 1
expand = true
heading-split-level = 3
```
## Environment Variables
All configuration values can be overridden from the command line by setting the
corresponding environment variable. Because many operating systems restrict
environment variables to be alphanumeric characters or `_`, the configuration
key needs to be formatted slightly differently to the normal `foo.bar.baz` form.
Variables starting with `MDBOOK_` are used for configuration. The key is
created by removing the `MDBOOK_` prefix and turning the resulting
string into `kebab-case`. Double underscores (`__`) separate nested
keys, while a single underscore (`_`) is replaced with a dash (`-`).
For example:
- `MDBOOK_foo` -> `foo`
- `MDBOOK_FOO` -> `foo`
- `MDBOOK_FOO__BAR` -> `foo.bar`
- `MDBOOK_FOO_BAR` -> `foo-bar`
- `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can
override the book's title without needing to touch your `book.toml`.
> **Note:** To facilitate setting more complex config items, the value
> of an environment variable is first parsed as JSON, falling back to a
> string if the parse fails.
>
> This means, if you so desired, you could override all book metadata
> when building the book with something like
>
> ```text
> $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
> $ mdbook build
> ```
The latter case may be useful in situations where `mdbook` is invoked
from a script or CI, where it sometimes isn't possible to update the
`book.toml` before building.

View File

@@ -13,7 +13,7 @@ mathjax-support = true
The usual delimiters MathJax uses are not yet supported. You can't currently use `$$ ... $$` as delimiters and the `\[ ... \]` delimiters need an extra backslash to work. Hopefully this limitation will be lifted soon.
### Inline equations
Inline equations are delimited by `\\[` and `\\]`. So for example, to render the following inline equation \\( \int x dx = \frac{x^2}{2} + C \\) you would write the following:
Inline equations are delimited by `\\(` and `\\)`. So for example, to render the following inline equation \\( \int x dx = \frac{x^2}{2} + C \\) you would write the following:
```
\\( \int x dx = \frac{x^2}{2} + C \\)
```

View File

@@ -0,0 +1,64 @@
# mdBook-specific markdown
## Hiding code lines
There is a feature in mdBook that lets you hide code lines by prepending them with a `#`.
```bash
# fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
# }
```
Will render as
```rust
# fn main() {
let x = 5;
let y = 7;
println!("{}", x + y);
# }
```
## Including files
With the following syntax, you can include files into your book:
```hbs
\{{#include file.rs}}
```
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:
```hbs
\{{#include file.rs:2}}
\{{#include file.rs::10}}
\{{#include file.rs:2:}}
\{{#include file.rs:2:10}}
```
The first command only includes the second line from file `file.rs`. The second command includes all lines up to line 10, i.e. the lines from 11 till the end of 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.
## Inserting runnable Rust files
With the following syntax, you can insert runnable Rust files into your book:
```hbs
\{{#playpen file.rs}}
```
The path to the Rust file has to be relative from the current source file.
When play is clicked, the code snippet will be sent to the [Rust Playpen] to be compiled and run. The result is sent back and displayed directly underneath the code.
Here is what a rendered code snippet looks like:
{{#playpen example.rs}}
[Rust Playpen]: https://play.rust-lang.org/

View File

@@ -1,42 +0,0 @@
# Rust code specific features
## Hiding code lines
There is a feature in mdBook that let's you hide code lines by prepending them with a `#`.
```bash
# fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
# }
```
Will render as
```rust
# fn main() {
let x = 5;
let y = 7;
println!("{}", x + y);
# }
```
## Inserting runnable Rust files
With the following syntax, you can insert runnable Rust files into your book:
```hbs
\{{#playpen file.rs}}
```
The path to the Rust file has to be relative from the current source file.
When play is clicked, the code snippet will be send to the [Rust Playpen]() to be compiled and run. The result is send back and displayed directly underneath the code.
Here is what a rendered code snippet looks like:
{{#playpen example.rs}}

View File

@@ -0,0 +1,40 @@
# Editor
In addition to providing runnable code playpens, mdBook optionally allows them to be editable. In order to enable editable code blocks, the following needs to be added to the ***book.toml***:
```toml
[output.html.playpen]
editable = true
```
To make a specific block available for editing, the attribute `editable` needs to be added to it:
<pre><code class="language-markdown">```rust,editable
fn main() {
let number = 5;
print!("{}", number);
}
```</code></pre>
The above will result in this editable playpen:
```rust,editable
fn main() {
let number = 5;
print!("{}", number);
}
```
Note the new `Undo Changes` button in the editable playpens.
## Customizing the Editor
By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired, the functionality may be overriden by providing a different folder:
```toml
[output.html.playpen]
editable = true
editor = "/path/to/editor"
```
Note that for the editor changes to function correctly, the `book.js` inside of the `theme` folder will need to be overriden as it has some couplings with the default Ace editor.

View File

@@ -87,5 +87,5 @@ In addition to the properties you can access, there are some handlebars helpers
------
*If you would like me to expose other properties or helpers, please [create a new issue](https://github.com/azerupi/mdBook/issues)
*If you would like me to expose other properties or helpers, please [create a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
and I will consider it.*

View File

@@ -53,7 +53,7 @@ Will render as
## Improve default theme
If you think the default theme doesn't look quite right for a specific language, or could be improved.
Feel free to [submit a new issue](https://github.com/azerupi/mdBook/issues) explaining what you have in mind and I will take a look at it.
Feel free to [submit a new issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you have in mind and I will take a look at it.
You could also create a pull-request with the proposed improvements.

View File

@@ -1,24 +0,0 @@
# Rust Library
mdBook is not only a command line tool, it can be used as a crate. You can extend it,
integrate it in current projects. Here is a short example:
```rust,ignore
extern crate mdbook;
use mdbook::MDBook;
use std::path::Path;
# #[allow(unused_variables)]
fn main() {
let mut book = MDBook::new("my-book") // Path to root
.with_source("src") // Path from root to source directory
.with_destination("book") // Path from root to output directory
.read_config() // Parse book.toml or book.json configuration file
.expect("I don't handle configuration file error, but you should!");
book.build().unwrap(); // Render the book
}
```
Check here for the [API docs](mdbook/index.html) generated by rustdoc.

View File

@@ -11,3 +11,8 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
- Wayne Nilsen ([waynenilsen](https://github.com/waynenilsen))
- [funnkill](https://github.com/funkill)
- Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang))
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
- [Chris Spiegel](https://github.com/cspiegel)
- [projektir](https://github.com/projektir)
- [Phaiax](https://github.com/Phaiax)
- [Matt Ickstadt](https://github.com/mattico)

View File

@@ -32,26 +32,23 @@ error_chain!{
}
fn program_exists(program: &str) -> Result<()> {
execs::cmd(program)
.arg("-v")
.output()
.chain_err(|| format!("Please install '{}'!", program))?;
execs::cmd(program).arg("-v")
.output()
.chain_err(|| format!("Please install '{}'!", program))?;
Ok(())
}
fn npm_package_exists(package: &str) -> Result<()> {
let status = execs::cmd("npm")
.args(&["list", "-g"])
.arg(package)
.output();
let status = execs::cmd("npm").args(&["list", "-g"])
.arg(package)
.output();
match status {
Ok(ref out) if out.status.success() => Ok(()),
_ => {
bail!("Missing npm package '{0}' \
install with: 'npm -g install {0}'",
bail!("Missing npm package '{0}' install with: 'npm -g install {0}'",
package)
},
}
}
}
@@ -59,7 +56,7 @@ pub enum Resource<'a> {
Program(&'a str),
Package(&'a str),
}
use Resource::{Program, Package};
use Resource::{Package, Program};
impl<'a> Resource<'a> {
pub fn exists(&self) -> Result<()> {
@@ -71,7 +68,6 @@ impl<'a> Resource<'a> {
}
fn run() -> Result<()> {
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
// Check dependencies
Program("npm").exists()?;
@@ -85,15 +81,15 @@ fn run() -> Result<()> {
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
let stylus_dir = theme_dir.join("stylus/book.styl");
if !execs::cmd("stylus")
.arg(stylus_dir)
.arg("--out")
.arg(theme_dir)
.arg("--use")
.arg("nib")
.status()?
.success() {
bail!("Stylus encoutered an error");
if !execs::cmd("stylus").arg(stylus_dir)
.arg("--out")
.arg(theme_dir)
.arg("--use")
.arg("nib")
.status()?
.success()
{
bail!("Stylus encountered an error");
}
}
Ok(())

View File

@@ -1,32 +1,29 @@
# `before_deploy` phase: here we package the build artifacts
# This script takes care of building your crate and packaging it for release
set -ex
mktempd() {
echo $(mktemp -d 2>/dev/null || mktemp -d -t tmp)
}
mk_artifacts() {
cargo build --target $TARGET --release
}
mk_tarball() {
local td=$(mktempd)
local out_dir=$(pwd)
cp target/$TARGET/release/mdbook $td
pushd $td
tar czf $out_dir/${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.tar.gz *
popd $td
rm -r $td
}
main() {
mk_artifacts
mk_tarball
local src=$(pwd) \
stage=
case $TRAVIS_OS_NAME in
linux)
stage=$(mktemp -d)
;;
osx)
stage=$(mktemp -d -t tmp)
;;
esac
cargo rustc --bin mdbook --target $TARGET --release -- -C lto
cp target/release/mdbook $stage/
cd $stage
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
cd $src
rm -rf $stage
}
main

50
ci/github_pages.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Deploys the `book-example` to GitHub Pages
set -ex
# Only run this on the master branch for stable
if [ "$TRAVIS_PULL_REQUEST" != "false" ] ||
[ "$TRAVIS_BRANCH" != "master" ] ||
[ "$TRAVIS_RUST_VERSION" != "stable" ] ||
[ "$TARGET" != "x86_64-unknown-linux-gnu" ]; then
exit 0
fi
# Make sure we have the css dependencies
npm install -g stylus nib
NC='\033[39m'
CYAN='\033[36m'
GREEN='\033[32m'
rev=$(git rev-parse --short HEAD)
echo -e "${CYAN}Running cargo doc${NC}"
cargo doc --features regenerate-css > /dev/null
echo -e "${CYAN}Running mdbook build${NC}"
cargo run -- build book-example/
echo -e "${CYAN}Copying book to target/doc${NC}"
cp -R book-example/book/* target/doc/
cd target/doc
echo -e "${CYAN}Initializing Git${NC}"
git init
git config user.name "Michael Bryan"
git config user.email "michaelfbryan@gmail.com"
git remote add upstream "https://$GH_TOKEN@github.com/rust-lang-nursery/mdBook.git"
git fetch upstream --quiet
git reset upstream/gh-pages --quiet
touch .
echo -e "${CYAN}Pushing changes to gh-pages${NC}"
git add -A .
git commit -m "rebuild pages at ${rev}" --quiet
git push -q upstream HEAD:gh-pages --quiet
echo -e "${GREEN}Deployed docs to GitHub Pages${NC}"

View File

@@ -1,59 +1,47 @@
# `install` phase: install stuff needed for the `script` phase
set -ex
case "$TRAVIS_OS_NAME" in
linux)
host=x86_64-unknown-linux-gnu
;;
osx)
host=x86_64-apple-darwin
;;
esac
mktempd() {
echo $(mktemp -d 2>/dev/null || mktemp -d -t tmp)
}
install_rustup() {
local td=$(mktempd)
pushd $td
curl -O https://static.rust-lang.org/rustup/dist/$host/rustup-setup
chmod +x rustup-setup
./rustup-setup -y
popd
rm -r $td
rustup self update
rustup install "$CHANNEL"
rustup default "$CHANNEL"
rustc -V
cargo -V
}
install_standard_crates() {
if [ "$host" != "$TARGET" ]; then
if [ ! "$CHANNEL" = "stable" ]; then
rustup target add $TARGET
else
local version=$(rustc -V | cut -d' ' -f2)
local tarball=rust-std-${version}-${TARGET}
local td=$(mktempd)
curl -s https://static.rust-lang.org/dist/${tarball}.tar.gz | \
tar --strip-components 1 -C $td -xz
$td/install.sh --prefix=$(rustc --print sysroot)
rm -r $td
fi
fi
}
main() {
install_rustup
install_standard_crates
local target=
if [ $TRAVIS_OS_NAME = linux ]; then
target=x86_64-unknown-linux-musl
sort=sort
else
target=x86_64-apple-darwin
sort=gsort # for `sort --sort-version`, from brew's coreutils.
fi
# Builds for iOS are done on OSX, but require the specific target to be
# installed.
case $TARGET in
aarch64-apple-ios)
rustup target install aarch64-apple-ios
;;
armv7-apple-ios)
rustup target install armv7-apple-ios
;;
armv7s-apple-ios)
rustup target install armv7s-apple-ios
;;
i386-apple-ios)
rustup target install i386-apple-ios
;;
x86_64-apple-ios)
rustup target install x86_64-apple-ios
;;
esac
# This fetches latest stable release
local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \
| cut -d/ -f3 \
| grep -E '^v[0.1.0-9.]+$' \
| $sort --version-sort \
| tail -n1)
curl -LSfs https://japaric.github.io/trust/install.sh | \
sh -s -- \
--force \
--git japaric/cross \
--tag $tag \
--target $target
}
main
main

View File

@@ -1,45 +0,0 @@
# `script` phase: you usually build, test and generate docs in this phase
set -ex
# NOTE Workaround for rust-lang/rust#31907 - disable doc tests when cross compiling
# This has been fixed in the nightly channel but it would take a while to reach the other channels
disable_cross_doctests() {
local host
case "$TRAVIS_OS_NAME" in
linux)
host=x86_64-unknown-linux-gnu
;;
osx)
host=x86_64-apple-darwin
;;
esac
if [ "$host" != "$TARGET" ] && [ "$CHANNEL" != "nightly" ]; then
if [ "$TRAVIS_OS_NAME" = "osx" ]; then
brew install gnu-sed --default-names
fi
find src -name '*.rs' -type f | xargs sed -i -e 's:\(//.\s*```\):\1 ignore,:g'
fi
}
run_test_suite() {
# Extra test without default features to avoid bitrot. We only test on a single target (but with
# all the channels) to avoid significantly increasing the build times
if [ $TARGET = x86_64-unknown-linux-gnu ]; then
cargo build --target $TARGET --no-default-features --verbose
cargo test --target $TARGET --no-default-features --verbose
cargo clean
fi
cargo build --target $TARGET --verbose
cargo test --target $TARGET --verbose
}
main() {
disable_cross_doctests
run_test_suite
}
main

View File

@@ -1,42 +0,0 @@
#!/bin/bash
# Exit on error or variable unset
set -o errexit -o nounset
NC='\033[39m'
CYAN='\033[36m'
GREEN='\033[32m'
rev=$(git rev-parse --short HEAD)
echo -e "${CYAN}Running cargo doc${NC}"
# Run cargo doc
cargo doc
echo -e "${CYAN}Running mdbook build${NC}"
# Run mdbook to generate the book
target/"$TARGET"/debug/mdbook build book-example/
echo -e "${CYAN}Copying book to target/doc${NC}"
# Copy files from rendered book to doc root
cp -R book-example/book/* target/doc/
cd target/doc
echo -e "${CYAN}Initializing Git${NC}"
git init
git config user.name "Mathieu David"
git config user.email "mathieudavid@mathieudavid.org"
git remote add upstream "https://$GH_TOKEN@github.com/azerupi/mdBook.git"
git fetch upstream
git reset upstream/gh-pages
touch .
echo -e "${CYAN}Pushing changes to gh-pages${NC}"
git add -A .
git commit -m "rebuild pages at ${rev}"
git push -q upstream HEAD:gh-pages
echo -e "${GREEN}Deployement done${NC}"

94
examples/de-emphasize.rs Normal file
View File

@@ -0,0 +1,94 @@
//! 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::errors::{Error, Result};
use mdbook::MDBook;
use mdbook::book::{Book, BookItem, Chapter};
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use pulldown_cmark::{Event, Parser, Tag};
use pulldown_cmark_to_cmark::fmt::cmark;
use std::ffi::OsString;
use std::env::{args, args_os};
use std::process;
struct Deemphasize;
impl Preprocessor for Deemphasize {
fn name(&self) -> &str {
"md-links-to-html-links"
}
fn run(&self, _ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
eprintln!("Running '{}' preprocessor", self.name());
let mut res: Option<_> = None;
let mut num_removed_items = 0;
book.for_each_mut(|item: &mut BookItem| {
if let Some(Err(_)) = res {
return;
}
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),
},
);
}
});
eprintln!(
"{}: removed {} events from markdown stream.",
self.name(),
num_removed_items
);
match res {
Some(res) => res,
None => Ok(()),
}
}
}
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);
}
}
impl Deemphasize {
fn remove_emphasis(num_removed_items: &mut i32, 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

@@ -1,16 +1,7 @@
write_mode = "Overwrite"
array_layout = "Visual"
chain_indent = "Visual"
fn_args_layout = "Visual"
fn_call_style = "Visual"
format_strings = true
generics_indent = "Visual"
max_width = 120
ideal_width = 120
fn_call_width = 100
fn_args_density = "Compressed"
enum_trailing_comma = true
match_block_trailing_comma = true
struct_trailing_comma = "Always"
wrap_comments = true
use_try_shorthand = true
report_todo = "Always"
report_fixme = "Always"

View File

@@ -1,4 +1,5 @@
use clap::{ArgMatches, SubCommand, App};
use std::path::PathBuf;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use {get_book_dir, open};
@@ -8,34 +9,29 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("build")
.about("Build the book from the markdown files")
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
.arg_from_usage("--no-create 'Will not create non-existent files linked from SUMMARY.md'")
.arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'")
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book \
when omitted)'",
)
.arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'",
)
}
// Build command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args);
let book = MDBook::new(&book_dir).read_config()?;
let mut book = MDBook::load(&book_dir)?;
let mut book = match args.value_of("dest-dir") {
Some(dest_dir) => book.with_destination(dest_dir),
None => book,
};
if args.is_present("no-create") {
book.create_missing = false;
}
if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = PathBuf::from(dest_dir);
}
book.build()?;
if args.is_present("open") {
open(book.get_destination().join("index.html"));
// FIXME: What's the right behaviour if we don't use the HTML renderer?
open(book.build_dir_for("html").join("index.html"));
}
Ok(())

30
src/bin/clean.rs Normal file
View File

@@ -0,0 +1,30 @@
use std::fs;
use std::path::PathBuf;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::*;
use get_book_dir;
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("clean")
.about("Delete built book")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'The directory of built book{n}(Defaults to ./book when \
omitted)'",
)
}
// Clean command implementation
pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
let book_dir = get_book_dir(args);
let book = MDBook::load(&book_dir)?;
let dir_to_remove = match args.value_of("dest-dir") {
Some(dest_dir) => PathBuf::from(dest_dir),
None => book.root.join(&book.config.build.build_dir),
};
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
Ok(())
}

View File

@@ -1,8 +1,11 @@
use std::io;
use std::io::Write;
use clap::{ArgMatches, SubCommand, App};
use std::process::Command;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use mdbook::utils;
use mdbook::config;
use get_book_dir;
// Create clap subcommand arguments
@@ -10,62 +13,90 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("init")
.about("Create boilerplate structure and files in the directory")
// the {n} denotes a newline which will properly aligned in all help messages
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory \
when omitted)'")
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
.arg_from_usage("--force 'skip confirmation prompts'")
}
// Init command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir);
// Call the function that does the initialization
book.init()?;
let mut builder = MDBook::init(&book_dir);
let mut config = config::Config::default();
// If flag `--theme` is present, copy theme to src
if args.is_present("theme") {
config.set("output.html.theme", "src/theme")?;
// Skip this if `--force` is present
if !args.is_present("force") {
// Print warning
print!("\nCopying the default theme to {:?}", book.get_source());
println!("could potentially overwrite files already present in that directory.");
println!();
println!(
"Copying the default theme to {}",
builder.config().book.src.display()
);
println!("This could potentially overwrite files already present in that directory.");
print!("\nAre you sure you want to continue? (y/n) ");
// Read answer from user and exit if it's not 'yes'
if !confirm() {
println!("\nSkipping...\n");
println!("All done, no errors...");
::std::process::exit(0);
if confirm() {
builder.copy_theme(true);
}
}
// Call the function that copies the theme
book.copy_theme()?;
println!("\nTheme copied.");
}
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
let is_dest_inside_root = book.get_destination().starts_with(book.get_root());
if !args.is_present("force") && is_dest_inside_root {
println!("\nDo you want a .gitignore to be created? (y/n)");
if confirm() {
book.create_gitignore();
println!("\n.gitignore created.");
} else {
builder.copy_theme(true);
}
}
println!("\nDo you want a .gitignore to be created? (y/n)");
if confirm() {
builder.create_gitignore(true);
}
config.book.title = request_book_title();
if let Some(author) = get_author_name() {
debug!("Obtained user name from gitconfig: {:?}", author);
config.book.authors.push(author);
builder.with_config(config);
}
builder.build()?;
println!("\nAll done, no errors...");
Ok(())
}
// Simple function that user comfirmation
/// Obtains author name from git config file by running the `git config` command.
fn get_author_name() -> Option<String> {
let output = Command::new("git")
.args(&["config", "--get", "user.name"])
.output()
.ok()?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).trim().to_owned())
} else {
None
}
}
/// Request book title from user and return if provided.
fn request_book_title() -> Option<String> {
println!("What title would you like to give the book? ");
io::stdout().flush().unwrap();
let mut resp = String::new();
io::stdin().read_line(&mut resp).unwrap();
let resp = resp.trim();
if resp.is_empty() {
None
} else {
Some(resp.into())
}
}
// Simple function for user confirmation
fn confirm() -> bool {
io::stdout().flush().unwrap();
let mut s = String::new();

View File

@@ -1,19 +1,25 @@
extern crate mdbook;
extern crate chrono;
#[macro_use]
extern crate clap;
extern crate log;
extern crate env_logger;
extern crate error_chain;
#[macro_use]
extern crate log;
extern crate mdbook;
extern crate open;
use std::env;
use std::ffi::OsStr;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use clap::{App, ArgMatches, AppSettings};
use log::{LogRecord, LogLevelFilter};
use env_logger::LogBuilder;
use std::io::Write;
use clap::{App, AppSettings, ArgMatches};
use chrono::Local;
use log::LevelFilter;
use env_logger::Builder;
use mdbook::utils;
pub mod build;
pub mod clean;
pub mod init;
pub mod test;
#[cfg(feature = "serve")]
@@ -32,11 +38,15 @@ fn main() {
.author("Mathieu David <mathieudavid@mathieudavid.org>")
// Get the version from our Cargo.toml using clap's crate_version!() macro
.version(concat!("v",crate_version!()))
.setting(AppSettings::SubcommandRequired)
.after_help("For more information about a specific command, try `mdbook <command> --help`\nSource code for mdbook available at: https://github.com/azerupi/mdBook")
.setting(AppSettings::ArgRequiredElseHelp)
.after_help("For more information about a specific command, \
try `mdbook <command> --help`\n\
Source code for mdbook available \
at: https://github.com/rust-lang-nursery/mdBook")
.subcommand(init::make_subcommand())
.subcommand(build::make_subcommand())
.subcommand(test::make_subcommand());
.subcommand(test::make_subcommand())
.subcommand(clean::make_subcommand());
#[cfg(feature = "watch")]
let app = app.subcommand(watch::make_subcommand());
@@ -47,6 +57,7 @@ fn main() {
let res = match app.get_matches().subcommand() {
("init", Some(sub_matches)) => init::execute(sub_matches),
("build", Some(sub_matches)) => build::execute(sub_matches),
("clean", Some(sub_matches)) => clean::execute(sub_matches),
#[cfg(feature = "watch")]
("watch", Some(sub_matches)) => watch::execute(sub_matches),
#[cfg(feature = "serve")]
@@ -56,25 +67,36 @@ fn main() {
};
if let Err(e) = res {
writeln!(&mut io::stderr(), "An error occured:\n{}", e).ok();
utils::log_backtrace(&e);
::std::process::exit(101);
}
}
fn init_logger() {
let format = |record: &LogRecord| {
let module_path = record.location().module_path();
format!("{}:{}: {}", record.level(), module_path, record.args())
};
let mut builder = Builder::new();
let mut builder = LogBuilder::new();
builder.format(format).filter(None, LogLevelFilter::Info);
builder.format(|formatter, record| {
writeln!(
formatter,
"{} [{}] ({}): {}",
Local::now().format("%Y-%m-%d %H:%M:%S"),
record.level(),
record.target(),
record.args()
)
});
if let Ok(var) = env::var("RUST_LOG") {
builder.parse(&var);
builder.parse(&var);
} else {
// if no RUST_LOG provided, default to logging at the Info level
builder.filter(None, LevelFilter::Info);
// Filter extraneous html5ever not-implemented messages
builder.filter(Some("html5ever"), LevelFilter::Error);
}
builder.init().unwrap();
builder.init();
}
fn get_book_dir(args: &ArgMatches) -> PathBuf {
@@ -87,12 +109,12 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
p.to_path_buf()
}
} else {
env::current_dir().unwrap()
env::current_dir().expect("Unable to determine the current directory")
}
}
fn open<P: AsRef<OsStr>>(path: P) {
if let Err(e) = open::that(path) {
println!("Error opening web browser: {}", e);
error!("Error opening web browser: {}", e);
}
}

View File

@@ -3,11 +3,12 @@ extern crate staticfile;
extern crate ws;
use std;
use std::path::Path;
use self::iron::{Iron, AfterMiddleware, IronResult, IronError, Request, Response, status, Set, Chain};
use clap::{ArgMatches, SubCommand, App};
use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response,
Set};
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use mdbook::utils;
use mdbook::errors::*;
use {get_book_dir, open};
#[cfg(feature = "watch")]
use watch;
@@ -18,31 +19,28 @@ struct ErrorRecover;
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("serve")
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.")
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
.arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'")
.arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'",
)
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
.arg_from_usage("-w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)'")
.arg_from_usage("-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'")
.arg_from_usage("-a, --address=[address] 'Address that the browser can reach the websocket server from{n}(Defaults to the interface address)'")
.arg_from_usage(
"-w, --websocket-port=[ws-port] 'Use another port for the websocket connection \
(livereload){n}(Defaults to 3001)'",
)
.arg_from_usage(
"-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'",
)
.arg_from_usage(
"-a, --address=[address] 'Address that the browser can reach the websocket server \
from{n}(Defaults to the interface address)'",
)
.arg_from_usage("-o, --open 'Open the book server in a web browser'")
}
// Watch command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
const RELOAD_COMMAND: &'static str = "reload";
let book_dir = get_book_dir(args);
let book = MDBook::new(&book_dir).read_config()?;
let mut book = match args.value_of("dest-dir") {
Some(dest_dir) => book.with_destination(Path::new(dest_dir)),
None => book,
};
if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
}
let mut book = MDBook::load(&book_dir)?;
let port = args.value_of("port").unwrap_or("3000");
let ws_port = args.value_of("websocket-port").unwrap_or("3001");
@@ -53,52 +51,56 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let address = format!("{}:{}", interface, port);
let ws_address = format!("{}:{}", interface, ws_port);
book.set_livereload(format!(r#"
<script type="text/javascript">
var socket = new WebSocket("ws://{}:{}");
socket.onmessage = function (event) {{
if (event.data === "{}") {{
socket.close();
location.reload(true); // force reload from server (not from cache)
}}
}};
window.onbeforeunload = function() {{
socket.close();
}}
</script>
"#,
public_address,
ws_port,
RELOAD_COMMAND));
let livereload_url = format!("ws://{}:{}", public_address, ws_port);
book.config
.set("output.html.livereload-url", &livereload_url)?;
book.build()?;
let mut chain = Chain::new(staticfile::Static::new(book.get_destination()));
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
chain.link_after(ErrorRecover);
let _iron = Iron::new(chain).http(&*address).unwrap();
let _iron = Iron::new(chain)
.http(&*address)
.chain_err(|| "Unable to launch the server")?;
let ws_server = ws::WebSocket::new(|_| |_| Ok(())).unwrap();
let ws_server =
ws::WebSocket::new(|_| |_| Ok(())).chain_err(|| "Unable to start the websocket")?;
let broadcaster = ws_server.broadcaster();
std::thread::spawn(move || { ws_server.listen(&*ws_address).unwrap(); });
std::thread::spawn(move || {
ws_server.listen(&*ws_address).unwrap();
});
let serving_url = format!("http://{}", address);
println!("\nServing on: {}", serving_url);
info!("Serving on: {}", serving_url);
if open_browser {
open(serving_url);
}
#[cfg(feature = "watch")]
watch::trigger_on_change(&mut book, move |path, book| {
println!("File changed: {:?}\nBuilding book...\n", path);
match book.build() {
Err(e) => println!("Error while building: {:?}", e),
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
watch::trigger_on_change(&mut book, move |path, book_dir| {
info!("File changed: {:?}", path);
info!("Building book...");
// FIXME: This area is really ugly because we need to re-set livereload :(
let livereload_url = livereload_url.clone();
let result = MDBook::load(&book_dir)
.and_then(move |mut b| {
b.config.set("output.html.livereload-url", &livereload_url)?;
Ok(b)
})
.and_then(|b| b.build());
if let Err(e) = result {
error!("Unable to load the book");
utils::log_backtrace(&e);
} else {
let _ = broadcaster.send("reload");
}
println!("");
});
Ok(())

View File

@@ -1,4 +1,4 @@
use clap::{ArgMatches, SubCommand, App};
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use get_book_dir;
@@ -7,14 +7,18 @@ use get_book_dir;
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("test")
.about("Test that code samples compile")
.arg_from_usage("-L, --library-path [DIR]... 'directory to add to crate search path'")
.arg_from_usage(
"-L, --library-path [DIR]... 'directory to add to crate search path'",
)
}
// test command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let library_paths: Vec<&str> = args.values_of("library-path").map(|v| v.collect()).unwrap_or_default();
let library_paths: Vec<&str> = args.values_of("library-path")
.map(|v| v.collect())
.unwrap_or_default();
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config()?;
let mut book = MDBook::load(&book_dir)?;
book.test(library_paths)?;

View File

@@ -4,8 +4,9 @@ use std::path::Path;
use self::notify::Watcher;
use std::time::Duration;
use std::sync::mpsc::channel;
use clap::{ArgMatches, SubCommand, App};
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::utils;
use mdbook::errors::Result;
use {get_book_dir, open};
@@ -14,44 +15,38 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("watch")
.about("Watch the files for changes")
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
.arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'")
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
.arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'",
)
}
// Watch command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args);
let book = MDBook::new(&book_dir).read_config()?;
let mut book = match args.value_of("dest-dir") {
Some(dest_dir) => book.with_destination(dest_dir),
None => book,
};
if args.is_present("curly-quotes") {
book = book.with_curly_quotes(true);
}
let book = MDBook::load(&book_dir)?;
if args.is_present("open") {
book.build()?;
open(book.get_destination().join("index.html"));
open(book.build_dir_for("html").join("index.html"));
}
trigger_on_change(&mut book, |path, book| {
println!("File changed: {:?}\nBuilding book...\n", path);
if let Err(e) = book.build() {
println!("Error while building: {:?}", e);
trigger_on_change(&book, |path, book_dir| {
info!("File changed: {:?}\nBuilding book...\n", path);
let result = MDBook::load(&book_dir).and_then(|b| b.build());
if let Err(e) = result {
error!("Unable to build the book");
utils::log_backtrace(&e);
}
println!("");
});
Ok(())
}
// Calls the closure when a book source file is changed. This is blocking!
pub fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
where F: Fn(&Path, &mut MDBook) -> ()
/// 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),
{
use self::notify::RecursiveMode::*;
use self::notify::DebouncedEvent::*;
@@ -62,52 +57,31 @@ pub fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) {
Ok(w) => w,
Err(e) => {
println!("Error while trying to watch the files:\n\n\t{:?}", e);
::std::process::exit(0);
},
error!("Error while trying to watch the files:\n\n\t{:?}", e);
::std::process::exit(1)
}
};
// Add the source directory to the watcher
if let Err(e) = watcher.watch(book.get_source(), Recursive) {
println!("Error while watching {:?}:\n {:?}", book.get_source(), e);
::std::process::exit(0);
if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
::std::process::exit(1);
};
// Add the theme directory to the watcher
watcher.watch(book.get_theme_path(), Recursive).unwrap_or_default();
let _ = watcher.watch(book.theme_dir(), Recursive);
// Add the book.toml file to the watcher if it exists
let _ = watcher.watch(book.root.join("book.toml"), NonRecursive);
// Add the book.{json,toml} file to the watcher if it exists, because it's not
// located in the source directory
if watcher
.watch(book.get_root().join("book.json"), NonRecursive)
.is_err() {
// do nothing if book.json is not found
}
if watcher
.watch(book.get_root().join("book.toml"), NonRecursive)
.is_err() {
// do nothing if book.toml is not found
}
info!("Listening for changes...");
println!("\nListening for changes...\n");
loop {
match rx.recv() {
Ok(event) => {
match event {
Create(path) |
Write(path) |
Remove(path) |
Rename(_, path) => {
closure(&path, book);
},
_ => {},
}
},
Err(e) => {
println!("An error occured: {:?}", e);
},
for event in rx.iter() {
debug!("Received filesystem event: {:?}", event);
match event {
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
closure(&path, &book.root);
}
_ => {}
}
}
}

572
src/book/book.rs Normal file
View File

@@ -0,0 +1,572 @@
use std::fmt::{self, Display, Formatter};
use std::path::{Path, PathBuf};
use std::collections::VecDeque;
use std::fs::{self, File};
use std::io::{Read, Write};
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use config::BuildConfig;
use errors::*;
/// Load a book into memory from its `src/` directory.
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
let src_dir = src_dir.as_ref();
let summary_md = src_dir.join("SUMMARY.md");
let mut summary_content = String::new();
File::open(summary_md)
.chain_err(|| "Couldn't open SUMMARY.md")?
.read_to_string(&mut summary_content)?;
let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?;
if cfg.create_missing {
create_missing(&src_dir, &summary).chain_err(|| "Unable to create missing chapters")?;
}
load_book_from_disk(&summary, src_dir)
}
fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
let mut items: Vec<_> = summary
.prefix_chapters
.iter()
.chain(summary.numbered_chapters.iter())
.chain(summary.suffix_chapters.iter())
.collect();
while !items.is_empty() {
let next = items.pop().expect("already checked");
if let SummaryItem::Link(ref link) = *next {
let filename = src_dir.join(&link.location);
if !filename.exists() {
if let Some(parent) = filename.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
debug!("Creating missing file {}", filename.display());
let mut f = File::create(&filename)?;
writeln!(f, "# {}", link.name)?;
}
items.extend(&link.nested_items);
}
}
Ok(())
}
/// A dumb tree structure representing a book.
///
/// For the moment a book is just a collection of `BookItems` which are
/// accessible by either iterating (immutably) over the book with [`iter()`], or
/// recursively applying a closure to each section to mutate the chapters, using
/// [`for_each_mut()`].
///
/// [`iter()`]: #method.iter
/// [`for_each_mut()`]: #method.for_each_mut
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Book {
/// The sections in this book.
pub sections: Vec<BookItem>,
__non_exhaustive: (),
}
impl Book {
/// Create an empty book.
pub fn new() -> Self {
Default::default()
}
/// Get a depth-first iterator over the items in the book.
pub fn iter(&self) -> BookItems {
BookItems {
items: self.sections.iter().collect(),
}
}
/// Recursively apply a closure to each item in the book, allowing you to
/// mutate them.
///
/// # Note
///
/// Unlike the `iter()` method, this requires a closure instead of returning
/// an iterator. This is because using iterators can possibly allow you
/// to have iterator invalidation errors.
pub fn for_each_mut<F>(&mut self, mut func: F)
where
F: FnMut(&mut BookItem),
{
for_each_mut(&mut func, &mut self.sections);
}
/// Append a `BookItem` to the `Book`.
pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
self.sections.push(item.into());
self
}
}
pub fn for_each_mut<'a, F, I>(func: &mut F, items: I)
where
F: FnMut(&mut BookItem),
I: IntoIterator<Item = &'a mut BookItem>,
{
for item in items {
if let &mut BookItem::Chapter(ref mut ch) = item {
for_each_mut(func, &mut ch.sub_items);
}
func(item);
}
}
/// Enum representing any type of item which can be added to a book.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BookItem {
/// A nested chapter.
Chapter(Chapter),
/// A section separator.
Separator,
}
impl From<Chapter> for BookItem {
fn from(other: Chapter) -> BookItem {
BookItem::Chapter(other)
}
}
/// The representation of a "chapter", usually mapping to a single file on
/// disk however it may contain multiple sub-chapters.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Chapter {
/// The chapter's name.
pub name: String,
/// The chapter's contents.
pub content: String,
/// The chapter's section number, if it has one.
pub number: Option<SectionNumber>,
/// Nested items.
pub sub_items: Vec<BookItem>,
/// The chapter's location, relative to the `SUMMARY.md` file.
pub path: PathBuf,
/// An ordered list of the names of each chapter above this one, in the hierarchy.
pub parent_names: Vec<String>,
}
impl Chapter {
/// Create a new chapter with the provided content.
pub fn new<P: Into<PathBuf>>(
name: &str,
content: String,
path: P,
parent_names: Vec<String>,
) -> Chapter {
Chapter {
name: name.to_string(),
content: content,
path: path.into(),
parent_names: parent_names,
..Default::default()
}
}
}
/// Use the provided `Summary` to load a `Book` from disk.
///
/// You need to pass in the book's source directory because all the links in
/// `SUMMARY.md` give the chapter locations relative to it.
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();
let prefix = summary.prefix_chapters.iter();
let numbered = summary.numbered_chapters.iter();
let suffix = summary.suffix_chapters.iter();
let summary_items = prefix.chain(numbered).chain(suffix);
let mut chapters = Vec::new();
for summary_item in summary_items {
let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
chapters.push(chapter);
}
Ok(Book {
sections: chapters,
__non_exhaustive: (),
})
}
fn load_summary_item<P: AsRef<Path>>(
item: &SummaryItem,
src_dir: P,
parent_names: Vec<String>,
) -> Result<BookItem> {
match *item {
SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(ref link) => {
load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c))
}
}
}
fn load_chapter<P: AsRef<Path>>(
link: &Link,
src_dir: P,
parent_names: Vec<String>,
) -> Result<Chapter> {
debug!("Loading {} ({})", link.name, link.location.display());
let src_dir = src_dir.as_ref();
let location = if link.location.is_absolute() {
link.location.clone()
} else {
src_dir.join(&link.location)
};
let mut f = File::open(&location)
.chain_err(|| format!("Chapter file not found, {}", link.location.display()))?;
let mut content = String::new();
f.read_to_string(&mut content)
.chain_err(|| format!("Unable to read \"{}\" ({})", link.name, location.display()))?;
let stripped = location
.strip_prefix(&src_dir)
.expect("Chapters are always inside a book");
let mut sub_item_parents = parent_names.clone();
let mut ch = Chapter::new(&link.name, content, stripped, parent_names);
ch.number = link.number.clone();
sub_item_parents.push(link.name.clone());
let sub_items = link.nested_items
.iter()
.map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
.collect::<Result<Vec<_>>>()?;
ch.sub_items = sub_items;
Ok(ch)
}
/// A depth-first iterator over the items in a book.
///
/// # Note
///
/// This struct shouldn't be created directly, instead prefer the
/// [`Book::iter()`] method.
///
/// [`Book::iter()`]: struct.Book.html#method.iter
pub struct BookItems<'a> {
items: VecDeque<&'a BookItem>,
}
impl<'a> Iterator for BookItems<'a> {
type Item = &'a BookItem;
fn next(&mut self) -> Option<Self::Item> {
let item = self.items.pop_front();
if let Some(&BookItem::Chapter(ref ch)) = item {
// if we wanted a breadth-first iterator we'd `extend()` here
for sub_item in ch.sub_items.iter().rev() {
self.items.push_front(sub_item);
}
}
item
}
}
impl Display for Chapter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if let Some(ref section_number) = self.number {
write!(f, "{} ", section_number)?;
}
write!(f, "{}", self.name)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::{TempDir, Builder as TempFileBuilder};
use std::io::Write;
const DUMMY_SRC: &'static str = "
# Dummy Chapter
this is some dummy text.
And here is some \
more text.
";
/// Create a dummy `Link` in a temporary directory.
fn dummy_link() -> (Link, TempDir) {
let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let chapter_path = temp.path().join("chapter_1.md");
File::create(&chapter_path)
.unwrap()
.write(DUMMY_SRC.as_bytes())
.unwrap();
let link = Link::new("Chapter 1", chapter_path);
(link, temp)
}
/// Create a nested `Link` written to a temporary directory.
fn nested_links() -> (Link, TempDir) {
let (mut root, temp_dir) = dummy_link();
let second_path = temp_dir.path().join("second.md");
File::create(&second_path)
.unwrap()
.write_all("Hello World!".as_bytes())
.unwrap();
let mut second = Link::new("Nested Chapter 1", &second_path);
second.number = Some(SectionNumber(vec![1, 2]));
root.nested_items.push(second.clone().into());
root.nested_items.push(SummaryItem::Separator);
root.nested_items.push(second.clone().into());
(root, temp_dir)
}
#[test]
fn load_a_single_chapter_from_disk() {
let (link, temp_dir) = dummy_link();
let should_be = Chapter::new(
"Chapter 1",
DUMMY_SRC.to_string(),
"chapter_1.md",
Vec::new(),
);
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn cant_load_a_nonexistent_chapter() {
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
let got = load_chapter(&link, "", Vec::new());
assert!(got.is_err());
}
#[test]
fn load_recursive_link_with_separators() {
let (root, temp) = nested_links();
let nested = Chapter {
name: String::from("Nested Chapter 1"),
content: String::from("Hello World!"),
number: Some(SectionNumber(vec![1, 2])),
path: PathBuf::from("second.md"),
parent_names: vec![String::from("Chapter 1")],
sub_items: Vec::new(),
};
let should_be = BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("chapter_1.md"),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(nested.clone()),
BookItem::Separator,
BookItem::Chapter(nested.clone()),
],
});
let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn load_a_book_with_a_single_chapter() {
let (link, temp) = dummy_link();
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(link)],
..Default::default()
};
let should_be = Book {
sections: vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
path: PathBuf::from("chapter_1.md"),
..Default::default()
}),
],
..Default::default()
};
let got = load_book_from_disk(&summary, temp.path()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn book_iter_iterates_over_sequential_items() {
let book = Book {
sections: vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
..Default::default()
}),
BookItem::Separator,
],
..Default::default()
};
let should_be: Vec<_> = book.sections.iter().collect();
let got: Vec<_> = book.iter().collect();
assert_eq!(got, should_be);
}
#[test]
fn iterate_over_nested_book_items() {
let book = Book {
sections: vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("Chapter_1/index.md"),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
Vec::new(),
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
Vec::new(),
)),
],
}),
BookItem::Separator,
],
..Default::default()
};
let got: Vec<_> = book.iter().collect();
assert_eq!(got.len(), 5);
// checking the chapter names are in the order should be sufficient here...
let chapter_names: Vec<String> = got.into_iter()
.filter_map(|i| match *i {
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
_ => None,
})
.collect();
let should_be: Vec<_> = vec![
String::from("Chapter 1"),
String::from("Hello World"),
String::from("Goodbye World"),
];
assert_eq!(chapter_names, should_be);
}
#[test]
fn for_each_mut_visits_all_items() {
let mut book = Book {
sections: vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("Chapter_1/index.md"),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
Vec::new(),
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
Vec::new(),
)),
],
}),
BookItem::Separator,
],
..Default::default()
};
let num_items = book.iter().count();
let mut visited = 0;
book.for_each_mut(|_| visited += 1);
assert_eq!(visited, num_items);
}
#[test]
fn cant_load_chapters_with_an_empty_path() {
let (_, temp) = dummy_link();
let summary = Summary {
numbered_chapters: vec![
SummaryItem::Link(Link {
name: String::from("Empty"),
location: PathBuf::from(""),
..Default::default()
}),
],
..Default::default()
};
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());
}
#[test]
fn cant_load_chapters_when_the_link_is_a_directory() {
let (_, temp) = dummy_link();
let dir = temp.path().join("nested");
fs::create_dir(&dir).unwrap();
let summary = Summary {
numbered_chapters: vec![
SummaryItem::Link(Link {
name: String::from("nested"),
location: dir,
..Default::default()
}),
],
..Default::default()
};
let got = load_book_from_disk(&summary, temp.path());
assert!(got.is_err());
}
}

View File

@@ -1,87 +0,0 @@
use serde::{Serialize, Serializer};
use serde::ser::SerializeStruct;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub enum BookItem {
Chapter(String, Chapter), // String = section
Affix(Chapter),
Spacer,
}
#[derive(Debug, Clone)]
pub struct Chapter {
pub name: String,
pub path: PathBuf,
pub sub_items: Vec<BookItem>,
}
#[derive(Debug, Clone)]
pub struct BookItems<'a> {
pub items: &'a [BookItem],
pub current_index: usize,
pub stack: Vec<(&'a [BookItem], usize)>,
}
impl Chapter {
pub fn new(name: String, path: PathBuf) -> Self {
Chapter {
name: name,
path: path,
sub_items: vec![],
}
}
}
impl Serialize for Chapter {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where S: Serializer
{
let mut struct_ = serializer.serialize_struct("Chapter", 2)?;
struct_.serialize_field("name", &self.name)?;
struct_.serialize_field("path", &self.path)?;
struct_.end()
}
}
// Shamelessly copied from Rustbook
// (https://github.com/rust-lang/rust/blob/master/src/rustbook/book.rs)
impl<'a> Iterator for BookItems<'a> {
type Item = &'a BookItem;
fn next(&mut self) -> Option<&'a BookItem> {
loop {
if self.current_index >= self.items.len() {
match self.stack.pop() {
None => return None,
Some((parent_items, parent_idx)) => {
self.items = parent_items;
self.current_index = parent_idx + 1;
},
}
} else {
let cur = &self.items[self.current_index];
match *cur {
BookItem::Chapter(_, ref ch) |
BookItem::Affix(ref ch) => {
self.stack.push((self.items, self.current_index));
self.items = &ch.sub_items[..];
self.current_index = 0;
},
BookItem::Spacer => {
self.current_index += 1;
},
}
return Some(cur);
}
}
}
}

187
src/book/init.rs Normal file
View File

@@ -0,0 +1,187 @@
use std::fs::{self, File};
use std::path::PathBuf;
use std::io::Write;
use toml;
use config::Config;
use super::MDBook;
use theme;
use errors::*;
/// A helper for setting up a new book and its directory structure.
#[derive(Debug, Clone, PartialEq)]
pub struct BookBuilder {
root: PathBuf,
create_gitignore: bool,
config: Config,
copy_theme: bool,
}
impl BookBuilder {
/// Create a new `BookBuilder` which will generate a book in the provided
/// root directory.
pub fn new<P: Into<PathBuf>>(root: P) -> BookBuilder {
BookBuilder {
root: root.into(),
create_gitignore: false,
config: Config::default(),
copy_theme: false,
}
}
/// Set the `Config` to be used.
pub fn with_config(&mut self, cfg: Config) -> &mut BookBuilder {
self.config = cfg;
self
}
/// Get the config used by the `BookBuilder`.
pub fn config(&self) -> &Config {
&self.config
}
/// Should the theme be copied into the generated book (so users can tweak
/// it)?
pub fn copy_theme(&mut self, copy: bool) -> &mut BookBuilder {
self.copy_theme = copy;
self
}
/// Should we create a `.gitignore` file?
pub fn create_gitignore(&mut self, create: bool) -> &mut BookBuilder {
self.create_gitignore = create;
self
}
/// Generate the actual book. This will:
///
/// - Create the directory structure.
/// - Stub out some dummy chapters and the `SUMMARY.md`.
/// - Create a `.gitignore` (if applicable)
/// - Create a themes directory and populate it (if applicable)
/// - Generate a `book.toml` file,
/// - Then load the book so we can build it or run tests.
pub fn build(&self) -> Result<MDBook> {
info!("Creating a new book with stub content");
self.create_directory_structure()
.chain_err(|| "Unable to create directory structure")?;
self.create_stub_files()
.chain_err(|| "Unable to create stub files")?;
if self.create_gitignore {
self.build_gitignore()
.chain_err(|| "Unable to create .gitignore")?;
}
if self.copy_theme {
self.copy_across_theme()
.chain_err(|| "Unable to copy across the theme")?;
}
self.write_book_toml()?;
match MDBook::load(&self.root) {
Ok(book) => Ok(book),
Err(e) => {
error!("{}", e);
panic!(
"The BookBuilder should always create a valid book. If you are seeing this it \
is a bug and should be reported."
);
}
}
}
fn write_book_toml(&self) -> Result<()> {
debug!("Writing book.toml");
let book_toml = self.root.join("book.toml");
let cfg = toml::to_vec(&self.config).chain_err(|| "Unable to serialize the config")?;
File::create(book_toml)
.chain_err(|| "Couldn't create book.toml")?
.write_all(&cfg)
.chain_err(|| "Unable to write config to book.toml")?;
Ok(())
}
fn copy_across_theme(&self) -> Result<()> {
debug!("Copying theme");
let themedir = self.config
.html_config()
.and_then(|html| html.theme)
.unwrap_or_else(|| self.config.book.src.join("theme"));
let themedir = self.root.join(themedir);
if !themedir.exists() {
debug!(
"{} does not exist, creating the directory",
themedir.display()
);
fs::create_dir(&themedir)?;
}
let mut index = File::create(themedir.join("index.hbs"))?;
index.write_all(theme::INDEX)?;
let mut css = File::create(themedir.join("book.css"))?;
css.write_all(theme::CSS)?;
let mut favicon = File::create(themedir.join("favicon.png"))?;
favicon.write_all(theme::FAVICON)?;
let mut js = File::create(themedir.join("book.js"))?;
js.write_all(theme::JS)?;
let mut highlight_css = File::create(themedir.join("highlight.css"))?;
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
Ok(())
}
fn build_gitignore(&self) -> Result<()> {
debug!("Creating .gitignore");
let mut f = File::create(self.root.join(".gitignore"))?;
writeln!(f, "{}", self.config.build.build_dir.display())?;
Ok(())
}
fn create_stub_files(&self) -> Result<()> {
debug!("Creating example book contents");
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")?;
Ok(())
}
fn create_directory_structure(&self) -> Result<()> {
debug!("Creating directory tree");
fs::create_dir_all(&self.root)?;
let src = self.root.join(&self.config.book.src);
fs::create_dir_all(&src)?;
let build = self.root.join(&self.config.build.build_dir);
fs::create_dir_all(&build)?;
Ok(())
}
}

View File

@@ -1,80 +1,96 @@
pub mod bookitem;
//! The internal representation of a book and infrastructure for loading it from
//! disk and building it.
//!
//! For examples on using `MDBook`, consult the [top-level documentation][1].
//!
//! [1]: ../index.html
pub use self::bookitem::{BookItem, BookItems};
mod summary;
mod book;
mod init;
use std::path::{Path, PathBuf};
use std::fs::{self, File};
use std::io::{Read, Write};
pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
pub use self::init::BookBuilder;
use std::path::PathBuf;
use std::io::Write;
use std::process::Command;
use tempdir::TempDir;
use tempfile::Builder as TempFileBuilder;
use toml::Value;
use {theme, parse, utils};
use renderer::{Renderer, HtmlHandlebars};
use preprocess;
use utils;
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
use preprocess::{LinkPreprocessor, Preprocessor, PreprocessorContext};
use errors::*;
use config::BookConfig;
use config::tomlconfig::TomlConfig;
use config::htmlconfig::HtmlConfig;
use config::jsonconfig::JsonConfig;
use config::Config;
/// The object used to manage and build a book.
pub struct MDBook {
config: BookConfig,
/// The book's root directory.
pub root: PathBuf,
/// The configuration used to tweak now a book is built.
pub config: Config,
/// A representation of the book's contents in memory.
pub book: Book,
renderers: Vec<Box<Renderer>>,
pub content: Vec<BookItem>,
renderer: Box<Renderer>,
livereload: Option<String>,
/// Should `mdbook build` create files referenced from SUMMARY.md if they
/// don't exist
pub create_missing: bool,
/// List of pre-processors to be run on the book
preprocessors: Vec<Box<Preprocessor>>,
}
impl MDBook {
/// Create a new `MDBook` struct with root directory `root`
///
/// # Examples
///
/// ```no_run
/// # extern crate mdbook;
/// # use mdbook::MDBook;
/// # #[allow(unused_variables)]
/// # fn main() {
/// let book = MDBook::new("root_dir");
/// # }
/// ```
///
/// In this example, `root_dir` will be the root directory of our book
/// and is specified in function of the current working directory
/// by using a relative path instead of an
/// absolute path.
///
/// Default directory paths:
///
/// - source: `root/src`
/// - output: `root/book`
/// - theme: `root/theme`
///
/// They can both be changed by using [`set_src()`](#method.set_src) and
/// [`set_dest()`](#method.set_dest)
/// Load a book from its root directory on disk.
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
let book_root = book_root.into();
let config_location = book_root.join("book.toml");
pub fn new<P: Into<PathBuf>>(root: P) -> MDBook {
let root = root.into();
if !root.exists() || !root.is_dir() {
warn!("{:?} No directory with that name", root);
// the book.json file is no longer used, so we should emit a warning to
// let people know to migrate to book.toml
if book_root.join("book.json").exists() {
warn!("It appears you are still using book.json for configuration.");
warn!("This format is no longer used, so you should migrate to the");
warn!("book.toml format.");
warn!("Check the user guide for migration information:");
warn!("\thttps://rust-lang-nursery.github.io/mdBook/format/config.html");
}
MDBook {
config: BookConfig::new(root),
let mut config = if config_location.exists() {
debug!("Loading config from {}", config_location.display());
Config::from_disk(&config_location)?
} else {
Config::default()
};
content: vec![],
renderer: Box::new(HtmlHandlebars::new()),
config.update_from_env();
livereload: None,
create_missing: true,
if log_enabled!(::log::Level::Trace) {
for line in format!("Config: {:#?}", config).lines() {
trace!("{}", line);
}
}
MDBook::load_with_config(book_root, config)
}
/// Load a book from its root directory using a custom config.
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
let root = book_root.into();
let src_dir = root.join(&config.book.src);
let book = book::load_book(&src_dir, &config.build)?;
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,
@@ -84,15 +100,14 @@ impl MDBook {
/// ```no_run
/// # extern crate mdbook;
/// # use mdbook::MDBook;
/// # use mdbook::BookItem;
/// # use mdbook::book::BookItem;
/// # #[allow(unused_variables)]
/// # fn main() {
/// # let book = MDBook::new("mybook");
/// # let book = MDBook::load("mybook").unwrap();
/// for item in book.iter() {
/// match item {
/// &BookItem::Chapter(ref section, ref chapter) => {},
/// &BookItem::Affix(ref chapter) => {},
/// &BookItem::Spacer => {},
/// match *item {
/// BookItem::Chapter(ref chapter) => {},
/// BookItem::Separator => {},
/// }
/// }
///
@@ -105,17 +120,15 @@ impl MDBook {
/// // etc.
/// # }
/// ```
pub fn iter(&self) -> BookItems {
BookItems {
items: &self.content[..],
current_index: 0,
stack: Vec::new(),
}
self.book.iter()
}
/// `init()` creates some boilerplate files and directories
/// to get you started with your book.
/// `init()` gives you a `BookBuilder` which you can use to setup a new book
/// and its accompanying directory structure.
///
/// The `BookBuilder` creates some boilerplate files and directories to get
/// you started with your book.
///
/// ```text
/// book-test/
@@ -125,265 +138,110 @@ impl MDBook {
/// └── SUMMARY.md
/// ```
///
/// It uses the paths given as source and output directories
/// and adds a `SUMMARY.md` and a
/// `chapter_1.md` to the source directory.
pub fn init(&mut self) -> Result<()> {
debug!("[fn]: init");
if !self.config.get_root().exists() {
fs::create_dir_all(&self.config.get_root()).unwrap();
info!("{:?} created", &self.config.get_root());
}
{
if !self.get_destination().exists() {
debug!("[*]: {:?} does not exist, trying to create directory", self.get_destination());
fs::create_dir_all(self.get_destination())?;
}
if !self.config.get_source().exists() {
debug!("[*]: {:?} does not exist, trying to create directory", self.config.get_source());
fs::create_dir_all(self.config.get_source())?;
}
let summary = self.config.get_source().join("SUMMARY.md");
if !summary.exists() {
// Summary does not exist, create it
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", &summary);
let mut f = File::create(&summary)?;
debug!("[*]: Writing to SUMMARY.md");
writeln!(f, "# Summary")?;
writeln!(f, "")?;
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
}
}
// parse SUMMARY.md, and create the missing item related file
self.parse_summary()?;
debug!("[*]: constructing paths for missing files");
for item in self.iter() {
debug!("[*]: item: {:?}", item);
let ch = match *item {
BookItem::Spacer => continue,
BookItem::Chapter(_, ref ch) |
BookItem::Affix(ref ch) => ch,
};
if !ch.path.as_os_str().is_empty() {
let path = self.config.get_source().join(&ch.path);
if !path.exists() {
if !self.create_missing {
return Err(format!("'{}' referenced from SUMMARY.md does not exist.", path.to_string_lossy())
.into());
}
debug!("[*]: {:?} does not exist, trying to create file", path);
::std::fs::create_dir_all(path.parent().unwrap())?;
let mut f = File::create(path)?;
// debug!("[*]: Writing to {:?}", path);
writeln!(f, "# {}", ch.name)?;
}
}
}
debug!("[*]: init done");
Ok(())
/// It uses the path provided as the root directory for your book, then adds
/// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
/// to get you started.
pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
BookBuilder::new(book_root)
}
pub fn create_gitignore(&self) {
let gitignore = self.get_gitignore();
/// Tells the renderer to build our book and put it in the build directory.
pub fn build(&self) -> Result<()> {
info!("Book building has started");
let destination = self.config.get_html_config()
.get_destination();
let mut preprocessed_book = self.book.clone();
let preprocess_ctx = PreprocessorContext::new(self.root.clone(), self.config.clone());
// Check that the gitignore does not extist and that the destination path begins with the root path
// We assume tha if it does begin with the root path it is contained within. This assumption
// will not hold true for paths containing double dots to go back up e.g. `root/../destination`
if !gitignore.exists() && destination.starts_with(self.config.get_root()) {
let relative = destination
.strip_prefix(self.config.get_root())
.expect("Could not strip the root prefix, path is not relative to root")
.to_str()
.expect("Could not convert to &str");
debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore);
let mut f = File::create(&gitignore).expect("Could not create file.");
debug!("[*]: Writing to .gitignore");
writeln!(f, "/{}", relative).expect("Could not write to file.");
}
}
/// The `build()` method is the one where everything happens.
/// First it parses `SUMMARY.md` to construct the book's structure
/// in the form of a `Vec<BookItem>` and then calls `render()`
/// method of the current renderer.
///
/// It is the renderer who generates all the output files.
pub fn build(&mut self) -> Result<()> {
debug!("[fn]: build");
self.init()?;
// Clean output directory
utils::fs::remove_dir_content(self.config.get_html_config().get_destination())?;
self.renderer.render(self)
}
pub fn get_gitignore(&self) -> PathBuf {
self.config.get_root().join(".gitignore")
}
pub fn copy_theme(&self) -> Result<()> {
debug!("[fn]: copy_theme");
let themedir = self.config.get_html_config().get_theme();
if !themedir.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", themedir);
fs::create_dir(&themedir)?;
for preprocessor in &self.preprocessors {
debug!("Running the {} preprocessor.", preprocessor.name());
preprocessor.run(&preprocess_ctx, &mut preprocessed_book)?;
}
// index.hbs
let mut index = File::create(&themedir.join("index.hbs"))?;
index.write_all(theme::INDEX)?;
// book.css
let mut css = File::create(&themedir.join("book.css"))?;
css.write_all(theme::CSS)?;
// favicon.png
let mut favicon = File::create(&themedir.join("favicon.png"))?;
favicon.write_all(theme::FAVICON)?;
// book.js
let mut js = File::create(&themedir.join("book.js"))?;
js.write_all(theme::JS)?;
// highlight.css
let mut highlight_css = File::create(&themedir.join("highlight.css"))?;
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
// highlight.js
let mut highlight_js = File::create(&themedir.join("highlight.js"))?;
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
for renderer in &self.renderers {
info!("Running the {} backend", renderer.name());
self.run_renderer(&preprocessed_book, renderer.as_ref())?;
}
Ok(())
}
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<()> {
let path = self.get_destination()
.join(filename);
fn run_renderer(&self, preprocessed_book: &Book, renderer: &Renderer) -> Result<()> {
let name = renderer.name();
let build_dir = self.build_dir_for(name);
if build_dir.exists() {
debug!(
"Cleaning build dir for the \"{}\" renderer ({})",
name,
build_dir.display()
);
utils::fs::create_file(&path)?
.write_all(content)
.map_err(|e| e.into())
}
/// Parses the `book.json` file (if it exists) to extract
/// the configuration parameters.
/// The `book.json` file should be in the root directory of the book.
/// The root directory is the one specified when creating a new `MDBook`
pub fn read_config(mut self) -> Result<Self> {
let toml = self.get_root().join("book.toml");
let json = self.get_root().join("book.json");
if toml.exists() {
let mut file = File::open(toml)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let parsed_config = TomlConfig::from_toml(&content)?;
self.config.fill_from_tomlconfig(parsed_config);
} else if json.exists() {
warn!("The JSON configuration file is deprecated, please use the TOML configuration.");
let mut file = File::open(json)?;
let mut content = String::new();
file.read_to_string(&mut content)?;
let parsed_config = JsonConfig::from_json(&content)?;
self.config.fill_from_jsonconfig(parsed_config);
utils::fs::remove_dir_content(&build_dir)
.chain_err(|| "Unable to clear output directory")?;
}
Ok(self)
let render_context = RenderContext::new(
self.root.clone(),
preprocessed_book.clone(),
self.config.clone(),
build_dir,
);
renderer
.render(&render_context)
.chain_err(|| "Rendering failed")
}
/// You can change the default renderer to another one
/// by using this method. The only requirement
/// is for your renderer to implement the
/// [Renderer trait](../../renderer/renderer/trait.Renderer.html)
///
/// ```no_run
/// extern crate mdbook;
/// use mdbook::MDBook;
/// use mdbook::renderer::HtmlHandlebars;
///
/// # #[allow(unused_variables)]
/// fn main() {
/// let book = MDBook::new("mybook")
/// .set_renderer(Box::new(HtmlHandlebars::new()));
///
/// // In this example we replace the default renderer
/// // by the default renderer...
/// // Don't forget to put your renderer in a Box
/// }
/// ```
///
/// **note:** Don't forget to put your renderer in a `Box`
/// before passing it to `set_renderer()`
pub fn set_renderer(mut self, renderer: Box<Renderer>) -> Self {
self.renderer = renderer;
/// You can change the default renderer to another one by using this method.
/// The only requirement is for your renderer to implement the [`Renderer`
/// trait](../renderer/trait.Renderer.html)
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
self.renderers.push(Box::new(renderer));
self
}
/// 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 {
self.preprocessors.push(Box::new(preprocessor));
self
}
/// Run `rustdoc` tests on the book, linking against the provided libraries.
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
// read in the chapters
self.parse_summary().chain_err(|| "Couldn't parse summary")?;
let library_args: Vec<&str> = (0..library_paths.len()).map(|_| "-L")
.zip(library_paths.into_iter())
.flat_map(|x| vec![x.0, x.1])
.collect();
let temp_dir = TempDir::new("mdbook")?;
let library_args: Vec<&str> = (0..library_paths.len())
.map(|_| "-L")
.zip(library_paths.into_iter())
.flat_map(|x| vec![x.0, x.1])
.collect();
let temp_dir = TempFileBuilder::new().prefix("mdbook").tempdir()?;
let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone());
LinkPreprocessor::new().run(&preprocess_context, &mut self.book)?;
for item in self.iter() {
if let BookItem::Chapter(_, ref ch) = *item {
if let BookItem::Chapter(ref ch) = *item {
if !ch.path.as_os_str().is_empty() {
let path = self.get_source().join(&ch.path);
let base = path.parent().ok_or_else(
|| String::from("Invalid bookitem path!"),
)?;
let path = self.source_dir().join(&ch.path);
let content = utils::fs::file_to_string(&path)?;
// Parse and expand links
let content = preprocess::links::replace_all(&content, base)?;
println!("[*]: Testing file: {:?}", path);
info!("Testing file: {:?}", path);
//write preprocessed file to tempdir
// 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())?;
let output = Command::new("rustdoc").arg(&path).arg("--test").args(&library_args).output()?;
let output = Command::new("rustdoc")
.arg(&path)
.arg("--test")
.args(&library_args)
.output()?;
if !output.status.success() {
bail!(ErrorKind::Subprocess("Rustdoc returned an error".to_string(), output));
bail!(ErrorKind::Subprocess(
"Rustdoc returned an error".to_string(),
output
));
}
}
}
@@ -391,132 +249,215 @@ impl MDBook {
Ok(())
}
pub fn get_root(&self) -> &Path {
self.config.get_root()
/// The logic for determining where a backend should put its build
/// artefacts.
///
/// If there is only 1 renderer, put it in the directory pointed to by the
/// `build.build_dir` key in `Config`. If there is more than one then the
/// renderer gets its own directory within the main build dir.
///
/// i.e. If there were only one renderer (in this case, the HTML renderer):
///
/// - build/
/// - index.html
/// - ...
///
/// Otherwise if there are multiple:
///
/// - build/
/// - epub/
/// - my_awesome_book.epub
/// - html/
/// - index.html
/// - ...
/// - latex/
/// - my_awesome_book.tex
///
pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
let build_dir = self.root.join(&self.config.build.build_dir);
if self.renderers.len() <= 1 {
build_dir
} else {
build_dir.join(backend_name)
}
}
pub fn with_destination<T: Into<PathBuf>>(mut self, destination: T) -> Self {
let root = self.config.get_root().to_owned();
self.config.get_mut_html_config()
.set_destination(&root, &destination.into());
self
/// Get the directory containing this book's source files.
pub fn source_dir(&self) -> PathBuf {
self.root.join(&self.config.book.src)
}
pub fn get_destination(&self) -> &Path {
self.config.get_html_config()
.get_destination()
}
pub fn with_source<T: Into<PathBuf>>(mut self, source: T) -> Self {
self.config.set_source(source);
self
}
pub fn get_source(&self) -> &Path {
self.config.get_source()
}
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
self.config.set_title(title);
self
}
pub fn get_title(&self) -> &str {
self.config.get_title()
}
pub fn with_description<T: Into<String>>(mut self, description: T) -> Self {
self.config.set_description(description);
self
}
pub fn get_description(&self) -> &str {
self.config.get_description()
}
pub fn set_livereload(&mut self, livereload: String) -> &mut Self {
self.livereload = Some(livereload);
self
}
pub fn unset_livereload(&mut self) -> &Self {
self.livereload = None;
self
}
pub fn get_livereload(&self) -> Option<&String> {
self.livereload.as_ref()
}
pub fn with_theme_path<T: Into<PathBuf>>(mut self, theme_path: T) -> Self {
let root = self.config.get_root().to_owned();
self.config.get_mut_html_config()
.set_theme(&root, &theme_path.into());
self
}
pub fn get_theme_path(&self) -> &Path {
self.config.get_html_config()
.get_theme()
}
pub fn with_curly_quotes(mut self, curly_quotes: bool) -> Self {
self.config.get_mut_html_config()
.set_curly_quotes(curly_quotes);
self
}
pub fn get_curly_quotes(&self) -> bool {
self.config.get_html_config()
.get_curly_quotes()
}
pub fn with_mathjax_support(mut self, mathjax_support: bool) -> Self {
self.config.get_mut_html_config()
.set_mathjax_support(mathjax_support);
self
}
pub fn get_mathjax_support(&self) -> bool {
self.config.get_html_config()
.get_mathjax_support()
}
pub fn get_google_analytics_id(&self) -> Option<String> {
self.config.get_html_config()
.get_google_analytics_id()
}
pub fn has_additional_js(&self) -> bool {
self.config.get_html_config()
.has_additional_js()
}
pub fn get_additional_js(&self) -> &[PathBuf] {
self.config.get_html_config()
.get_additional_js()
}
pub fn has_additional_css(&self) -> bool {
self.config.get_html_config()
.has_additional_css()
}
pub fn get_additional_css(&self) -> &[PathBuf] {
self.config.get_html_config()
.get_additional_css()
}
pub fn get_html_config(&self) -> &HtmlConfig {
self.config.get_html_config()
}
// Construct book
fn parse_summary(&mut self) -> Result<()> {
// When append becomes stable, use self.content.append() ...
self.content = parse::construct_bookitems(&self.get_source().join("SUMMARY.md"))?;
Ok(())
/// Get the directory containing the theme resources for the book.
pub fn theme_dir(&self) -> PathBuf {
self.config
.html_config()
.unwrap_or_default()
.theme_dir(&self.root)
}
}
/// 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();
if let Some(output_table) = config.get("output").and_then(|o| o.as_table()) {
for (key, table) in output_table.iter() {
// the "html" backend has its own Renderer
if key == "html" {
renderers.push(Box::new(HtmlHandlebars::new()));
} else {
let renderer = interpret_custom_renderer(key, table);
renderers.push(renderer);
}
}
}
// if we couldn't find anything, add the HTML renderer as a default
if renderers.is_empty() {
renderers.push(Box::new(HtmlHandlebars::new()));
}
renderers
}
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
vec![Box::new(LinkPreprocessor::new())]
}
/// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
let preprocess_list = match config.build.preprocess {
Some(ref p) => p,
// If no preprocessor field is set, default to the LinkPreprocessor. This allows you
// to disable the LinkPreprocessor by setting "preprocess" to an empty list.
None => return Ok(default_preprocessors()),
};
let mut preprocessors: Vec<Box<Preprocessor>> = Vec::new();
for key in preprocess_list {
match key.as_ref() {
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
_ => bail!("{:?} is not a recognised preprocessor", key),
}
}
Ok(preprocessors)
}
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
// look for the `command` field, falling back to using the key
// prepended by "mdbook-"
let table_dot_command = table
.get("command")
.and_then(|c| c.as_str())
.map(|s| s.to_string());
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use toml::value::{Table, Value};
#[test]
fn config_defaults_to_html_renderer_if_empty() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
assert!(cfg.get("output").is_none());
let got = determine_renderers(&cfg);
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "html");
}
#[test]
fn add_a_random_renderer_to_the_config() {
let mut cfg = Config::default();
cfg.set("output.random", Table::new()).unwrap();
let got = determine_renderers(&cfg);
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random");
}
#[test]
fn add_a_random_renderer_with_custom_command_to_the_config() {
let mut cfg = Config::default();
let mut table = Table::new();
table.insert("command".to_string(), Value::String("false".to_string()));
cfg.set("output.random", table).unwrap();
let got = determine_renderers(&cfg);
assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random");
}
#[test]
fn config_defaults_to_link_preprocessor_if_not_set() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
assert!(cfg.build.preprocess.is_none());
let got = determine_preprocessors(&cfg);
assert!(got.is_ok());
assert_eq!(got.as_ref().unwrap().len(), 1);
assert_eq!(got.as_ref().unwrap()[0].name(), "links");
}
#[test]
fn config_doesnt_default_if_empty() {
let cfg_str: &'static str = r#"
[book]
title = "Some Book"
[build]
build-dir = "outputs"
create-missing = false
preprocess = []
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure we have something in the `output` table
assert!(cfg.build.preprocess.is_some());
let got = determine_preprocessors(&cfg);
assert!(got.is_ok());
assert!(got.unwrap().is_empty());
}
#[test]
fn config_complains_if_unimplemented_preprocessor() {
let cfg_str: &'static str = r#"
[book]
title = "Some Book"
[build]
build-dir = "outputs"
create-missing = false
preprocess = ["random"]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure we have something in the `output` table
assert!(cfg.build.preprocess.is_some());
let got = determine_preprocessors(&cfg);
assert!(got.is_err());
}
}

726
src/book/summary.rs Normal file
View File

@@ -0,0 +1,726 @@
use std::fmt::{self, Display, Formatter};
use std::iter::FromIterator;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use memchr::{self, Memchr};
use pulldown_cmark::{self, Event, Tag};
use errors::*;
/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
/// used when loading a book from disk.
///
/// # Summary Format
///
/// **Title:** It's common practice to begin with a title, generally
/// "# Summary". It's not mandatory and the parser (currently) ignores it, so
/// you can too if you feel like it.
///
/// **Prefix Chapter:** Before the main numbered chapters you can add a couple
/// of elements that will not be numbered. This is useful for forewords,
/// introductions, etc. There are however some constraints. You can not nest
/// prefix chapters, they should all be on the root level. And you can not add
/// prefix chapters once you have added numbered chapters.
///
/// ```markdown
/// [Title of prefix element](relative/path/to/markdown.md)
/// ```
///
/// **Numbered Chapter:** Numbered chapters are the main content of the book,
/// they
/// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
/// sub-chapters, etc.)
///
/// ```markdown
/// - [Title of the Chapter](relative/path/to/markdown.md)
/// ```
///
/// You can either use - or * to indicate a numbered chapter, the parser doesn't
/// care but you'll probably want to stay consistent.
///
/// **Suffix Chapter:** After the numbered chapters you can add a couple of
/// non-numbered chapters. They are the same as prefix chapters but come after
/// the numbered chapters instead of before.
///
/// All other elements are unsupported and will be ignored at best or result in
/// an error.
pub fn parse_summary(summary: &str) -> Result<Summary> {
let parser = SummaryParser::new(summary);
parser.parse()
}
/// The parsed `SUMMARY.md`, specifying how the book should be laid out.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Summary {
/// An optional title for the `SUMMARY.md`, currently just ignored.
pub title: Option<String>,
/// Chapters before the main text (e.g. an introduction).
pub prefix_chapters: Vec<SummaryItem>,
/// The main chapters in the document.
pub numbered_chapters: Vec<SummaryItem>,
/// Items which come after the main document (e.g. a conclusion).
pub suffix_chapters: Vec<SummaryItem>,
}
/// A struct representing an entry in the `SUMMARY.md`, possibly with nested
/// entries.
///
/// This is roughly the equivalent of `[Some section](./path/to/file.md)`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Link {
/// The name of the chapter.
pub name: String,
/// The location of the chapter's source file, taking the book's `src`
/// directory as the root.
pub location: PathBuf,
/// The section number, if this chapter is in the numbered section.
pub number: Option<SectionNumber>,
/// Any nested items this chapter may contain.
pub nested_items: Vec<SummaryItem>,
}
impl Link {
/// Create a new link with no nested items.
pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
Link {
name: name.into(),
location: location.as_ref().to_path_buf(),
number: None,
nested_items: Vec::new(),
}
}
}
impl Default for Link {
fn default() -> Self {
Link {
name: String::new(),
location: PathBuf::new(),
number: None,
nested_items: Vec::new(),
}
}
}
/// An item in `SUMMARY.md` which could be either a separator or a `Link`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SummaryItem {
/// A link to a chapter.
Link(Link),
/// A separator (`---`).
Separator,
}
impl SummaryItem {
fn maybe_link_mut(&mut self) -> Option<&mut Link> {
match *self {
SummaryItem::Link(ref mut l) => Some(l),
_ => None,
}
}
}
impl From<Link> for SummaryItem {
fn from(other: Link) -> SummaryItem {
SummaryItem::Link(other)
}
}
/// A recursive descent (-ish) parser for a `SUMMARY.md`.
///
///
/// # Grammar
///
/// The `SUMMARY.md` file has a grammar which looks something like this:
///
/// ```text
/// summary ::= title prefix_chapters numbered_chapters
/// suffix_chapters
/// title ::= "# " TEXT
/// | EPSILON
/// prefix_chapters ::= item*
/// suffix_chapters ::= item*
/// numbered_chapters ::= dotted_item+
/// dotted_item ::= INDENT* DOT_POINT item
/// item ::= link
/// | separator
/// separator ::= "---"
/// link ::= "[" TEXT "]" "(" TEXT ")"
/// DOT_POINT ::= "-"
/// | "*"
/// ```
///
/// > **Note:** the `TEXT` terminal is "normal" text, and should (roughly)
/// > match the following regex: "[^<>\n[]]+".
struct SummaryParser<'a> {
src: &'a str,
stream: pulldown_cmark::Parser<'a>,
}
/// Reads `Events` from the provided stream until the corresponding
/// `Event::End` is encountered which matches the `$delimiter` pattern.
///
/// This is the equivalent of doing
/// `$stream.take_while(|e| e != $delimeter).collect()` but it allows you to
/// use pattern matching and you won't get errors because `take_while()`
/// moves `$stream` out of self.
macro_rules! collect_events {
($stream:expr, start $delimiter:pat) => {
collect_events!($stream, Event::Start($delimiter))
};
($stream:expr, end $delimiter:pat) => {
collect_events!($stream, Event::End($delimiter))
};
($stream:expr, $delimiter:pat) => {
{
let mut events = Vec::new();
loop {
let event = $stream.next();
trace!("Next event: {:?}", event);
match event {
Some($delimiter) => break,
Some(other) => events.push(other),
None => {
debug!("Reached end of stream without finding the closing pattern, {}", stringify!($delimiter));
break;
}
}
}
events
}
}
}
impl<'a> SummaryParser<'a> {
fn new(text: &str) -> SummaryParser {
let pulldown_parser = pulldown_cmark::Parser::new(text);
SummaryParser {
src: text,
stream: pulldown_parser,
}
}
/// Get the current line and column to give the user more useful error
/// messages.
fn current_location(&self) -> (usize, usize) {
let byte_offset = self.stream.get_offset();
let previous_text = self.src[..byte_offset].as_bytes();
let line = Memchr::new(b'\n', previous_text).count() + 1;
let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
let col = self.src[start_of_line..byte_offset].chars().count();
(line, col)
}
/// Parse the text the `SummaryParser` was created with.
fn parse(mut self) -> Result<Summary> {
let title = self.parse_title();
let prefix_chapters = self.parse_affix(true)
.chain_err(|| "There was an error parsing the prefix chapters")?;
let numbered_chapters = self.parse_numbered()
.chain_err(|| "There was an error parsing the numbered chapters")?;
let suffix_chapters = self.parse_affix(false)
.chain_err(|| "There was an error parsing the suffix chapters")?;
Ok(Summary {
title,
prefix_chapters,
numbered_chapters,
suffix_chapters,
})
}
/// Parse the affix chapters. This expects the first event (start of
/// paragraph) to have already been consumed by the previous parser.
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
let mut items = Vec::new();
debug!(
"Parsing {} items",
if is_prefix { "prefix" } else { "suffix" }
);
loop {
match self.next_event() {
Some(Event::Start(Tag::List(..))) => {
if is_prefix {
// we've finished prefix chapters and are at the start
// of the numbered section.
break;
} else {
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
}
}
Some(Event::Start(Tag::Link(href, _))) => {
let link = self.parse_link(href.to_string())?;
items.push(SummaryItem::Link(link));
}
Some(Event::Start(Tag::Rule)) => items.push(SummaryItem::Separator),
Some(_) => {}
None => break,
}
}
Ok(items)
}
fn parse_link(&mut self, href: String) -> Result<Link> {
let link_content = collect_events!(self.stream, end Tag::Link(..));
let name = stringify_events(link_content);
if href.is_empty() {
Err(self.parse_error("You can't have an empty link."))
} else {
Ok(Link {
name: name,
location: PathBuf::from(href.to_string()),
number: None,
nested_items: Vec::new(),
})
}
}
/// Parse the numbered chapters. This assumes the opening list tag has
/// already been consumed by a previous parser.
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
let mut items = Vec::new();
let root_number = SectionNumber::default();
// we need to do this funny loop-match-if-let dance because a rule will
// close off any currently running list. Therefore we try to read the
// list items before the rule, then if we encounter a rule we'll add a
// separator and try to resume parsing numbered chapters if we start a
// list immediately afterwards.
//
// If you can think of a better way to do this then please make a PR :)
loop {
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
// if we've resumed after something like a rule the root sections
// will be numbered from 1. We need to manually go back and update
// them
update_section_numbers(&mut bunch_of_items, 0, items.len() as u32);
items.extend(bunch_of_items);
match self.next_event() {
Some(Event::Start(Tag::Paragraph)) => {
// we're starting the suffix chapters
break;
}
Some(Event::Start(other_tag)) => {
if other_tag == Tag::Rule {
items.push(SummaryItem::Separator);
}
trace!("Skipping contents of {:?}", other_tag);
// Skip over the contents of this tag
while let Some(event) = self.next_event() {
if event == Event::End(other_tag.clone()) {
break;
}
}
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
continue;
} else {
break;
}
}
Some(_) => {
// something else... ignore
continue;
}
None => {
// EOF, bail...
break;
}
}
}
Ok(items)
}
fn next_event(&mut self) -> Option<Event<'a>> {
let next = self.stream.next();
trace!("Next event: {:?}", next);
next
}
fn parse_nested_numbered(&mut self, parent: &SectionNumber) -> Result<Vec<SummaryItem>> {
debug!("Parsing numbered chapters at level {}", parent);
let mut items = Vec::new();
loop {
match self.next_event() {
Some(Event::Start(Tag::Item)) => {
let item = self.parse_nested_item(parent, items.len())?;
items.push(item);
}
Some(Event::Start(Tag::List(..))) => {
// recurse to parse the nested list
let (_, last_item) = get_last_link(&mut items)?;
let last_item_number = last_item
.number
.as_ref()
.expect("All numbered chapters have numbers");
let sub_items = self.parse_nested_numbered(last_item_number)?;
last_item.nested_items = sub_items;
}
Some(Event::End(Tag::List(..))) => break,
Some(_) => {}
None => break,
}
}
Ok(items)
}
fn parse_nested_item(
&mut self,
parent: &SectionNumber,
num_existing_items: usize,
) -> Result<SummaryItem> {
loop {
match self.next_event() {
Some(Event::Start(Tag::Paragraph)) => continue,
Some(Event::Start(Tag::Link(href, _))) => {
let mut link = self.parse_link(href.to_string())?;
let mut number = parent.clone();
number.0.push(num_existing_items as u32 + 1);
trace!(
"Found chapter: {} {} ({})",
number,
link.name,
link.location.display()
);
link.number = Some(number);
return Ok(SummaryItem::Link(link));
}
other => {
warn!("Expected a start of a link, actually got {:?}", other);
bail!(self.parse_error(
"The link items for nested chapters must only contain a hyperlink"
));
}
}
}
}
fn parse_error<D: Display>(&self, msg: D) -> Error {
let (line, col) = self.current_location();
ErrorKind::ParseError(line, col, msg.to_string()).into()
}
/// Try to parse the title line.
fn parse_title(&mut self) -> Option<String> {
if let Some(Event::Start(Tag::Header(1))) = self.next_event() {
debug!("Found a h1 in the SUMMARY");
let tags = collect_events!(self.stream, end Tag::Header(1));
Some(stringify_events(tags))
} else {
None
}
}
}
fn update_section_numbers(sections: &mut [SummaryItem], level: usize, by: u32) {
for section in sections {
if let SummaryItem::Link(ref mut link) = *section {
if let Some(ref mut number) = link.number {
number.0[level] += by;
}
update_section_numbers(&mut link.nested_items, level, by);
}
}
}
/// Gets a pointer to the last `Link` in a list of `SummaryItem`s, and its
/// index.
fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
links
.iter_mut()
.enumerate()
.filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
.rev()
.next()
.ok_or_else(|| {
"Unable to get last link because the list of SummaryItems doesn't contain any Links"
.into()
})
}
/// Removes the styling from a list of Markdown events and returns just the
/// plain text.
fn stringify_events(events: Vec<Event>) -> String {
events
.into_iter()
.filter_map(|t| match t {
Event::Text(text) => Some(text.into_owned()),
_ => None,
})
.collect()
}
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
/// a pretty `Display` impl.
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
pub struct SectionNumber(pub Vec<u32>);
impl Display for SectionNumber {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if self.0.is_empty() {
write!(f, "0")
} else {
for item in &self.0 {
write!(f, "{}.", item)?;
}
Ok(())
}
}
}
impl Deref for SectionNumber {
type Target = Vec<u32>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SectionNumber {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromIterator<u32> for SectionNumber {
fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
SectionNumber(it.into_iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn section_number_has_correct_dotted_representation() {
let inputs = vec![
(vec![0], "0."),
(vec![1, 3], "1.3."),
(vec![1, 2, 3], "1.2.3."),
];
for (input, should_be) in inputs {
let section_number = SectionNumber(input).to_string();
assert_eq!(section_number, should_be);
}
}
#[test]
fn parse_initial_title() {
let src = "# Summary";
let should_be = String::from("Summary");
let mut parser = SummaryParser::new(src);
let got = parser.parse_title().unwrap();
assert_eq!(got, should_be);
}
#[test]
fn parse_title_with_styling() {
let src = "# My **Awesome** Summary";
let should_be = String::from("My Awesome Summary");
let mut parser = SummaryParser::new(src);
let got = parser.parse_title().unwrap();
assert_eq!(got, should_be);
}
#[test]
fn convert_markdown_events_to_a_string() {
let src = "Hello *World*, `this` is some text [and a link](./path/to/link)";
let should_be = "Hello World, this is some text and a link";
let events = pulldown_cmark::Parser::new(src).collect();
let got = stringify_events(events);
assert_eq!(got, should_be);
}
#[test]
fn parse_some_prefix_items() {
let src = "[First](./first.md)\n[Second](./second.md)\n";
let mut parser = SummaryParser::new(src);
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
..Default::default()
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
..Default::default()
}),
];
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(true).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn parse_prefix_items_with_a_separator() {
let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(true).unwrap();
assert_eq!(got.len(), 3);
assert_eq!(got[1], SummaryItem::Separator);
}
#[test]
fn suffix_items_cannot_be_followed_by_a_list() {
let src = "[First](./first.md)\n- [Second](./second.md)\n";
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(false);
assert!(got.is_err());
}
#[test]
fn parse_a_link() {
let src = "[First](./first.md)";
let should_be = Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
..Default::default()
};
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // skip past start of paragraph
let href = match parser.stream.next() {
Some(Event::Start(Tag::Link(href, _))) => href.to_string(),
other => panic!("Unreachable, {:?}", other),
};
let got = parser.parse_link(href).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn parse_a_numbered_chapter() {
let src = "- [First](./first.md)\n";
let link = Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
number: Some(SectionNumber(vec![1])),
..Default::default()
};
let should_be = vec![SummaryItem::Link(link)];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser.parse_numbered().unwrap();
assert_eq!(got, should_be);
}
#[test]
fn parse_nested_numbered_chapters() {
let src = "- [First](./first.md)\n - [Nested](./nested.md)\n- [Second](./second.md)";
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
number: Some(SectionNumber(vec![1])),
nested_items: vec![
SummaryItem::Link(Link {
name: String::from("Nested"),
location: PathBuf::from("./nested.md"),
number: Some(SectionNumber(vec![1, 1])),
nested_items: Vec::new(),
}),
],
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser.parse_numbered().unwrap();
assert_eq!(got, should_be);
}
/// This test ensures the book will continue to pass because it breaks the
/// `SUMMARY.md` up using level 2 headers ([example]).
///
/// [example]: https://github.com/rust-lang/book/blob/2c942dc094f4ddcdc7aba7564f80782801197c99/second-edition/src/SUMMARY.md#basic-rust-literacy
#[test]
fn can_have_a_subheader_between_nested_items() {
let src = "- [First](./first.md)\n\n## Subheading\n\n- [Second](./second.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::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser.parse_numbered().unwrap();
assert_eq!(got, should_be);
}
#[test]
fn an_empty_link_location_is_an_error() {
let src = "- [Empty]()\n";
let mut parser = SummaryParser::new(src);
parser.stream.next();
let got = parser.parse_numbered();
assert!(got.is_err());
}
}

790
src/config.rs Normal file
View File

@@ -0,0 +1,790 @@
//! Mdbook's configuration system.
//!
//! 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.
//!
//!
//! # Examples
//!
//! ```rust
//! # extern crate mdbook;
//! # use mdbook::errors::*;
//! # extern crate toml;
//! use std::path::PathBuf;
//! use mdbook::Config;
//! use toml::Value;
//!
//! # fn run() -> Result<()> {
//! let src = r#"
//! [book]
//! title = "My Book"
//! authors = ["Michael-F-Bryan"]
//!
//! [build]
//! src = "out"
//!
//! [other-table.foo]
//! bar = 123
//! "#;
//!
//! // load the `Config` from a toml string
//! let mut cfg = Config::from_str(src)?;
//!
//! // retrieve a nested value
//! let bar = cfg.get("other-table.foo.bar").cloned();
//! assert_eq!(bar, Some(Value::Integer(123)));
//!
//! // Set the `output.html.theme` directory
//! assert!(cfg.get("output.html").is_none());
//! 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"));
//! # Ok(())
//! # }
//! # fn main() { run().unwrap() }
//! ```
#![deny(missing_docs)]
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Read;
use std::env;
use toml::{self, Value};
use toml::value::Table;
use toml_query::read::TomlValueReadExt;
use toml_query::insert::TomlValueInsertExt;
use toml_query::delete::TomlValueDeleteExt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json;
use errors::*;
/// The overall configuration object for MDBook, essentially an in-memory
/// representation of `book.toml`.
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
/// Metadata about the book.
pub book: BookConfig,
/// Information about the build environment.
pub build: BuildConfig,
rest: Value,
}
impl Config {
/// Load a `Config` from some string.
pub fn from_str(src: &str) -> Result<Config> {
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
}
/// Load the configuration file from disk.
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
let mut buffer = String::new();
File::open(config_file)
.chain_err(|| "Unable to open the configuration file")?
.read_to_string(&mut buffer)
.chain_err(|| "Couldn't read the file")?;
Config::from_str(&buffer)
}
/// Updates the `Config` from the available environment variables.
///
/// Variables starting with `MDBOOK_` are used for configuration. The key is
/// created by removing the `MDBOOK_` prefix and turning the resulting
/// string into `kebab-case`. Double underscores (`__`) separate nested
/// keys, while a single underscore (`_`) is replaced with a dash (`-`).
///
/// For example:
///
/// - `MDBOOK_foo` -> `foo`
/// - `MDBOOK_FOO` -> `foo`
/// - `MDBOOK_FOO__BAR` -> `foo.bar`
/// - `MDBOOK_FOO_BAR` -> `foo-bar`
/// - `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
///
/// So by setting the `MDBOOK_BOOK__TITLE` environment variable you can
/// override the book's title without needing to touch your `book.toml`.
///
/// > **Note:** To facilitate setting more complex config items, the value
/// > of an environment variable is first parsed as JSON, falling back to a
/// > string if the parse fails.
/// >
/// > This means, if you so desired, you could override all book metadata
/// > when building the book with something like
/// >
/// > ```text
/// > $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
/// > $ mdbook build
/// > ```
///
/// The latter case may be useful in situations where `mdbook` is invoked
/// from a script or CI, where it sometimes isn't possible to update the
/// `book.toml` before building.
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,
});
for (key, value) in overrides {
trace!("{} => {}", key, value);
let parsed_value = serde_json::from_str(&value)
.unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
self.set(key, parsed_value).expect("unreachable");
}
}
/// Fetch an arbitrary item from the `Config` as a `toml::Value`.
///
/// You can use dotted indices to access nested items (e.g.
/// `output.html.playpen` will fetch the "playpen" out of the html output
/// table).
pub fn get(&self, key: &str) -> Option<&Value> {
match self.rest.read(key) {
Ok(inner) => inner,
Err(_) => None,
}
}
/// Fetch a value from the `Config` so you can mutate it.
pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
match self.rest.read_mut(key) {
Ok(inner) => inner,
Err(_) => None,
}
}
/// Convenience method for getting the html renderer's configuration.
///
/// # Note
///
/// This is for compatibility only. It will be removed completely once the
/// 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()
}
/// 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> {
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)
}
}
/// Set a config key, clobbering any existing values along the way.
///
/// The only way this can fail is if we can't serialize `value` into a
/// `toml::Value`.
pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
let index = index.as_ref();
let value =
Value::try_from(value).chain_err(|| "Unable to represent the item as a JSON Value")?;
if index.starts_with("book.") {
self.book.update_value(&index[5..], value);
} else if index.starts_with("build.") {
self.build.update_value(&index[6..], value);
} else {
self.rest.insert(index, value)?;
}
Ok(())
}
fn from_legacy(mut table: Value) -> Config {
let mut cfg = Config::default();
// we use a macro here instead of a normal loop because the $out
// variable can be different types. This way we can make type inference
// figure out what try_into() deserializes to.
macro_rules! get_and_insert {
($table:expr, $key:expr => $out:expr) => {
let got = $table.as_table_mut()
.and_then(|t| t.remove($key))
.and_then(|v| v.try_into().ok());
if let Some(value) = got {
$out = value;
}
};
}
get_and_insert!(table, "title" => cfg.book.title);
get_and_insert!(table, "authors" => cfg.book.authors);
get_and_insert!(table, "source" => cfg.book.src);
get_and_insert!(table, "description" => cfg.book.description);
if let Ok(Some(dest)) = table.delete("output.html.destination") {
if let Ok(destination) = dest.try_into() {
cfg.build.build_dir = destination;
}
}
cfg.rest = table;
cfg
}
}
impl Default for Config {
fn default() -> Config {
Config {
book: BookConfig::default(),
build: BuildConfig::default(),
rest: Value::Table(Table::default()),
}
}
}
impl<'de> Deserialize<'de> for Config {
fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
let raw = Value::deserialize(de)?;
if is_legacy_format(&raw) {
warn!("It looks like you are using the legacy book.toml format.");
warn!("We'll parse it for now, but you should probably convert to the new format.");
warn!("See the mdbook documentation for more details, although as a rule of thumb");
warn!("just move all top level configuration entries like `title`, `author` and");
warn!("`description` under a table called `[book]`, move the `destination` entry");
warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
warn!("`[build]`, and it should all work.");
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
return Ok(Config::from_legacy(raw));
}
let mut table = match raw {
Value::Table(t) => t,
_ => {
use serde::de::Error;
return Err(D::Error::custom(
"A config file should always be a toml table",
));
}
};
let book: BookConfig = table
.remove("book")
.and_then(|value| value.try_into().ok())
.unwrap_or_default();
let build: BuildConfig = table
.remove("build")
.and_then(|value| value.try_into().ok())
.unwrap_or_default();
Ok(Config {
book: book,
build: build,
rest: Value::Table(table),
})
}
}
impl Serialize for Config {
fn serialize<S: Serializer>(&self, s: S) -> ::std::result::Result<S::Ok, S::Error> {
use serde::ser::Error;
let mut table = self.rest.clone();
let book_config = match Value::try_from(self.book.clone()) {
Ok(cfg) => cfg,
Err(_) => {
return Err(S::Error::custom("Unable to serialize the BookConfig"));
}
};
table.insert("book", book_config).expect("unreachable");
table.serialize(s)
}
}
fn parse_env(key: &str) -> Option<String> {
const PREFIX: &str = "MDBOOK_";
if key.starts_with(PREFIX) {
let key = &key[PREFIX.len()..];
Some(key.to_lowercase().replace("__", ".").replace("_", "-"))
} else {
None
}
}
fn is_legacy_format(table: &Value) -> bool {
let legacy_items = [
"title",
"authors",
"source",
"description",
"output.html.destination",
];
for item in &legacy_items {
if let Ok(Some(_)) = table.read(item) {
return true;
}
}
false
}
/// Configuration options which are specific to the book and required for
/// loading it from disk.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct BookConfig {
/// The book's title.
pub title: Option<String>,
/// The book's authors.
pub authors: Vec<String>,
/// An optional description for the book.
pub description: Option<String>,
/// Location of the book source relative to the book's root directory.
pub src: PathBuf,
/// Does this book support more than one language?
pub multilingual: bool,
}
impl Default for BookConfig {
fn default() -> BookConfig {
BookConfig {
title: None,
authors: Vec::new(),
description: None,
src: PathBuf::from("src"),
multilingual: false,
}
}
}
/// Configuration for the build procedure.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct BuildConfig {
/// Where to put built artefacts relative to the book's root directory.
pub build_dir: PathBuf,
/// Should non-existent markdown files specified in `SETTINGS.md` be created
/// if they don't exist?
pub create_missing: bool,
/// Which preprocessors should be applied
pub preprocess: Option<Vec<String>>,
}
impl Default for BuildConfig {
fn default() -> BuildConfig {
BuildConfig {
build_dir: PathBuf::from("book"),
create_missing: true,
preprocess: None,
}
}
}
/// Configuration for the HTML renderer.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct HtmlConfig {
/// The theme directory, if specified.
pub theme: Option<PathBuf>,
/// Use "smart quotes" instead of the usual `"` character.
pub curly_quotes: bool,
/// Should mathjax be enabled?
pub mathjax_support: bool,
/// An optional google analytics code.
pub google_analytics: Option<String>,
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
pub additional_css: Vec<PathBuf>,
/// Additional JS scripts to include at the bottom of the rendered page's
/// `<body>`.
pub additional_js: Vec<PathBuf>,
/// Playpen settings.
pub playpen: Playpen,
/// This is used as a bit of a workaround for the `mdbook serve` command.
/// Basically, because you set the websocket port from the command line, the
/// `mdbook serve` command needs a way to let the HTML renderer know where
/// to point livereloading at, if it has been enabled.
///
/// This config item *should not be edited* by the end user.
#[doc(hidden)]
pub livereload_url: Option<String>,
/// 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 {
/// Returns the directory of theme from the provided root directory. If the
/// directory is not present it will append the default directory of "theme"
pub fn theme_dir(&self, root: &PathBuf) -> PathBuf {
match self.theme {
Some(ref d) => root.join(d),
None => root.join("theme"),
}
}
}
/// 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,
/// Copy JavaScript files for the editor to the output directory?
/// Default: `true`.
pub copy_js: bool,
}
impl Default for Playpen {
fn default() -> Playpen {
Playpen {
editable: false,
copy_js: true,
}
}
}
/// Configuration of the search functionality of the HTML renderer.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Search {
/// Maximum number of visible results. Default: `30`.
pub limit_results: u32,
/// 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`.
pub use_boolean_and: bool,
/// Boost factor for the search result score if a search word appears in the header.
/// Default: `2`.
pub boost_title: u8,
/// Boost factor for the search result score if a search word appears in the hierarchy.
/// The hierarchy contains all titles of the parent documents and all parent headings.
/// Default: `1`.
pub boost_hierarchy: u8,
/// Boost factor for the search result score if a search word appears in the text.
/// Default: `1`.
pub boost_paragraph: u8,
/// True if the searchword `micro` should match `microwave`. Default: `true`.
pub expand: bool,
/// Documents are split into smaller parts, seperated by headings. This defines, until which
/// level of heading documents should be split. Default: `3`. (`### This is a level 3 heading`)
pub heading_split_level: u8,
/// Copy JavaScript files for the search functionality to the output directory?
/// Default: `true`.
pub copy_js: bool,
}
impl Default for Search {
fn default() -> Search {
// Please update the documentation of `Search` when changing values!
Search {
limit_results: 30,
teaser_word_count: 30,
use_boolean_and: false,
boost_title: 2,
boost_hierarchy: 1,
boost_paragraph: 1,
expand: true,
heading_split_level: 3,
copy_js: true,
}
}
}
/// Allows you to "update" any arbitrary field in a struct by round-tripping via
/// a `toml::Value`.
///
/// This is definitely not the most performant way to do things, which means you
/// should probably keep it away from tight loops...
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(updated) = raw.try_into() {
*self = updated;
}
}
}
impl<'de, T> Updateable<'de> for T
where
T: Serialize + Deserialize<'de>,
{
}
#[cfg(test)]
mod tests {
use super::*;
const COMPLEX_CONFIG: &'static str = r#"
[book]
title = "Some Book"
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
description = "A completely useless book"
multilingual = true
src = "source"
[build]
build-dir = "outputs"
create-missing = false
preprocess = ["first_preprocessor", "second_preprocessor"]
[output.html]
theme = "./themedir"
curly-quotes = true
google-analytics = "123456"
additional-css = ["./foo/bar/baz.css"]
[output.html.playpen]
editable = true
editor = "ace"
"#;
#[test]
fn load_a_complex_config_file() {
let src = COMPLEX_CONFIG;
let book_should_be = BookConfig {
title: Some(String::from("Some Book")),
authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
description: Some(String::from("A completely useless book")),
multilingual: true,
src: PathBuf::from("source"),
..Default::default()
};
let build_should_be = BuildConfig {
build_dir: PathBuf::from("outputs"),
create_missing: false,
preprocess: Some(vec![
"first_preprocessor".to_string(),
"second_preprocessor".to_string(),
]),
};
let playpen_should_be = Playpen {
editable: true,
copy_js: true,
};
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")),
playpen: playpen_should_be,
..Default::default()
};
let got = Config::from_str(src).unwrap();
assert_eq!(got.book, book_should_be);
assert_eq!(got.build, build_should_be);
assert_eq!(got.html_config().unwrap(), html_should_be);
}
#[test]
fn load_arbitrary_output_type() {
#[derive(Debug, Deserialize, PartialEq)]
struct RandomOutput {
foo: u32,
bar: String,
baz: Vec<bool>,
}
let src = r#"
[output.random]
foo = 5
bar = "Hello World"
baz = [true, true, false]
"#;
let should_be = RandomOutput {
foo: 5,
bar: String::from("Hello World"),
baz: vec![true, true, false],
};
let cfg = Config::from_str(src).unwrap();
let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
assert_eq!(got, should_be);
let baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
let baz_should_be = vec![true, true, false];
assert_eq!(baz, baz_should_be);
}
#[test]
fn mutate_some_stuff() {
// really this is just a sanity check to make sure the borrow checker
// is happy...
let src = COMPLEX_CONFIG;
let mut config = Config::from_str(src).unwrap();
let key = "output.html.playpen.editable";
assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
*config.get_mut(key).unwrap() = Value::Boolean(false);
assert_eq!(config.get(key).unwrap(), &Value::Boolean(false));
}
/// The config file format has slightly changed (metadata stuff is now under
/// the `book` table instead of being at the top level) so we're adding a
/// **temporary** compatibility check. You should be able to still load the
/// old format, emitting a warning.
#[test]
fn can_still_load_the_previous_format() {
let src = r#"
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David"]
source = "./source"
[output.html]
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
theme = "my-theme"
curly-quotes = true
google-analytics = "123456"
additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"]
"#;
let book_should_be = BookConfig {
title: Some(String::from("mdBook Documentation")),
description: Some(String::from(
"Create book from markdown files. Like Gitbook but implemented in Rust",
)),
authors: vec![String::from("Mathieu David")],
src: PathBuf::from("./source"),
..Default::default()
};
let build_should_be = BuildConfig {
build_dir: PathBuf::from("my-book"),
create_missing: true,
preprocess: None,
};
let html_should_be = HtmlConfig {
theme: Some(PathBuf::from("my-theme")),
curly_quotes: true,
google_analytics: Some(String::from("123456")),
additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
additional_js: vec![PathBuf::from("custom.js")],
..Default::default()
};
let got = Config::from_str(src).unwrap();
assert_eq!(got.book, book_should_be);
assert_eq!(got.build, build_should_be);
assert_eq!(got.html_config().unwrap(), html_should_be);
}
#[test]
fn set_a_config_item() {
let mut cfg = Config::default();
let key = "foo.bar.baz";
let value = "Something Interesting";
assert!(cfg.get(key).is_none());
cfg.set(key, value).unwrap();
let got: String = cfg.get_deserialized(key).unwrap();
assert_eq!(got, value);
}
#[test]
fn parse_env_vars() {
let inputs = vec![
("FOO", None),
("MDBOOK_foo", Some("foo")),
("MDBOOK_FOO__bar__baz", Some("foo.bar.baz")),
("MDBOOK_FOO_bar__baz", Some("foo-bar.baz")),
];
for (src, should_be) in inputs {
let got = parse_env(src);
let should_be = should_be.map(|s| s.to_string());
assert_eq!(got, should_be);
}
}
fn encode_env_var(key: &str) -> String {
format!(
"MDBOOK_{}",
key.to_uppercase().replace('.', "__").replace("-", "_")
)
}
#[test]
fn update_config_using_env_var() {
let mut cfg = Config::default();
let key = "foo.bar";
let value = "baz";
assert!(cfg.get(key).is_none());
let encoded_key = encode_env_var(key);
env::set_var(encoded_key, value);
cfg.update_from_env();
assert_eq!(cfg.get_deserialized::<String, _>(key).unwrap(), value);
}
#[test]
fn update_config_using_env_var_and_complex_value() {
let mut cfg = Config::default();
let key = "foo-bar.baz";
let value = json!({"array": [1, 2, 3], "number": 3.14});
let value_str = serde_json::to_string(&value).unwrap();
assert!(cfg.get(key).is_none());
let encoded_key = encode_env_var(key);
env::set_var(encoded_key, value_str);
cfg.update_from_env();
assert_eq!(
cfg.get_deserialized::<serde_json::Value, _>(key).unwrap(),
value
);
}
#[test]
fn update_book_title_via_env() {
let mut cfg = Config::default();
let should_be = "Something else".to_string();
assert_ne!(cfg.book.title, Some(should_be.clone()));
env::set_var("MDBOOK_BOOK__TITLE", &should_be);
cfg.update_from_env();
assert_eq!(cfg.book.title, Some(should_be));
}
}

View File

@@ -1,229 +0,0 @@
use std::path::{PathBuf, Path};
use super::HtmlConfig;
use super::tomlconfig::TomlConfig;
use super::jsonconfig::JsonConfig;
/// Configuration struct containing all the configuration options available in mdBook.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BookConfig {
root: PathBuf,
source: PathBuf,
title: String,
authors: Vec<String>,
description: String,
multilingual: bool,
indent_spaces: i32,
html_config: HtmlConfig,
}
impl BookConfig {
/// Creates a new `BookConfig` struct with as root path the path given as parameter.
/// The source directory is `root/src` and the destination for the rendered book is `root/book`.
///
/// ```
/// # use std::path::PathBuf;
/// # use mdbook::config::{BookConfig, HtmlConfig};
/// #
/// let root = PathBuf::from("directory/to/my/book");
/// let config = BookConfig::new(&root);
///
/// assert_eq!(config.get_root(), &root);
/// assert_eq!(config.get_source(), PathBuf::from("directory/to/my/book/src"));
/// assert_eq!(config.get_html_config(), &HtmlConfig::new(PathBuf::from("directory/to/my/book")));
/// ```
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
let root: PathBuf = root.into();
let htmlconfig = HtmlConfig::new(&root);
BookConfig {
root: root.clone(),
source: root.join("src"),
title: String::new(),
authors: Vec::new(),
description: String::new(),
multilingual: false,
indent_spaces: 4,
html_config: htmlconfig,
}
}
/// Builder method to set the source directory
pub fn with_source<T: Into<PathBuf>>(mut self, source: T) -> Self {
self.source = source.into();
self
}
/// Builder method to set the book's title
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
self.title = title.into();
self
}
/// Builder method to set the book's description
pub fn with_description<T: Into<String>>(mut self, description: T) -> Self {
self.description = description.into();
self
}
/// Builder method to set the book's authors
pub fn with_authors<T: Into<Vec<String>>>(mut self, authors: T) -> Self {
self.authors = authors.into();
self
}
pub fn from_tomlconfig<T: Into<PathBuf>>(root: T, tomlconfig: TomlConfig) -> Self {
let root = root.into();
let mut config = BookConfig::new(&root);
config.fill_from_tomlconfig(tomlconfig);
config
}
pub fn fill_from_tomlconfig(&mut self, tomlconfig: TomlConfig) -> &mut Self {
if let Some(s) = tomlconfig.source {
self.set_source(s);
}
if let Some(t) = tomlconfig.title {
self.set_title(t);
}
if let Some(d) = tomlconfig.description {
self.set_description(d);
}
if let Some(a) = tomlconfig.authors {
self.set_authors(a);
}
if let Some(a) = tomlconfig.author {
self.set_authors(vec![a]);
}
if let Some(tomlhtmlconfig) = tomlconfig.output.and_then(|o| o.html) {
let root = self.root.clone();
self.get_mut_html_config()
.fill_from_tomlconfig(root, tomlhtmlconfig);
}
self
}
/// The JSON configuration file is **deprecated** and should not be used anymore.
/// Please, migrate to the TOML configuration file.
pub fn from_jsonconfig<T: Into<PathBuf>>(root: T, jsonconfig: JsonConfig) -> Self {
let root = root.into();
let mut config = BookConfig::new(&root);
config.fill_from_jsonconfig(jsonconfig);
config
}
/// The JSON configuration file is **deprecated** and should not be used anymore.
/// Please, migrate to the TOML configuration file.
pub fn fill_from_jsonconfig(&mut self, jsonconfig: JsonConfig) -> &mut Self {
if let Some(s) = jsonconfig.src {
self.set_source(s);
}
if let Some(t) = jsonconfig.title {
self.set_title(t);
}
if let Some(d) = jsonconfig.description {
self.set_description(d);
}
if let Some(a) = jsonconfig.author {
self.set_authors(vec![a]);
}
if let Some(d) = jsonconfig.dest {
let root = self.get_root().to_owned();
self.get_mut_html_config()
.set_destination(&root, &d);
}
if let Some(d) = jsonconfig.theme_path {
let root = self.get_root().to_owned();
self.get_mut_html_config()
.set_theme(&root, &d);
}
self
}
pub fn set_root<T: Into<PathBuf>>(&mut self, root: T) -> &mut Self {
self.root = root.into();
self
}
pub fn get_root(&self) -> &Path {
&self.root
}
pub fn set_source<T: Into<PathBuf>>(&mut self, source: T) -> &mut Self {
let mut source = source.into();
// If the source path is relative, start with the root path
if source.is_relative() {
source = self.root.join(source);
}
self.source = source;
self
}
pub fn get_source(&self) -> &Path {
&self.source
}
pub fn set_title<T: Into<String>>(&mut self, title: T) -> &mut Self {
self.title = title.into();
self
}
pub fn get_title(&self) -> &str {
&self.title
}
pub fn set_description<T: Into<String>>(&mut self, description: T) -> &mut Self {
self.description = description.into();
self
}
pub fn get_description(&self) -> &str {
&self.description
}
pub fn set_authors<T: Into<Vec<String>>>(&mut self, authors: T) -> &mut Self {
self.authors = authors.into();
self
}
/// Returns the authors of the book as specified in the configuration file
pub fn get_authors(&self) -> &[String] {
self.authors.as_slice()
}
pub fn set_html_config(&mut self, htmlconfig: HtmlConfig) -> &mut Self {
self.html_config = htmlconfig;
self
}
/// Returns the configuration for the HTML renderer or None of there isn't any
pub fn get_html_config(&self) -> &HtmlConfig {
&self.html_config
}
pub fn get_mut_html_config(&mut self) -> &mut HtmlConfig {
&mut self.html_config
}
}

View File

@@ -1,173 +0,0 @@
use std::path::{PathBuf, Path};
use super::tomlconfig::TomlHtmlConfig;
use super::playpenconfig::PlaypenConfig;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HtmlConfig {
destination: PathBuf,
theme: PathBuf,
curly_quotes: bool,
mathjax_support: bool,
google_analytics: Option<String>,
additional_css: Vec<PathBuf>,
additional_js: Vec<PathBuf>,
playpen: PlaypenConfig,
}
impl HtmlConfig {
/// Creates a new `HtmlConfig` struct containing the configuration parameters for the HTML renderer.
///
/// ```
/// # use std::path::PathBuf;
/// # use mdbook::config::HtmlConfig;
/// #
/// let output = PathBuf::from("root/book");
/// let config = HtmlConfig::new(PathBuf::from("root"));
///
/// assert_eq!(config.get_destination(), &output);
/// ```
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
let root = root.into();
let theme = root.join("theme");
HtmlConfig {
destination: root.clone().join("book"),
theme: theme.clone(),
curly_quotes: false,
mathjax_support: false,
google_analytics: None,
additional_css: Vec::new(),
additional_js: Vec::new(),
playpen: PlaypenConfig::new(theme),
}
}
pub fn fill_from_tomlconfig<T: Into<PathBuf>>(&mut self, root: T, tomlconfig: TomlHtmlConfig) -> &mut Self {
let root = root.into();
if let Some(d) = tomlconfig.destination {
self.set_destination(&root, &d);
}
if let Some(t) = tomlconfig.theme {
self.set_theme(&root, &t);
}
if let Some(curly_quotes) = tomlconfig.curly_quotes {
self.curly_quotes = curly_quotes;
}
if let Some(mathjax_support) = tomlconfig.mathjax_support {
self.mathjax_support = mathjax_support;
}
if tomlconfig.google_analytics.is_some() {
self.google_analytics = tomlconfig.google_analytics;
}
if let Some(stylepaths) = tomlconfig.additional_css {
for path in stylepaths {
if path.is_relative() {
self.additional_css.push(root.join(path));
} else {
self.additional_css.push(path);
}
}
}
if let Some(scriptpaths) = tomlconfig.additional_js {
for path in scriptpaths {
if path.is_relative() {
self.additional_js.push(root.join(path));
} else {
self.additional_js.push(path);
}
}
}
if let Some(playpen) = tomlconfig.playpen {
self.playpen.fill_from_tomlconfig(&self.theme, playpen);
}
self
}
pub fn set_destination<T: Into<PathBuf>>(&mut self, root: T, destination: T) -> &mut Self {
let d = destination.into();
if d.is_relative() {
self.destination = root.into().join(d);
} else {
self.destination = d;
}
self
}
pub fn get_destination(&self) -> &Path {
&self.destination
}
pub fn get_theme(&self) -> &Path {
&self.theme
}
pub fn set_theme<T: Into<PathBuf>>(&mut self, root: T, theme: T) -> &mut Self {
let d = theme.into();
if d.is_relative() {
self.theme = root.into().join(d);
} else {
self.theme = d;
}
self
}
pub fn get_curly_quotes(&self) -> bool {
self.curly_quotes
}
pub fn set_curly_quotes(&mut self, curly_quotes: bool) {
self.curly_quotes = curly_quotes;
}
pub fn get_mathjax_support(&self) -> bool {
self.mathjax_support
}
pub fn set_mathjax_support(&mut self, mathjax_support: bool) {
self.mathjax_support = mathjax_support;
}
pub fn get_google_analytics_id(&self) -> Option<String> {
self.google_analytics.clone()
}
pub fn set_google_analytics_id(&mut self, id: Option<String>) -> &mut Self {
self.google_analytics = id;
self
}
pub fn has_additional_css(&self) -> bool {
!self.additional_css.is_empty()
}
pub fn get_additional_css(&self) -> &[PathBuf] {
&self.additional_css
}
pub fn has_additional_js(&self) -> bool {
!self.additional_js.is_empty()
}
pub fn get_additional_js(&self) -> &[PathBuf] {
&self.additional_js
}
pub fn get_playpen_config(&self) -> &PlaypenConfig {
&self.playpen
}
pub fn get_mut_playpen_config(&mut self) -> &mut PlaypenConfig {
&mut self.playpen
}
}

View File

@@ -1,42 +0,0 @@
extern crate serde_json;
use std::path::PathBuf;
use errors::*;
/// The JSON configuration is **deprecated** and will be removed in the near future.
/// Please migrate to the TOML configuration.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct JsonConfig {
pub src: Option<PathBuf>,
pub dest: Option<PathBuf>,
pub title: Option<String>,
pub author: Option<String>,
pub description: Option<String>,
pub theme_path: Option<PathBuf>,
pub google_analytics: Option<String>,
}
/// Returns a `JsonConfig` from a JSON string
///
/// ```
/// # use mdbook::config::jsonconfig::JsonConfig;
/// # use std::path::PathBuf;
/// let json = r#"{
/// "title": "Some title",
/// "dest": "htmlbook"
/// }"#;
///
/// let config = JsonConfig::from_json(&json).expect("Should parse correctly");
/// assert_eq!(config.title, Some(String::from("Some title")));
/// assert_eq!(config.dest, Some(PathBuf::from("htmlbook")));
/// ```
impl JsonConfig {
pub fn from_json(input: &str) -> Result<Self> {
let config: JsonConfig = serde_json::from_str(input)
.chain_err(|| "Could not parse JSON")?;
Ok(config)
}
}

View File

@@ -1,11 +0,0 @@
pub mod bookconfig;
pub mod htmlconfig;
pub mod playpenconfig;
pub mod tomlconfig;
pub mod jsonconfig;
// Re-export the config structs
pub use self::bookconfig::BookConfig;
pub use self::htmlconfig::HtmlConfig;
pub use self::playpenconfig::PlaypenConfig;
pub use self::tomlconfig::TomlConfig;

View File

@@ -1,68 +0,0 @@
use std::path::{PathBuf, Path};
use super::tomlconfig::TomlPlaypenConfig;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PlaypenConfig {
editor: PathBuf,
editable: bool,
}
impl PlaypenConfig {
/// Creates a new `PlaypenConfig` for playpen configuration.
///
/// ```
/// # use std::path::PathBuf;
/// # use mdbook::config::PlaypenConfig;
/// #
/// let editor = PathBuf::from("root/editor");
/// let config = PlaypenConfig::new(PathBuf::from("root"));
///
/// assert_eq!(config.get_editor(), &editor);
/// assert_eq!(config.is_editable(), false);
/// ```
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
PlaypenConfig {
editor: root.into().join("editor"),
editable: false,
}
}
pub fn fill_from_tomlconfig<T: Into<PathBuf>>(&mut self, root: T, tomlplaypenconfig: TomlPlaypenConfig) -> &mut Self {
let root = root.into();
if let Some(editor) = tomlplaypenconfig.editor {
if editor.is_relative() {
self.editor = root.join(editor);
} else {
self.editor = editor;
}
}
if let Some(editable) = tomlplaypenconfig.editable {
self.editable = editable;
}
self
}
pub fn is_editable(&self) -> bool {
self.editable
}
pub fn get_editor(&self) -> &Path {
&self.editor
}
pub fn set_editor<T: Into<PathBuf>>(&mut self, root: T, editor: T) -> &mut Self {
let editor = editor.into();
if editor.is_relative() {
self.editor = root.into().join(editor);
} else {
self.editor = editor;
}
self
}
}

View File

@@ -1,61 +0,0 @@
extern crate toml;
use std::path::PathBuf;
use errors::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TomlConfig {
pub source: Option<PathBuf>,
pub title: Option<String>,
pub author: Option<String>,
pub authors: Option<Vec<String>>,
pub description: Option<String>,
pub output: Option<TomlOutputConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TomlOutputConfig {
pub html: Option<TomlHtmlConfig>,
}
#[serde(rename_all = "kebab-case")]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TomlHtmlConfig {
pub destination: Option<PathBuf>,
pub theme: Option<PathBuf>,
pub google_analytics: Option<String>,
pub curly_quotes: Option<bool>,
pub mathjax_support: Option<bool>,
pub additional_css: Option<Vec<PathBuf>>,
pub additional_js: Option<Vec<PathBuf>>,
pub playpen: Option<TomlPlaypenConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TomlPlaypenConfig {
pub editor: Option<PathBuf>,
pub editable: Option<bool>,
}
/// Returns a `TomlConfig` from a TOML string
///
/// ```
/// # use mdbook::config::tomlconfig::TomlConfig;
/// # use std::path::PathBuf;
/// let toml = r#"title="Some title"
/// [output.html]
/// destination = "htmlbook" "#;
///
/// let config = TomlConfig::from_toml(&toml).expect("Should parse correctly");
/// assert_eq!(config.title, Some(String::from("Some title")));
/// assert_eq!(config.output.unwrap().html.unwrap().destination, Some(PathBuf::from("htmlbook")));
/// ```
impl TomlConfig {
pub fn from_toml(input: &str) -> Result<Self> {
let config: TomlConfig = toml::from_str(input)
.chain_err(|| "Could not parse TOML")?;
Ok(config)
}
}

View File

@@ -1,94 +1,113 @@
//! # mdBook
//!
//! **mdBook** is similar to Gitbook but implemented in Rust.
//! It offers a command line interface, but can also be used as a regular crate.
//! **mdBook** is a tool for rendering a collection of markdown documents into
//! a form more suitable for end users like HTML or EPUB. It offers a command
//! line interface, but this crate can be used if more control is required.
//!
//! This is the API doc, but you can find a [less "low-level" documentation here](../index.html) that
//! contains information about the command line tool, format, structure etc.
//! It is also rendered with mdBook to showcase the features and default theme.
//! This is the API doc, the [user guide] is also available if you want
//! information about the command line tool, format, structure etc. It is also
//! rendered with mdBook to showcase the features and default theme.
//!
//! Some reasons why you would want to use the crate (over the cli):
//!
//! - Integrate mdbook in a current project
//! - Extend the capabilities of mdBook
//! - Do some processing or test before building your book
//! - Write a new Renderer
//! - Accessing the public API to help create a new Renderer
//! - ...
//!
//! ## Example
//! > **Note:** While we try to ensure `mdbook`'s command-line interface and
//! > behaviour are backwards compatible, the tool's internals are still
//! > evolving and being iterated on. If you wish to prevent accidental
//! > breakages it is recommended to pin any tools building on top of the
//! > `mdbook` crate to a specific release.
//!
//! ```no_run
//! extern crate mdbook;
//! # Examples
//!
//! If creating a new book from scratch, you'll want to get a `BookBuilder` via
//! the `MDBook::init()` method.
//!
//! ```rust,no_run
//! use mdbook::MDBook;
//! use mdbook::config::Config;
//!
//! let root_dir = "/path/to/book/root";
//!
//! // create a default config and change a couple things
//! let mut cfg = Config::default();
//! cfg.book.title = Some("My Book".to_string());
//! cfg.book.authors.push("Michael-F-Bryan".to_string());
//!
//! MDBook::init(root_dir)
//! .create_gitignore(true)
//! .with_config(cfg)
//! .build()
//! .expect("Book generation failed");
//! ```
//!
//! You can also load an existing book and build it.
//!
//! ```rust,no_run
//! use mdbook::MDBook;
//!
//! # #[allow(unused_variables)]
//! fn main() {
//! let mut book = MDBook::new("my-book") // Path to root
//! .with_source("src") // Path from root to source directory
//! .with_destination("book") // Path from root to output directory
//! .read_config() // Parse book.toml configuration file
//! .expect("I don't handle configuration file errors, but you should!");
//! let root_dir = "/path/to/book/root";
//!
//! book.build().unwrap(); // Render the book
//! }
//! let mut md = MDBook::load(root_dir)
//! .expect("Unable to load the book");
//! md.build().expect("Building failed");
//! ```
//!
//! ## Implementing a new Renderer
//! ## Implementing a new Backend
//!
//! If you want to create a new renderer for mdBook, the only thing you have to do is to implement
//! the [Renderer trait](renderer/renderer/trait.Renderer.html)
//! `mdbook` has a fairly flexible mechanism for creating additional backends
//! for your book. The general idea is you'll add an extra table in the book's
//! `book.toml` which specifies an executable to be invoked by `mdbook`. This
//! executable will then be called during a build, with an in-memory
//! representation ([`RenderContext`]) of the book being passed to the
//! subprocess via `stdin`.
//!
//! And then you can swap in your renderer like this:
//! The [`RenderContext`] gives the backend access to the contents of
//! `book.toml` and lets it know which directory all generated artefacts should
//! be placed in. For a much more in-depth explanation, consult the [relevant
//! chapter] in the *For Developers* section of the user guide.
//!
//! ```no_run
//! # extern crate mdbook;
//! #
//! # use mdbook::MDBook;
//! # use mdbook::renderer::HtmlHandlebars;
//! #
//! # #[allow(unused_variables)]
//! # fn main() {
//! # let your_renderer = HtmlHandlebars::new();
//! #
//! let book = MDBook::new("my-book").set_renderer(Box::new(your_renderer));
//! # }
//! ```
//! If you make a renderer, you get the book constructed in form of `Vec<BookItems>` and you get
//! the book config in a `BookConfig` struct.
//! To make creating a backend easier, the `mdbook` crate can be imported
//! directly, making deserializing the `RenderContext` easy and giving you
//! access to the various methods for working with the [`Config`].
//!
//! It's your responsability to create the necessary files in the correct directories.
//!
//! ## utils
//!
//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the
//! following function [`utils::fs::create_file(path:
//! &Path)`](utils/fs/fn.create_file.html)
//!
//! This function creates a file and returns it. But before creating the file
//! it checks every directory in the path to see if it exists, and if it does
//! not it will be created.
//!
//! Make sure to take a look at it.
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/
//! [`RenderContext`]: renderer/struct.RenderContext.html
//! [relevant chapter]: https://rust-lang-nursery.github.io/mdBook/for_developers/backends.html
//! [`Config`]: config/struct.Config.html
#![deny(missing_docs)]
#[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;
#[macro_use]
extern crate serde_derive;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
extern crate tempdir;
extern crate shlex;
extern crate tempfile;
extern crate toml;
extern crate toml_query;
mod parse;
mod preprocess;
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
pub mod preprocess;
pub mod book;
pub mod config;
pub mod renderer;
@@ -98,22 +117,50 @@ pub mod utils;
pub use book::MDBook;
pub use book::BookItem;
pub use renderer::Renderer;
pub use config::Config;
/// The error types used through out this crate.
pub mod errors {
use std::path::PathBuf;
error_chain!{
foreign_links {
Io(::std::io::Error);
HandlebarsRender(::handlebars::RenderError);
HandlebarsTemplate(::handlebars::TemplateError);
Utf8(::std::string::FromUtf8Error);
Io(::std::io::Error) #[doc = "A wrapper around `std::io::Error`"];
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
SerdeJson(::serde_json::Error) #[doc = "JSON conversion failed"];
}
links {
TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind) #[doc = "A TomlQuery error"];
}
errors {
/// A subprocess exited with an unsuccessful return code.
Subprocess(message: String, output: ::std::process::Output) {
description("A subprocess failed")
display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
}
/// An error was encountered while parsing the `SUMMARY.md` file.
ParseError(line: usize, col: usize, message: String) {
description("A SUMMARY.md parsing error")
display("Error at line {}, column {}: {}", line, col, message)
}
/// The user tried to use a reserved filename.
ReservedFilenameError(filename: PathBuf) {
description("Reserved Filename")
display("{} is reserved for internal use", filename.display())
}
}
}
// Box to halve the size of Error
impl From<::handlebars::TemplateError> for Error {
fn from(e: ::handlebars::TemplateError) -> Error {
From::from(Box::new(e))
}
}
}

View File

@@ -1,3 +0,0 @@
pub use self::summary::construct_bookitems;
pub mod summary;

View File

@@ -1,231 +0,0 @@
use std::path::PathBuf;
use std::fs::File;
use std::io::{Read, Result, Error, ErrorKind};
use book::bookitem::{BookItem, Chapter};
pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
debug!("[fn]: construct_bookitems");
let mut summary = String::new();
File::open(path)?.read_to_string(&mut summary)?;
debug!("[*]: Parse SUMMARY.md");
let top_items = parse_level(&mut summary.split('\n').collect(), 0, vec![0])?;
debug!("[*]: Done parsing SUMMARY.md");
Ok(top_items)
}
fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32>) -> Result<Vec<BookItem>> {
debug!("[fn]: parse_level");
let mut items: Vec<BookItem> = vec![];
// Construct the book recursively
while !summary.is_empty() {
let item: BookItem;
// Indentation level of the line to parse
let level = level(summary[0], 4)?;
// if level < current_level we remove the last digit of section,
// exit the current function,
// and return the parsed level to the calling function.
if level < current_level {
break;
}
// if level > current_level we call ourselves to go one level deeper
if level > current_level {
// Level can not be root level !!
// Add a sub-number to section
section.push(0);
let last = items
.pop()
.expect("There should be at least one item since this can't be the root level");
if let BookItem::Chapter(ref s, ref ch) = last {
let mut ch = ch.clone();
ch.sub_items = parse_level(summary, level, section.clone())?;
items.push(BookItem::Chapter(s.clone(), ch));
// Remove the last number from the section, because we got back to our level..
section.pop();
continue;
} else {
return Err(Error::new(ErrorKind::Other,
"Your summary.md is messed up\n\n
Prefix, \
Suffix and Spacer elements can only exist on the root level.\n
\
Prefix elements can only exist before any chapter and there can be \
no chapters after suffix elements."));
};
} else {
// level and current_level are the same, parse the line
item = if let Some(parsed_item) = parse_line(summary[0]) {
// Eliminate possible errors and set section to -1 after suffix
match parsed_item {
// error if level != 0 and BookItem is != Chapter
BookItem::Affix(_) |
BookItem::Spacer if level > 0 => {
return Err(Error::new(ErrorKind::Other,
"Your summary.md is messed up\n\n
\
Prefix, Suffix and Spacer elements can only exist on the \
root level.\n
Prefix \
elements can only exist before any chapter and there can be \
no chapters after suffix elements."))
},
// error if BookItem == Chapter and section == -1
BookItem::Chapter(_, _) if section[0] == -1 => {
return Err(Error::new(ErrorKind::Other,
"Your summary.md is messed up\n\n
\
Prefix, Suffix and Spacer elements can only exist on the \
root level.\n
Prefix \
elements can only exist before any chapter and there can be \
no chapters after suffix elements."))
},
// Set section = -1 after suffix
BookItem::Affix(_) if section[0] > 0 => {
section[0] = -1;
},
_ => {},
}
match parsed_item {
BookItem::Chapter(_, ch) => {
// Increment section
let len = section.len() - 1;
section[len] += 1;
let s = section
.iter()
.fold("".to_owned(), |s, i| s + &i.to_string() + ".");
BookItem::Chapter(s, ch)
},
_ => parsed_item,
}
} else {
// If parse_line does not return Some(_) continue...
summary.remove(0);
continue;
};
}
summary.remove(0);
items.push(item)
}
debug!("[*]: Level: {:?}", items);
Ok(items)
}
fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
debug!("[fn]: level");
let mut spaces = 0;
let mut level = 0;
for ch in line.chars() {
match ch {
' ' => spaces += 1,
'\t' => level += 1,
_ => break,
}
if spaces >= spaces_in_tab {
level += 1;
spaces = 0;
}
}
// If there are spaces left, there is an indentation error
if spaces > 0 {
debug!("[SUMMARY.md]:");
debug!("\t[line]: {}", line);
debug!("[*]: There is an indentation error on this line. Indentation should be {} spaces", spaces_in_tab);
return Err(Error::new(ErrorKind::Other, format!("Indentation error on line:\n\n{}", line)));
}
Ok(level)
}
fn parse_line(l: &str) -> Option<BookItem> {
debug!("[fn]: parse_line");
// Remove leading and trailing spaces or tabs
let line = l.trim_matches(|c: char| c == ' ' || c == '\t');
// Spacers are "------"
if line.starts_with("--") {
debug!("[*]: Line is spacer");
return Some(BookItem::Spacer);
}
if let Some(c) = line.chars().nth(0) {
match c {
// List item
'-' | '*' => {
debug!("[*]: Line is list element");
if let Some((name, path)) = read_link(line) {
return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path)));
} else {
return None;
}
},
// Non-list element
'[' => {
debug!("[*]: Line is a link element");
if let Some((name, path)) = read_link(line) {
return Some(BookItem::Affix(Chapter::new(name, path)));
} else {
return None;
}
},
_ => {},
}
}
None
}
fn read_link(line: &str) -> Option<(String, PathBuf)> {
let mut start_delimitor;
let mut end_delimitor;
// In the future, support for list item that is not a link
// Not sure if I should error on line I can't parse or just ignore them...
if let Some(i) = line.find('[') {
start_delimitor = i;
} else {
debug!("[*]: '[' not found, this line is not a link. Ignoring...");
return None;
}
if let Some(i) = line[start_delimitor..].find("](") {
end_delimitor = start_delimitor + i;
} else {
debug!("[*]: '](' not found, this line is not a link. Ignoring...");
return None;
}
let name = line[start_delimitor + 1..end_delimitor].to_owned();
start_delimitor = end_delimitor + 1;
if let Some(i) = line[start_delimitor..].find(')') {
end_delimitor = start_delimitor + i;
} else {
debug!("[*]: ')' not found, this line is not a link. Ignoring...");
return None;
}
let path = PathBuf::from(line[start_delimitor + 1..end_delimitor].to_owned());
Some((name, path))
}

View File

@@ -1,35 +1,129 @@
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
use std::path::{Path, PathBuf};
use regex::{CaptureMatches, Captures, Regex};
use utils::fs::file_to_string;
use utils::take_lines;
use errors::*;
use super::{Preprocessor, PreprocessorContext};
use book::{Book, BookItem};
const ESCAPE_CHAR: char = '\\';
pub fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> Result<String> {
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
/// helpers in a chapter.
pub struct LinkPreprocessor;
impl LinkPreprocessor {
/// Create a new `LinkPreprocessor`.
pub fn new() -> Self {
LinkPreprocessor
}
}
impl Preprocessor for LinkPreprocessor {
fn name(&self) -> &str {
"links"
}
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
let src_dir = ctx.root.join(&ctx.config.book.src);
book.for_each_mut(|section: &mut BookItem| {
if let BookItem::Chapter(ref mut ch) = *section {
let base = ch.path
.parent()
.map(|dir| src_dir.join(dir))
.expect("All book items have a parent");
let content = replace_all(&ch.content, base);
ch.content = content;
}
});
Ok(())
}
}
fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> String {
// When replacing one thing in a string by something with a different length,
// the indices after that will not correspond,
// we therefore have to store the difference to correct this
let path = path.as_ref();
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]);
replaced.push_str(&playpen.render_with_path(&path)?);
previous_end_index = playpen.end_index;
match playpen.render_with_path(&path) {
Ok(new_content) => {
replaced.push_str(&new_content);
previous_end_index = playpen.end_index;
}
Err(e) => {
error!("Error updating \"{}\", {}", playpen.link_text, e);
// This should make sure we include the raw `{{# ... }}` snippet
// in the page content if there are any errors.
previous_end_index = playpen.start_index;
}
}
}
replaced.push_str(&s[previous_end_index..]);
Ok(replaced)
replaced
}
#[derive(PartialOrd, PartialEq, Debug, Clone)]
#[derive(PartialEq, Debug, Clone)]
enum LinkType<'a> {
Escaped,
Include(PathBuf),
IncludeRange(PathBuf, Range<usize>),
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
IncludeRangeTo(PathBuf, RangeTo<usize>),
IncludeRangeFull(PathBuf, RangeFull),
Playpen(PathBuf, Vec<&'a str>),
}
#[derive(PartialOrd, PartialEq, Debug, Clone)]
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.checked_sub(1).unwrap_or(0));
let end = parts.next();
let has_end = end.is_some();
let end = end.and_then(|s| s.parse::<usize>().ok());
match start {
Some(start) => match end {
Some(end) => LinkType::IncludeRange(
path,
Range {
start: start,
end: end,
},
),
None => if has_end {
LinkType::IncludeRangeFrom(path, RangeFrom { start: start })
} else {
LinkType::IncludeRange(
path,
Range {
start: start,
end: start + 1,
},
)
},
},
None => match end {
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end: end }),
None => LinkType::IncludeRangeFull(path, RangeFull),
},
}
}
#[derive(PartialEq, Debug, Clone)]
struct Link<'a> {
start_index: usize,
end_index: usize,
@@ -39,33 +133,31 @@ struct Link<'a> {
impl<'a> Link<'a> {
fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
(_, Some(typ), Some(rest)) => {
let mut path_props = rest.as_str().split_whitespace();
let file_path = path_props.next().map(PathBuf::from);
let file_arg = path_props.next();
let props: Vec<&str> = path_props.collect();
match (typ.as_str(), file_path) {
("include", Some(pth)) => Some(LinkType::Include(pth)),
("playpen", Some(pth)) => Some(LinkType::Playpen(pth, props)),
match (typ.as_str(), file_arg) {
("include", Some(pth)) => Some(parse_include_path(pth)),
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
_ => None,
}
},
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => Some(LinkType::Escaped),
}
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
Some(LinkType::Escaped)
}
_ => None,
};
link_type.and_then(|lnk| {
cap.get(0)
.map(|mat| {
Link {
start_index: mat.start(),
end_index: mat.end(),
link: lnk,
link_text: mat.as_str(),
}
})
cap.get(0).map(|mat| Link {
start_index: mat.start(),
end_index: mat.end(),
link: lnk,
link_text: mat.as_str(),
})
})
}
@@ -74,15 +166,28 @@ impl<'a> Link<'a> {
match self.link {
// omit the escape char
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
LinkType::Include(ref pat) => {
file_to_string(base.join(pat)).chain_err(|| format!("Could not read file for link {}", self.link_text))
},
LinkType::IncludeRange(ref pat, ref range) => file_to_string(base.join(pat))
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(base.join(pat))
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::IncludeRangeTo(ref pat, ref range) => file_to_string(base.join(pat))
.map(|s| take_lines(&s, range.clone()))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::IncludeRangeFull(ref pat, _) => file_to_string(base.join(pat))
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
LinkType::Playpen(ref pat, ref attrs) => {
let contents = file_to_string(base.join(pat))
.chain_err(|| format!("Could not read file for link {}", self.link_text))?;
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
Ok(format!("```{}{}\n{}\n```\n", ftype, attrs.join(","), contents))
},
Ok(format!(
"```{}{}\n{}\n```\n",
ftype,
attrs.join(","),
contents
))
}
}
}
}
@@ -118,123 +223,260 @@ fn find_links(contents: &str) -> LinkIter {
LinkIter(RE.captures_iter(contents))
}
// ---------------------------------------------------------------------------------
// Tests
//
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_links_no_link() {
let s = "Some random text without link...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}
#[test]
fn test_find_links_partial_link() {
let s = "Some random text with {{#playpen...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
let s = "Some random text with {{#include...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
let s = "Some random text with \\{{#include...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}
#[test]
fn test_find_links_empty_link() {
let s = "Some random text with {{#playpen}} and {{#playpen }} {{}} {{#}}...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}
#[test]
fn test_find_links_unknown_link_type() {
let s = "Some random text with {{#playpenz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}
#[test]
fn test_find_links_simple_link() {
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 42,
link: 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_text: "{{#playpen test.rs }}",
},
]
);
}
#[test]
fn test_find_links_with_range() {
let s = "Some random text with {{#include file.rs:10:20}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 48,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
link_text: "{{#include file.rs:10:20}}",
},
]
);
}
#[test]
fn test_find_links_with_line_number() {
let s = "Some random text with {{#include file.rs:10}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 45,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
link_text: "{{#include file.rs:10}}",
},
]
);
}
#[test]
fn test_find_links_with_from_range() {
let s = "Some random text with {{#include file.rs:10:}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
link_text: "{{#include file.rs:10:}}",
},
]
);
}
#[test]
fn test_find_links_with_to_range() {
let s = "Some random text with {{#include file.rs::20}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
link_text: "{{#include file.rs::20}}",
},
]
);
}
#[test]
fn test_find_links_with_full_range() {
let s = "Some random text with {{#include file.rs::}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 44,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs::}}",
},
]
);
}
#[test]
fn test_find_links_with_no_range_specified() {
let s = "Some random text with {{#include file.rs}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 22,
end_index: 42,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs}}",
},
]
);
}
#[test]
fn test_find_links_escaped_link() {
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 38,
end_index: 68,
link: LinkType::Escaped,
link_text: "\\{{#playpen file.rs editable}}",
},
]
);
}
#[test]
fn test_find_playpens_with_properties() {
let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some \
more\n text {{#playpen my.rs editable no_run should_panic}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![
Link {
start_index: 38,
end_index: 68,
link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
link_text: "{{#playpen file.rs editable }}",
},
Link {
start_index: 89,
end_index: 136,
link: LinkType::Playpen(
PathBuf::from("my.rs"),
vec!["editable", "no_run", "should_panic"],
),
link_text: "{{#playpen my.rs editable no_run should_panic}}",
},
]
);
}
#[test]
fn test_find_all_link_types() {
let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are \
insignifficant in escaped link}} some more\n text {{#playpen my.rs editable \
no_run should_panic}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(res.len(), 3);
assert_eq!(
res[0],
Link {
start_index: 38,
end_index: 58,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs}}",
}
);
assert_eq!(
res[1],
Link {
start_index: 63,
end_index: 112,
link: LinkType::Escaped,
link_text: "\\{{#contents are insignifficant in escaped link}}",
}
);
assert_eq!(
res[2],
Link {
start_index: 130,
end_index: 177,
link: LinkType::Playpen(
PathBuf::from("my.rs"),
vec!["editable", "no_run", "should_panic"]
),
link_text: "{{#playpen my.rs editable no_run should_panic}}",
}
);
}
#[test]
fn test_find_links_no_link() {
let s = "Some random text without link...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}
#[test]
fn test_find_links_partial_link() {
let s = "Some random text with {{#playpen...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
let s = "Some random text with {{#include...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
let s = "Some random text with \\{{#include...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}
#[test]
fn test_find_links_empty_link() {
let s = "Some random text with {{#playpen}} and {{#playpen }} {{}} {{#}}...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}
#[test]
fn test_find_links_unknown_link_type() {
let s = "Some random text with {{#playpenz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}
#[test]
fn test_find_links_simple_link() {
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(res,
vec![Link {
start_index: 22,
end_index: 42,
link: 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_text: "{{#playpen test.rs }}",
}]);
}
#[test]
fn test_find_links_escaped_link() {
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(res,
vec![Link {
start_index: 38,
end_index: 68,
link: LinkType::Escaped,
link_text: "\\{{#playpen file.rs editable}}",
}]);
}
#[test]
fn test_find_playpens_with_properties() {
let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some more\n text {{#playpen my.rs editable no_run should_panic}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(res,
vec![Link {
start_index: 38,
end_index: 68,
link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
link_text: "{{#playpen file.rs editable }}",
},
Link {
start_index: 90,
end_index: 137,
link: LinkType::Playpen(PathBuf::from("my.rs"), vec!["editable", "no_run", "should_panic"]),
link_text: "{{#playpen my.rs editable no_run should_panic}}",
}]);
}
#[test]
fn test_find_all_link_types() {
let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are insignifficant in escaped link}} some more\n text {{#playpen my.rs editable no_run should_panic}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(res.len(), 3);
assert_eq!(res[0],
Link {
start_index: 38,
end_index: 58,
link: LinkType::Include(PathBuf::from("file.rs")),
link_text: "{{#include file.rs}}",
});
assert_eq!(res[1],
Link {
start_index: 63,
end_index: 112,
link: LinkType::Escaped,
link_text: "\\{{#contents are insignifficant in escaped link}}",
});
assert_eq!(res[2],
Link {
start_index: 130,
end_index: 177,
link: LinkType::Playpen(PathBuf::from("my.rs"), vec!["editable", "no_run", "should_panic"]),
link_text: "{{#playpen my.rs editable no_run should_panic}}",
});
}

View File

@@ -1 +1,38 @@
pub mod links;
//! Book preprocessing.
pub use self::links::LinkPreprocessor;
mod links;
use book::Book;
use config::Config;
use errors::*;
use std::path::PathBuf;
/// Extra information for a `Preprocessor` to give them more context when
/// processing a book.
pub struct PreprocessorContext {
/// The location of the book directory on disk.
pub root: PathBuf,
/// The book configuration (`book.toml`).
pub config: Config,
}
impl PreprocessorContext {
/// Create a new `PreprocessorContext`.
pub(crate) fn new(root: PathBuf, config: Config) -> Self {
PreprocessorContext { root, config }
}
}
/// An operation which is run immediately after loading a book into memory and
/// before it gets rendered.
pub trait Preprocessor {
/// Get the `Preprocessor`'s name.
fn name(&self) -> &str;
/// Run this `Preprocessor`, allowing it to update the book before it is
/// given to a renderer.
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
}

View File

@@ -1,23 +1,19 @@
use renderer::html_handlebars::helpers;
use preprocess;
use renderer::Renderer;
use book::MDBook;
use book::bookitem::{BookItem, Chapter};
use config::PlaypenConfig;
use {utils, theme};
use theme::{Theme, playpen_editor};
use book::{Book, BookItem, Chapter};
use config::{Config, HtmlConfig, Playpen};
use errors::*;
use regex::{Regex, Captures};
use renderer::{RenderContext, Renderer};
use renderer::html_handlebars::helpers;
use theme::{self, Theme, playpen_editor};
use utils;
use std::ascii::AsciiExt;
use std::path::{Path, PathBuf};
use std::fs::{self, File};
use std::io::{self, Read};
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use handlebars::Handlebars;
use regex::{Captures, Regex};
use serde_json;
#[derive(Default)]
@@ -28,73 +24,78 @@ impl HtmlHandlebars {
HtmlHandlebars
}
fn render_item(&self, item: &BookItem, mut ctx: RenderItemContext, print_content: &mut String)
-> Result<()> {
fn render_item(
&self,
item: &BookItem,
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) |
BookItem::Affix(ref ch) if !ch.path.as_os_str().is_empty() => {
let path = ctx.book.get_source().join(&ch.path);
let content = utils::fs::file_to_string(&path)?;
let base = path.parent().ok_or_else(
|| String::from("Invalid bookitem path!"),
)?;
// Parse and expand links
let content = preprocess::links::replace_all(&content, base)?;
let content = utils::render_markdown(&content, ctx.book.get_curly_quotes());
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);
// Update the context with data for this file
let path = ch.path.to_str().ok_or_else(|| {
io::Error::new(io::ErrorKind::Other, "Could not convert path to str")
})?;
let path = ch.path
.to_str()
.chain_err(|| "Could not convert path to str")?;
let filepath = Path::new(&ch.path)
.with_extension("html");
let filepathstr = filepath.to_str()
.chain_err(|| "Could not convert HTML path to str")?;
let filepathstr = utils::fs::normalize_path(filepathstr);
// Non-lexical lifetimes needed :'(
// "print.html" is used for the print page.
if ch.path == Path::new("print.md") {
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
};
// Non-lexical lifetimes needed :'(
let title: String;
{
let book_title = ctx.data.get("book_title").and_then(serde_json::Value::as_str).unwrap_or("");
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)),
);
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");
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
let filepath = Path::new(&ch.path).with_extension("html");
let rendered = self.post_process(rendered,
&normalize_path(filepath.to_str()
.ok_or(Error::from(format!("Bad file name: {}", filepath.display())))?),
ctx.book.get_html_config().get_playpen_config()
let rendered = self.post_process(
rendered,
&filepathstr,
&ctx.html_config.playpen,
);
// Write to file
info!("[*] Creating {:?} ✓", filepath.display());
ctx.book.write_file(filepath, &rendered.into_bytes())?;
debug!("Creating {} ✓", filepathstr);
utils::fs::write_file(&ctx.destination, &filepath, &rendered.into_bytes())?;
if ctx.is_index {
self.render_index(ctx.book, ch, &ctx.destination)?;
self.render_index(ch, &ctx.destination)?;
}
},
_ => {},
}
_ => {}
}
Ok(())
}
/// Create an index.html from the first element in SUMMARY.md
fn render_index(&self, book: &MDBook, ch: &Chapter, destination: &Path) -> Result<()> {
debug!("[*]: index.html");
fn render_index(&self, ch: &Chapter, destination: &Path) -> Result<()> {
debug!("index.html");
let mut content = String::new();
@@ -104,289 +105,317 @@ impl HtmlHandlebars {
// This could cause a problem when someone displays
// code containing <base href=...>
// on the front page, however this case should be very very rare...
content = content
.lines()
.filter(|line| !line.contains("<base href="))
.collect::<Vec<&str>>()
.join("\n");
content = content.lines()
.filter(|line| !line.contains("<base href="))
.collect::<Vec<&str>>()
.join("\n");
book.write_file("index.html", content.as_bytes())?;
utils::fs::write_file(destination, "index.html", content.as_bytes())?;
info!(
"[*] Creating index.html from {:?} ✓",
book.get_destination()
.join(&ch.path.with_extension("html"))
debug!(
"Creating index.html from {} ✓",
destination.join(&ch.path.with_extension("html")).display()
);
Ok(())
}
fn post_process(&self, rendered: String, filepath: &str, playpen_config: &PlaypenConfig) -> String {
let rendered = build_header_links(&rendered, &filepath);
let rendered = fix_anchor_links(&rendered, &filepath);
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
fn post_process(&self,
rendered: String,
filepath: &str,
playpen_config: &Playpen)
-> String {
let rendered = build_header_links(&rendered, filepath);
let rendered = fix_anchor_links(&rendered, filepath);
let rendered = fix_code_blocks(&rendered);
let rendered = add_playpen_pre(&rendered, playpen_config);
rendered
}
fn copy_static_files(&self, book: &MDBook, theme: &Theme) -> Result<()> {
book.write_file("book.js", &theme.js)?;
book.write_file("book.css", &theme.css)?;
book.write_file("favicon.png", &theme.favicon)?;
book.write_file("jquery.js", &theme.jquery)?;
book.write_file("highlight.css", &theme.highlight_css)?;
book.write_file(
"tomorrow-night.css",
&theme.tomorrow_night_css,
)?;
book.write_file(
"ayu-highlight.css",
&theme.ayu_highlight_css,
)?;
book.write_file("highlight.js", &theme.highlight_js)?;
book.write_file("clipboard.min.js", &theme.clipboard_js)?;
book.write_file("store.js", &theme.store_js)?;
book.write_file(
fn copy_static_files(
&self,
destination: &Path,
theme: &Theme,
html_config: &HtmlConfig,
) -> Result<()> {
use utils::fs::write_file;
write_file(destination, "book.js", &theme.js)?;
write_file(destination, "book.css", &theme.css)?;
write_file(destination, "favicon.png", &theme.favicon)?;
write_file(destination, "highlight.css", &theme.highlight_css)?;
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
write_file(destination, "highlight.js", &theme.highlight_js)?;
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
write_file(
destination,
"_FontAwesome/css/font-awesome.css",
theme::FONT_AWESOME,
)?;
book.write_file(
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.eot",
theme::FONT_AWESOME_EOT,
)?;
book.write_file(
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.svg",
theme::FONT_AWESOME_SVG,
)?;
book.write_file(
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.ttf",
theme::FONT_AWESOME_TTF,
)?;
book.write_file(
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.woff",
theme::FONT_AWESOME_WOFF,
)?;
book.write_file(
write_file(
destination,
"_FontAwesome/fonts/fontawesome-webfont.woff2",
theme::FONT_AWESOME_WOFF2,
)?;
book.write_file(
write_file(
destination,
"_FontAwesome/fonts/FontAwesome.ttf",
theme::FONT_AWESOME_TTF,
)?;
let playpen_config = book.get_html_config().get_playpen_config();
let playpen_config = &html_config.playpen;
// Ace is a very large dependency, so only load it when requested
if playpen_config.is_editable() {
if playpen_config.editable && playpen_config.copy_js {
// Load the editor
let editor = playpen_editor::PlaypenEditor::new(playpen_config.get_editor());
book.write_file("editor.js", &editor.js)?;
book.write_file("ace.js", &editor.ace_js)?;
book.write_file("mode-rust.js", &editor.mode_rust_js)?;
book.write_file("theme-dawn.js", &editor.theme_dawn_js)?;
book.write_file("theme-tomorrow_night.js", &editor.theme_tomorrow_night_js)?;
write_file(destination, "editor.js", playpen_editor::JS)?;
write_file(destination, "ace.js", playpen_editor::ACE_JS)?;
write_file(destination, "mode-rust.js", playpen_editor::MODE_RUST_JS)?;
write_file(destination, "theme-dawn.js", playpen_editor::THEME_DAWN_JS)?;
write_file(destination,
"theme-tomorrow_night.js",
playpen_editor::THEME_TOMORROW_NIGHT_JS,
)?;
}
Ok(())
}
/// Helper function to write a file to the build directory, normalizing
/// the path to be relative to the book root.
fn write_custom_file(&self, custom_file: &Path, book: &MDBook) -> Result<()> {
let mut data = Vec::new();
let mut f = File::open(custom_file)?;
f.read_to_end(&mut data)?;
let name = match custom_file.strip_prefix(book.get_root()) {
Ok(p) => p.to_str().expect("Could not convert to str"),
Err(_) => {
custom_file
.file_name()
.expect("File has a file name")
.to_str()
.expect("Could not convert to str")
},
};
book.write_file(name, &data)?;
Ok(())
}
/// Update the context with data for this file
fn configure_print_version(&self, data: &mut serde_json::Map<String, serde_json::Value>, print_content: &str) {
fn configure_print_version(&self,
data: &mut serde_json::Map<String, serde_json::Value>,
print_content: &str) {
// Make sure that the Print chapter does not display the title from
// the last rendered chapter by removing it from its context
data.remove("title");
data.insert("is_print".to_owned(), json!(true));
data.insert("path".to_owned(), json!("print.md"));
data.insert("content".to_owned(), json!(print_content));
data.insert("path_to_root".to_owned(), json!(utils::fs::path_to_root(Path::new("print.md"))));
data.insert("path_to_root".to_owned(),
json!(utils::fs::path_to_root(Path::new("print.md"))));
}
fn register_hbs_helpers(&self, handlebars: &mut Handlebars) {
handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc));
fn register_hbs_helpers(&self, handlebars: &mut Handlebars, html_config: &HtmlConfig) {
handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc {no_section_label: html_config.no_section_label}));
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
handlebars.register_helper("next", Box::new(helpers::navigation::next));
}
/// Copy across any additional CSS and JavaScript files which the book
/// has been configured to use.
fn copy_additional_css_and_js(&self, book: &MDBook) -> Result<()> {
let custom_files = book.get_additional_css().iter().chain(
book.get_additional_js()
.iter(),
);
fn copy_additional_css_and_js(&self, html: &HtmlConfig, root: &Path, destination: &Path) -> Result<()> {
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
debug!("Copying additional CSS and JS");
for custom_file in custom_files {
self.write_custom_file(custom_file, book)?;
let input_location = root.join(custom_file);
let output_location = destination.join(custom_file);
if let Some(parent) = output_location.parent() {
fs::create_dir_all(parent)
.chain_err(|| format!("Unable to create {}", parent.display()))?;
}
debug!(
"Copying {} -> {}",
input_location.display(),
output_location.display()
);
fs::copy(&input_location, &output_location).chain_err(|| {
format!(
"Unable to copy {} to {}",
input_location.display(),
output_location.display()
)
})?;
}
Ok(())
}
}
impl Renderer for HtmlHandlebars {
fn render(&self, book: &MDBook) -> Result<()> {
debug!("[fn]: render");
fn name(&self) -> &str {
"html"
}
fn render(&self, ctx: &RenderContext) -> Result<()> {
let html_config = ctx.config.html_config().unwrap_or_default();
let src_dir = ctx.root.join(&ctx.config.book.src);
let destination = &ctx.destination;
let book = &ctx.book;
trace!("render");
let mut handlebars = Handlebars::new();
let theme = theme::Theme::new(book.get_theme_path());
let theme_dir = match html_config.theme {
Some(ref theme) => theme.to_path_buf(),
None => src_dir.join("theme"),
};
debug!("[*]: Register handlebars template");
handlebars.register_template_string(
"index",
String::from_utf8(theme.index.clone())?,
)?;
let theme = theme::Theme::new(theme_dir);
debug!("[*]: Register handlebars helpers");
self.register_hbs_helpers(&mut handlebars);
debug!("Register the index handlebars template");
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
let mut data = make_data(book)?;
debug!("Register the header handlebars template");
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
debug!("Register handlebars helpers");
self.register_hbs_helpers(&mut handlebars, &html_config);
let mut data = make_data(&ctx.root, &book, &ctx.config, &html_config)?;
// Print version
let mut print_content = String::new();
let destination = book.get_destination();
fs::create_dir_all(&destination)
.chain_err(|| "Unexpected error when constructing destination path")?;
debug!("[*]: Check if destination directory exists");
if fs::create_dir_all(&destination).is_err() {
bail!("Unexpected error when constructing destination path");
}
for (i, item) in book.iter().enumerate() {
let mut is_index = true;
for item in book.iter() {
let ctx = RenderItemContext {
book: book,
handlebars: &handlebars,
destination: destination.to_path_buf(),
data: data.clone(),
is_index: i == 0,
is_index: is_index,
html_config: html_config.clone(),
};
self.render_item(item, ctx, &mut print_content)?;
self.render_item(item,
ctx,
&mut print_content)?;
is_index = false;
}
// Print version
self.configure_print_version(&mut data, &print_content);
data.insert("title".to_owned(), json!(book.get_title()));
if let Some(ref title) = ctx.config.book.title {
data.insert("title".to_owned(), json!(title));
}
// Render the handlebars template with the data
debug!("[*]: Render template");
debug!("Render template");
let rendered = handlebars.render("index", &data)?;
let rendered = self.post_process(rendered, "print.html",
book.get_html_config().get_playpen_config());
let rendered = self.post_process(rendered,
"print.html",
&html_config.playpen);
book.write_file(
Path::new("print").with_extension("html"),
&rendered.into_bytes(),
)?;
info!("[*] Creating print.html ✓");
utils::fs::write_file(&destination, "print.html", &rendered.into_bytes())?;
debug!("Creating print.html");
// Copy static files (js, css, images, ...)
debug!("[*] Copy static files");
self.copy_static_files(book, &theme)?;
self.copy_additional_css_and_js(book)?;
debug!("Copy static files");
self.copy_static_files(&destination, &theme, &html_config)
.chain_err(|| "Unable to copy across static files")?;
self.copy_additional_css_and_js(&html_config, &ctx.root, &destination)
.chain_err(|| "Unable to copy across additional CSS and JS")?;
// Render search index
#[cfg(feature = "search")]
super::search::create_files(&html_config.search.unwrap_or_default(), &destination, &book)?;
// Copy all remaining files
utils::fs::copy_files_except_ext(book.get_source(), destination, true, &["md"])?;
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
Ok(())
}
}
fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>> {
debug!("[fn]: make_data");
fn make_data(root: &Path, book: &Book, config: &Config, 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("book_title".to_owned(), json!(book.get_title()));
data.insert("description".to_owned(), json!(book.get_description()));
data.insert("book_title".to_owned(), json!(config.book.title.clone().unwrap_or_default()));
data.insert("description".to_owned(), json!(config.book.description.clone().unwrap_or_default()));
data.insert("favicon".to_owned(), json!("favicon.png"));
if let Some(livereload) = book.get_livereload() {
if let Some(ref livereload) = html_config.livereload_url {
data.insert("livereload".to_owned(), json!(livereload));
}
// Add google analytics tag
if let Some(ref ga) = book.get_google_analytics_id() {
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
data.insert("google_analytics".to_owned(), json!(ga));
}
if book.get_mathjax_support() {
if html.mathjax_support {
data.insert("mathjax_support".to_owned(), json!(true));
}
// Add check to see if there is an additional style
if book.has_additional_css() {
if !html.additional_css.is_empty() {
let mut css = Vec::new();
for style in book.get_additional_css() {
match style.strip_prefix(book.get_root()) {
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
Err(_) => {
css.push(
style
.file_name()
.expect("File has a file name")
.to_str()
.expect("Could not convert to str"),
)
for style in &html.additional_css {
match style.strip_prefix(root) {
Ok(p) => {
css.push(p.to_str().expect("Could not convert to str"))
},
Err(_) => {
css.push(style.to_str()
.expect("Could not convert to str"))
}
}
}
data.insert("additional_css".to_owned(), json!(css));
}
// Add check to see if there is an additional script
if book.has_additional_js() {
if !html.additional_js.is_empty() {
let mut js = Vec::new();
for script in book.get_additional_js() {
match script.strip_prefix(book.get_root()) {
for script in &html.additional_js {
match script.strip_prefix(root) {
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
Err(_) => {
js.push(
script
.file_name()
.expect("File has a file name")
.to_str()
.expect("Could not convert to str"),
)
},
js.push(script.file_name()
.expect("File has a file name")
.to_str()
.expect("Could not convert to str"))
}
}
}
data.insert("additional_js".to_owned(), json!(js));
}
if book.get_html_config().get_playpen_config().is_editable() {
data.insert("playpens_editable".to_owned(), json!(true));
data.insert("editor_js".to_owned(), json!("editor.js"));
data.insert("ace_js".to_owned(), json!("ace.js"));
data.insert("mode_rust_js".to_owned(), json!("mode-rust.js"));
data.insert("theme_dawn_js".to_owned(), json!("theme-dawn.js"));
data.insert("theme_tomorrow_night_js".to_owned(), json!("theme-tomorrow_night.js"));
if html.playpen.editable && html.playpen.copy_js {
data.insert("playpen_js".to_owned(), json!(true));
}
let search = html_config.search.clone();
if cfg!(feature = "search") {
data.insert("search_enabled".to_owned(), json!(true));
if search.unwrap_or_default().copy_js {
data.insert("search_js".to_owned(), json!(true));
}
} else if search.is_some() {
warn!("mdBook compiled without search support, ignoring `output.html.search` table");
warn!("please reinstall with `cargo install mdbook --force --features search`\
to use the search feature")
}
let mut chapters = vec![];
for item in book.iter() {
@@ -394,25 +423,20 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
let mut chapter = BTreeMap::new();
match *item {
BookItem::Affix(ref ch) => {
chapter.insert("name".to_owned(), json!(ch.name));
let path = ch.path.to_str().ok_or_else(|| {
io::Error::new(io::ErrorKind::Other, "Could not convert path to str")
})?;
chapter.insert("path".to_owned(), json!(path));
},
BookItem::Chapter(ref s, ref ch) => {
chapter.insert("section".to_owned(), json!(s));
chapter.insert("name".to_owned(), json!(ch.name));
let path = ch.path.to_str().ok_or_else(|| {
io::Error::new(io::ErrorKind::Other, "Could not convert path to str")
})?;
chapter.insert("path".to_owned(), json!(path));
},
BookItem::Spacer => {
chapter.insert("spacer".to_owned(), json!("_spacer_"));
},
BookItem::Chapter(ref ch) => {
if let Some(ref section) = ch.number {
chapter.insert("section".to_owned(), json!(section.to_string()));
}
chapter.insert("name".to_owned(), json!(ch.name));
let path = ch.path
.to_str()
.chain_err(|| "Could not convert path to str")?;
chapter.insert("path".to_owned(), json!(path));
}
BookItem::Separator => {
chapter.insert("spacer".to_owned(), json!("_spacer_"));
}
}
chapters.push(chapter);
@@ -430,22 +454,23 @@ fn build_header_links(html: &str, filepath: &str) -> String {
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
let mut id_counter = HashMap::new();
regex
.replace_all(html, |caps: &Captures| {
let level = caps[1].parse().expect(
"Regex should ensure we only ever get numbers here",
);
regex.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, filepath)
})
.into_owned()
wrap_header_with_link(level, &caps[2], &mut id_counter, filepath)
})
.into_owned()
}
/// Wraps a single header tag with a link, making sure each tag gets its own
/// unique ID by appending an auto-incremented number (if necessary).
fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap<String, usize>, filepath: &str)
-> String {
let raw_id = id_from_content(content);
fn wrap_header_with_link(level: usize,
content: &str,
id_counter: &mut HashMap<String, usize>,
filepath: &str)
-> String {
let raw_id = utils::id_from_content(content);
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
@@ -465,55 +490,23 @@ fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap<S
)
}
/// Generate an id for use with anchors which is derived from a "normalised"
/// string.
fn id_from_content(content: &str) -> String {
let mut content = content.to_string();
// Skip any tags or html-encoded stuff
const REPL_SUB: &[&str] = &[
"<em>",
"</em>",
"<code>",
"</code>",
"<strong>",
"</strong>",
"&lt;",
"&gt;",
"&amp;",
"&#39;",
"&quot;",
];
for sub in REPL_SUB {
content = content.replace(sub, "");
}
// Remove spaces and hastags indicating a header
let trimmed = content.trim().trim_left_matches("#").trim();
normalize_id(trimmed)
}
// anchors to the same page (href="#anchor") do not work because of
// <base href="../"> pointing to the root folder. This function *fixes*
// that in a very inelegant way
fn fix_anchor_links(html: &str, filepath: &str) -> String {
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
regex
.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let anchor = &caps[2];
let after = &caps[3];
regex.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let anchor = &caps[2];
let after = &caps[3];
format!(
"<a{before}href=\"{filepath}#{anchor}\"{after}>",
format!("<a{before}href=\"{filepath}#{anchor}\"{after}>",
before = before,
filepath = filepath,
anchor = anchor,
after = after
)
})
.into_owned()
after = after)
})
.into_owned()
}
@@ -527,46 +520,51 @@ fn fix_anchor_links(html: &str, filepath: &str) -> String {
// This function replaces all commas by spaces in the code block classes
fn fix_code_blocks(html: &str) -> String {
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
regex
.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let classes = &caps[2].replace(",", " ");
let after = &caps[3];
regex.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let classes = &caps[2].replace(",", " ");
let after = &caps[3];
format!(r#"<code{before}class="{classes}"{after}>"#, before = before, classes = classes, after = after)
})
.into_owned()
format!(r#"<code{before}class="{classes}"{after}>"#,
before = before,
classes = classes,
after = after)
}).into_owned()
}
fn add_playpen_pre(html: &str, playpen_config: &PlaypenConfig) -> String {
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
regex
.replace_all(html, |caps: &Captures| {
let text = &caps[1];
let classes = &caps[2];
let code = &caps[3];
regex.replace_all(html, |caps: &Captures| {
let text = &caps[1];
let classes = &caps[2];
let code = &caps[3];
if classes.contains("language-rust") && !classes.contains("ignore") {
// wrap the contents in an external pre block
if playpen_config.is_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=\"{}\">\n# #![allow(unused_variables)]\n\
{}#fn main() {{\n\
{}\
#}}</code></pre>",
classes, attrs, code)
}
if (classes.contains("language-rust") && !classes.contains("ignore")) ||
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 {
// not language-rust, so no-op
text.to_owned()
// 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)
}
})
.into_owned()
} else {
// not language-rust, so no-op
text.to_owned()
}
}).into_owned()
}
fn partition_source(s: &str) -> (String, String) {
@@ -590,37 +588,14 @@ fn partition_source(s: &str) -> (String, String) {
(before, after)
}
struct RenderItemContext<'a> {
handlebars: &'a Handlebars,
book: &'a MDBook,
destination: PathBuf,
data: serde_json::Map<String, serde_json::Value>,
is_index: bool,
html_config: HtmlConfig,
}
pub fn normalize_path(path: &str) -> String {
use std::path::is_separator;
path.chars()
.map(|ch| if is_separator(ch) { '/' } else { ch })
.collect::<String>()
}
pub fn normalize_id(content: &str) -> String {
content.chars()
.filter_map(|ch|
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
Some(ch.to_ascii_lowercase())
} else if ch.is_whitespace() {
Some('-')
} else {
None
}
)
.collect::<String>()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -642,15 +617,15 @@ mod tests {
),
(
"<h4></h4>",
r##"<a class="header" href="./some_chapter/some_section.html#" id=""><h4></h4></a>"##
r##"<a class="header" href="./some_chapter/some_section.html#" id=""><h4></h4></a>"##,
),
(
"<h4><em>Hï</em></h4>",
r##"<a class="header" href="./some_chapter/some_section.html#hï" id="hï"><h4><em>Hï</em></h4></a>"##
r##"<a class="header" href="./some_chapter/some_section.html#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
),
(
"<h1>Foo</h1><h3>Foo</h3>",
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a><a class="header" href="./some_chapter/some_section.html#foo-1" id="foo-1"><h3>Foo</h3></a>"##
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a><a class="header" href="./some_chapter/some_section.html#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
),
];
@@ -664,10 +639,4 @@ mod tests {
assert_eq!(got, should_be);
}
}
#[test]
fn anchor_generation() {
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"), "--passes-add-more-rustdoc-passes");
assert_eq!(id_from_content("## Method-call expressions"), "method-call-expressions");
}
}

View File

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

View File

@@ -2,158 +2,236 @@ use std::path::Path;
use std::collections::BTreeMap;
use serde_json;
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable, Context};
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
type StringMap = BTreeMap<String, String>;
// Handlebars helper for navigation
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
debug!("[fn]: previous (handlebars helper)");
debug!("[*]: Get data from context");
let chapters = rc.evaluate_absolute("chapters")
.and_then(|c| {
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let current = rc.evaluate_absolute("path")?
.as_str().ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
let mut previous: Option<BTreeMap<String, String>> = None;
debug!("[*]: Search for current Chapter");
// Search for current chapter and return previous entry
for item in chapters {
match item.get("path") {
Some(path) if !path.is_empty() => {
if path == &current {
debug!("[*]: Found current chapter");
if let Some(previous) = previous {
debug!("[*]: Creating BTreeMap to inject in context");
// Create new BTreeMap to extend the context: 'title' and 'link'
let mut previous_chapter = BTreeMap::new();
// Chapter title
previous
.get("name").ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
.and_then(|n| {
previous_chapter.insert("title".to_owned(), json!(n));
Ok(())
})?;
// Chapter link
previous
.get("path").ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
.and_then(|p| {
Path::new(p)
.with_extension("html")
.to_str().ok_or_else(|| RenderError::new("Link could not be converted to str"))
.and_then(|p| {
previous_chapter
.insert("link".to_owned(), json!(p.replace("\\", "/")));
Ok(())
})
})?;
debug!("[*]: Render template");
// Render template
_h.template().ok_or_else(|| RenderError::new("Error with the handlebars template"))
.and_then(|t| {
let mut local_rc = rc.with_context(Context::wraps(&previous_chapter));
t.render(r, &mut local_rc)
})?;
}
break;
} else {
previous = Some(item.clone());
}
},
_ => continue,
}
}
Ok(())
/// Target for `find_chapter`.
enum Target {
Previous,
Next,
}
impl Target {
/// Returns target if found.
fn find(
&self,
base_path: &String,
current_path: &String,
current_item: &StringMap,
previous_item: &StringMap,
) -> Result<Option<StringMap>, RenderError> {
match self {
&Target::Next => {
let previous_path = previous_item
.get("path")
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
if previous_path == base_path {
return Ok(Some(current_item.clone()));
}
}
&Target::Previous => {
if current_path == base_path {
return Ok(Some(previous_item.clone()));
}
}
}
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
debug!("[fn]: next (handlebars helper)");
Ok(None)
}
}
debug!("[*]: Get data from context");
let chapters = rc.evaluate_absolute("chapters")
.and_then(|c| {
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let current = rc.evaluate_absolute("path")?
.as_str().ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
fn find_chapter(rc: &mut RenderContext, target: Target) -> Result<Option<StringMap>, RenderError> {
debug!("Get data from context");
let chapters = rc.evaluate_absolute("chapters", true).and_then(|c| {
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let base_path = rc.evaluate_absolute("path", true)?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
let mut previous: Option<BTreeMap<String, String>> = None;
let mut previous: Option<StringMap> = None;
debug!("Search for chapter");
debug!("[*]: Search for current Chapter");
// Search for current chapter and return previous entry
for item in chapters {
match item.get("path") {
Some(path) if !path.is_empty() => {
if let Some(previous) = previous {
let previous_path = previous
.get("path").ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
if previous_path == &current {
debug!("[*]: Found current chapter");
debug!("[*]: Creating BTreeMap to inject in context");
// Create new BTreeMap to extend the context: 'title' and 'link'
let mut next_chapter = BTreeMap::new();
item.get("name").ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
.and_then(|n| {
next_chapter.insert("title".to_owned(), json!(n));
Ok(())
})?;
Path::new(path)
.with_extension("html")
.to_str().ok_or_else(|| RenderError::new("Link could not converted to str"))
.and_then(|l| {
debug!("[*]: Inserting link: {:?}", l);
// Hack for windows who tends to use `\` as separator instead of `/`
next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/")));
Ok(())
})?;
debug!("[*]: Render template");
// Render template
_h.template().ok_or_else(|| RenderError::new("Error with the handlebars template"))
.and_then(|t| {
let mut local_rc = rc.with_context(Context::wraps(&next_chapter));
t.render(r, &mut local_rc)
})?;
break;
if let Some(item) = target.find(&base_path, &path, &item, &previous)? {
return Ok(Some(item));
}
}
previous = Some(item.clone());
},
}
_ => continue,
}
}
Ok(None)
}
fn render(
_h: &Helper,
r: &Handlebars,
rc: &mut RenderContext,
chapter: &StringMap,
) -> Result<(), RenderError> {
trace!("Creating BTreeMap to inject in context");
let mut context = BTreeMap::new();
chapter
.get("name")
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
.map(|name| context.insert("title".to_owned(), json!(name)))?;
chapter
.get("path")
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
.and_then(|p| {
Path::new(p)
.with_extension("html")
.to_str()
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
})?;
trace!("Render template");
_h.template()
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
.and_then(|t| {
let mut local_rc = rc.with_context(Context::wraps(&context)?);
t.render(r, &mut local_rc)
})?;
Ok(())
}
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
trace!("previous (handlebars helper)");
if let Some(previous) = find_chapter(rc, Target::Previous)? {
render(_h, r, rc, &previous)?;
}
Ok(())
}
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
trace!("next (handlebars helper)");
if let Some(next) = find_chapter(rc, Target::Next)? {
render(_h, r, rc, &next)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
static TEMPLATE: &'static str =
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
#[test]
fn test_next_previous() {
let data = json!({
"name": "two",
"path": "two.path",
"chapters": [
{
"name": "one",
"path": "one.path"
},
{
"name": "two",
"path": "two.path",
},
{
"name": "three",
"path": "three.path"
}
]
});
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
h.register_helper("next", Box::new(next));
assert_eq!(
h.render_template(TEMPLATE, &data).unwrap(),
"one: one.html|three: three.html"
);
}
#[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"
}
]
});
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
h.register_helper("next", Box::new(next));
assert_eq!(
h.render_template(TEMPLATE, &data).unwrap(),
"|two: two.html"
);
}
#[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"
}
]
});
let mut h = Handlebars::new();
h.register_helper("previous", Box::new(previous));
h.register_helper("next", Box::new(next));
assert_eq!(
h.render_template(TEMPLATE, &data).unwrap(),
"two: two.html|"
);
}
}

View File

@@ -2,38 +2,37 @@ use std::path::Path;
use std::collections::BTreeMap;
use serde_json;
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper};
use pulldown_cmark::{Parser, html, Event, Tag};
use handlebars::{Handlebars, Helper, HelperDef, RenderContext, RenderError};
use pulldown_cmark::{html, Event, Parser, Tag};
// Handlebars helper to construct TOC
#[derive(Clone, Copy)]
pub struct RenderToc;
pub struct RenderToc {
pub no_section_label: bool,
}
impl HelperDef for RenderToc {
fn call(&self, _h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> 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("chapters")
.and_then(|c| {
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let current = rc.evaluate_absolute("path")?
.as_str().ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
let chapters = rc.evaluate_absolute("chapters", true).and_then(|c| {
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let current = rc.evaluate_absolute("path", true)?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
rc.writer.write_all(b"<ul class=\"chapter\">")?;
rc.writer.write_all(b"<ol class=\"chapter\">")?;
let mut current_level = 1;
for item in chapters {
// Spacer
if item.get("spacer").is_some() {
rc.writer
.write_all(b"<li class=\"spacer\"></li>")?;
rc.writer.write_all(b"<li class=\"spacer\"></li>")?;
continue;
}
@@ -46,13 +45,13 @@ impl HelperDef for RenderToc {
if level > current_level {
while level > current_level {
rc.writer.write_all(b"<li>")?;
rc.writer.write_all(b"<ul class=\"section\">")?;
rc.writer.write_all(b"<ol class=\"section\">")?;
current_level += 1;
}
rc.writer.write_all(b"<li>")?;
} else if level < current_level {
while level < current_level {
rc.writer.write_all(b"</ul>")?;
rc.writer.write_all(b"</ol>")?;
rc.writer.write_all(b"</li>")?;
current_level -= 1;
}
@@ -94,11 +93,13 @@ impl HelperDef for RenderToc {
false
};
// Section does not necessarily exist
if let Some(section) = item.get("section") {
rc.writer.write_all(b"<strong>")?;
rc.writer.write_all(section.as_bytes())?;
rc.writer.write_all(b"</strong> ")?;
if !self.no_section_label {
// Section does not necessarily exist
if let Some(section) = item.get("section") {
rc.writer.write_all(b"<strong aria-hidden=\"true\">")?;
rc.writer.write_all(section.as_bytes())?;
rc.writer.write_all(b"</strong> ")?;
}
}
if let Some(name) = item.get("name") {
@@ -106,12 +107,12 @@ 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,
_ => false,
});
Event::Start(Tag::Code)
| Event::End(Tag::Code)
| Event::InlineHtml(_)
| Event::Text(_) => true,
_ => false,
});
// render markdown to html
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
@@ -126,15 +127,14 @@ impl HelperDef for RenderToc {
}
rc.writer.write_all(b"</li>")?;
}
while current_level > 1 {
rc.writer.write_all(b"</ul>")?;
rc.writer.write_all(b"</ol>")?;
rc.writer.write_all(b"</li>")?;
current_level -= 1;
}
rc.writer.write_all(b"</ul>")?;
rc.writer.write_all(b"</ol>")?;
Ok(())
}
}

View File

@@ -1,4 +1,9 @@
#![allow(missing_docs)] // FIXME: Document this
pub use self::hbs_renderer::HtmlHandlebars;
mod hbs_renderer;
mod helpers;
#[cfg(feature = "search")]
mod search;

View File

@@ -0,0 +1,231 @@
extern crate ammonia;
extern crate elasticlunr;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use pulldown_cmark::*;
use serde_json;
use self::elasticlunr::Index;
use book::{Book, BookItem};
use config::Search;
use errors::*;
use utils;
use theme::searcher;
/// Creates all files required for search.
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
let mut index = Index::new(&["title", "body", "breadcrumbs"]);
for item in book.iter() {
render_item(&mut index, &search_config, item)?;
}
let index = write_to_js(index, &search_config)?;
debug!("Writing search index ✓");
if search_config.copy_js {
utils::fs::write_file(destination, "searchindex.js", index.as_bytes())?;
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
debug!("Copying search files ✓");
}
Ok(())
}
/// Uses the given arguments to construct a search document, then inserts it to the given index.
fn add_doc<'a>(
index: &mut Index,
anchor_base: &'a str,
section_id: &Option<String>,
items: &[&str],
) {
let doc_ref: Cow<'a, str> = if let &Some(ref id) = section_id {
format!("{}#{}", anchor_base, id).into()
} else {
anchor_base.into()
};
let doc_ref = utils::collapse_whitespace(doc_ref.trim());
let items = items.iter().map(|&x| utils::collapse_whitespace(x.trim()));
index.add_doc(&doc_ref, items);
}
/// Renders markdown into flat unformatted text and adds it to the search index.
fn render_item(
index: &mut Index,
search_config: &Search,
item: &BookItem,
) -> Result<()> {
let chapter = match item {
&BookItem::Chapter(ref ch) => ch,
_ => return Ok(()),
};
let filepath = Path::new(&chapter.path).with_extension("html");
let filepath = filepath
.to_str()
.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 mut in_header = false;
let max_section_depth = search_config.heading_split_level as i32;
let mut section_id = None;
let mut heading = String::new();
let mut body = String::new();
let mut breadcrumbs = chapter.parent_names.clone();
let mut footnote_numbers = HashMap::new();
for event in p {
match event {
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
if heading.len() > 0 {
// Section finished, the next header is following now
// Write the data to the index, and clear it for the next section
add_doc(
index,
&anchor_base,
&section_id,
&[&heading, &body, &breadcrumbs.join(" » ")],
);
section_id = None;
heading.clear();
body.clear();
breadcrumbs.pop();
}
in_header = true;
}
Event::End(Tag::Header(i)) if i <= max_section_depth => {
in_header = false;
section_id = Some(utils::id_from_content(&heading));
breadcrumbs.push(heading.clone());
}
Event::Start(Tag::FootnoteDefinition(name)) => {
let number = footnote_numbers.len() + 1;
footnote_numbers.entry(name).or_insert(number);
}
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
// Insert spaces where HTML output would usually seperate text
// to ensure words don't get merged together
if in_header {
heading.push(' ');
} else {
body.push(' ');
}
}
Event::Text(text) => {
if in_header {
heading.push_str(&text);
} else {
body.push_str(&text);
}
}
Event::Html(html) | Event::InlineHtml(html) => {
body.push_str(&clean_html(&html));
}
Event::FootnoteReference(name) => {
let len = footnote_numbers.len() + 1;
let number = footnote_numbers.entry(name).or_insert(len);
body.push_str(&format!(" [{}] ", number));
}
}
}
if heading.len() > 0 {
// Make sure the last section is added to the index
add_doc(
index,
&anchor_base,
&section_id,
&[&heading, &body, &breadcrumbs.join(" » ")],
);
}
Ok(())
}
/// Exports the index and search options to a JS script which stores the index in `window.search`.
/// Using a JS script is a workaround for CORS in `file://` URIs. It also removes the need for
/// downloading/parsing JSON in JS.
fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
use std::collections::BTreeMap;
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
#[derive(Serialize)]
struct ResultsOptions {
limit_results: u32,
teaser_word_count: u32,
}
#[derive(Serialize)]
struct SearchindexJson {
/// The options used for displaying search results
resultsoptions: ResultsOptions,
/// The searchoptions for elasticlunr.js
searchoptions: SearchOptions,
/// The index for elasticlunr.js
index: elasticlunr::Index,
}
let mut fields = BTreeMap::new();
let mut opt = SearchOptionsField::default();
opt.boost = Some(search_config.boost_title);
fields.insert("title".into(), opt);
opt.boost = Some(search_config.boost_paragraph);
fields.insert("body".into(), opt);
opt.boost = Some(search_config.boost_hierarchy);
fields.insert("breadcrumbs".into(), opt);
let searchoptions = SearchOptions {
bool: if search_config.use_boolean_and {
SearchBool::And
} else {
SearchBool::Or
},
expand: search_config.expand,
fields,
};
let resultsoptions = ResultsOptions {
limit_results: search_config.limit_results,
teaser_word_count: search_config.teaser_word_count,
};
let json_contents = SearchindexJson {
resultsoptions,
searchoptions,
index,
};
let json_contents = serde_json::to_string(&json_contents)?;
Ok(format!("window.search = {};", json_contents))
}
fn clean_html(html: &str) -> String {
lazy_static! {
static ref AMMONIA: ammonia::Builder<'static> = {
let mut clean_content = HashSet::new();
clean_content.insert("script");
clean_content.insert("style");
let mut builder = ammonia::Builder::new();
builder
.tags(HashSet::new())
.tag_attributes(HashMap::new())
.generic_attributes(HashSet::new())
.link_rel(None)
.allowed_classes(HashMap::new())
.clean_content_tags(clean_content);
builder
};
}
AMMONIA.clean(html).to_string()
}

View File

@@ -1,9 +1,202 @@
//! `mdbook`'s low level rendering interface.
//!
//! # Note
//!
//! You usually don't need to work with this module directly. If you want to
//! implement your own backend, then check out the [For Developers] section of
//! the user guide.
//!
//! The definition for [RenderContext] may be useful though.
//!
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/lib/index.html
//! [RenderContext]: struct.RenderContext.html
pub use self::html_handlebars::HtmlHandlebars;
mod html_handlebars;
use errors::*;
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use serde_json;
use shlex::Shlex;
use errors::*;
use config::Config;
use book::Book;
const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
/// An arbitrary `mdbook` backend.
///
/// Although it's quite possible for you to import `mdbook` as a library and
/// provide your own renderer, there are two main renderer implementations that
/// 99% of users will ever use:
///
/// - [HtmlHandlebars] - the built-in HTML renderer
/// - [CmdRenderer] - a generic renderer which shells out to a program to do the
/// actual rendering
///
/// [HtmlHandlebars]: struct.HtmlHandlebars.html
/// [CmdRenderer]: struct.CmdRenderer.html
pub trait Renderer {
fn render(&self, book: &::book::MDBook) -> Result<()>;
/// The `Renderer`'s name.
fn name(&self) -> &str;
/// Invoke the `Renderer`, passing in all the necessary information for
/// describing a book.
fn render(&self, ctx: &RenderContext) -> Result<()>;
}
/// The context provided to all renderers.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RenderContext {
/// Which version of `mdbook` did this come from (as written in `mdbook`'s
/// `Cargo.toml`). Useful if you know the renderer is only compatible with
/// certain versions of `mdbook`.
pub version: String,
/// The book's root directory.
pub root: PathBuf,
/// A loaded representation of the book itself.
pub book: Book,
/// The loaded configuration file.
pub config: Config,
/// Where the renderer *must* put any build artefacts generated. To allow
/// renderers to cache intermediate results, this directory is not
/// guaranteed to be empty or even exist.
pub destination: PathBuf,
}
impl RenderContext {
/// Create a new `RenderContext`.
pub fn new<P, Q>(root: P, book: Book, config: Config, destination: Q) -> RenderContext
where
P: Into<PathBuf>,
Q: Into<PathBuf>,
{
RenderContext {
book: book,
config: config,
version: MDBOOK_VERSION.to_string(),
root: root.into(),
destination: destination.into(),
}
}
/// Get the source directory's (absolute) path on disk.
pub fn source_dir(&self) -> PathBuf {
self.root.join(&self.config.book.src)
}
/// Load a `RenderContext` from its JSON representation.
pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> {
serde_json::from_reader(reader).chain_err(|| "Unable to deserialize the `RenderContext`")
}
}
/// A generic renderer which will shell out to an arbitrary executable.
///
/// # Rendering Protocol
///
/// When the renderer's `render()` method is invoked, `CmdRenderer` will spawn
/// the `cmd` as a subprocess. The `RenderContext` is passed to the subprocess
/// as a JSON string (using `serde_json`).
///
/// > **Note:** The command used doesn't necessarily need to be a single
/// > executable (i.e. `/path/to/renderer`). The `cmd` string lets you pass
/// > in command line arguments, so there's no reason why it couldn't be
/// > `python /path/to/renderer --from mdbook --to epub`.
///
/// Anything the subprocess writes to `stdin` or `stdout` will be passed through
/// to the user. While this gives the renderer maximum flexibility to output
/// whatever it wants, to avoid spamming users it is recommended to avoid
/// unnecessary output.
///
/// To help choose the appropriate output level, the `RUST_LOG` environment
/// variable will be passed through to the subprocess, if set.
///
/// If the subprocess wishes to indicate that rendering failed, it should exit
/// with a non-zero return code.
#[derive(Debug, Clone, PartialEq)]
pub struct CmdRenderer {
name: String,
cmd: String,
}
impl CmdRenderer {
/// Create a new `CmdRenderer` which will invoke the provided `cmd` string.
pub fn new(name: String, cmd: String) -> CmdRenderer {
CmdRenderer { name, cmd }
}
fn compose_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 Renderer for CmdRenderer {
fn name(&self) -> &str {
&self.name
}
fn render(&self, ctx: &RenderContext) -> Result<()> {
info!("Invoking the \"{}\" renderer", self.name);
let _ = fs::create_dir_all(&ctx.destination);
let mut child = match self.compose_command()?
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.current_dir(&ctx.destination)
.spawn() {
Ok(c) => c,
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
warn!("The command wasn't found, is the \"{}\" backend installed?", self.name);
warn!("\tCommand: {}", self.cmd);
return Ok(());
}
Err(e) => {
return Err(e).chain_err(|| "Unable to start the backend")?;
}
};
{
let mut stdin = child.stdin.take().expect("Child has stdin");
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
// Looks like the backend hung up before we could finish
// sending it the render context. Log the error and keep going
warn!("Error writing the RenderContext to the backend, {}", e);
}
// explicitly close the `stdin` file handle
drop(stdin);
}
let status = child
.wait()
.chain_err(|| "Error waiting for the backend to complete")?;
trace!("{} exited with output: {:?}", self.cmd, status);
if !status.success() {
error!("Renderer exited with non-zero return code.");
bail!("The \"{}\" renderer failed", self.name);
} else {
Ok(())
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,115 @@
$( document ).ready(function() {
// Fix back button cache problem
window.onunload = function () { };
// url
var url = window.location.pathname;
// Global variable, shared between modules
function playpen_text(playpen) {
let code_block = playpen.querySelector("code");
// Fix back button cache problem
window.onunload = function(){};
if (window.ace && code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
return editor.getValue();
} else {
return code_block.textContent;
}
}
// Set theme
var theme = store.get('theme');
if (theme === null || theme === undefined) { theme = 'light'; }
(function codeSnippets() {
// Hide Rust code lines prepended with a specific character
var hiding_character = "#";
var request = fetch("https://play.rust-lang.org/meta/crates", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
});
set_theme(theme);
function handle_crate_list_update(playpen_block, playground_crates) {
// update the play buttons after receiving the response
update_play_button(playpen_block, playground_crates);
// and install on change listener to dynamically update ACE editors
if (window.ace) {
let code_block = playpen_block.querySelector("code");
if (code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
editor.addEventListener("change", function (e) {
update_play_button(playpen_block, playground_crates);
});
}
}
}
// updates the visibility of play button based on `no_run` class and
// used crates vs ones available on http://play.rust-lang.org
function update_play_button(pre_block, playground_crates) {
var play_button = pre_block.querySelector(".play-button");
// skip if code is `no_run`
if (pre_block.querySelector('code').classList.contains("no_run")) {
play_button.classList.add("hidden");
return;
}
// get list of `extern crate`'s from snippet
var txt = playpen_text(pre_block);
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
var snippet_crates = [];
while (item = re.exec(txt)) {
snippet_crates.push(item[1]);
}
// check if all used crates are available on play.rust-lang.org
var all_available = snippet_crates.every(function (elem) {
return playground_crates.indexOf(elem) > -1;
});
if (all_available) {
play_button.classList.remove("hidden");
} else {
play_button.classList.add("hidden");
}
}
function run_rust_code(code_block) {
var result_block = code_block.querySelector(".result");
if (!result_block) {
result_block = document.createElement('code');
result_block.className = 'result hljs language-bash';
code_block.append(result_block);
}
let text = playpen_text(code_block);
var params = {
channel: "stable",
mode: "debug",
crateType: "bin",
tests: false,
code: text,
}
if (text.indexOf("#![feature") !== -1) {
params.channel = "nightly";
}
result_block.innerText = "Running...";
var request = fetch("https://play.rust-lang.org/execute", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
body: JSON.stringify(params)
});
request
.then(function (response) { return response.json(); })
.then(function (response) { result_block.innerText = response.success ? response.stdout : response.stderr; })
.catch(function (error) { result_block.innerText = "Playground communication" + error.message; });
}
// Syntax highlighting Configuration
hljs.configure({
@@ -21,380 +120,471 @@ $( document ).ready(function() {
if (window.ace) {
// language-rust class needs to be removed for editable
// blocks or highlightjs will capture events
$('code.editable').removeClass('language-rust');
Array
.from(document.querySelectorAll('code.editable'))
.forEach(function (block) { block.classList.remove('language-rust'); });
$('code').not('.editable').each(function(i, block) {
hljs.highlightBlock(block);
});
Array
.from(document.querySelectorAll('code:not(.editable)'))
.forEach(function (block) { hljs.highlightBlock(block); });
} else {
$('code').each(function(i, block) {
hljs.highlightBlock(block);
});
Array
.from(document.querySelectorAll('code'))
.forEach(function (block) { hljs.highlightBlock(block); });
}
// Adding the hljs class gives code blocks the color css
// even if highlighting doesn't apply
$('code').addClass('hljs');
Array
.from(document.querySelectorAll('code'))
.forEach(function (block) { block.classList.add('hljs'); });
var KEY_CODES = {
PREVIOUS_KEY: 37,
NEXT_KEY: 39
};
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
$(document).on('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
switch (e.keyCode) {
case KEY_CODES.NEXT_KEY:
e.preventDefault();
if($('.nav-chapters.next').length) {
window.location.href = $('.nav-chapters.next').attr('href');
}
break;
case KEY_CODES.PREVIOUS_KEY:
e.preventDefault();
if($('.nav-chapters.previous').length) {
window.location.href = $('.nav-chapters.previous').attr('href');
}
break;
}
});
// Interesting DOM Elements
var sidebar = $("#sidebar");
// Help keyboard navigation by always focusing on page content
$(".page").focus();
// Toggle sidebar
$("#sidebar-toggle").click(sidebarToggle);
// Hide sidebar on section link click if it occupies large space
// in relation to the whole screen (phone in portrait)
$("#sidebar a").click(function(event){
if (sidebar.width() > window.screen.width * 0.4) {
sidebarToggle();
}
});
// Scroll sidebar to current active section
var activeSection = sidebar.find(".active");
if(activeSection.length) {
sidebar.scrollTop(activeSection.offset().top);
}
// Print button
$("#print-button").click(function(){
var printWindow = window.open("print.html");
});
if( url.substring(url.lastIndexOf('/')+1) == "print.html" ) {
window.print();
}
// Theme button
$("#theme-toggle").click(function(){
if($('.theme-popup').length) {
$('.theme-popup').remove();
} else {
var popup = $('<div class="theme-popup"></div>')
.append($('<div class="theme" id="light">Light <span class="default">(default)</span><div>'))
.append($('<div class="theme" id="rust">Rust</div>'))
.append($('<div class="theme" id="coal">Coal</div>'))
.append($('<div class="theme" id="navy">Navy</div>'))
.append($('<div class="theme" id="ayu">Ayu</div>'));
popup.insertAfter(this);
$('.theme').click(function(){
var theme = $(this).attr('id');
set_theme(theme);
});
}
});
// Hide theme selector popup when clicking outside of it
$(document).click(function(event){
var popup = $('.theme-popup');
if(popup.length) {
var target = $(event.target);
if(!target.closest('.theme').length && !target.closest('#theme-toggle').length) {
popup.remove();
}
}
});
function set_theme(theme) {
let ace_theme;
if (theme == 'coal' || theme == 'navy') {
$("[href='ayu-highlight.css']").prop('disabled', true);
$("[href='tomorrow-night.css']").prop('disabled', false);
$("[href='highlight.css']").prop('disabled', true);
ace_theme = "ace/theme/tomorrow_night";
} else if (theme == 'ayu') {
$("[href='ayu-highlight.css']").prop('disabled', false);
$("[href='tomorrow-night.css']").prop('disabled', true);
$("[href='highlight.css']").prop('disabled', true);
ace_theme = "ace/theme/tomorrow_night";
} else {
$("[href='ayu-highlight.css']").prop('disabled', true);
$("[href='tomorrow-night.css']").prop('disabled', true);
$("[href='highlight.css']").prop('disabled', false);
ace_theme = "ace/theme/dawn";
}
if (window.ace && window.editors) {
window.editors.forEach(function(editor) {
editor.setTheme(ace_theme);
});
}
store.set('theme', theme);
$('body').removeClass().addClass(theme);
}
// Hide Rust code lines prepended with a specific character
var hiding_character = "#";
$("code.language-rust").each(function(i, block){
var code_block = $(this);
var pre_block = $(this).parent();
var code_block = block;
var pre_block = block.parentNode;
// hide lines
var lines = code_block.html().split("\n");
var lines = code_block.innerHTML.split("\n");
var first_non_hidden_line = false;
var lines_hidden = false;
for(var n = 0; n < lines.length; n++){
if($.trim(lines[n])[0] == hiding_character){
if(first_non_hidden_line){
for (var n = 0; n < lines.length; n++) {
if (lines[n].trim()[0] == 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[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)# ?/, "$1") + "\n" + "</span>";
}
lines_hidden = true;
}
else if(first_non_hidden_line) {
else if (first_non_hidden_line) {
lines[n] = "\n" + lines[n];
}
else {
first_non_hidden_line = true;
}
}
code_block.html(lines.join(""));
code_block.innerHTML = lines.join("");
// If no lines were hidden, return
if(!lines_hidden) { return; }
if (!lines_hidden) { return; }
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
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\"></i></div>");
pre_block.prepend(buttons);
pre_block.find("i").click(function(e){
if( $(this).hasClass("fa-expand") ) {
$(this).removeClass("fa-expand").addClass("fa-compress");
pre_block.find("span.hidden").removeClass("hidden").addClass("unhidden");
}
else {
$(this).removeClass("fa-compress").addClass("fa-expand");
pre_block.find("span.unhidden").removeClass("unhidden").addClass("hidden");
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');
});
} 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');
});
}
});
});
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.prepend(buttons);
}
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.prepend(clipButton);
}
});
// Process playpen code blocks
$(".playpen").each(function(block){
var pre_block = $(this);
Array.from(document.querySelectorAll(".playpen")).forEach(function (pre_block) {
// Add play button
var buttons = pre_block.find(".buttons");
if( buttons.length === 0 ) {
pre_block.prepend("<div class=\"buttons\"></div>");
buttons = pre_block.find(".buttons");
}
buttons.prepend("<i class=\"fa fa-play play-button hidden\"></i>");
buttons.prepend("<i class=\"fa fa-copy clip-button\"><i class=\"tooltiptext\"></i></i>");
let code_block = pre_block.find("code").first();
if (window.ace && code_block.hasClass("editable")) {
buttons.prepend("<i class=\"fa fa-history reset-button\"></i>");
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.prepend(buttons);
}
buttons.find(".play-button").click(function(e){
var runCodeButton = document.createElement('button');
runCodeButton.className = 'fa fa-play play-button';
runCodeButton.hidden = true;
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.prepend(runCodeButton);
buttons.prepend(copyCodeClipboardButton);
runCodeButton.addEventListener('click', function (e) {
run_rust_code(pre_block);
});
buttons.find(".clip-button").mouseout(function(e){
hideTooltip(e.currentTarget);
});
buttons.find(".reset-button").click(function() {
if (!window.ace) { return; }
let editor = window.ace.edit(code_block.get(0));
editor.setValue(editor.originalCode);
editor.clearSelection();
});
let code_block = pre_block.querySelector("code");
if (window.ace && code_block.classList.contains("editable")) {
var undoChangesButton = document.createElement('button');
undoChangesButton.className = 'fa fa-history reset-button';
undoChangesButton.title = 'Undo changes';
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
buttons.prepend(undoChangesButton);
undoChangesButton.addEventListener('click', function () {
let editor = window.ace.edit(code_block);
editor.setValue(editor.originalCode);
editor.clearSelection();
});
}
});
request
.then(function (response) { return response.json(); })
.then(function (response) {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(function (item) { return item["id"]; });
Array.from(document.querySelectorAll(".playpen")).forEach(function (block) {
handle_crate_list_update(block, playground_crates);
});
});
})();
(function themes() {
var html = document.querySelector('html');
var themeToggleButton = document.getElementById('theme-toggle');
var themePopup = document.getElementById('theme-list');
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
var stylesheets = {
ayuHighlight: document.querySelector("[href='ayu-highlight.css']"),
tomorrowNight: document.querySelector("[href='tomorrow-night.css']"),
highlight: document.querySelector("[href='highlight.css']"),
};
function showThemes() {
themePopup.style.display = 'block';
themeToggleButton.setAttribute('aria-expanded', true);
themePopup.querySelector("button#" + document.body.className).focus();
}
function hideThemes() {
themePopup.style.display = 'none';
themeToggleButton.setAttribute('aria-expanded', false);
themeToggleButton.focus();
}
function set_theme(theme) {
let ace_theme;
if (theme == 'coal' || theme == 'navy') {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = false;
stylesheets.highlight.disabled = true;
ace_theme = "ace/theme/tomorrow_night";
} else if (theme == 'ayu') {
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";
}
setTimeout(function () {
themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor;
}, 1);
if (window.ace && window.editors) {
window.editors.forEach(function (editor) {
editor.setTheme(ace_theme);
});
}
var previousTheme;
try { previousTheme = localStorage.getItem('mdbook-theme'); } catch (e) { }
if (previousTheme === null || previousTheme === undefined) { previousTheme = 'light'; }
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
document.body.className = theme;
html.classList.remove(previousTheme);
html.classList.add(theme);
}
// Set theme
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
set_theme(theme);
themeToggleButton.addEventListener('click', function () {
if (themePopup.style.display === 'block') {
hideThemes();
} else {
showThemes();
}
});
themePopup.addEventListener('click', function (e) {
var theme = e.target.id || e.target.parentElement.id;
set_theme(theme);
});
themePopup.addEventListener('focusout', function(e) {
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
if (!!e.relatedTarget && !themePopup.contains(e.relatedTarget)) {
hideThemes();
}
});
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang-nursery/mdBook/issues/628
document.addEventListener('click', function(e) {
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
hideThemes();
}
});
document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (!themePopup.contains(e.target)) { return; }
switch (e.key) {
case 'Escape':
e.preventDefault();
hideThemes();
break;
case 'ArrowUp':
e.preventDefault();
var li = document.activeElement.parentElement;
if (li && li.previousElementSibling) {
li.previousElementSibling.querySelector('button').focus();
}
break;
case 'ArrowDown':
e.preventDefault();
var li = document.activeElement.parentElement;
if (li && li.nextElementSibling) {
li.nextElementSibling.querySelector('button').focus();
}
break;
case 'Home':
e.preventDefault();
themePopup.querySelector('li:first-child button').focus();
break;
case 'End':
e.preventDefault();
themePopup.querySelector('li:last-child button').focus();
break;
}
});
})();
(function sidebar() {
var html = document.querySelector("html");
var sidebar = document.getElementById("sidebar");
var sidebarLinks = document.querySelectorAll('#sidebar a');
var sidebarToggleButton = document.getElementById("sidebar-toggle");
var firstContact = null;
function showSidebar() {
html.classList.remove('sidebar-hidden')
html.classList.add('sidebar-visible');
Array.from(sidebarLinks).forEach(function (link) {
link.setAttribute('tabIndex', 0);
});
sidebarToggleButton.setAttribute('aria-expanded', true);
sidebar.setAttribute('aria-hidden', false);
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
}
function hideSidebar() {
html.classList.remove('sidebar-visible')
html.classList.add('sidebar-hidden');
Array.from(sidebarLinks).forEach(function (link) {
link.setAttribute('tabIndex', -1);
});
sidebarToggleButton.setAttribute('aria-expanded', false);
sidebar.setAttribute('aria-hidden', true);
try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { }
}
// Toggle sidebar
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
if (html.classList.contains("sidebar-hidden")) {
showSidebar();
} else if (html.classList.contains("sidebar-visible")) {
hideSidebar();
} else {
if (getComputedStyle(sidebar)['transform'] === 'none') {
hideSidebar();
} else {
showSidebar();
}
}
});
document.addEventListener('touchstart', function (e) {
firstContact = {
x: e.touches[0].clientX,
time: Date.now()
};
}, { passive: true });
document.addEventListener('touchmove', function (e) {
if (!firstContact)
return;
var curX = e.touches[0].clientX;
var xDiff = curX - firstContact.x,
tDiff = Date.now() - firstContact.time;
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300))
showSidebar();
else if (xDiff < 0 && curX < 300)
hideSidebar();
firstContact = null;
}
}, { passive: true });
// Scroll sidebar to current active section
var activeSection = sidebar.querySelector(".active");
if (activeSection) {
sidebar.scrollTop = activeSection.offsetTop;
}
})();
(function chapterNavigation() {
document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (window.search && window.search.hasFocus()) { return; }
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
var nextButton = document.querySelector('.nav-chapters.next');
if (nextButton) {
window.location.href = nextButton.href;
}
break;
case 'ArrowLeft':
e.preventDefault();
var previousButton = document.querySelector('.nav-chapters.previous');
if (previousButton) {
window.location.href = previousButton.href;
}
break;
}
});
})();
(function clipboard() {
var clipButtons = document.querySelectorAll('.clip-button');
function hideTooltip(elem) {
elem.firstChild.innerText = "";
elem.className = 'fa fa-copy clip-button';
}
function showTooltip(elem, msg) {
elem.firstChild.innerText = msg;
elem.className = 'fa fa-copy tooltipped';
}
var clipboardSnippets = new Clipboard('.clip-button', {
text: function(trigger) {
text: function (trigger) {
hideTooltip(trigger);
let playpen = $(trigger).parents(".playpen");
let playpen = trigger.closest("pre");
return playpen_text(playpen);
}
});
clipboardSnippets.on('success', function(e) {
e.clearSelection();
showTooltip(e.trigger, "Copied!");
});
clipboardSnippets.on('error', function(e) {
showTooltip(e.trigger, "Clipboard error!");
Array.from(clipButtons).forEach(function (clipButton) {
clipButton.addEventListener('mouseout', function (e) {
hideTooltip(e.currentTarget);
});
});
$.ajax({
url: "https://play.rust-lang.org/meta/crates",
method: "POST",
crossDomain: true,
dataType: "json",
contentType: "application/json",
success: function(response){
// get list of crates available in the rust playground
let playground_crates = response.crates.map(function(item) {return item["id"];} );
$(".playpen").each(function(block) {
handle_crate_list_update($(this), playground_crates);
});
},
clipboardSnippets.on('success', function (e) {
e.clearSelection();
showTooltip(e.trigger, "Copied!");
});
});
clipboardSnippets.on('error', function (e) {
showTooltip(e.trigger, "Clipboard error!");
});
})();
function playpen_text(playpen) {
let code_block = playpen.find("code").first();
(function scrollToTop () {
var menuTitle = document.querySelector('.menu-title');
if (window.ace && code_block.hasClass("editable")) {
let editor = window.ace.edit(code_block.get(0));
return editor.getValue();
} else {
return code_block.get(0).textContent;
}
}
menuTitle.addEventListener('click', function () {
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
});
})();
function handle_crate_list_update(playpen_block, playground_crates) {
// update the play buttons after receiving the response
update_play_button(playpen_block, playground_crates);
(function autoHideMenu() {
var menu = document.getElementById('menu-bar');
// and install on change listener to dynamically update ACE editors
if (window.ace) {
let code_block = playpen_block.find("code").first();
if (code_block.hasClass("editable")) {
let editor = window.ace.edit(code_block.get(0));
editor.on("change", function(e){
update_play_button(playpen_block, playground_crates);
});
var previousScrollTop = document.scrollingElement.scrollTop;
document.addEventListener('scroll', function () {
if (menu.classList.contains('folded') && document.scrollingElement.scrollTop < previousScrollTop) {
menu.classList.remove('folded');
} else if (!menu.classList.contains('folded') && document.scrollingElement.scrollTop > previousScrollTop) {
menu.classList.add('folded');
}
}
}
// updates the visibility of play button based on `no_run` class and
// used crates vs ones available on http://play.rust-lang.org
function update_play_button(pre_block, playground_crates) {
var play_button = pre_block.find(".play-button");
var classes = pre_block.find("code").attr("class").split(" ");
// skip if code is `no_run`
if (classes.indexOf("no_run") > -1) {
play_button.addClass("hidden");
return;
}
// get list of `extern crate`'s from snippet
var txt = playpen_text(pre_block);
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
var snippet_crates = [];
while (item = re.exec(txt)) {
snippet_crates.push(item[1]);
}
// check if all used crates are available on play.rust-lang.org
var all_available = snippet_crates.every(function(elem) {
return playground_crates.indexOf(elem) > -1;
});
if (all_available) {
play_button.removeClass("hidden");
} else {
play_button.addClass("hidden");
}
}
function hideTooltip(elem) {
elem.firstChild.innerText="";
elem.setAttribute('class', 'fa fa-copy clip-button');
}
function showTooltip(elem, msg) {
elem.firstChild.innerText=msg;
elem.setAttribute('class', 'fa fa-copy tooltipped');
}
function sidebarToggle() {
var html = $("html");
if ( html.hasClass("sidebar-hidden") ) {
html.removeClass("sidebar-hidden").addClass("sidebar-visible");
store.set('sidebar', 'visible');
} else if ( html.hasClass("sidebar-visible") ) {
html.removeClass("sidebar-visible").addClass("sidebar-hidden");
store.set('sidebar', 'hidden');
} else {
if($("#sidebar").position().left === 0){
html.addClass("sidebar-hidden");
store.set('sidebar', 'hidden');
} else {
html.addClass("sidebar-visible");
store.set('sidebar', 'visible');
if (!menu.classList.contains('bordered') && document.scrollingElement.scrollTop > 0) {
menu.classList.add('bordered');
}
}
}
function run_rust_code(code_block) {
var result_block = code_block.find(".result");
if(result_block.length === 0) {
code_block.append("<code class=\"result hljs language-bash\"></code>");
result_block = code_block.find(".result");
}
if (menu.classList.contains('bordered') && document.scrollingElement.scrollTop === 0) {
menu.classList.remove('bordered');
}
let text = playpen_text(code_block);;
var params = {
version: "stable",
optimize: "0",
code: text,
};
if(text.indexOf("#![feature") !== -1) {
params.version = "nightly";
}
result_block.text("Running...");
$.ajax({
url: "https://play.rust-lang.org/evaluate.json",
method: "POST",
crossDomain: true,
dataType: "json",
contentType: "application/json",
data: JSON.stringify(params),
timeout: 15000,
success: function(response){
result_block.text(response.result);
},
error: function(qXHR, textStatus, errorThrown){
result_block.text("Playground communication " + textStatus);
},
});
}
previousScrollTop = document.scrollingElement.scrollTop;
}, { passive: true });
})();

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

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

View File

@@ -1,11 +1,13 @@
<!DOCTYPE HTML>
<html lang="{{ language }}">
<html lang="{{ language }}" class="sidebar-visible">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>{{ title }}</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<base href="{{ path_to_root }}">
@@ -40,123 +42,234 @@
}
</script>
<!-- Fetch JQuery from CDN but have a local fallback -->
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script>
if (typeof jQuery == 'undefined') {
document.write(unescape("%3Cscript src='jquery.js'%3E%3C/script%3E"));
}
</script>
<!-- Fetch store.js from local - TODO add CDN when 2.x.x is available on cdnjs -->
<script src="store.js"></script>
<!-- Custom JS script -->
{{#each additional_js}}
<script type="text/javascript" src="{{this}}"></script>
{{/each}}
<noscript>
<style type="text/css">
.javascript-only {
display: none;
}
</style>
</noscript>
</head>
<body class="light">
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme = store.get('theme');
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
$('body').removeClass().addClass(theme);
document.body.className = theme;
document.querySelector('html').className = theme;
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var sidebar = store.get('sidebar');
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
var html = document.querySelector('html');
var sidebar = 'hidden';
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<div id="sidebar" class="sidebar">
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
{{#toc}}{{/toc}}
</div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page" tabindex="-1">
<div class="page">
{{> header}}
<div id="menu-bar" class="menu-bar">
<div class="left-buttons">
<i id="sidebar-toggle" class="fa fa-bars"></i>
<i id="theme-toggle" class="fa fa-paint-brush"></i>
</div>
<div id="menu-bar-sticky-container">
<div class="left-buttons javascript-only">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
{{#if search_enabled}}
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
{{/if}}
</div>
<h1 class="menu-title">{{ book_title }}</h1>
<h1 class="menu-title">{{ book_title }}</h1>
<div class="right-buttons">
<i id="print-button" class="fa fa-print" title="Print this book"></i>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
</div>
{{#if search_enabled}}
<div id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</div>
<div id="searchresults-outer" class="searchresults-outer">
<div class="searchresults-header" id="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
{{/if}}
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
{{{ content }}}
</div>
<main>
{{{ content }}}
</main>
<!-- Mobile navigation buttons -->
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
{{#previous}}
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a rel="next" href="{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
{{#previous}}
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous">
<a href="{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a rel="next" href="{{link}}" class="mobile-nav-chapters next">
<a href="{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
</div>
{{#previous}}
<a href="{{link}}" class="nav-chapters previous" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a href="{{link}}" class="nav-chapters next" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
</nav>
</div>
<!-- Local fallback for Font Awesome -->
<script>
if ($(".fa").css("font-family") !== "FontAwesome") {
$('<link rel="stylesheet" type="text/css" href="_FontAwesome/css/font-awesome.css">').prependTo('head');
if (getComputedStyle(document.querySelector(".fa")).fontFamily !== "FontAwesome") {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = '_FontAwesome/css/font-awesome.css';
document.head.insertBefore(link, document.head.firstChild)
}
</script>
{{#if livereload}}
<!-- Livereload script (if served using the cli tool) -->
{{{livereload}}}
<script type="text/javascript">
var socket = new WebSocket("{{{livereload}}}");
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
location.reload(true); // force reload from server (not from cache)
}
};
{{#if google_analytics}}
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{{google_analytics}}', 'auto');
ga('send', 'pageview');
window.onbeforeunload = function() {
socket.close();
}
</script>
{{/if}}
{{#if playpens_editable}}
<script src="{{ ace_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ editor_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ mode_rust_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ theme_dawn_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ theme_tomorrow_night_js }}" type="text/javascript" charset="utf-8"></script>
{{#if google_analytics}}
<!-- Google Analytics Tag -->
<script>
var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is
// inspecting the book locally...
if (localAddrs.indexOf(document.location.hostname) === -1) {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{{google_analytics}}', 'auto');
ga('send', 'pageview');
}
</script>
{{/if}}
{{#if playpen_js}}
<script src="ace.js" type="text/javascript" charset="utf-8"></script>
<script src="editor.js" type="text/javascript" charset="utf-8"></script>
<script src="mode-rust.js" type="text/javascript" charset="utf-8"></script>
<script src="theme-dawn.js" type="text/javascript" charset="utf-8"></script>
<script src="theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if search_enabled}}
<script src="searchindex.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if search_js}}
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if is_print}}
<script>
document.addEventListener('DOMContentLoaded', function() {
window.print();
})
</script>
{{/if}}
<script src="highlight.js"></script>
<script src="book.js"></script>
<!-- Custom JS script -->
{{#each additional_js}}
<script type="text/javascript" src="{{this}}"></script>
{{/each}}
</body>
</html>

4
src/theme/jquery.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,10 @@
#![allow(missing_docs)]
pub mod playpen_editor;
#[cfg(feature = "search")]
pub mod searcher;
use std::path::Path;
use std::fs::File;
use std::io::Read;
@@ -7,6 +12,7 @@ use std::io::Read;
use errors::*;
pub static INDEX: &'static [u8] = include_bytes!("index.hbs");
pub static HEADER: &'static [u8] = include_bytes!("header.hbs");
pub static CSS: &'static [u8] = include_bytes!("book.css");
pub static FAVICON: &'static [u8] = include_bytes!("favicon.png");
pub static JS: &'static [u8] = include_bytes!("book.js");
@@ -14,15 +20,18 @@ 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 JQUERY: &'static [u8] = include_bytes!("jquery.js");
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.js");
pub static STORE_JS: &'static [u8] = include_bytes!("store.js");
pub static FONT_AWESOME: &'static [u8] = include_bytes!("_FontAwesome/css/font-awesome.min.css");
pub static FONT_AWESOME_EOT: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot");
pub static FONT_AWESOME_SVG: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
pub static FONT_AWESOME_TTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.ttf");
pub static FONT_AWESOME_WOFF: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff");
pub static FONT_AWESOME_WOFF2: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff2");
pub static FONT_AWESOME_EOT: &'static [u8] =
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot");
pub static FONT_AWESOME_SVG: &'static [u8] =
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
pub static FONT_AWESOME_TTF: &'static [u8] =
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.ttf");
pub static FONT_AWESOME_WOFF: &'static [u8] =
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff");
pub static FONT_AWESOME_WOFF2: &'static [u8] =
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff2");
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/FontAwesome.otf");
@@ -35,6 +44,7 @@ pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/
#[derive(Debug, PartialEq)]
pub struct Theme {
pub index: Vec<u8>,
pub header: Vec<u8>,
pub css: Vec<u8>,
pub favicon: Vec<u8>,
pub js: Vec<u8>,
@@ -43,11 +53,11 @@ pub struct Theme {
pub ayu_highlight_css: Vec<u8>,
pub highlight_js: Vec<u8>,
pub clipboard_js: Vec<u8>,
pub store_js: Vec<u8>,
pub jquery: Vec<u8>,
}
impl Theme {
/// Creates a `Theme` from the given `theme_dir`.
/// If a file is found in the theme dir, it will override the default version.
pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
let theme_dir = theme_dir.as_ref();
let mut theme = Theme::default();
@@ -61,16 +71,15 @@ impl Theme {
{
let files = vec![
(theme_dir.join("index.hbs"), &mut theme.index),
(theme_dir.join("header.hbs"), &mut theme.header),
(theme_dir.join("book.js"), &mut theme.js),
(theme_dir.join("book.css"), &mut theme.css),
(theme_dir.join("favicon.png"), &mut theme.favicon),
(theme_dir.join("highlight.js"), &mut theme.highlight_js),
(theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
(theme_dir.join("store.js"), &mut theme.store_js),
(theme_dir.join("highlight.css"), &mut theme.highlight_css),
(theme_dir.join("tomorrow-night.css"), &mut theme.tomorrow_night_css),
(theme_dir.join("ayu-highlight.css"), &mut theme.ayu_highlight_css),
(theme_dir.join("jquery.js"), &mut theme.jquery),
];
for (filename, dest) in files {
@@ -92,6 +101,7 @@ impl Default for Theme {
fn default() -> Theme {
Theme {
index: INDEX.to_owned(),
header: HEADER.to_owned(),
css: CSS.to_owned(),
favicon: FAVICON.to_owned(),
js: JS.to_owned(),
@@ -100,8 +110,6 @@ impl Default for Theme {
ayu_highlight_css: AYU_HIGHLIGHT_CSS.to_owned(),
highlight_js: HIGHLIGHT_JS.to_owned(),
clipboard_js: CLIPBOARD_JS.to_owned(),
store_js: STORE_JS.to_owned(),
jquery: JQUERY.to_owned(),
}
}
}
@@ -126,7 +134,7 @@ fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result
#[cfg(test)]
mod tests {
use super::*;
use tempdir::TempDir;
use tempfile::Builder as TempFileBuilder;
use std::path::PathBuf;
#[test]
@@ -151,7 +159,7 @@ mod tests {
.map(|f| f.path())
.filter(|p| p.is_file() && !p.ends_with(".rs"));
let temp = TempDir::new("mdbook").unwrap();
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
// "touch" all of the special files so we have empty copies
for special_file in special_files {
@@ -163,6 +171,7 @@ mod tests {
let empty = Theme {
index: Vec::new(),
header: Vec::new(),
css: Vec::new(),
favicon: Vec::new(),
js: Vec::new(),
@@ -171,8 +180,6 @@ mod tests {
ayu_highlight_css: Vec::new(),
highlight_js: Vec::new(),
clipboard_js: Vec::new(),
store_js: Vec::new(),
jquery: Vec::new(),
};
assert_eq!(got, empty);

View File

@@ -4,8 +4,8 @@ window.editors = [];
return;
}
$(".editable").each(function() {
let editor = ace.edit(this);
Array.from(document.querySelectorAll('.editable')).forEach(function(editable) {
let editor = ace.edit(editable);
editor.setOptions({
highlightActiveLine: false,
showPrintMargin: false,

View File

@@ -1,71 +1,7 @@
use std::path::Path;
use theme::load_file_contents;
//! 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");
/// Integration of a JavaScript editor for playpens.
/// Uses the Ace editor: https://ace.c9.io/.
/// The Ace editor itself, the mode, and the theme files are the
/// generated minified no conflict versions.
///
/// The `PlaypenEditor` struct should be used instead of the static variables because
/// the `new()` method
/// will look if the user has an editor directory in his source folder and use
/// the users editor instead
/// of the default.
///
/// You should exceptionnaly use the static variables only if you need the
/// default editor even if the
/// user has specified another editor.
pub struct PlaypenEditor {
pub js: Vec<u8>,
pub ace_js: Vec<u8>,
pub mode_rust_js: Vec<u8>,
pub theme_dawn_js: Vec<u8>,
pub theme_tomorrow_night_js: Vec<u8>,
}
impl PlaypenEditor {
pub fn new(src: &Path) -> Self {
let mut editor = PlaypenEditor {
js: JS.to_owned(),
ace_js: ACE_JS.to_owned(),
mode_rust_js: MODE_RUST_JS.to_owned(),
theme_dawn_js: THEME_DAWN_JS.to_owned(),
theme_tomorrow_night_js: THEME_TOMORROW_NIGHT_JS.to_owned(),
};
// Check if the given path exists
if !src.exists() || !src.is_dir() {
return editor;
}
// Check for individual files if they exist
{
let files = vec![
(src.join("editor.js"), &mut editor.js),
(src.join("ace.js"), &mut editor.ace_js),
(src.join("mode-rust.js"), &mut editor.mode_rust_js),
(src.join("theme-dawn.js"), &mut editor.theme_dawn_js),
(src.join("theme-tomorrow_night.js"), &mut editor.theme_tomorrow_night_js),
];
for (filename, dest) in files {
if !filename.exists() {
continue;
}
if let Err(e) = load_file_contents(&filename, dest) {
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
}
}
}
editor
}
}

10
src/theme/searcher/elasticlunr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
src/theme/searcher/mark.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +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");

View File

@@ -0,0 +1,464 @@
window.search = window.search || {};
(function search(search) {
// Search functionality
//
// You can use !hasFocus() to prevent keyhandling in your key
// event handlers while the user is typing his search.
if (!Mark || !elasticlunr) {
return;
}
var searchbar = document.getElementById('searchbar'),
searchbar_outer = document.getElementById('searchbar-outer'),
searchresults = document.getElementById('searchresults'),
searchresults_outer = document.getElementById('searchresults-outer'),
searchresults_header = document.getElementById('searchresults-header'),
searchicon = document.getElementById('search-toggle'),
content = document.getElementById('content'),
searchindex = null,
resultsoptions = {
teaser_word_count: 30,
limit_results: 30,
},
searchoptions = {
bool: "AND",
expand: true,
fields: {
title: {boost: 1},
body: {boost: 1},
breadcrumbs: {boost: 0}
}
},
mark_exclude = [],
marker = new Mark(content),
current_searchterm = "",
URL_SEARCH_PARAM = 'search',
URL_MARK_PARAM = 'highlight',
teaser_count = 0,
SEARCH_HOTKEY_KEYCODE = 83,
ESCAPE_KEYCODE = 27,
DOWN_KEYCODE = 40,
UP_KEYCODE = 38,
SELECT_KEYCODE = 13;
function hasFocus() {
return searchbar === document.activeElement;
}
function removeChildren(elem) {
while (elem.firstChild) {
elem.removeChild(elem.firstChild);
}
}
// Helper to parse a url into its building blocks.
function parseURL(url) {
var a = document.createElement('a');
a.href = url;
return {
source: url,
protocol: a.protocol.replace(':',''),
host: a.hostname,
port: a.port,
params: (function(){
var ret = {};
var seg = a.search.replace(/^\?/,'').split('&');
var len = seg.length, i = 0, s;
for (;i<len;i++) {
if (!seg[i]) { continue; }
s = seg[i].split('=');
ret[s[0]] = s[1];
}
return ret;
})(),
file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
hash: a.hash.replace('#',''),
path: a.pathname.replace(/^([^/])/,'/$1')
};
}
// Helper to recreate a url string from its building blocks.
function renderURL(urlobject) {
var url = urlobject.protocol + "://" + urlobject.host;
if (urlobject.port != "") {
url += ":" + urlobject.port;
}
url += urlobject.path;
var joiner = "?";
for(var prop in urlobject.params) {
if(urlobject.params.hasOwnProperty(prop)) {
url += joiner + prop + "=" + urlobject.params[prop];
joiner = "&";
}
}
if (urlobject.hash != "") {
url += "#" + urlobject.hash;
}
return url;
}
// Helper to escape html special chars for displaying the teasers
var escapeHTML = (function() {
var MAP = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&#34;',
"'": '&#39;'
};
var repl = function(c) { return MAP[c]; };
return function(s) {
return s.replace(/[&<>'"]/g, repl);
};
})();
function formatSearchMetric(count, searchterm) {
if (count == 1) {
return count + " search result for '" + searchterm + "':";
} else if (count == 0) {
return "No search results for '" + searchterm + "'.";
} else {
return count + " search results for '" + searchterm + "':";
}
}
function formatSearchResult(result, searchterms) {
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
teaser_count++;
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
var url = result.ref.split("#");
if (url.length == 1) { // no anchor found
url.push("");
}
return '<a href="' + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
+ teaser + '</span>';
}
function makeTeaser(body, searchterms) {
// The strategy is as follows:
// First, assign a value to each word in the document:
// Words that correspond to search terms (stemmer aware): 40
// Normal words: 2
// First word in a sentence: 8
// Then use a sliding window with a constant number of words and count the
// sum of the values of the words within the window. Then use the window that got the
// maximum sum. If there are multiple maximas, then get the last one.
// Enclose the terms in <em>.
var stemmed_searchterms = searchterms.map(function(w) {
return elasticlunr.stemmer(w.toLowerCase());
});
var searchterm_weight = 40;
var weighted = []; // contains elements of ["word", weight, index_in_document]
// split in sentences, then words
var sentences = body.toLowerCase().split('. ');
var index = 0;
var value = 0;
var searchterm_found = false;
for (var sentenceindex in sentences) {
var words = sentences[sentenceindex].split(' ');
value = 8;
for (var wordindex in words) {
var word = words[wordindex];
if (word.length > 0) {
for (var searchtermindex in stemmed_searchterms) {
if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) {
value = searchterm_weight;
searchterm_found = true;
}
};
weighted.push([word, value, index]);
value = 2;
}
index += word.length;
index += 1; // ' ' or '.' if last word in sentence
};
index += 1; // because we split at a two-char boundary '. '
};
if (weighted.length == 0) {
return body;
}
var window_weight = [];
var window_size = Math.min(weighted.length, resultsoptions.teaser_word_count);
var cur_sum = 0;
for (var wordindex = 0; wordindex < window_size; wordindex++) {
cur_sum += weighted[wordindex][1];
};
window_weight.push(cur_sum);
for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
cur_sum -= weighted[wordindex][1];
cur_sum += weighted[wordindex + window_size][1];
window_weight.push(cur_sum);
};
if (searchterm_found) {
var max_sum = 0;
var max_sum_window_index = 0;
// backwards
for (var i = window_weight.length - 1; i >= 0; i--) {
if (window_weight[i] > max_sum) {
max_sum = window_weight[i];
max_sum_window_index = i;
}
};
} else {
max_sum_window_index = 0;
}
// add <em/> around searchterms
var teaser_split = [];
var index = weighted[max_sum_window_index][2];
for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) {
var word = weighted[i];
if (index < word[2]) {
// missing text from index to start of `word`
teaser_split.push(body.substring(index, word[2]));
index = word[2];
}
if (word[1] == searchterm_weight) {
teaser_split.push("<em>")
}
index = word[2] + word[0].length;
teaser_split.push(body.substring(word[2], index));
if (word[1] == searchterm_weight) {
teaser_split.push("</em>")
}
};
return teaser_split.join('');
}
function init() {
resultsoptions = window.search.resultsoptions;
searchoptions = window.search.searchoptions;
searchindex = elasticlunr.Index.load(window.search.index);
// Set up events
searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false);
document.addEventListener('keydown', function (e) { globalKeyHandler(e); }, false);
// If the user uses the browser buttons, do the same as if a reload happened
window.onpopstate = function(e) { doSearchOrMarkFromUrl(); };
// If reloaded, do the search or mark again, depending on the current url parameters
doSearchOrMarkFromUrl();
}
function unfocusSearchbar() {
// hacky, but just focusing a div only works once
var tmp = document.createElement('input');
tmp.setAttribute('style', 'position: absolute; opacity: 0;');
searchicon.appendChild(tmp);
tmp.focus();
tmp.remove();
}
// On reload or browser history backwards/forwards events, parse the url and do search or mark
function doSearchOrMarkFromUrl() {
// Check current URL for search request
var url = parseURL(window.location.href);
if (url.params.hasOwnProperty(URL_SEARCH_PARAM)
&& url.params[URL_SEARCH_PARAM] != "") {
showSearch(true);
searchbar.value = decodeURIComponent(
(url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20'));
searchbarKeyUpHandler(); // -> doSearch()
} else {
showSearch(false);
}
if (url.params.hasOwnProperty(URL_MARK_PARAM)) {
var words = url.params[URL_MARK_PARAM].split(' ');
marker.mark(words, {
exclude: mark_exclude
});
var markers = document.querySelectorAll("mark");
function hide() {
for (var i = 0; i < markers.length; i++) {
markers[i].classList.add("fade-out");
window.setTimeout(function(e) { marker.unmark(); }, 300);
}
}
for (var i = 0; i < markers.length; i++) {
markers[i].addEventListener('click', hide);
}
}
}
// Eventhandler for keyevents on `document`
function globalKeyHandler(e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (e.keyCode == ESCAPE_KEYCODE) {
e.preventDefault();
searchbar.classList.remove("active");
setSearchUrlParameters("",
(searchbar.value.trim() != "") ? "push" : "replace");
if (hasFocus()) {
unfocusSearchbar();
}
showSearch(false);
marker.unmark();
return;
}
if (!hasFocus() && e.keyCode == SEARCH_HOTKEY_KEYCODE) {
e.preventDefault();
showSearch(true);
window.scrollTo(0, 0);
searchbar.focus();
return;
}
if (hasFocus() && e.keyCode == DOWN_KEYCODE) {
e.preventDefault();
unfocusSearchbar();
searchresults.children('li').first().classList.add("focus");
return;
}
if (!hasFocus() && (e.keyCode == DOWN_KEYCODE
|| e.keyCode == UP_KEYCODE
|| e.keyCode == SELECT_KEYCODE)) {
// not `:focus` because browser does annoying scrolling
var current_focus = search.searchresults.find("li.focus");
if (current_focus.length == 0) return;
e.preventDefault();
if (e.keyCode == DOWN_KEYCODE) {
var next = current_focus.next()
if (next.length > 0) {
current_focus.classList.remove("focus");
next.classList.add("focus");
}
} else if (e.keyCode == UP_KEYCODE) {
current_focus.classList.remove("focus");
var prev = current_focus.prev();
if (prev.length == 0) {
searchbar.focus();
} else {
prev.classList.add("focus");
}
} else {
window.location = current_focus.children('a').attr('href');
}
}
}
function showSearch(yes) {
if (yes) {
searchbar_outer.style.display = 'block';
content.style.display = 'none';
searchicon.setAttribute('aria-expanded', 'true');
} else {
content.style.display = 'block';
searchbar_outer.style.display = 'none';
searchresults_outer.style.display = 'none';
searchbar.value = '';
removeChildren(searchresults);
searchicon.setAttribute('aria-expanded', 'false');
}
}
function showResults(yes) {
if (yes) {
searchbar_outer.style.display = 'block';
content.style.display = 'none';
searchresults_outer.style.display = 'block';
} else {
content.style.display = 'block';
searchresults_outer.style.display = 'none';
}
}
// Eventhandler for search icon
function searchIconClickHandler() {
if (searchbar_outer.style.display === 'block') {
showSearch(false);
} else {
showSearch(true);
window.scrollTo(0, 0);
searchbar.focus();
}
}
// Eventhandler for keyevents while the searchbar is focused
function searchbarKeyUpHandler() {
var searchterm = searchbar.value.trim();
if (searchterm != "") {
searchbar.classList.add("active");
doSearch(searchterm);
} else {
searchbar.classList.remove("active");
showResults(false);
removeChildren(searchresults);
}
setSearchUrlParameters(searchterm, "push_if_new_search_else_replace");
// Remove marks
marker.unmark();
}
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor .
// `action` can be one of "push", "replace", "push_if_new_search_else_replace"
// and replaces or pushes a new browser history item.
// "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
function setSearchUrlParameters(searchterm, action) {
var url = parseURL(window.location.href);
var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM);
if (searchterm != "" || action == "push_if_new_search_else_replace") {
url.params[URL_SEARCH_PARAM] = searchterm;
delete url.params[URL_MARK_PARAM];
url.hash = "";
} else {
delete url.params[URL_SEARCH_PARAM];
}
// A new search will also add a new history item, so the user can go back
// to the page prior to searching. A updated search term will only replace
// the url.
if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) {
history.pushState({}, document.title, renderURL(url));
} else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) {
history.replaceState({}, document.title, renderURL(url));
}
}
function doSearch(searchterm) {
// Don't search the same twice
if (current_searchterm == searchterm) { return; }
else { current_searchterm = searchterm; }
if (searchindex == null) { return; }
// Do the actual search
var results = searchindex.search(searchterm, searchoptions);
var resultcount = Math.min(results.length, resultsoptions.limit_results);
// Display search metrics
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
// Clear and insert results
var searchterms = searchterm.split(' ');
removeChildren(searchresults);
for(var i = 0; i < resultcount ; i++){
var resultElem = document.createElement('li');
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
searchresults.appendChild(resultElem);
}
// Display results
showResults(true);
}
init();
// Exported functions
search.hasFocus = hasFocus;
})(window.search);

File diff suppressed because one or more lines are too long

View File

@@ -9,3 +9,4 @@
@import 'themes'
@import 'print'
@import 'tooltip'
@import 'searchbar'

View File

@@ -1,10 +1,12 @@
html, body {
html {
font-family: "Open Sans", sans-serif
color: #333
}
body {
margin: 0;
font-size: 1rem;
overflow-x: hidden;
}
code {
@@ -33,6 +35,16 @@ h4, h5 { margin-top: 2em }
.header + .header h3, .header + .header h4, .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 {
display: inline-block;
content: "»";
margin-left: -30px;
width: 30px;
}
table {
margin: 0 auto;
border-collapse: collapse;
@@ -46,3 +58,15 @@ table {
td { font-weight: 700; }
}
}
:not(.footnote-definition) + .footnote-definition,
.footnote-definition + :not(.footnote-definition) {
margin-top: 2em;
}
.footnote-definition {
font-size: 0.9em;
margin: 0.5em 0;
p { display: inline; }
}

View File

@@ -1,8 +1,16 @@
.menu-bar {
position: relative
height: 50px
#menu-bar {
position: -webkit-sticky
position: sticky
top: 0
z-index: 101
i {
& > #menu-bar-sticky-container {
display: flex
flex-wrap: wrap
transition: transform 0.5s, border-bottom-color 0.5s
}
i, .icon-button {
position: relative
margin: 0 10px
z-index: 10
@@ -12,9 +20,11 @@
&:hover { cursor: pointer }
}
}
.left-buttons { float: left }
.right-buttons { float: right }
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
transform: translateY(-60px);
}
.menu-title {
@@ -22,18 +32,11 @@
font-weight: 200
font-size: 20px
line-height: 50px
position: absolute
top: 0
left: 0
right: 0
bottom: 0
text-align: center
margin: 0
opacity: 0
transition: opacity 0.5s ease-in-out
}
.menu-bar:hover .menu-title {
opacity: 1
flex: 1
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
cursor: pointer;
}

View File

@@ -9,6 +9,7 @@
margin: 0
max-width: 150px
min-width: 90px
display: flex
justify-content: center
align-content: center
@@ -17,22 +18,38 @@
transition: color 0.5s
}
.mobile-nav-chapters { display: none }
.nav-chapters:hover { text-decoration: none }
.previous {
left: $sidebar-width + $page-padding
transition: left 0.5s
@media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
left: $page-padding
.nav-wrapper {
margin-top: 50px
display: none
}
.mobile-nav-chapters {
font-size: 2.5em
text-align: center
text-decoration: none
width: 90px
border-radius: 5px
}
.previous {
float: left
}
.next {
float: right
right: $page-padding
}
@media only screen and (max-width: $page-plus-sidebar-width) {
.nav-wide-wrapper { display: none }
.nav-wrapper { display: block }
}
@media only screen and (max-width: $page-plus-sidebar-width + $sidebar-width) {
.sidebar-visible {
.nav-wide-wrapper { display: none }
.nav-wrapper { display: block }
}
}
.next { right: $page-padding }
.sidebar-hidden .previous {
left: $page-padding
}
.sidebar-visible .previous {
left: $sidebar-width + $page-padding
}

View File

@@ -1,25 +1,19 @@
@require 'variables'
.page-wrapper {
padding-left: $sidebar-width
box-sizing: border-box
min-height: 100%
left: 0
position: absolute
right: 0
top: 0
bottom: 0
// Animation: slide away
transition: padding-left 0.5s
@media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
padding-left: 0
}
}
.sidebar-hidden .page-wrapper {
padding-left: 0
transition: padding-left 0.5s, margin-left 0.5s, left 0.5s
}
.sidebar-visible .page-wrapper {
padding-left: $sidebar-width
left: $sidebar-width
}
.page {
@@ -28,11 +22,21 @@
}
.content {
margin-left: auto
margin-right:auto
max-width: 750px
position: relative
top: 0
bottom: 0
overflow-y: auto
right: 0
left: 0
padding: 0 15px
padding-bottom: 50px
main {
margin-left: auto
margin-right: auto
max-width: $content-max-width
}
a {
text-decoration: none;
&:hover { text-decoration: underline; }
@@ -40,3 +44,8 @@
img { max-width: 100%; }
}
.sidebar-visible .content {
position: absolute
top: 52px
}

View File

@@ -12,6 +12,10 @@
overflow-y: initial;
}
#page-wrapper.page-wrapper {
padding-left: 0px;
}
#content {
max-width: none;
margin: 0;

View File

@@ -0,0 +1,67 @@
@require 'variables'
#searchresults a {
text-decoration: none;
}
mark {
border-radius: 2px;
padding: 0 3px 1px 3px;
margin: 0 -3px -1px -3px;
transition: background-color 300ms linear;
}
.fade-out {
background-color: rgba(0,0,0,0) !important
}
.searchbar-outer {
display: none;
margin-left: auto;
margin-right: auto;
max-width: $content-max-width;
}
#searchbar {
display: block;
width: 100%;
margin: 5px auto 0px auto;
padding: 10px 16px;
transition: box-shadow 300ms ease-in-out;
}
.searchresults-header {
font-weight: bold;
font-size: 1em;
padding: 18px 0 0 5px;
}
.searchresults-outer {
display: none;
margin-left: auto;
margin-right: auto;
max-width: $content-max-width;
}
ul#searchresults {
list-style: none;
padding-left: 20px;
li {
margin: 10px 0px;
padding: 2px;
border-radius: 2px;
}
span.teaser {
display: block;
clear: both;
margin: 5px 0 0 20px;
font-size: 0.8em;
}
span.teaser em {
font-weight: bold;
font-style: normal;
}
}

View File

@@ -11,13 +11,10 @@
font-size: 0.875em
box-sizing: border-box
-webkit-overflow-scrolling: touch
overscroll-behavior-y: contain;
// Animation: slide away
transition: left 0.5s
@media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
left: - $sidebar-width
}
transition: transform 0.5s
code {
line-height: 2em;
@@ -25,11 +22,7 @@
}
.sidebar-hidden .sidebar {
left: - $sidebar-width
}
.sidebar-visible .sidebar {
left: 0
transform: translateX(- $sidebar-width)
}
.chapter {
@@ -38,16 +31,19 @@
line-height: 2.2em
li a {
padding: 5px 0
display: block;
padding: 0
text-decoration: none
@media (-moz-touch-enabled: 1), (pointer: coarse) { padding: 5px 0; }
&:hover { text-decoration: none }
}
.spacer {
width: 100%
height: 3px
margin: 10px 0px
margin: 5px 0px
@media (-moz-touch-enabled: 1), (pointer: coarse) { margin: 10px 0; }
}
}

View File

@@ -1,5 +1,5 @@
.theme-popup {
position: relative
position: absolute
left: 10px
z-index: 1000;
@@ -8,11 +8,18 @@
font-size: 0.7em
.theme {
display: inline
border: 0
margin: 0
padding: 2px 10px
line-height: 25px
width: 100%
white-space: nowrap
text-align: left
cursor: pointer
color inherit
background: inherit;
font-size: inherit;
&:hover:first-child,
&:hover:last-child {
@@ -21,37 +28,4 @@
}
}
}
@media only screen and (max-width: 1250px) {
.nav-chapters {
display: none
}
.mobile-nav-chapters {
font-size: 2.5em
text-align: center
text-decoration: none
max-width: 150px
min-width: 90px
justify-content: center
align-content: center
position: relative
display: inline-block
margin-bottom: 50px
border-radius: 5px
}
.next {
float: right
}
.previous {
float: left
}
}
}

View File

@@ -9,6 +9,8 @@ $sidebar-non-existant = #5c6773
$sidebar-active = #ffb454
$sidebar-spacer = #2d334f
$scrollbar = $sidebar-fg
$icons = #737480
$icons-hover = #b7b9cc
@@ -27,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
$searchbar-border-color = #848484
$searchbar-bg = #424242
$searchbar-fg = #fff
$searchbar-shadow-color = #d4c89f
$searchresults-header-fg = #666
$searchresults-border-color = #888
$searchresults-li-bg = #252932
$search-mark-bg = #e3b171
@import 'base'

View File

@@ -12,9 +12,34 @@
}
}
.menu-bar {
margin: auto (- $page-padding);
& > #menu-bar-sticky-container {
background-color: $bg
border-bottom-color: $bg
border-bottom-width: 1px
border-bottom-style: solid
}
&.bordered > #menu-bar-sticky-container {
border-bottom-color: $table-border-color
}
}
$table-border-color
.sidebar {
background-color: $sidebar-bg
color: $sidebar-fg
&::-webkit-scrollbar {
background: $sidebar-bg;
}
&::-webkit-scrollbar-thumb {
background: $scrollbar;
}
}
.chapter li {
@@ -38,11 +63,14 @@
.nav-chapters,
.nav-chapters:visited,
.mobile-nav-chapters,
.mobile-nav-chapters:visited {
.mobile-nav-chapters:visited,
.menu-bar .icon-button,
.menu-bar a i {
color: $icons
}
.menu-bar i:hover,
.menu-bar .icon-button:hover,
.nav-chapters:hover,
.mobile-nav-chapters i:hover {
color: $icons-hover
@@ -56,7 +84,10 @@
background-color: $sidebar-bg
}
.content a:link, a:visited, a > .hljs {
#searchresults a,
.content a:link,
a:visited,
a > .hljs {
color: $links
}
@@ -64,6 +95,10 @@
color: $fg
background: $theme-popup-bg
border: 1px solid $theme-popup-border
margin: 0;
padding: 0;
list-style: none;
display: none;
.theme:hover { background-color: $theme-hover }
@@ -126,9 +161,62 @@
:hover { color: $sidebar-active; }
i { margin-left: 8px; }
button {
color: inherit;
background: transparent;
border: none;
cursor: inherit;
}
}
& > .result { margin-top: 10px; }
}
.icon-button {
border: none;
background: none;
padding: 0;
color: inherit;
i {
margin: 0;
}
}
::-webkit-scrollbar {
background: $bg;
}
::-webkit-scrollbar-thumb {
background: $scrollbar;
}
/* Search */
#searchbar {
border: 1px solid $searchbar-border-color;
border-radius: 3px;
background-color: $searchbar-bg;
color: $searchbar-fg
&:focus, &.active {
box-shadow: 0 0 3px $searchbar-shadow-color;
}
}
.searchresults-header {
color: $searchresults-header-fg;
}
.searchresults-outer {
border-bottom: 1px dashed $searchresults-border-color;
}
ul#searchresults li.focus {
background-color: $searchresults-li-bg;
}
mark {
background-color: $search-mark-bg;
}
}

View File

@@ -9,6 +9,8 @@ $sidebar-non-existant = #505254
$sidebar-active = #3473ad
$sidebar-spacer = #393939
$scrollbar = $sidebar-fg
$icons = #43484d
$icons-hover = #b3c0cc
@@ -27,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
$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
@import 'base'

View File

@@ -9,12 +9,14 @@ $sidebar-non-existant = #aaaaaa
$sidebar-active = #008cff
$sidebar-spacer = #f4f4f4
$scrollbar = #cccccc
$icons = #cccccc
$icons-hover = #333333
$links = #4183c4
$inline-code-color = #6e6b5e;
$inline-code-color = #6e6b5e
$theme-popup-bg = #fafafa
$theme-popup-border = #cccccc
@@ -27,4 +29,13 @@ $table-border-color = darken($bg, 5%)
$table-header-bg = darken($bg, 20%)
$table-alternate-bg = darken($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #fafafa
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #666
$searchresults-border-color = #888
$searchresults-li-bg = #e4f2fe
$search-mark-bg = #a2cff5
@import 'base'

View File

@@ -9,6 +9,8 @@ $sidebar-non-existant = #505274
$sidebar-active = #2b79a2
$sidebar-spacer = #2d334f
$scrollbar = $sidebar-fg
$icons = #737480
$icons-hover = #b7b9cc
@@ -27,4 +29,13 @@ $table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #aeaec6
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #5f5f71
$searchresults-border-color = #5c5c68
$searchresults-li-bg = #242430
$search-mark-bg = #a2cff5
@import 'base'

View File

@@ -9,6 +9,8 @@ $sidebar-non-existant = #505254
$sidebar-active = #e69f67
$sidebar-spacer = #45373a
$scrollbar = $sidebar-fg
$icons = #737480
$icons-hover = #262625
@@ -27,4 +29,13 @@ $table-border-color = darken($bg, 5%)
$table-header-bg = #b3a497
$table-alternate-bg = darken($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #fafafa
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #666
$searchresults-border-color = #888
$searchresults-li-bg = #dec2a2
$search-mark-bg = #e69f67
@import 'base'

View File

@@ -1,3 +1,4 @@
$sidebar-width = 300px
$page-padding = 15px
$max-page-width-with-hidden-sidebar = 1060px
$content-max-width = 750px
$page-plus-sidebar-width = $content-max-width + $sidebar-width + $page-padding * 2

View File

@@ -1,29 +1,42 @@
use std::path::{Path, PathBuf, Component};
use errors::*;
use std::io::Read;
use std::fs::{self, File};
use std::io::{Read, 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 file = match File::open(path) {
Ok(f) => f,
Err(e) => {
debug!("[*]: Failed to open {:?}", path);
bail!(e);
},
};
let mut content = String::new();
if let Err(e) = file.read_to_string(&mut content) {
debug!("[*]: Failed to read {:?}", path);
bail!(e);
}
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;
path.chars()
.map(|ch| if is_separator(ch) { '/' } else { ch })
.collect::<String>()
}
/// Write the given data to a file, creating it first if necessary
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())
}
/// Takes a path and returns a path containing just enough `../` to point to
/// the root of the given path.
///
@@ -44,11 +57,10 @@ pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
///
/// **note:** it's not very fool-proof, if you find a situation where
/// it doesn't return the correct path.
/// Consider [submitting a new issue](https://github.com/azerupi/mdBook/issues)
/// or a [pull-request](https://github.com/azerupi/mdBook/pulls) to improve it.
/// Consider [submitting a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
/// or a [pull-request](https://github.com/rust-lang-nursery/mdBook/pulls) to improve it.
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
debug!("[fn]: path_to_root");
debug!("path_to_root");
// Remove filename and add "../" for every directory
path.into()
@@ -59,35 +71,30 @@ pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
match c {
Component::Normal(_) => s.push_str("../"),
_ => {
debug!("[*]: Other path component... {:?}", c);
},
debug!("Other path component... {:?}", c);
}
}
s
})
}
/// This function creates a file and returns it. But before creating the file
/// it checks every directory in the path to see if it exists,
/// and if it does not it will be created.
pub fn create_file(path: &Path) -> Result<File> {
debug!("[fn]: create_file");
debug!("Creating {}", path.display());
// Construct path
if let Some(p) = path.parent() {
debug!("Parent directory is: {:?}", p);
trace!("Parent directory is: {:?}", p);
fs::create_dir_all(p)?;
}
debug!("[*]: Create file: {:?}", path);
File::create(path).map_err(|e| e.into())
}
/// Removes all the content of a directory but not the directory itself
pub fn remove_dir_content(dir: &Path) -> Result<()> {
for item in fs::read_dir(dir)? {
if let Ok(item) = item {
@@ -102,22 +109,28 @@ pub fn remove_dir_content(dir: &Path) -> Result<()> {
Ok(())
}
///
///
/// Copies all files of a directory to another one except the files
/// with the extensions given in the `ext_blacklist` array
pub fn copy_files_except_ext(
from: &Path,
to: &Path,
recursive: bool,
ext_blacklist: &[&str],
) -> Result<()> {
debug!(
"Copying all files from {} to {} (blacklist: {:?})",
from.display(),
to.display(),
ext_blacklist
);
pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blacklist: &[&str])
-> Result<()> {
debug!("[fn] copy_files_except_ext");
// Check that from and to are different
if from == to {
return Ok(());
}
debug!("[*] Loop");
for entry in fs::read_dir(from)? {
let entry = entry?;
debug!("[*] {:?}", entry.path());
let metadata = entry.metadata()?;
// If the entry is a dir and the recursive option is enabled, call itself
@@ -125,60 +138,69 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl
if entry.path() == to.to_path_buf() {
continue;
}
debug!("[*] is dir");
// check if output dir already exists
if !to.join(entry.file_name()).exists() {
fs::create_dir(&to.join(entry.file_name()))?;
}
copy_files_except_ext(&from.join(entry.file_name()), &to.join(entry.file_name()), true, ext_blacklist)?;
copy_files_except_ext(
&from.join(entry.file_name()),
&to.join(entry.file_name()),
true,
ext_blacklist,
)?;
} else if metadata.is_file() {
// Check if it is in the blacklist
if let Some(ext) = entry.path().extension() {
if ext_blacklist.contains(&ext.to_str().unwrap()) {
continue;
}
}
debug!("[*] creating path for file: {:?}",
&to.join(entry
.path()
.file_name()
.expect("a file should have a file name...")));
debug!(
"creating path for file: {:?}",
&to.join(
entry
.path()
.file_name()
.expect("a file should have a file name...")
)
);
info!("[*] Copying file: {:?}\n to {:?}",
entry.path(),
&to.join(entry
.path()
.file_name()
.expect("a file should have a file name...")));
fs::copy(entry.path(),
&to.join(entry
.path()
.file_name()
.expect("a file should have a file name...")))?;
debug!(
"Copying {:?} to {:?}",
entry.path(),
&to.join(
entry
.path()
.file_name()
.expect("a file should have a file name...")
)
);
fs::copy(
entry.path(),
&to.join(
entry
.path()
.file_name()
.expect("a file should have a file name..."),
),
)?;
}
}
Ok(())
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
// tests
#[cfg(test)]
mod tests {
extern crate tempdir;
extern crate tempfile;
use super::copy_files_except_ext;
use std::fs;
#[test]
fn copy_files_except_ext_test() {
let tmp = match tempdir::TempDir::new("") {
let tmp = match tempfile::TempDir::new() {
Ok(t) => t,
Err(_) => panic!("Could not create a temp dir"),
};
@@ -216,7 +238,7 @@ mod tests {
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
Err(e) => panic!("Error while executing the function:\n{:?}", e),
Ok(_) => {},
Ok(_) => {}
}
// Check if the correct files where created
@@ -235,6 +257,5 @@ mod tests {
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
panic!("output/sub_dir/file.png should exist")
}
}
}

View File

@@ -1,13 +1,74 @@
pub mod fs;
#![allow(missing_docs)] // FIXME: Document this
pub mod fs;
mod string;
use errors::Error;
use regex::Regex;
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
OPTION_ENABLE_TABLES};
use pulldown_cmark::{Parser, Event, Tag, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
use std::borrow::Cow;
pub use self::string::{RangeArgument, take_lines};
///
///
/// Wrapper around the pulldown-cmark parser and renderer to render markdown
/// Replaces multiple consecutive whitespace characters with a single space character.
pub fn collapse_whitespace<'a>(text: &'a str) -> Cow<'a, 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
pub fn normalize_id(content: &str) -> String {
let mut ret = content
.chars()
.filter_map(|ch| {
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
Some(ch.to_ascii_lowercase())
} else if ch.is_whitespace() {
Some('-')
} 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
}
/// Generate an ID for use with anchors which is derived from a "normalised"
/// string.
pub fn id_from_content(content: &str) -> String {
let mut content = content.to_string();
// Skip any tags or html-encoded stuff
const REPL_SUB: &[&str] = &["<em>",
"</em>",
"<code>",
"</code>",
"<strong>",
"</strong>",
"&lt;",
"&gt;",
"&amp;",
"&#39;",
"&quot;"];
for sub in REPL_SUB {
content = content.replace(sub, "");
}
// Remove spaces and hashes indicating a header
let trimmed = content.trim().trim_left_matches('#').trim();
normalize_id(trimmed)
}
/// 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);
@@ -17,7 +78,8 @@ pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
let p = Parser::new_ext(text, opts);
let mut converter = EventQuoteConverter::new(curly_quotes);
let events = p.map(clean_codeblock_headers).map(|event| converter.convert(event));
let events = p.map(clean_codeblock_headers)
.map(|event| converter.convert(event));
html::push_html(&mut s, events);
s
@@ -30,7 +92,10 @@ struct EventQuoteConverter {
impl EventQuoteConverter {
fn new(enabled: bool) -> Self {
EventQuoteConverter { enabled: enabled, convert_text: true }
EventQuoteConverter {
enabled: enabled,
convert_text: true,
}
}
fn convert<'a>(&mut self, event: Event<'a>) -> Event<'a> {
@@ -39,17 +104,17 @@ impl EventQuoteConverter {
}
match event {
Event::Start(Tag::CodeBlock(_)) |
Event::Start(Tag::Code) => {
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
self.convert_text = false;
event
},
Event::End(Tag::CodeBlock(_)) |
Event::End(Tag::Code) => {
}
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
self.convert_text = true;
event
},
Event::Text(ref text) if self.convert_text => Event::Text(Cow::from(convert_quotes_to_curly(text))),
}
Event::Text(ref text) if self.convert_text => {
Event::Text(Cow::from(convert_quotes_to_curly(text)))
}
_ => event,
}
}
@@ -58,13 +123,10 @@ impl EventQuoteConverter {
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();
let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect();
Event::Start(Tag::CodeBlock(Cow::from(info)))
},
}
_ => event,
}
}
@@ -74,20 +136,40 @@ fn convert_quotes_to_curly(original_text: &str) -> String {
// We'll consider the start to be "whitespace".
let mut preceded_by_whitespace = true;
original_text
.chars()
.map(|original_char| {
let converted_char = match original_char {
'\'' => if preceded_by_whitespace { '' } else { '' },
'"' => if preceded_by_whitespace { '“' } else { '”' },
_ => original_char,
};
original_text.chars()
.map(|original_char| {
let converted_char = match original_char {
'\'' => {
if preceded_by_whitespace {
''
} else {
''
}
}
'"' => {
if preceded_by_whitespace {
'“'
} else {
'”'
}
}
_ => original_char,
};
preceded_by_whitespace = original_char.is_whitespace();
preceded_by_whitespace = original_char.is_whitespace();
converted_char
})
.collect()
converted_char
})
.collect()
}
/// Prints a "backtrace" of some `Error`.
pub fn log_backtrace(e: &Error) {
error!("Error: {}", e);
for cause in e.iter().skip(1) {
error!("\tCaused By: {}", cause);
}
}
#[cfg(test)]
@@ -146,7 +228,8 @@ more text with spaces
```
"#;
let expected = r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
let expected =
r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
@@ -159,7 +242,8 @@ more text with spaces
```
"#;
let expected = r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre>
let expected =
r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
@@ -168,7 +252,7 @@ more text with spaces
#[test]
fn rust_code_block_without_properties_has_proper_html_class() {
let input = r#"
```rust
```rust
```
"#;
@@ -183,7 +267,29 @@ more text with spaces
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}
}
mod html_munging {
use super::super::{id_from_content, normalize_id};
#[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");
}
#[test]
fn it_normalizes_ids() {
assert_eq!(normalize_id("`--passes`: add more rustdoc passes"),
"a--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(""), "");
}
}
@@ -192,12 +298,14 @@ 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]

73
src/utils/string.rs Normal file
View File

@@ -0,0 +1,73 @@
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
use itertools::Itertools;
// 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)
}
}
/// 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);
let mut lines = s.lines().skip(start);
match range.end() {
Some(&end) => lines.take(end.checked_sub(start).unwrap_or(0)).join("\n"),
None => lines.join("\n"),
}
}
#[cfg(test)]
mod tests {
use super::take_lines;
#[test]
fn take_lines_test() {
let s = "Lorem\nipsum\ndolor\nsit\namet";
assert_eq!(take_lines(s, 1..3), "ipsum\ndolor");
assert_eq!(take_lines(s, 3..), "sit\namet");
assert_eq!(take_lines(s, ..3), "Lorem\nipsum\ndolor");
assert_eq!(take_lines(s, ..), s);
// corner cases
assert_eq!(take_lines(s, 4..3), "");
assert_eq!(take_lines(s, ..100), s);
}
}

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