Compare commits

..

537 Commits

Author SHA1 Message Date
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
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
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
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
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
Mathieu David
441bcb5963 (cargo-release) version 0.0.25 2017-09-08 20:20:48 +02:00
Mathieu David
84ef4d2617 preserve dashes when generating anchors and trim whitespace 2017-09-08 19:59:04 +02:00
Daan van Berkel
bd30cae17e Correct inline mathematics delimiters
This fixes #424
2017-09-08 09:21:20 +02:00
Mathieu David
6f0b67f44f (cargo-release) start next development iteration 0.0.25-alpha.0 2017-09-07 23:32:57 +02:00
Mathieu David
abf86eefd9 (cargo-release) version 0.0.24 2017-09-07 23:31:57 +02:00
Mathieu David
016ec8836c Merge pull request #415 from azerupi/fix-print-title
Fix the print title that was using the title from the last rendered chapter
2017-09-07 23:29:54 +02:00
Mathieu David
881a1b39ff Remove the logic in handlebars and expose the 3 different titles in the handlebars variables 2017-09-07 23:19:22 +02:00
Mathieu David
a1e58229b2 Merge pull request #418 from behnam/manifest
[Cargo.toml] Fix package.exclude warnings
2017-09-07 22:46:49 +02:00
Mathieu David
276eab095c Merge pull request #427 from budziq/spurious_reloads
Do not trigger spurious watch events on Write and Remove
2017-09-07 22:45:12 +02:00
Mathieu David
f4513d3b5c Merge pull request #419 from behnam/nested
Fix heading links in nested pages
2017-09-07 22:39:51 +02:00
Michal Budzynski
570ce6681f Do not trigger spurious watch events on Write and Remove 2017-09-06 22:33:56 +02:00
Behnam Esfahbod
ddee839d9c [renderer] Err on bad file names, instead of panic
Addressing the review comments.
2017-09-06 02:25:10 -07:00
Behnam Esfahbod
99945542ca [renderer] Add normalize_path()
On the web, the normalized path separator is forward-slash (`/`), so we
use the built-in `is_separator()` method to replace any path separator
with the forward-slash, to ensure consistent output on unix and windows
machines.
2017-09-06 00:52:17 -07:00
Behnam Esfahbod
956a5cc7fd Fix heading links in nested pages
Plus fixing the whitespace chars not being replaced by hyphen.

Also expand tests for link creations, and add test for nested pages.

Fixes <https://github.com/azerupi/mdBook/issues/416>
Fixes <https://github.com/azerupi/mdBook/issues/417>
2017-09-06 00:52:17 -07:00
Behnam Esfahbod
cef62ec42e Fix build and test warnings
Move non-test test module files into their own directories to prevent
cargo from running them as tests. Then suppress the left-over warnings.

Move *dummy book* code and data into a shared folder, and leave the rest
of helper utilities (one function) in the original module.
2017-09-06 00:52:17 -07:00
Behnam Esfahbod
b1362bfa06 [watch] Fix build warnings 2017-09-06 00:52:15 -07:00
Behnam Esfahbod
a529ca5e65 [Cargo.toml] Fix package.exclude warnings
IIUC, the existing exclude rule has meant to do what it will do in the
future (gitignore-like matching, not glob-only matching). This fix makes
the rule to do what it was expected to do, and is forward-compatible,
therefore fixing the warning messages.
2017-09-06 00:52:15 -07:00
Michal Budzynski
6bc3039b4f Both static and ACE editable snippets have optional play button
- list of available crates is dynamically loaded from play.rust-lang.org
- play button is enabled only if crates used in snippet are available on playground
- ACE editor's play button is dynamically updated on each text change
- `no_run` is honored by always disabling the play button
- minor cleanups
2017-09-06 00:18:24 +02:00
Michal Budzynski
cd90fdd407 first prototype of play-button enabling only if crate list supported
also minor refactor of clipboard handling
TODO:
- `no_run` support
- test with ACE
- disable play button with tooltip instead of hiding
2017-09-06 00:18:24 +02:00
Mathieu David
a6a7c95c78 Merge pull request #343 from budziq/appveyor_css
fixed `cargo build --features=regenerate-css` on win 10 / nightly
2017-09-05 19:33:57 +02:00
Mathieu David
0a4a2b66da Fix the print title that was using the title from the last rendered chapter. Fixes #414 2017-09-01 08:22:24 +02:00
Mathieu David
2f3c14d609 ignore vscode dir in git 2017-09-01 08:22:01 +02:00
Mathieu David
ebcf1e495d Merge pull request #413 from behnam/gitignore
[book] Prevent over-matching in gitignore rule
2017-08-31 19:27:38 +02:00
Behnam Esfahbod
40a4840867 [book] Prevent over-matching in gitignore rule
To only ignore the output destination (default: `book`) and no other
file/directory with the same name under the mdbook root, we should
prefix the gitignore rule with a leading slash (default: `/book`).
2017-08-30 16:01:45 -07:00
steveklabnik
313f9b9403 Bump version in Cargo.toml for next release 2017-08-29 12:53:32 -04:00
Steve Klabnik
d94c097495 Merge pull request #411 from steveklabnik/master
Fix toml problem and release 0.0.23
2017-08-29 12:51:11 -04:00
steveklabnik
b7372d3bf2 Fix toml problem and release 0.0.23
The toml crate removed its serde dependency in 0.3, but in 0.4, it
went away. Cargo didn't warn on this, but now it does. As such, we
need a release so that rust's build doesn't warn constantly.
2017-08-29 10:53:20 -04:00
Michal Budzynski
d0a6aea3aa fixed cargo build --features=regenerate-css on windows 10
also added cargo build --features=regenerate-css to appveyor.yml
2017-08-12 13:20:26 +02:00
Mathieu David
a1926bbe8e Update documentation for mathjax 2017-08-11 13:55:11 +02:00
Mathieu David
094c1e7a52 Add documentation about additional-js 2017-08-11 13:55:11 +02:00
Mathieu David
4528e24080 Improve wording of documentation 2017-08-11 13:55:11 +02:00
Michal Budzynski
31983cae6c fixed missing playpen css class when codeblock properties had whitespace 2017-08-11 12:39:27 +02:00
Michal Budzynski
ddf31dcc08 Fixed mdbook test for {{#playpen file.rs}}
- now `mdbook test` does full link expansion to temp file prior to running
- also minor reformat and cleanup of `HtmlHandlebars::render_item`
2017-08-07 21:42:28 +02:00
Michal Budzynski
c36eca15c2 renamed Playpen to Playground in ajax error handling 2017-08-06 17:10:52 +02:00
Mathieu David
798225bcdc Merge pull request #393 from budziq/playpen_errors
handle play.rust-lang.org communication errors in playpens
2017-08-06 15:16:11 +02:00
Michal Budzynski
eed1a0a591 handle play.rust-lang.org communication errors in playpens
also add 15s communication timeout
2017-08-06 14:59:19 +02:00
Mathieu David
35a447d08a Merge pull request #338 from projektir/ace_editor
Ace editor
2017-08-05 12:24:12 +02:00
projektir
16aa545c5b Integrating Ace #247 2017-08-03 22:45:33 -04:00
projektir
6601dbdd61 Adding ace.js, Rust highlighter, and themes #247 2017-08-03 20:00:39 -04:00
Mathieu David
373e36ebfb Merge pull request #380 from ffissore/master
Fixed wrong filename when post processing html
2017-08-03 16:27:18 +02:00
Mathieu David
ef435825b0 Merge pull request #377 from Michael-F-Bryan/dry-themes
Make themes module more DRY
2017-08-03 14:06:45 +02:00
Michael Bryan
2f8d5ce263 Removed a lot of the repetition in Theme::new() 2017-08-03 07:01:52 +08:00
Michael Bryan
ce2d7153f7 removed some repetition from the themes module 2017-08-02 23:31:37 +08:00
Mathieu David
6628757d8e Merge pull request #374 from Michael-F-Bryan/moar-tests
High level integration tests
2017-08-02 17:01:40 +02:00
Michael Bryan
e2eb40bded Got some feedback from azerupi and added a DummyBook builder 2017-08-02 22:29:28 +08:00
Mathieu David
4f754a73ba Remove the empty introduction chapter 2017-08-02 15:04:03 +02:00
Mathieu David
ba719c00be Possible fix for automatic deployment to gh-pages (GH_TOKEN has been updated on Travis) 2017-08-02 12:58:59 +02:00
Mathieu David
64f6f78663 Merge pull request #385 from azerupi/readme-pin-ci
Add a note about version pinning in the README
2017-08-01 14:32:43 +02:00
Mathieu David
3b0d2d1238 Add a note about version pinning for people doing automatic deployments of their books 2017-08-01 14:16:39 +02:00
Mathieu David
6ab3d3da2a ignore MacOS .DS_Store temporary files 2017-08-01 13:55:51 +02:00
Michal Budzynski
ee29b9d5f6 added clone derives and made the separating space mandatory in links with paths 2017-08-01 13:50:12 +02:00
Michal Budzynski
d7ecb1a80c Rewrite of {{#}} links handling in preprocess module
- Replaced link parser with a Regex
- Implemented {{#include}} links
- Will display relatively nice error when cannot open {{#}} linked file
- Escaped links no longer render with escape char
- utils::fs::file_to_path no takes AsRef<Path>
- sorted export/mod in lib.rs
2017-08-01 13:50:12 +02:00
Michal Budzynski
f3f6b40ea9 Moved playpen.rs renderer helper to preprocess/links.rs module 2017-08-01 13:50:12 +02:00
Michał Budzyński
c482650e56 Merge pull request #384 from budziq/travis-rustup-beta
fix travis ci for beta channel
2017-08-01 01:39:29 +02:00
Michał Budzyński
5da75bc798 fix travis ci for beta channel
travis started to fail for beta channel.
2017-08-01 01:04:05 +02:00
Federico Fissore
07b80723b6 Fixed wrong filename when post processing html 2017-07-24 11:37:31 +02:00
Michael Bryan
0c3a2b80f8 Tested MDBook::init with custom args 2017-07-10 18:23:51 +08:00
Michael Bryan
29e00c0d97 Broke the integration tests out into individual sections 2017-07-10 18:17:19 +08:00
Michael Bryan
7e8819f4d2 Bye bye println() debugging 2017-07-09 22:08:57 +08:00
Michael Bryan
c90c0f7848 Updated the book testing to take into account #340 2017-07-09 20:02:31 +08:00
Michael Bryan
cd11035a69 trivial change to make travis run again 2017-07-09 19:59:29 +08:00
Michael Bryan
8b7c95e02f Made sure the rendered content actually contains the original text 2017-07-09 19:59:29 +08:00
Michael Bryan
e3f047a35d Added playpen and mdbook init tests 2017-07-09 19:59:29 +08:00
Michael Bryan
9fab267da1 Added some end-user "sanity" tests 2017-07-09 19:59:29 +08:00
Mathieu David
55e7e82e5c Merge pull request #340 from messense/feature/mdbook-test-library-path
Add library path argument for `mdbook test`
2017-07-08 23:27:28 +02:00
Mathieu David
325458c957 Merge pull request #369 from budziq/info_logger
Increased default logging level to info unless RUST_LOG is set
2017-07-08 19:55:59 +02:00
Michał Budzyński
9c21fe32c1 Merge pull request #373 from Michael-F-Bryan/appveyor
Updated appveyor.yml to use rustup for installing Rust
2017-07-08 11:25:28 +02:00
Michael Bryan
eaec9eff37 Updated appveyor.yml to use rustup for installing Rust 2017-07-08 12:47:27 +08:00
Michal Budzynski
287f539b7d Increased default logging level to info unless RUST_LOG is set 2017-06-28 23:37:03 +02:00
Mathieu David
a220528c15 Merge pull request #367 from messense/feature/fix-mdbook-test-error
Print stdout when `mdbook test` failed
2017-06-28 08:41:13 +02:00
messense
7c023e2d1d Add library path argument for mdbook test 2017-06-28 10:33:52 +08:00
messense
f2544e0707 Print stdout when mdbook test failed 2017-06-28 10:28:50 +08:00
Mathieu David
4974d2cfa1 Merge pull request #362 from budziq/nonopt_htmlconfig
Make HtmlConfig is no longer optional
2017-06-27 14:33:56 +02:00
Michal Budzynski
b1ca9cf5b5 HtmlConfig is no longer optional
`HtmlConfig` was both guaranteed to exist within `BookConfig`
and `expect`ed in few places.
This simplifies the API a little by representing the fact that
`HtmlConfig` is currently mandatory for proper mdBook binary operation.
2017-06-27 14:01:33 +02:00
Mathieu David
5a27207844 Merge pull request #366 from budziq/split_commands
Split commands to separate files and register conditional ones if required features enabled
2017-06-27 13:37:57 +02:00
Michal Budzynski
5e088d92c9 Merge remote-tracking branch 'upstream/master' into split_commands 2017-06-27 13:06:19 +02:00
Mathieu David
ea0c8ddea6 Merge pull request #364 from budziq/clippy_nits
Correct clippy complaints
2017-06-27 12:17:10 +02:00
Michal Budzynski
b3c9ba4555 Correct clippy nits 2017-06-27 09:08:58 +02:00
Michal Budzynski
7f51039f9a Rename and move the clap sub-command generation functions 2017-06-27 07:59:50 +02:00
Michal Budzynski
7799ce285e Do not use wildcard imports if not needed 2017-06-26 23:17:46 +02:00
Michal Budzynski
fe62d0c407 Merge remote-tracking branch 'upstream/master' into split_commands 2017-06-26 23:11:00 +02:00
Mathieu David
c9a117cc4e Merge pull request #361 from Michael-F-Bryan/error-chain
Add error-chain throughout the codebase
2017-06-26 17:07:51 +02:00
Mathieu David
13ab20ea49 Merge pull request #363 from budziq/opt_mathjax
Make MathJax support optional
2017-06-26 17:01:52 +02:00
Michal Budzynski
f3c8535870 Extracted mdbook test and mdbook init to separate files/modules 2017-06-26 01:24:33 +02:00
Michal Budzynski
35ed9fc286 corrected indentation in serve and watch subcommands definitions 2017-06-26 01:22:38 +02:00
Michal Budzynski
efdd0330c1 Extracted mdbook init to separate file/module 2017-06-26 01:02:32 +02:00
Michal Budzynski
4c78fdf431 Extracted mdbook build to separate file/module 2017-06-26 01:00:18 +02:00
Michal Budzynski
b09fdf07e4 Register serve and watch subcommands only if given features enabled 2017-06-26 00:43:28 +02:00
Michal Budzynski
5c524da3c2 Extracted mdbook watch to separate file/module 2017-06-25 23:44:28 +02:00
Michal Budzynski
99224f40d5 Extracted mdbook serve to separate file/module 2017-06-25 23:05:58 +02:00
Michael Bryan
83354ab24b Fixed up some unused-imports warnings 2017-06-25 14:21:23 +08:00
Michael Bryan
af05306046 Tried making sure travis installs all the musl tools when required 2017-06-25 14:11:43 +08:00
Michael Bryan
b796ee7c36 Made sure travis installs musl for the musl builds 2017-06-25 13:17:44 +08:00
Michal Budzynski
db94b3d839 Add minimal testing for the optional MathJax support 2017-06-25 00:39:57 +02:00
Michal Budzynski
f214c7108f Make MathJax support optional
to enable add following to book.toml
```toml
[output.html]
mathjax-support = true
```
2017-06-25 00:32:33 +02:00
Michael Bryan
2abebfb244 Removed the default error-chain features
On `x86_64-unknown-linux-musl` it looks like travis can't compile
the `backtrace-sys` crate because the `./configure` step fails.

The error message `./configure` gives is:

configure: error: in `/home/travis/build/azerupi/mdBook/target/x86_64-unknown-linux-musl/debug/build/backtrace-sys-204dc57c91e9a514/out':
configure: error: C compiler cannot create executables
2017-06-25 00:40:56 +08:00
Michael Bryan
fd821a5ead the binary now uses error-chain 2017-06-25 00:13:41 +08:00
Michael Bryan
487f5ce339 Added error-chain to the renderer module 2017-06-25 00:10:06 +08:00
Michael Bryan
1356e0f068 Added error-chain to the book and utils modules 2017-06-25 00:04:57 +08:00
Michael Bryan
0f93cd002b Added error-chain to the config files 2017-06-24 23:53:08 +08:00
Michael Bryan
6761442241 Added error-chain to lib.rs 2017-06-24 23:48:50 +08:00
Mathieu David
b441066105 Merge pull request #335 from Michael-F-Bryan/refactor-hbs-renderer
Refactor hbs renderer
2017-06-24 14:44:12 +02:00
Mathieu David
d50486e337 Merge pull request #314 from budziq/fix_theme
Fixes missing the default "theme" dir location
2017-06-24 14:03:10 +02:00
Michael Bryan
c3dfabd5a2 Merge branch 'upstream/master' into refactor-hbs-renderer
Notably, this takes into account the curly-quotes pull request (#305)
2017-06-24 16:07:01 +08:00
Michael Bryan
4c187bcb9f Explained what HtmlHandlebars::write_custom_function() does 2017-06-24 15:50:51 +08:00
Michal Budzynski
d42ef1cdbc reduced code repetition in fill_from_tomlconfig 2017-06-23 17:01:11 +02:00
Mathieu David
03193e0bd7 Merge pull request #334 from budziq/hide_popup
Hide theme selection popup after interaction
2017-06-23 14:15:54 +02:00
Michal Budzynski
672d91e6c2 Hide theme selector popup on interaction outside of it
Also set cursor to pointer on theme selector items.
2017-06-23 13:31:28 +02:00
Michal Budzynski
6d8ac6a23c Fixes missing the default "theme" dir location
if not specified in book.toml
2017-06-23 13:29:46 +02:00
Mathieu David
69b3e2b5cb Merge pull request #332 from budziq/silence_404s
error spewing on iron 404 errors
2017-06-23 11:27:31 +02:00
Mathieu David
5e93decf6e Merge pull request #328 from sunng87/feature/handlebars-upgrade
Update handlebars and some helpers
2017-06-23 11:16:52 +02:00
Michal Budzynski
79cdcb46de extract serving code to a separate module 2017-06-23 08:59:42 +02:00
Michal Budzynski
f889eb3d12 first draft of silencing 404 errors 2017-06-23 08:54:14 +02:00
Mathieu David
3306c030e1 Merge branch 'master' of github.com:azerupi/mdBook 2017-06-23 01:10:38 +02:00
Mathieu David
239886a5cf Merge branch 'budziq-minor_refactor' 2017-06-23 01:10:29 +02:00
Mathieu David
f3cb4265ca Fix typo 2017-06-23 01:10:18 +02:00
Mathieu David
28afebdca2 Merge branch 'minor_refactor' of https://github.com/budziq/mdBook into budziq-minor_refactor 2017-06-23 01:09:26 +02:00
Mathieu David
ab31f4b027 Merge pull request #310 from jimmydo/ios-scroll-to-top
On iOS, allow scrolling to the top of the page by tapping the top of the screen
2017-06-23 01:01:19 +02:00
Mathieu David
4128a78171 Merge branch 'master' of github.com:azerupi/mdBook 2017-06-23 00:50:11 +02:00
Mathieu David
7f60db069f Merge branch 'jimmydo-curly-quotes' 2017-06-23 00:49:46 +02:00
Mathieu David
26fc980ffb Remove 'curly_quotes' key from the json config 2017-06-23 00:48:59 +02:00
Mathieu David
d252dc82d6 Merge branch 'curly-quotes' of https://github.com/jimmydo/mdBook into jimmydo-curly-quotes 2017-06-23 00:43:57 +02:00
Mathieu David
c186d72b40 Merge pull request #346 from projektir/playpen_no_html
Creating markdown code from playpen files instead of HTML #345
2017-06-21 16:46:05 +02:00
projektir
73160877b3 Creating markdown code from playpen files instead of HTML #345 2017-06-21 09:33:41 -04:00
Michael Bryan
33f3bec301 Cleaned up the filter_map for normalizing id's using a more readable procedural style 2017-06-20 11:23:53 +08:00
Michael Bryan
8c30de16d6 Used the Entry API to make id counter incrementing nicer 2017-06-20 11:15:12 +08:00
Michael Bryan
fa95546988 Broke the header link wrapping out into smaller functions 2017-06-20 11:06:30 +08:00
Michael Bryan
ac16d7aef1 Added some tests for the original build_header_links function 2017-06-20 10:54:32 +08:00
Michael Bryan
e2a7adaa79 Introduced a RenderItemContext to make item rendering easier
I also accidentally ran `rustfmt` instead of `rustfmt-nightly`, so there are a lot of unnecessary style changes :(
2017-06-20 08:54:39 +08:00
Michael Bryan
75f0196c55 Pulled index rendering out into its own method 2017-06-20 07:53:46 +08:00
Mathieu David
49336e0698 Merge branch 'master' of github.com:azerupi/mdBook 2017-06-18 19:21:08 +02:00
Mathieu David
f0c697afd5 Merge branch 'jmillikan-master' 2017-06-18 19:20:23 +02:00
Mathieu David
cff1ed5e08 remove #content 2017-06-18 19:19:48 +02:00
Mathieu David
73c845fbbe Merge branch 'master' of git://github.com/jmillikan/mdBook into jmillikan-master 2017-06-18 19:14:32 +02:00
Jimmy Do
193f014a5b Add an option to convert to curly quotes when rendering to HTML 2017-06-18 10:11:04 -07:00
Jimmy Do
bd9b0d29ea On iOS, allow scrolling to the top of the page by tapping the top of the screen
* This is a built-in function of iOS Safari that didn't work because the
  page content was inside absolutely-positioned, scrollable divs.

* The fix is to stop using absolute positioning on `.page-wrapper` and
  `.page`, so that the content uses static positioning and flows
  naturally down the page.

* Consequently, `.sidebar` and `.nav-chapter` now have to use `position:
  fixed` in order to be positioned relative to the viewport.

* This fix also enables Safari's built-in behavior of automatically
  hiding the top and bottom toolbars when scrolling down the page.
2017-06-18 09:58:52 -07:00
Michael Bryan
4af10ce60c Renamed a couple functions to be more descriptive and ran rustfmt 2017-06-17 21:15:54 +08:00
Mathieu David
29708db467 Merge pull request #337 from budziq/focus_fix
Select right pane content on page load
2017-06-16 13:40:36 +02:00
Michael Bryan
deab3ba751 Tiny whitespace changes 2017-06-16 06:50:13 +08:00
Michal Budzynski
c1c06d6dc1 Auto focus on content to allow keyboard navigation 2017-06-15 23:15:41 +02:00
Michael Bryan
b7aa78c3c0 Minor refactoring 2017-06-15 18:39:41 +08:00
Michael Bryan
2568986fd5 fixed a typo 2017-06-15 18:17:16 +08:00
Michael Bryan
f946ef6327 Pulled some more little bits out into their own helper functions 2017-06-15 18:03:10 +08:00
Michael Bryan
0d0deb7c40 Pulled page rendering out into its own method 2017-06-15 17:43:44 +08:00
Michal Budzynski
e8908e32c9 Minor cleanup
- removing need to explicitly use `Path::new` all over the place
- removed warnings from doctests (normally invisible unless `cargo test -- --nocapture`)
- no doctests are norun/ignore now
- updated docs both in book-example and in docs not to refer to nonexisting API's
2017-06-14 21:55:42 +02:00
Mathieu David
9e9a08806d Merge pull request #329 from budziq/ios_playpen_buttons
Fix for playpen buttons missing on mobile safari and chrome IOS
2017-06-13 18:43:34 +02:00
Michal Budzynski
ee9fa8c86f Fix for playpen buttons missing on mobile safari and chrome IOS 2017-06-13 16:59:29 +02:00
Ning Sun
e890579141 (fix) some merge issue 2017-06-13 20:53:25 +08:00
Ning Sun
9aa39a6a12 (chore) update handlebars to 0.27.0
Signed-off-by: Ning Sun <sunng@about.me>
2017-06-13 20:43:36 +08:00
Ning Sun
6ee6da074e (refactor) rework helpers based on new handlebars api
Signed-off-by: Ning Sun <sunng@about.me>
2017-06-13 20:43:17 +08:00
Ning Sun
2bb274d424 Merge branch 'master' of github.com:azerupi/mdBook 2017-06-13 20:40:46 +08:00
Mathieu David
19692c76df Merge pull request #326 from budziq/fix_clipboard
copying to clipboard no longer copies the compilation results from "play"
2017-06-12 14:15:41 +02:00
Michal Budzynski
a6275ebcdb copying to clipboard no longer copies the compilation results from "play" 2017-06-12 14:02:53 +02:00
Mathieu David
6a279e2775 Merge branch 'Cldfire-master' 2017-06-12 11:26:19 +02:00
Mathieu David
9ce6eebe43 Merge branch 'master' of git://github.com/Cldfire/mdBook into Cldfire-master 2017-06-12 11:19:31 +02:00
Mathieu David
0b6378eb13 Merge branch 'budziq-custom_js' 2017-06-12 11:10:46 +02:00
Mathieu David
350c86155b Merge branch 'custom_js' of git://github.com/budziq/mdBook into budziq-custom_js 2017-06-12 11:09:07 +02:00
Mathieu David
ad9bda2d69 Merge pull request #324 from budziq/store_js
Move from localStorage to store.js (v2.0.3) - also hide sidebar on mobile
2017-06-12 11:03:03 +02:00
Michal Budzynski
08fd255a56 Move from localStorage to store.js (v2.0.3)
Fixes a lot of browser incompatibilities in localStorage/cookie handling
Including but not limited to:

- loss of styling and functionality on chromium private mode
- loss of styling and functionality on safari and safari private mode
- awaiting verification if problems in mobile safari are solved.
2017-06-12 01:53:25 +02:00
Michal Budzynski
f607978780 Hide sidebar on link selection when it occupies large space
in relation to the whole screen width (solves problems on phones)
2017-06-11 15:13:31 +02:00
Michal Budzynski
f96e7e5cba Implemented support for additional JS 2017-06-11 15:08:09 +02:00
Mathieu David
6c279453d9 Merge pull request #321 from pravic/es5-fix
One more ES5 fix.
2017-06-09 22:10:03 +02:00
pravic
56163f69f8 One more ES5 fix. 2017-06-09 22:48:57 +03:00
Mathieu David
a9862a56b3 Merge pull request #320 from pravic/es5-fix
Fix ES5 compatibility.
2017-06-09 21:41:23 +02:00
pravic
eba90f5440 Fix ES5 compatibility. 2017-06-09 21:59:29 +03:00
Cldfire
44efc65c63 Add Ayu theme
Also adds the a new variable, `$inline-code-color`, to base.styl. The `Ayu` theme needed this to change the text color of inline code.
2017-06-06 16:35:44 -04:00
Steve Klabnik
8a05f0d499 Merge pull request #287 from azerupi/toml-config
Revamp config code
2017-06-05 10:56:44 -04:00
Mathieu David
f1121cf8c2 fix build failure 2017-06-04 20:47:34 +02:00
Mathieu David
1a8e54bb52 remove unused methods 2017-06-04 20:41:31 +02:00
Mathieu David
23efa9e146 Document the TOML configuration file 2017-06-04 20:41:31 +02:00
Mathieu David
bb4ceb481f Allow an additional custom stylesheets, closes #178 2017-06-04 20:41:31 +02:00
Mathieu David
c6bfe0b1d7 Adds a test for #240 2017-06-04 20:41:31 +02:00
Mathieu David
2e812db13c Fix for google-analytics 2017-06-04 20:41:31 +02:00
Mathieu David
70383d0a25 New config structs supports json again (the old style) for a little deprecation period 2017-06-04 20:41:31 +02:00
Mathieu David
d3ae2eda56 Replace the old book structure with the new one 2017-06-04 20:41:31 +02:00
Mathieu David
170bf8b1eb New configuration struct + tests #285 2017-06-04 20:41:31 +02:00
Mathieu David
272022621d Merge pull request #307 from budziq/theme_reload
Now changes to `theme` directory trigger rebuild for `mdbook serve`
2017-06-01 13:56:26 +02:00
Michal Budzynski
be3418a269 Now changes to theme directory trigger rebuild for mdbook serve
As `theme` dir is no longer under `src`. Updates to "theme" did not
trigger book rebuild.
Also fixed misleading docs about `theme` dir being located in `src`
2017-06-01 13:11:39 +02:00
Mathieu David
3e80268a44 Merge pull request #303 from budziq/clipboard
RFC - Initial implementation of clipboard handling
2017-05-31 22:22:15 +02:00
Michal Budzynski
3a809e4a1c Added local fallback for clipboard.js 2017-05-31 21:51:19 +02:00
Michal Budzynski
dfc24bec01 Fixed tooltip styling
Also fixed problem with garbage being put in clipboard
when triggered repeatedly
2017-05-31 21:07:47 +02:00
Michal Budzynski
e567d22f1c Initial implementation of clipboard handling 2017-05-31 19:56:17 +02:00
Mathieu David
bfc3fbb405 Merge pull request #304 from budziq/hljs_update
Updated highlight.js to v9.12.0
2017-05-31 15:50:06 +02:00
Michal Budzynski
8bfcd9939c Updated highlight.js to v9.12.0
Fixing problem with raw strings syntax highlighting
Also backported updates to atelier-dune-light.css
2017-05-31 15:12:20 +02:00
Mathieu David
316bcf7b5d Merge pull request #300 from budziq/serving_url
Reformatted "Serving on " message for easier consumption
2017-05-27 11:54:12 +02:00
Michal Budzynski
453b97bec0 Reformatted "Serving on " message for easier consumption
Now we have: `Serving on: http://localhost:3000`
2017-05-27 11:34:46 +02:00
Mathieu David
4364ec3a7b Merge pull request #298 from superstring/master
Fix websocket port option for serve
2017-05-26 13:54:07 +02:00
superstring
7de24f86a9 Change --ws-port to --websocket-port 2017-05-26 19:18:32 +08:00
superstring
027c21aef7 Fix websocket port option for serve 2017-05-26 12:04:20 +08:00
Mathieu David
64f0bdbfba Merge pull request #297 from aaaxx/master
CSS: better fallback stack for monospaced fonts
2017-05-24 10:28:25 +02:00
aaaxx
cc1cb9edb0 CSS: better fallback stack for monospaced fonts
List of system fonts (R, I, B means roman, italic and bold. Ubuntu probably comes with more fonts, but I couldn't find a list to confirm.):

```txt
Windows
----------
Consolas            R RI  B BI
Courier             R
Courier New         R RI  B BI
Lucida Console      R

Mac
----------
Andale Mono         R
Courier             R RI  B BI
Courier New         R RI  B BI
Menlo               R RI  B BI
Monaco              R

Ubuntu
----------
Ubuntu Mono         R RI  B BI
DejaVu Sans Mono    R RI  B BI
```

```css
font-family: Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
```

Consolas and Ubuntu are professionally designed fonts ([Lucas de Groot][1] and [Dalton Maag][2]), with true, calligraphic italic, so they go at the top of the stack.

Menlo is [based on DejaVu Sans Mono][3], the only difference being a few tweaked glyphs, so DejaVu serves as a fallback for it.

As for Courier New, other than being unreadably spindly, it's the default monospaced font in all browsers, so there's no need to include it in the stack.

The `monospace, monospace;` declaration is, by now, [a standard hack][4] that overrides some browsers' behaviour of defaulting the `monospace` elements to smaller font size. Without it, any relative font size you apply to them will be calculated from that reduced size (seems to be 13 px in all browsers).


[1]: https://en.wikipedia.org/wiki/Luc(as)_de_Groot
[2]: https://en.wikipedia.org/wiki/Dalton_Maag
[3]: http://www.leancrew.com/all-this/2009/10/the-compleat-menlovera-sans-comparison/
[4]: https://stackoverflow.com/questions/38781089/font-family-monospace-monospace
2017-05-24 06:07:58 +02:00
Mathieu David
35ef31757b Merge pull request #295 from budziq/err_ch
Friendlier errors from build.rs and support for Windows stylus build
2017-05-22 18:20:57 +02:00
Michal Budzynski
87f26a82b6 Fix for different naming of nodejs on ubuntu
also added static lifetime to str constants as these were failing on nightly windows
2017-05-22 17:06:38 +02:00
Michal Budzynski
1d141aa27b Workaround for not being able to run stylus on windows 2017-05-22 16:35:39 +02:00
Michal Budzynski
a84c1ecf33 Implemented a more friendly stylus error handling
Added error-chain as build-dependency
2017-05-22 04:25:59 +02:00
Mathieu David
2518d5c827 Fix error in contribution guide 2017-05-20 19:43:32 +02:00
Mathieu David
3f98d69690 Merge pull request #289 from budziq/highlight
Inline code with hyperlink is now highlighted
2017-05-20 17:27:56 +02:00
Michal Budzynski
9c8c819ec3 Inline code with hyperlink is now highlighted
Inline code with hyperlink has now a different color then
standard inline code and has a on hover underline.
2017-05-20 17:10:53 +02:00
Mathieu David
f038dcb404 Merge pull request #288 from budziq/rustfmt_update
Made changes with rustfmt including `use_try_shorthand`
2017-05-19 13:43:49 +02:00
Michal Budzynski
75bbd55128 Changes made with rustfmt including use_try_shorthand
Updated the project rustfmt.toml to include `use_try_shorthand = true`.
Run rustfmt on all rust sources.
2017-05-19 13:04:37 +02:00
Mathieu David
faa716d99c update all dependencies 2017-05-18 11:56:04 +02:00
Mathieu David
b1601b16ea Merge pull request #278 from azerupi/contributing
Contributing file
2017-05-18 09:55:26 +02:00
Mathieu David
379ed9dc16 Merge pull request #284 from budziq/indentation
Fix indentation of hidden code blocks
2017-05-18 09:36:53 +02:00
Mathieu David
7b6836b75e Merge pull request #283 from budziq/quick_main
Do not add playpen boilerplate when using quick_main!
2017-05-18 09:29:57 +02:00
Michal Budzynski
e4dd03c8f0 Fix indentation of hidden code blocks
Hidden code blocks are no longer indented with
one additional space (required for doctests to compile in some cases)
Now the behavior is similar to the rustdoc's
2017-05-18 00:04:09 +02:00
Michał Budzyński
c99ce06370 Do not add playpen boilerplate quick_main! is used
code snippets using quick_main! macro from error-chain
https://docs.rs/error-chain/0.10.0/error_chain/macro.quick_main.html
no longer have `fn main` implicitly added
2017-05-17 22:27:03 +02:00
Mathieu David
0f67cae1ef Wrap lines and add more whitespace in CONTRIBUTING.md 2017-05-17 18:36:52 +02:00
Mathieu David
0443f8a709 Merge pull request #262 from Rufflewind/master
Fix broken link in book-example/src/lib/lib.md
2017-05-17 11:37:07 +02:00
Mathieu David
1442923a0a Add simple issue examples 2017-05-16 22:16:59 +02:00
Mathieu David
9447274fa5 Modify the Readme for the new contribution guide 2017-05-16 22:10:24 +02:00
Mathieu David
7a59591109 Added a contributing file 2017-05-16 22:02:07 +02:00
Mathieu David
d636ca45e8 Merge pull request #276 from steveklabnik/gh158
Remove allow-failure on MUSL
2017-05-16 20:40:37 +02:00
steveklabnik
832ef446dd Remove allow-failure on MUSL
This should work now.

Fixes #158
2017-05-16 13:06:08 -04:00
Mathieu David
74e75d2cfb Merge pull request #273 from Michael-F-Bryan/google-analytics-docs
Added a note about google analytics to the docs
2017-05-16 14:19:14 +02:00
Michael-F-Bryan
95750be815 Added a note about google analytics to the docs 2017-05-16 17:40:14 +08:00
Mathieu David
4ad6fab5e3 Merge pull request #272 from Michael-F-Bryan/google-analytics
Support for Google Analytics
2017-05-16 09:13:57 +02:00
Michael-F-Bryan
94dce4f796 Added google_analytics so it can be inserted into handlebars 2017-05-16 13:28:59 +08:00
Michael-F-Bryan
ada1f29b34 Added a google_analytics field to BookConfig
This commit:
- Adds an Option<String> field to the BookConfig which should
  contain your google analytics ID
- Allows the google analytics ID to be extracted from the config
  file (key is google_analytics_id)
- Adds a test to make sure the field is populated from a config
  file correctly
2017-05-16 13:15:04 +08:00
Michael-F-Bryan
4a634f08da Updated google analytics to index.hbs and hbs_renderer.rs 2017-05-16 13:05:21 +08:00
Mathieu David
e8b88cfa5e Merge pull request #267 from Rufflewind/mathjax
Change MathJax to use CDNJS
2017-05-06 17:41:42 +02:00
Phil Ruffwind
3066597acc Change MathJax to use CDNJS
Because the MathJax CDN will soon be retired.
2017-05-05 17:08:59 -04:00
Ning Sun
d1f9174e7f (feat) adopt new handlebars navigate api
Signed-off-by: Ning Sun <sunng@about.me>
2017-05-05 08:41:50 +08:00
Phil Ruffwind
7eade3b101 Fix broken link in book-example/src/lib/lib.md
The template sets `<base href="../">` so it's not necessary to have
`../` in the link.
2017-05-01 20:32:06 -04:00
Mathieu David
435682e95c (cargo-release) start next development iteration 0.0.22-pre 2017-04-27 16:05:45 +02:00
Mathieu David
69188445e7 (cargo-release) version 0.0.21 2017-04-27 16:03:02 +02:00
Mathieu David
9b7a26effd Merge pull request #259 from azerupi/incorrect-conditional
Fix incorrect conditional.
2017-04-27 16:00:29 +02:00
Corey Farwell
4f4120b5a4 Fix incorrect conditional.
I accidentally introduced this in 4525810737.
2017-04-27 09:16:19 -04:00
Mathieu David
607bf4426e (cargo-release) start next development iteration 0.0.21-pre 2017-04-26 19:25:14 +02:00
Mathieu David
8b84b8fa82 (cargo-release) version 0.0.20 2017-04-26 19:22:37 +02:00
Corey Farwell
a4a708bdda Merge pull request #254 from frewsxcv/frewsxcv-no-create
Implement new 'no-create' build flag.
2017-04-21 23:03:23 -04:00
Steve Klabnik
a3b925e3ab Merge pull request #256 from steveklabnik/master
bump for 0.0.19
2017-04-18 10:52:33 -04:00
steveklabnik
cba988f009 bump for 0.0.19 2017-04-18 10:50:58 -04:00
Corey Farwell
4525810737 Rewrite an emptiness check. 2017-04-17 21:58:34 -04:00
Corey Farwell
5d72d966ad Wrap long line. 2017-04-17 21:56:01 -04:00
Corey Farwell
15dcca87d8 Refactor to prevent excessive indentation. 2017-04-17 21:55:32 -04:00
Corey Farwell
c6e81337fb Implement new 'no-create' build flag.
Fixes https://github.com/azerupi/mdBook/issues/253.
2017-04-17 21:53:27 -04:00
Steve Klabnik
9602acce80 Merge pull request #252 from crazymerlyn/fix-runnable-files
Remove the extra run button on runnable rust files
2017-04-16 11:58:07 -04:00
CrazyMerlyn
65d7e86024 Remove the extra run button on runnable rust files
The playpen helper now uses a simple pre block instead of a pre block
with class playpen as it led to nested playpens.
2017-04-16 18:17:59 +05:30
Mathieu David
b5ec813d2f Merge pull request #250 from regexident/master
Added monospace font with support for box-drawing chars
2017-04-15 20:37:17 +02:00
Vincent Esche
41735b4579 Added monospace font with support for box-drawing chars 2017-04-15 14:16:28 +02:00
Jesse Millikan
d24ad83a5c Empty header sections 2017-04-14 15:51:23 -04:00
Steve Klabnik
9cb232058b Merge pull request #243 from steveklabnik/gh241
Accept nightly examples.
2017-04-14 15:19:07 -04:00
Mathieu David
ef402c16e8 Merge pull request #248 from mthh/master
Fix alignement of chapters with three digit numbering
2017-04-14 20:25:21 +02:00
mthh
df5472ab5a Should fix sections created with chapter of more than two digits 2017-04-07 12:46:28 +02:00
mthh
d768963c30 Revert "should fix sections created with chapter of more than two digits"
This reverts commit 8e7ec6e1fd.
2017-04-07 10:47:45 +02:00
mthh
8e7ec6e1fd should fix sections created with chapter of more than two digits 2017-04-07 02:37:46 +02:00
steveklabnik
80f01d70c6 Accept nightly examples.
This also brings us to parity with rustdoc regarding attributes in
general; while this PR was focused on enabling nightly, that was a
happy accident.
2017-03-31 17:06:03 -04:00
Mathieu David
40f275bf21 Merge pull request #236 from steveklabnik/master
add a .gitattributes to ensure proper line ending settings
2017-03-31 15:27:11 +02:00
Mathieu David
af8300c0b4 Merge pull request #239 from tshepang/misplaced
move misplaced example
2017-03-31 15:26:00 +02:00
Tshepang Lekhonkhobe
793a88260c move misplaced example 2017-03-30 14:09:14 +02:00
Steve Klabnik
1ec776244d Merge pull request #238 from tshepang/misc
typos
2017-03-29 14:17:44 -04:00
Tshepang Lekhonkhobe
4af107b0ca typos 2017-03-29 16:42:55 +02:00
steveklabnik
35e2807138 add a .gitattributes to ensure proper line ending settings 2017-03-28 09:50:57 -04:00
Mathieu David
1632d2e339 Merge pull request #230 from crazymerlyn/ignore_arrow_keys_with_modifier
Fix keyboard navigation to trigger only if no modifier key is pressed
2017-03-26 18:49:27 +02:00
Steve Klabnik
7c3932cef9 Merge pull request #231 from crazymerlyn/fix-header-link-id
Fix header links
2017-03-24 10:24:53 -04:00
CrazyMerlyn
ed1a216121 Fix header links
Header fragment links now use "id" attribute instead of the depreciated
"name" attribute.

Similar headers are given numbered ids to avoid id collisions.
For instance, if there are three headers named "Example", their ids
would be "#example", "#example-1", and "#example-2" respectively.
2017-03-23 23:24:26 +05:30
CrazyMerlyn
f814e96459 Fix keyboard navigation to trigger only if no modifier key is pressed 2017-03-23 13:29:04 +05:30
Jesse Millikan
980ea5796e next and prev attributes on the next and prev links, and #content 2017-03-19 16:21:38 -04:00
Jesse Millikan
8500d1c8a7 Relative links for non-JS browsers 2017-03-19 03:53:24 -04:00
Steve Klabnik
a7272e0ff5 Merge pull request #226 from steveklabnik/master
bump version
2017-03-10 10:40:21 -08:00
steveklabnik
1cf4774737 bump version 2017-03-10 13:39:35 -05:00
Steve Klabnik
c6a5d12002 Merge pull request #222 from steveklabnik/gh29
Implement playpen support for ```rust
2017-03-10 08:59:15 -08:00
steveklabnik
b120ce7397 inject allow(unused_variables) 2017-03-10 09:46:11 -05:00
Steve Klabnik
c7916c4818 Merge pull request #225 from integer32llc/update-hljs
Update to highlight.js 9.10.0
2017-03-10 05:53:10 -08:00
Carol (Nichols || Goulding)
56f597b90c Update to highlight.js 9.10.0 2017-03-09 22:45:59 -05:00
steveklabnik
c5f9625feb inject main 2017-03-06 13:27:25 -05:00
steveklabnik
79f00eeea3 Implement playpen support for ```rust
Fixes #29
2017-03-06 12:23:15 -05:00
Mathieu David
677fa42458 (cargo-release) start next development iteration 0.0.18-pre 2017-02-28 17:22:51 +01:00
Mathieu David
a8bba0b94d (cargo-release) version 0.0.17 2017-02-28 17:20:28 +01:00
Mathieu David
e5a973a18d Merge pull request #212 from azerupi/fix-anchors
Fix anchors #211
2017-02-28 14:50:40 +01:00
Mathieu David
e218257e42 fix anchor links 2017-02-28 12:42:11 +01:00
Mathieu David
1345c05b18 Fix anchors, Fixes #211 2017-02-28 12:40:05 +01:00
Mathieu David
5e3a3f3482 Merge pull request #214 from azerupi/fix-rust-hide
Fix code blocks with comma separated classes
2017-02-28 12:33:52 +01:00
Mathieu David
7f46071faa Merge pull request #215 from steveklabnik/travis
Try to fix Travis
2017-02-28 10:59:02 +01:00
Steve Klabnik
5674da2afb fix travis 2017-02-27 20:21:35 -05:00
Mathieu David
01341a7705 Fix code blocks with comma separated classes 2017-02-28 01:41:06 +01:00
Mathieu David
0c624d0f74 bump version 2017-02-20 16:16:21 +01:00
Mathieu David
58cfef00f2 Merge pull request #209 from steveklabnik/gh204
Print version: fix up header links
2017-02-20 16:12:00 +01:00
Steve Klabnik
6af3eea24b Print version: fix up header links 2017-02-20 09:28:49 -05:00
Mathieu David
c88656284c Regenerate css 2017-02-19 11:13:19 +01:00
Mathieu David
14a28080c1 Merge pull request #208 from frewsxcv/bump
Bump crates.
2017-02-19 11:10:46 +01:00
Corey Farwell
7fa36f82b0 Bump ws crate to 0.6. 2017-02-18 20:28:12 -05:00
Corey Farwell
3a30e65eef Bump staticfile crate to 0.4, iron to 0.5. 2017-02-18 20:26:12 -05:00
Corey Farwell
fab24f5224 Bump notify crate to 0.4. 2017-02-18 20:24:27 -05:00
Corey Farwell
cfa4295d79 Bump toml crate to 0.3. 2017-02-18 20:22:55 -05:00
Mathieu David
d7f38d08fd Merge pull request #205 from frewsxcv/clippy
Address warnings found by rust-clippy.
2017-02-17 11:05:33 +01:00
Mathieu David
864be6cf42 Merge pull request #207 from steveklabnik/gh204
Generate links at compile-time rather than use JS
2017-02-17 11:00:39 +01:00
Steve Klabnik
ec42e2f771 convert to one pass
thanks @burntsushi ❤️
2017-02-16 19:31:52 -05:00
Steve Klabnik
aba153a271 update env_logger 2017-02-16 17:17:26 -05:00
Steve Klabnik
280dabecd7 update regex dep 2017-02-16 17:11:16 -05:00
Steve Klabnik
38b3516b60 Implement links in section headers.
This project already had a transitive dependency on regex; let's use it.

This isn't the most efficient solution, but it should be fine. It ends
up doing five full scans of the text. There's probably an easier way but
I'm mostly just trying to get this to work for now.

This also implements the same algorithm that rustdoc does for generating
the name for the link.

Fixes #204
2017-02-16 17:07:17 -05:00
Steve Klabnik
d609988264 remove js rendering 2017-02-16 17:07:16 -05:00
Corey Farwell
95fd292b4f Address warnings found by rust-clippy.
https://github.com/Manishearth/rust-clippy
2017-02-16 16:55:28 -05:00
Mathieu David
f3fb1f1e16 Merge pull request #206 from frewsxcv/serde
Bump serde, serde_json, and handlebars crates.
2017-02-16 22:51:37 +01:00
Corey Farwell
152ebba762 Bump serde, serde_json, and handlebars crates. 2017-02-15 23:31:05 -05:00
Mathieu David
23d25c853e Merge pull request #202 from paiv/paiv-widen-menu-hitregions
widen hit regions of menu buttons
2017-02-11 23:45:59 +01:00
Pavel Ivashkov
b97a8205f6 widen hit regions of menu buttons
![see here](http://i.imgur.com/jCZTCfr.png)
2017-02-11 21:20:12 +02:00
Mathieu David
82faec6b5a Merge pull request #201 from petehayes102/master
Add docs for --dest-dir option
2017-01-17 12:21:21 +01:00
Pete Hayes
32814f6f71 Remove blank ***note*** section 2017-01-17 00:19:09 +00:00
Pete Hayes
ac6f15cb27 Add docs for --dest-dir option 2017-01-17 00:19:09 +00:00
Mathieu David
0d6185ac96 Merge pull request #199 from petehayes102/master
Add --dest-dir option to build, watch and serve subcommands
2017-01-13 12:07:34 +01:00
Pete Hayes
4b31ae6789 Add --dest-dir arg to build, watch and serve subcommands 2017-01-12 12:26:22 +00:00
Pete Hayes
1afa2debc1 Fix spelling of omitted 2017-01-12 12:23:39 +00:00
Mathieu David
3a71371946 Merge pull request #198 from mbrubeck/watch
Update watch command to use `notify` 3.0
2017-01-02 19:58:47 +01:00
Mathieu David
9a318adc03 Merge pull request #197 from mbrubeck/cleanup
Clean up some Path code in bookconfig
2017-01-02 19:43:45 +01:00
Matt Brubeck
c7b4147ba7 Watch both book.json and book.toml 2017-01-01 16:03:49 -08:00
Matt Brubeck
1ac2602360 Update to notify 3.0
notify now does its own event debouncing, so it's no longer necessary
for mdbook to do this manually.
2017-01-01 16:03:49 -08:00
Matt Brubeck
09729aaca5 Clean up some Path code in bookconfig 2017-01-01 16:02:48 -08:00
Mathieu David
3ffd24df63 Merge pull request #196 from mbrubeck/open
Add a CLI option to open a web browser
2017-01-01 19:22:55 +01:00
Mathieu David
fe8d46b8e6 Merge pull request #195 from mbrubeck/refactor
Various refactoring and cleanup
2017-01-01 19:21:40 +01:00
Matt Brubeck
21bc3d47c8 Add a CLI option to open a web browser 2017-01-01 09:58:20 -08:00
Matt Brubeck
f2b87f7944 Factor common io error handling out of renderer 2016-12-31 23:12:38 -08:00
Matt Brubeck
894a03655e Simplify error handling in utils::fs 2016-12-31 23:12:38 -08:00
Matt Brubeck
6b2572e78d Simplify some as_str error handling code 2016-12-31 18:41:59 -08:00
Matt Brubeck
fe287a1eca Code cleanup: Remove unnecessary .remove() calls
`BTreeMap::insert` will replace any existing value, so there's no need
to remove the old value first.
2016-12-31 18:33:17 -08:00
Mathieu David
375502a6fa Merge pull request #194 from mbrubeck/warnings
Fix some rustc warnings.
2016-12-31 23:45:23 +01:00
Mathieu David
a6e1844aad Merge pull request #193 from mbrubeck/chapter-title
Add current chapter title to handlebars context
2016-12-31 23:43:39 +01:00
Mathieu David
d92852867b Merge pull request #192 from mbrubeck/docs
Fix a broken link in the documentation
2016-12-31 23:38:09 +01:00
Matt Brubeck
0f0750df52 Fix unreachable code warning in parse::summary::parse_level 2016-12-31 10:39:48 -08:00
Matt Brubeck
712adcf737 Fix cfg attribute in bookconfig_test 2016-12-31 10:36:19 -08:00
Matt Brubeck
3a0cfc87df Add current chapter title to handlebars context 2016-12-31 10:34:36 -08:00
Matt Brubeck
b1e384b03b Fix a broken link in the documentation
This fixes a broken link on http://azerupi.github.io/mdBook/cli/init.html

The `..` is redundant because the document's base URI is set to
`path_to_root`.  It breaks if the base URI is not at the server root.
2016-12-31 09:20:54 -08:00
Mathieu David
6410e792d7 Merge pull request #191 from jessestricker/readme-typos
Fix some minor typos and text inconsistencies
2016-12-29 18:02:15 +01:00
Jesse Stricker
b75243f1f5 Fix some minor typos 2016-12-29 16:25:51 +01:00
Mathieu David
f1df53a4bb Merge pull request #190 from gambhiro/parse-toml
Parse toml
2016-12-26 13:36:16 +01:00
Gambhiro
8a178e311d fix test 2016-12-24 13:44:24 +00:00
Gambhiro
53ec61ac70 upd example 2016-12-24 13:34:22 +00:00
Gambhiro
97d46e79b7 convert json to toml before config parsing 2016-12-24 13:22:01 +00:00
Gambhiro
552e39c897 update example to encourage using book.toml 2016-12-23 08:17:04 +00:00
Gambhiro
791487bc84 parse either book.toml or book.json 2016-12-23 08:15:32 +00:00
Gambhiro
f67ae7c71a update dependency versions 2016-12-23 08:10:42 +00:00
Mathieu David
e53dcdcf4d Merge pull request #186 from gambhiro/book-json-keys
Book json keys
2016-12-13 00:55:51 +01:00
Gambhiro
85d8e2ebd3 use theme_path key in book.json when given 2016-12-07 14:22:32 +00:00
Gambhiro
a9e5dc63f1 use src key in book.json when given 2016-12-07 09:38:56 +00:00
Mathieu David
cf35e08abc Merge pull request #181 from thomastanck/master
Use fixed positioning and remove overflow-x's for smoother scrolling …
2016-11-22 10:06:41 +01:00
Mathieu David
c986b3afc4 Merge pull request #182 from integer32llc/hljs-class
Add hljs class to all code blocks, regardless of highlighting
2016-11-14 07:48:07 +01:00
Carol (Nichols || Goulding)
08b5d14f7e Add hljs class to all code blocks, regardless of highlighting
Fixes #179.

Highlight.js does not apply syntax highlighting to code blocks marked
no-highlight, nohighlight, plain, or text. When it finds blocks of those
languages, it does not add the `hljs` class to those code blocks either.

highlight.css and tomorrow-night.css use the `hljs` class to give code
blocks their backrgound color and text color, and we want that to apply
even if the code doesn't get syntax highlighting markup.

This is a somewhat hacky solution to get just that behavior! After this
commit, code blocks with no-highlight, nohighlight, plain, or text
language set on them will indeed get the hljs colors.
2016-11-13 21:14:00 -05:00
Thomas Tan
f9101ca62c Use fixed positioning and remove overflow-x's for smoother scrolling experience in iOS 2016-11-09 16:18:40 +00:00
Mathieu David
f0c0d71326 Merge pull request #177 from azerupi/serde
Switch from rustc_serialize to serde
2016-11-03 11:44:34 +01:00
Mathieu David
d2f3eb5007 remove unused imports 2016-11-03 02:05:35 +01:00
Mathieu David
67aee5c192 Switch from rustc_serialize to serde. Closes #18 2016-11-03 01:58:42 +01:00
Mathieu David
30eb85711e Merge pull request #175 from DenisKolodin/metadata-remove
Remove unused metadata import
2016-11-01 12:44:23 +01:00
Denis Kolodin
b0d33e76ec Remove unused metadata import 2016-11-01 11:19:08 +03:00
Mathieu David
eb65f3fd1e Merge pull request #174 from rnkaufman/update-highlight-js
Highlight js update
2016-10-29 16:42:32 +02:00
rnkaufman
2600c62cf9 Highlight js update 2016-10-27 18:26:02 -07:00
Mathieu David
ecae442d25 Merge pull request #173 from HParker/slugify-section-anchors
slugify section headers
2016-10-21 22:12:40 +02:00
Adam Hess
f26f41fde3 slugify section headers
The current section headers are url encoded.  Because of that they
have some funny characters like %20.  We can clean that up by removing
all of the non-word characters before placing them in the anchor.
2016-10-20 22:02:16 -07:00
Mathieu David
b91f817bfd Merge pull request #171 from rzlourenco/master
Fix a bug where files without an extension were not copied
2016-09-23 22:13:30 +02:00
Rodrigo Lourenço
528945d67d Copy files with no extension too. 2016-09-23 15:09:16 +01:00
Mathieu David
4852e9e65a Merge branch 'master' of https://github.com/azerupi/mdBook 2016-09-12 22:50:03 +02:00
Mathieu David
e54b6643e1 regenerate css 2016-09-12 22:43:29 +02:00
Mathieu David
c7a95ccb8b Fix round corners in theme selector, changes were previously comitted directly to the css file causing them to be overwritten 2016-09-12 22:19:36 +02:00
Mathieu David
81a8f946b7 Fix print.styl, changes were previously comitted directly to the css file causing them to be overwritten 2016-09-12 22:10:33 +02:00
Mathieu David
04a643805a Merge pull request #170 from JIghtuse/master
Make line-height for chapter greater than section
2016-09-12 22:01:13 +02:00
Boris Egorov
49608b560b Make line-height for chapter greater than section
Fixes #166
2016-09-04 22:04:55 +07:00
Mathieu David
9e634a4e83 Bump version number to 0.0.15, 0.0.14 has been published to crates.io 2016-08-31 15:11:33 +02:00
Mathieu David
c2c721025d Merge pull request #165 from waywardmonkeys/patch-1
Fix typo.
2016-08-26 20:14:14 +02:00
Bruce Mitchener
2dfc25fc6e Fix typo. 2016-08-27 00:28:17 +07:00
Mathieu David
8f8893bab2 Merge pull request #164 from gambhiro/use-log-crate
use macros from the log crate, issue #151
2016-08-14 16:19:17 +02:00
Gambhiro
4153db2624 env_logger 2016-08-14 14:55:10 +01:00
Gambhiro
db11ff27f4 use warn 2016-08-14 14:40:08 +01:00
Gambhiro
b584f6eb9c use macros from the log crate, issue #151 2016-08-14 13:34:02 +01:00
Mathieu David
a7ae0b99c4 Update README.md 2016-08-13 11:36:28 +02:00
Mathieu David
9732a3bc7d Merge pull request #161 from integer32llc/exit-status
Exit with a nonzero status if we get an error
2016-08-07 00:07:41 +02:00
Mathieu David
f3f9c93765 Merge pull request #162 from integer32llc/fix-example-book
Fix book-example tests
2016-08-07 00:06:05 +02:00
Carol (Nichols || Goulding)
a0d8013242 Tell rustdoc this mathjax is not rust
Use bash for the grey background though.
2016-08-06 15:25:40 -04:00
Carol (Nichols || Goulding)
1b9d55bcd5 Put spaces between # and hidden lines 2016-08-06 15:25:40 -04:00
Carol (Nichols || Goulding)
a459a3606e Exit with a nonzero status if we get an error
This is especially important when mdbook is used with CI.
2016-08-06 14:54:07 -04:00
Mathieu David
1e6bccd924 Merge branch 'master' of https://github.com/azerupi/mdBook 2016-08-06 11:39:17 +02:00
Mathieu David
6d77b7fd83 Fix CI for musl builds, musl builds now run but will not cause a build failure if they do not succeed 2016-08-06 11:39:00 +02:00
Mathieu David
f9ea6135c3 Merge pull request #160 from code-ape/master
Added option to configure serve interface and public websocket address.
2016-08-05 23:54:33 +02:00
Ferris
317023cd0e Added option to configure serve interface and address browser will use to connect to websocket server. 2016-08-05 21:40:00 +00:00
Mathieu David
0b88b043d0 Bump version number to 0.0.14, 0.0.13 has been published to crates.io [ci_skip] 2016-08-02 00:58:08 +02:00
Mathieu David
ac725cb39d bump version to 0.0.13 to publish to crates.io 2016-08-02 00:42:17 +02:00
Mathieu David
db0306a6d2 Fix bug in shell script that was preventing deployment of the docs to gh-pages 2016-08-02 00:15:38 +02:00
Mathieu David
02c5c971e7 (Travis-ci): Allow failure in musl builds #158 2016-08-01 20:05:03 +02:00
Mathieu David
5350d62591 Update all dependencies to latest version 2016-08-01 14:06:08 +02:00
Mathieu David
9c8a563223 Merge pull request #157 from icanrealizeum/anchorsfixxage
Fixes #156 - anchors are now URI encoded
2016-07-31 14:31:32 +02:00
icanrealizeum
b4948b680f Fixes #156 - anchors are now URI encoded
also fixes https://github.com/rust-lang/book/issues/166 anchors duplication

Thanks @azerupi for mentoring in #156 !
Cheers!
2016-07-31 15:21:58 +03:00
Mathieu David
b6df992420 Merge pull request #152 from quornian/master
Make sure <ul><li> and </li></ul> are balanced
2016-07-23 15:04:51 +02:00
Ian Thompson
b0e5f375ba Make sure <ul><li> and </li></ul> are balanced 2016-07-16 10:23:22 -04:00
Mathieu David
a4a277cb50 Merge pull request #145 from onur/light-theme-as-default
Use light theme when javascript is disabled
2016-06-15 16:45:10 +02:00
Onur Aslan
b9e22bb8f2 Use light theme when javascript is disabled
mdBook is setting theme (by adding a class attribute to body tag) with javascript.
Page is not using any theme by default and page is not using any styling unless
javascript is enabled.

This patch is adding class attribute to body tag and making mdBook to use `light`
theme when javascript is disabled.

Fixes: #144
2016-06-15 17:25:28 +03:00
Mathieu David
ab29e92071 Merge pull request #143 from austinhartzheim/us-issue-133
Fix azerupi/mdBook#133: Add link to source code
2016-06-12 10:37:38 +02:00
Austin Hartzheim
03373c6bf2 Fix azerupi/mdBook#133 by adding a link to the GitHub repo at the end of the --help output. 2016-06-11 23:08:48 +00:00
Mathieu David
425b583625 Merge pull request #142 from Bobo1239/printing
Add print media query
2016-06-10 23:32:52 +02:00
Boris-Chengbiao Zhou
dfef0d7585 Add print media query 2016-06-10 19:30:26 +02:00
Mathieu David
9b49acc2c9 Merge pull request #139 from japaric/no-default-features
ci: test without default features, closes #138
2016-05-09 09:53:25 +02:00
Jorge Aparicio
1ae0d4f637 ci: test without default features
closes #138
2016-05-08 18:26:33 -05:00
Mathieu David
f9aa9a6843 Merge pull request #137 from Bobo1239/fix-no-default-features
Fix no-default-features build, fixes #136
2016-05-08 23:29:35 +02:00
Boris-Chengbiao Zhou
9b1e224680 Fix no-default-features build 2016-05-08 21:51:34 +02:00
Mathieu David
cfcf6d952f Merge pull request #134 from Bobo1239/serve-print
Add host and port output when running mdbook serve Closes #132
2016-04-27 22:35:26 +02:00
Boris-Chengbiao Zhou
e3f398cff2 Add address output to mdbook serve 2016-04-27 22:29:48 +02:00
Mathieu David
6bc088db6e (Refactor) Move the Render trait into mod.rs instead of submodule 2016-04-27 14:19:59 +02:00
Mathieu David
e34bef0e53 (Refactor) Move mdbook.rs to mod.rs 2016-04-26 23:04:27 +02:00
Mathieu David
15d6227a11 Attempt to fix #119 replace \ with / in paths, so that Windows also uses / as separator (ugly hack) 2016-04-25 17:02:47 +02:00
Mathieu David
1b8af2bf57 Fix #120 destination and source directories can now be constructed correctly even if multiple directories do not exist on the path 2016-04-25 15:58:44 +02:00
Mathieu David
7f34512751 Merge branch 'master' of https://github.com/azerupi/mdBook 2016-04-25 15:51:27 +02:00
Mathieu David
876ea7895a Fix #131 where src and dest paths were not prefixed with the root directory if it was not the current directory 2016-04-25 15:50:34 +02:00
Mathieu David
bea80f6266 Merge pull request #130 from japaric/rustup
travis: use rustup instead of Travis built-in Rust support
2016-04-14 01:20:19 +02:00
Jorge Aparicio
334540835c travis: use rustup instead of Travis built-in Rust support
this ensures we install the correct set of standard crates when working
in the beta channel
2016-04-13 16:48:57 -05:00
Mathieu David
10e7a41d92 Bump version to 0.0.12, version 0.0.11 has been published to crates.io 2016-04-13 22:37:46 +02:00
Mathieu David
2ec5648587 Remove BookConfig field from MDBook
MDBook now stores the necessary information, BookConfig is not used as a field anymore. It is only used for parsing the configuration file. This allows to more easily replace the book.json config with the new tomlbased config
2016-04-05 12:44:14 +02:00
Mathieu David
6aa6546ce4 Merge pull request #128 from Bobo1239/serve-squashed
Implement Serve feature
2016-04-04 23:17:01 +02:00
Mathieu David
c071406fef Merge pull request #127 from japaric/mac32
travis: test/release on/for i686-apple-darwin
2016-04-04 19:23:36 +02:00
Boris-Chengbiao Zhou
c8051294b0 Switch from rust-websocket to ws-rs 2016-04-02 21:44:13 +02:00
Jorge Aparicio
807a2f116e travis: test/release on/for i686-apple-darwin 2016-04-02 09:33:05 -05:00
Boris-Chengbiao Zhou
2f43167b75 Add documentation for Serve feature 2016-04-02 05:43:21 +02:00
Boris-Chengbiao Zhou
e861880f95 Implement Serve feature 2016-04-02 05:20:46 +02:00
Mathieu David
c3564f1699 Add convenience function to read the content from a file into a string given a path 2016-03-27 18:40:50 +02:00
Mathieu David
15d26befcc Refactor: Move extern crate definitions to lib.rs 2016-03-27 18:22:17 +02:00
Mathieu David
925939e267 Merge pull request #124 from LucioFranco/insecure-content-fix
Fix for insecure content on HTTPS enabled sites
2016-03-23 23:22:44 +01:00
Lucio Franco
ceb139a848 Moved CDN's to https 2016-03-23 14:16:41 -06:00
Mathieu David
d0e39f469a Update installation instructions in the README
Closes #123
2016-03-22 14:06:32 +01:00
Mathieu David
c5752620d7 Merge pull request #122 from Bobo1239/fix_cooldown
Fix watch event cooldown
2016-03-19 19:40:58 +01:00
Mathieu David
0c93599242 Merge pull request #121 from Bobo1239/update_deps
Update dependencies
2016-03-19 19:38:01 +01:00
Boris Zhou
7f3a6c8130 Fix watch event cooldown 2016-03-19 18:28:34 +01:00
Boris-Chengbiao Zhou
b30a8bdc81 Update dependencies 2016-03-19 17:45:58 +01:00
Mathieu David
74fff81e4b Refactor: Move fs related functions from utils into their own submodule 2016-03-17 22:41:00 +01:00
Mathieu David
ad0794a0bd Add a rustfmt config and run rustfmt on the code base 2016-03-17 22:31:28 +01:00
Mathieu David
6bac41caa8 Merge pull request #118 from japaric/travis
Travis CI: expand to test and deploy for Linux and Mac
2016-03-08 17:25:52 +01:00
Jorge Aparicio
b094268b68 disable the i686-apple-darwin target 2016-03-08 09:21:50 -05:00
Jorge Aparicio
02a37e0ee9 disable doc tests when crossing 2016-03-08 08:57:35 -05:00
Mathieu David
9e34eccb3e Add windows (AppVeyor) build badge 2016-03-08 01:03:44 +01:00
Mathieu David
79fb92ed7c Merge pull request #117 from japaric/appveyor
set up AppVeyor to test and deploy on Windows
2016-03-08 00:35:54 +01:00
Jorge Aparicio
5e78697ab1 Travis CI: expand to test and deploy for Linux and Mac 2016-03-07 18:16:42 -05:00
Jorge Aparicio
469cb10d4a manually package artifact during before_deploy phase
The automatic packaging phase runs before the before_deploy phase which is too early so we can rely
on it.
2016-03-07 18:01:05 -05:00
Jorge Aparicio
0f9caf4410 set up AppVeyor to test and deploy on Windows 2016-03-07 17:14:05 -05:00
Mathieu David
f23a5f2729 Merge pull request #115 from vrinek/init-with-gitignore-take-2
Move `.gitignore` directly under the root folder
2016-03-07 12:09:25 +01:00
vrinek
bc41efe414 Move .gitignore directly under the root folder 2016-03-07 08:52:19 +00:00
Mathieu David
5316089e61 Modify wording of confirmation request before creation of .gitignore 2016-03-02 19:38:39 +01:00
Mathieu David
73ce3f814a Merge branch 'init-with-gitignore' of https://github.com/vrinek/mdBook into vrinek-init-with-gitignore 2016-03-02 19:20:21 +01:00
Mathieu David
075da959c9 bump version, v0.0.10 has been published to crates.io 2016-03-01 18:50:04 +01:00
Mathieu David
80deac90d9 Merge branch 'master' of https://github.com/azerupi/mdBook 2016-03-01 18:33:55 +01:00
Mathieu David
625f5081fa update notify and change dependency version restrictions 2016-03-01 18:32:43 +01:00
vrinek
1eb59428e6 Ask user to create .gitignore and skip on --force 2016-02-28 15:28:11 +00:00
Mathieu David
3e8151e8e3 Merge pull request #112 from jessestricker/feature-meta
Add description config option
2016-02-25 17:21:51 +01:00
Jesse Stricker
3c10a85735 Add documentation and example for description config 2016-02-25 15:01:16 +01:00
Jesse Stricker
330b1ad55d Add description config option 2016-02-25 14:32:49 +01:00
vrinek
596455f28c Generate simple .gitignore on init 2016-02-23 14:03:45 +00:00
Mathieu David
f24eb59753 Bump version number, v0.0.9 has been published to Crates.io 2016-02-22 19:03:31 +01:00
Mathieu David
01c5085725 Add an entry about the favicon in the docs 2016-02-22 18:01:36 +01:00
Mathieu David
9f17be2c32 Merge pull request #109 from jessestricker/feature-favicon
Add theme support for favicon
2016-02-22 17:28:46 +01:00
Jesse Stricker
88fabd76f0 Copy favicon on 'init --theme' 2016-02-22 17:20:54 +01:00
Jesse Stricker
f508db6113 Add favicon support to theme 2016-02-22 17:17:07 +01:00
Jesse Stricker
1083d1822d Add default favicon.png 2016-02-22 16:59:53 +01:00
Mathieu David
fc86b963bb Merge pull request #108 from funkill/styles
add rounding for first and last items in theme selector
2016-02-16 10:30:01 +01:00
funkill
f2b913c9dd add rounding for first and last items in theme selector 2016-02-16 11:15:08 +03:00
Mathieu David
dd0cfc14d4 bump version number to 0.0.9, v0.0.8 has been published to crates.io 2016-02-16 08:57:55 +01:00
Mathieu David
5891e4b5db Fix bug where theme-popup was under the navigation arrows making it impossible to change the theme 2016-02-16 08:50:57 +01:00
Mathieu David
394023f617 Bump version number from 0.0.7 to 0.0.8, version 0.0.7 has been published to Crates.io 2016-02-15 21:20:07 +01:00
Mathieu David
39a6fe4b3c Fix wildcard dependency on crossbeam 2016-02-15 21:18:43 +01:00
Mathieu David
75b98d7019 Merge pull request #106 from funkill/pulldown-mark-update
pulldown-mark version bump
2016-02-15 21:01:21 +01:00
funkill
814b21ad94 pulldown-mark version bump 2016-02-15 21:25:46 +03:00
Mathieu David
7364d41f0c Style tables, different header bg, alternate row color and border 2016-02-05 18:09:35 +01:00
Mathieu David
f6be4a7d7e Merge branch 'master' of https://github.com/azerupi/mdBook 2016-02-03 18:02:23 +01:00
Mathieu David
0b00c270d5 Fix a style bug caused by the insertion of the theme-popup div inside font awesome icon <i>
The div is now inserted after the <i>, the text color has also been changed to the foreground color and the "(default)" text that indicates the default theme is now grey to contrast with the theme name

Fixes #97
2016-02-03 17:55:19 +01:00
Mathieu David
8d2ca521c0 Merge pull request #100 from yurrriq/patch-1
Fix "it's" typos in README.md
2016-01-28 11:58:17 +01:00
Eric Bailey
276cd8d490 Update README.md
Replace two contractions with possessive pronouns: it's => its.
2016-01-28 00:55:25 -06:00
Mathieu David
e958fc8605 update readme 2016-01-13 22:40:30 +01:00
Mathieu David
e286b208da Bump version from 0.0.6 to 0.0.7, v0.0.6 has been published to Crates.io 2016-01-03 14:18:36 +01:00
101 changed files with 5619 additions and 2129 deletions

4
.gitattributes vendored Normal file
View File

@@ -0,0 +1,4 @@
[attr]rust text eol=lf whitespace=tab-in-indent,trailing-space,tabwidth=4
* text=auto eol=lf
*.rs rust

5
.gitignore vendored
View File

@@ -1,5 +1,10 @@
Cargo.lock
target
# MacOS temp file
.DS_Store
book-test
book-example/book
.vscode

View File

@@ -1,20 +1,43 @@
language: rust
sudo: false
cache: cargo
rust:
- stable
- beta
- nightly
matrix:
allow_failure:
- rust: nightly
env:
global:
secure: l3/qEC4krRerllLQzni8j5AjngFi6pluWvBWj//1mJLoIEYwxlQ9mYxEdd9BqccWWFn3K0bVYCVC/64+tP6sRfLkZCe2gPUtwe7ITwCDbapUxmkiRObVJCs5yMQZt6idyhHUDKAXKgNCrusfI2BM3tKGBfRK7Cnn/R/7p/U9+q7D1sgJtUKp6ypVzK6A3jLNp3dFLFI19a5KmbZMVsaa7tOhtdDJjjr7ebsc9z7HMW5/OItiWU3FSauVQQlUMaCiEgFuIG7H7OnBAYWB/gNEtLuwfLqU9UjtWk/njNNRnmJ7m3y5HbQhv5H5F5mJUOq9XFlPLwPwyTeVztSGdQm6k8Pp2pgKBUjY27afBl9BWU+msmN6k0oXfhvIebiBPe/x2udiKeFik1xqOOEU1q9dF0sZiuPxCSM1n7tgWklJ8epgaRQaMPPQw9pO/2H5/ynHCJqBlw6WcdiqWtwAyyr/GEx62u/cg5IVkqb7KLmYsWzjS8wYG4CYs1eIxCw2xPZxP0FGuUXvxTBUPipFze6Z7FqxVauXtVe2D7c1P4738HZP660rmR0GYtHtKLny1QxCCK9sxd9JmcezFCSz4YeQ1od9xc0OzGJ2ullKNGizmGfYmgL6X8faNylLIEdaiHAcY16xV3L0g3fXL1Qg360UHQyj7GIv+0nqQnf+H9xRTTU=
addons:
apt:
packages:
- nodejs
- npm
install:
- npm install stylus nib
- stable
- nightly
os:
- linux
- osx
script:
- cargo build --verbose
- cargo test --verbose
after_success:
- test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && bash deploy.sh
# Deploy the docs if the commit is on master
- test "$TRAVIS_PULL_REQUEST" == "false" &&
test "$TRAVIS_BRANCH" == "master" &&
test "$TRAVIS_RUST_VERSION" == "stable" &&
npm install stylus nib &&
bash ci/deploy.sh
before_deploy:
# Script to create packages from the build artefacts to upload to GitHub
- bash 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}-${TRAVIS_OS_NAME}.tar.gz
# don't delete the artifacts from previous phases
skip_cleanup: true
on:
condition: $TRAVIS_RUST_VERSION = stable
tags: true
notifications:
email:
on_success: never

84
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,84 @@
# Contributing
Welcome stranger!
If you have come here to learn how to contribute to mdBook, we have some tips for you!
First of all, don't hesitate to ask questions!
Use the [issue tracker](https://github.com/rust-lang-nursery/mdBook/issues), no question is too simple.
If we don't respond in a couple of days, ping us @Michael-F-Bryan, @budziq, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
### Issues to work on
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
[E-Easy issues](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
include documentation improvements, new tests, examples, updating dependencies, etc.
If you come from a web development background, you might be interested in issues related to web technologies tagged
[A-JavaScript](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
[A-Style](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
[A-HTML](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
[A-Mobile](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
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.
Issues on the issue tracker are categorized with the following labels:
- **A**-prefixed labels state which area of the project an issue relates to.
- **E**-prefixed labels show an estimate of the experience necessary to fix the issue.
- **M**-prefixed labels are meta-issues used for questions, discussions, or tracking issues
- **S**-prefixed labels show the status of the issue
- **T**-prefixed labels show the type of issue
### Building mdBook
mdBook builds on stable Rust, if you want to build mdBook from source, here are the steps to follow:
1. Navigate to the directory of your choice
0. Clone this repository with git.
```
git clone https://github.com/rust-lang-nursery/mdBook.git
```
0. Navigate into the newly created `mdBook` directory
0. Run `cargo build`
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
### Making changes to the style
mdBook doesn't use CSS directly but uses [Stylus](http://stylus-lang.com/), a CSS-preprocessor which compiles to CSS.
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/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.
```
npm install -g stylus nib
```
When that finished, you can simply regenerate the CSS files by building mdBook with the following command:
```
cargo build --features=regenerate-css
```
This should automatically call the appropriate stylus command to recompile the files to CSS and include them in the project.
### Making a pull-request
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
One of the core maintainers will then approve the changes or request some changes before it gets merged.
If you want to make your pull-request even better, you might want to run [Clippy](https://github.com/Manishearth/rust-clippy)
and [rustfmt](https://github.com/rust-lang-nursery/rustfmt) on the code first.
This is not a requirement though and will never block a pull-request from being merged.
That's it, happy contributions! :tada: :tada: :tada:

View File

@@ -1,50 +1,60 @@
[package]
name = "mdbook"
version = "0.0.6"
version = "0.0.28"
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"
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"
build = "build.rs"
exclude = [
"book-example/*",
"src/theme/stylus",
"src/theme/stylus/**",
]
[dependencies]
clap = "~1.5.3"
handlebars = "~0.12.0"
rustc-serialize = "~0.3.16"
pulldown-cmark = "~0.0.5"
clap = "2.24"
handlebars = "0.29"
serde = "1.0"
serde_derive = "1.0"
error-chain = "0.11.0"
serde_json = "1.0"
pulldown-cmark = "0.1"
lazy_static = "0.2"
log = "0.3"
env_logger = "0.4.0"
toml = "0.4"
open = "1.1"
regex = "0.2.1"
tempdir = "0.3.4"
# Watch feature
[dependencies.notify]
notify = "^2.4.1"
optional = true
notify = { version = "4.0", optional = true }
time = { version = "0.1.34", optional = true }
crossbeam = { version = "0.3", optional = true }
[dependencies.time]
time = "^0.1.33"
optional = true
# Serve feature
iron = { version = "0.5", optional = true }
staticfile = { version = "0.4", optional = true }
ws = { version = "0.7", optional = true}
[dependencies.crossbeam]
time = "^0.2.0"
optional = true
[build-dependencies]
error-chain = "0.11"
# Tests
[dev-dependencies]
tempdir = "~0.3.4"
select = "0.4"
pretty_assertions = "0.4"
walkdir = "1.0"
[features]
default = ["output", "watch"]
default = ["output", "watch", "serve"]
debug = []
output = []
regenerate-css = []
watch = ["notify", "time", "crossbeam"]
serve = ["iron", "staticfile", "ws"]
[[bin]]
doc = false

141
README.md
View File

@@ -1,80 +1,129 @@
# mdBook [![Travis-CI](https://travis-ci.org/azerupi/mdBook.svg?branch=master)](https://travis-ci.org/azerupi/mdBook) [![Crates.io version](https://img.shields.io/crates/v/mdbook.svg)](https://crates.io/crates/mdbook) [![License](https://img.shields.io/crates/l/mdbook.svg)](LICENSE)
# mdBook
Personal implementation of Gitbook in Rust
<table>
<tr>
<td><strong>Linux / OS X</strong></td>
<td>
<a href="https://travis-ci.org/rust-lang-nursery/mdBook"><img src="https://travis-ci.org/rust-lang-nursery/mdBook.svg?branch=master"></a>
</td>
</tr>
<tr>
<td><strong>Windows</strong></td>
<td>
<a href="https://ci.appveyor.com/project/azerupi/mdbook/"><img src="https://ci.appveyor.com/api/projects/status/o38racsnbcospyc8/branch/master?svg=true"></a>
</td>
</tr>
<tr>
<td colspan="2">
<a href="https://crates.io/crates/mdbook"><img src="https://img.shields.io/crates/v/mdbook.svg"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg"></a>
</td>
</tr>
</table>
**This project is still in it's early days.**
For more information about what is left on my to-do list, check the issue tracker
mdBook is a utility to create modern online books from Markdown files.
## Example
## What does it look like?
To have an idea of what a rendered book looks like,take a look at the [**Documentation**](http://azerupi.github.io/mdBook/). It is rendered by the latest version of mdBook.
The [**User Guide**](https://rust-lang-nursery.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.
## Installation
```
cargo install mdbook
```
There are multiple ways to install mdBook.
If you want to regenerate the css (stylesheet), clone the git repo locally and make sure that you installed `stylus` and `nib` from `npm` because it is used to compile the stylesheets
1. **Binaries**
Binaries are available for download [here](https://github.com/rust-lang-nursery/mdBook/releases). Make sure to put the path to the binary into your `PATH`.
Install [node.js](https://nodejs.org/en/)
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
```
```
npm install -g stylus nib
```
This will download and compile mdBook for you, the only thing left to do is to add the Cargo bin directory to your `PATH`.
Then build with the `regenerate-css` feature:
**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 build --release --features="regenerate-css"
```
```
cargo install mdbook --vers "^0.1.0"
```
## Structure
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***!
There are two main parts of this project:
```
cargo install --git https://github.com/rust-lang-nursery/mdBook.git
```
Again, make sure to add the Cargo bin directory to your `PATH`.
- **The library:** The crate is structured so that all the code that actually does something is part of the library. You could therefore easily hook mdbook into your existing project, extend it's functionality by wrapping it in some other code, etc.
- **The binary:** Is just a wrapper around the library functionality providing a nice and easy command line interface.
4. **For Contributions**
If you want to contribute to mdBook you will have to clone the repository on your local machine:
### Command line interface
```
git clone https://github.com/rust-lang-nursery/mdBook.git
```
`cd` into `mdBook/` and run
#### init
```
cargo build
```
If you run `mdbook init` in a directory, it will create a couple of folders and files you can start with.
This is the strucutre it creates at the moment:
```
book-test/
├── book
└── src
├── chapter_1.md
└── SUMMARY.md
```
`book` and `src` are both directories. `src` contains the markdown files that will be used to render the ouput to the `book` directory.
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
Please, take a look at the [**Documentation**](http://azerupi.github.io/mdBook/cli/init.html) for more information.
#### build
Use `mdbook build` in the directory to render the book. You can find more information in the [**Documentation**](http://azerupi.github.io/mdBook/cli/build.html)
## 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.
Here are the main commands you will want to run. For a more exhaustive explanation, check out the [User Guide](http://rust-lang-nursery.github.io/mdBook/).
- `mdbook init`
The init command will create a directory with the minimal boilerplate to start with.
```
book-test/
├── book
└── src
├── chapter_1.md
└── 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.
Please, take a look at the [**Documentation**](http://rust-lang-nursery.github.io/mdBook/cli/init.html) 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.
- `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.
- `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.
### 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 webapp 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 and 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](https://rust-lang-nursery.github.io/mdBook/) and the [API docs](https://docs.rs/mdbook/*/mdbook/) for more information.
## Contributions
Contributions are highly apreciated. Here are some ideas:
Contributions are highly appreciated and encouraged! Don't hesitate to participate to discussions in the issues, propose new features and ask for help.
- **Create new renderers**, at the moment I have only created a renderer that uses [handlebars](https://github.com/sunng87/handlebars-rust), [pulldown-cmark](https://github.com/google/pulldown-cmark) and renders to html. But you could create a renderer that uses another template engine, markdown parser or even outputs to another format like pdf.
- **Add tests** I have not much experience in writing tests, all help to write meaningful tests is thus very welcome
- **write documentation** documentation can always be improved
- **Smaller tasks** I try to add a lot of the remaining tasks on the issue tracker with the label: [`Enhancement`](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AEnhancement). Just pick one that looks interesting. The majority of the tasks are small enough to be tackled by people who are unfamiliar with the project.
If you are just starting out with Rust, there are a series of issus that are tagged [E-Easy](https://github.com/rust-lang-nursery/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.
For more info about contributing, check out our [contribution guide](CONTRIBUTING.md) who helps you go through the build and contribution process!
If you have an idea for improvement, create a new issue. Or a pull request if you can :)
## License
All the code 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](LICENSE) file.

69
appveyor.yml Normal file
View File

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

View File

@@ -1,4 +0,0 @@
{
"title": "mdBook Documentation",
"author": "Mathieu David"
}

6
book-example/book.toml Normal file
View File

@@ -0,0 +1,6 @@
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
author = "Mathieu David"
[output.html]
mathjax-support = true

View File

@@ -2,14 +2,13 @@
**mdBook** is a command line tool and Rust crate to create books using Markdown files. It's very similar to Gitbook but written in [Rust](http://www.rust-lang.org).
What you are reading serves as an example of the output of mdBook and at the same time as high-level docs.
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

@@ -5,6 +5,7 @@
- [init](cli/init.md)
- [build](cli/build.md)
- [watch](cli/watch.md)
- [serve](cli/serve.md)
- [test](cli/test.md)
- [Format](format/format.md)
- [SUMMARY.md](format/summary.md)

View File

@@ -21,6 +21,15 @@ current working directory.
mdbook build path/to/book
```
#### --open
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
your default web browser after building it.
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
-------------------
***note:*** *make sure to run the build command in the root directory and not in the source directory*

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

@@ -22,7 +22,7 @@ configuration files, etc.
- The `book` directory is where your book is rendered. All the output is ready to be uploaded
to a server to be seen by your audience.
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](../format/summary.html).
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](format/summary.html).
#### Tip & Trick: Hidden Feature
When a `SUMMARY.md` file already exists, the `init` command will first parse it and generate the missing files according to the paths used in the `SUMMARY.md`. This allows you to think and create the whole structure of your book and then let mdBook generate it for you.

View File

@@ -0,0 +1,40 @@
# The serve command
The `serve` command is useful when you want to preview your book. It also does hot reloading of the webpage whenever a file changes.
It achieves this by serving the books content over `localhost:3000` (unless otherwise configured, see below) and runs a websocket server on `localhost:3001` which triggers the reloads.
This preferred by many for writing books with mdbook because it allows for you to see the result of your work instantly after every file change.
#### Specify a directory
Like `watch`, `serve` can take a directory as argument to use instead of the
current working directory.
```bash
mdbook serve path/to/book
```
#### Server options
`serve` has four options: the http port, the websocket port, the interface to serve on, and the public address of the server so that the browser may reach the websocket server.
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
```
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.
#### --open
When you use the `--open` (`-o`) option, mdbook will open the book in your
your default web browser after starting the server.
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
-----
***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

@@ -10,7 +10,7 @@ mdBook supports a `test` command that will run all available tests in mdBook. At
- checking for unused files
- ...
In the future I would like the user to be able to enable / disable test from the `book.json` configuration file and support custom tests.
In the future I would like the user to be able to enable / disable test from the `book.toml` configuration file and support custom tests.
**How to use it:**
```bash

View File

@@ -12,7 +12,15 @@ current working directory.
mdbook watch path/to/book
```
#### --open
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
your default web browser.
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
-----
***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

@@ -1,21 +1,155 @@
# Configuration
You can configure the parameters for your book in the ***book.json*** file.
You can configure the parameters for your book in the ***book.toml*** file.
Here is an example of what a ***book.json*** file might look like:
Here is an example of what a ***book.toml*** file might look like:
```json
{
"title": "Example book",
"author": "Name",
"dest": "output/my-book"
}
```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]
additional-css = ["custom.css"]
```
#### Supported variables
## Supported configuration options
- **title:** title of the book
- **author:** author of the book
- **dest:** path to the directory where you want your book to be rendered. If a relative path is given it will be relative to the parent directory of the source directory
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.
***note:*** *the supported configurable parameters are scarce at the moment, but more will be added in the future*
### General metadata
This is general information about your book.
- **title:** The title of the book
- **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`
```
### 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
[build]
build-dir = "build"
create-missing = false
```
### HTML renderer options
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:
pub playpen: Playpen,
- **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.
- **playpen:** A subtable for configuring various playpen settings.
**book.toml**
```toml
[book]
title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
[output.html]
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
```
## For Developers
If you are developing a plugin or alternate backend then whenever your code is
called you will almost certainly be passed a reference to the book's `Config`.
This can be treated roughly as a nested hashmap which lets you call methods like
`get()` and `get_mut()` to get access to the config's contents.
By convention, plugin developers will have their settings as a subtable inside
`plugins` (e.g. a link checker would put its settings in `plugins.link_check`)
and backends should put their configuration under `output`, like the HTML
renderer does in the previous examples.
As an example, some hypothetical `random` renderer would typically want to load
its settings from the `Config` at the very start of its rendering process. The
author can take advantage of serde to deserialize the generic `toml::Value`
object retrieved from `Config` into a struct specific to its use case.
```rust
#[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 book_config = Config::from_str(src)?; // usually passed in by mdbook
let random: Value = book_config.get("output.random").unwrap_or_default();
let got: RandomOutput = random.try_into()?;
assert_eq!(got, should_be);
if let Some(baz) = book_config.get_deserialized::<Vec<bool>>("output.random.baz") {
println!("{:?}", baz); // prints [true, true, false]
// do something interesting with baz
}
// start the rendering process
```

View File

@@ -4,5 +4,5 @@ In this section you will learn how to:
- Structure your book correctly
- Format your `SUMMARY.md` file
- Configure your book using `book.json`
- Configure your book using `book.toml`
- Customize your theme

View File

@@ -1,21 +1,31 @@
# MathJax Support
mdBook supports math equations through [MathJax](https://www.mathjax.org/).
mdBook has optional support for math equations through [MathJax](https://www.mathjax.org/).
**However the normal method for indication math equations with `$$` does not work (yet?).**
To enable MathJax, you need to add the `mathjax-support` key to your `book.toml` under the `output.html` section.
To indicate an inline equation \\( \int x = \frac{x^2}{2} \\) use
```
\\( \int x = \frac{x^2}{2} \\)
```toml
[output.html]
mathjax-support = true
```
To indicate a block equation
>**Note:**
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:
```
\\( \int x dx = \frac{x^2}{2} + C \\)
```
### Block equations
Block equations are delimited by `\\[` and `\\]`. To render the following equation
\\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\]
use
you would write:
```
```bash
\\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\]
```

View File

@@ -5,23 +5,23 @@
There is a feature in mdBook that let's you hide code lines by prepending them with a `#`.
```bash
#fn main() {
# fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
#}
# }
```
Will render as
```rust
#fn main() {
# fn main() {
let x = 5;
let y = 7;
println!("{}", x + y);
#}
# }
```

View File

@@ -27,6 +27,6 @@ allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
```
You can either use `-` or `*` to indicate a numbered chapter.
4. ***Sufix 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.
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of non-numbered chapters. They are the same as prefix chapters but come after the numbered chapters instead of before.
All other elements are unsupported and will be ignored at best or result in an error.

View File

@@ -19,7 +19,8 @@ Here is a list of the properties that are exposed:
- ***language*** Language of the book in the form `en`. To use in <code class="language-html">\<html lang="{{ language }}"></code> for example.
At the moment it is hardcoded.
- ***title*** Title of the book, as specified in `book.json`
- ***title*** Title of the book, as specified in `book.toml`
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
- ***path*** Relative path to the original markdown file from the source directory
- ***content*** This is the rendered markdown.
@@ -86,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

@@ -28,32 +28,32 @@ There is a feature in mdBook that let's you hide code lines by prepending them w
```bash
#fn main() {
# fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
#}
# }
```
Will render as
```rust
#fn main() {
# fn main() {
let x = 5;
let y = 7;
println!("{}", x + y);
#}
# }
```
**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.json` so that everyone can benefit from it.**
**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.toml` so that everyone can benefit from it.**
## Improve default theme
If you think the default theme doesn't look quite right for a specific language, 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

@@ -4,7 +4,7 @@ The default renderer uses a [handlebars](http://handlebarsjs.com/) template to r
included in the mdBook binary.
The theme is totally customizable, you can selectively replace every file from the theme by your own by adding a
`theme` directory in your source folder. Create a new file with the name of the file you want to override
`theme` directory next to `src` folder in your project root. Create a new file with the name of the file you want to override
and now that file will be used instead of the default file.
Here are the files you can override:
@@ -14,6 +14,7 @@ Here are the files you can override:
- ***book.js*** is mostly used to add client side functionality, like hiding / un-hiding the sidebar, changing the theme, ...
- ***highlight.js*** is the JavaScript that is used to highlight code snippets, you should not need to modify this.
- ***highlight.css*** is the theme used for the code highlighting
- ***favicon.png*** the favicon that will be used
Generally, when you want to tweak the theme, you don't need to override all the files. If you only need changes in the stylesheet,
there is no point in overriding all the other files. Because custom files take precedence over built-in ones, they will not get updated with new fixes / features.

View File

@@ -9,14 +9,16 @@ extern crate mdbook;
use mdbook::MDBook;
use std::path::Path;
# #[allow(unused_variables)]
fn main() {
let mut book = MDBook::new(Path::new("my-book")) // Path to root
.set_src(Path::new("src")) // Path from root to source directory
.set_dest(Path::new("book")) // Path from root to output directory
.read_config(); // Parse book.json file for configuration
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
book.build().unwrap(); // Render the book
}
```
Check here for the [API docs](../mdbook/index.html) generated by rustdoc.
Check here for the [API docs](mdbook/index.html) generated by rustdoc.

View File

@@ -11,3 +11,5 @@ 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)

View File

@@ -0,0 +1,3 @@
# Introduction
A frontmatter chapter.

View File

@@ -1,29 +1,98 @@
// build.rs
use std::process::Command;
use std::env;
use std::path::Path;
#[macro_use]
extern crate error_chain;
fn main() {
#[cfg(windows)]
mod execs {
use std::process::Command;
pub fn cmd(program: &str) -> Command {
let mut cmd = Command::new("cmd");
cmd.args(&["/c", program]);
cmd
}
}
#[cfg(not(windows))]
mod execs {
use std::process::Command;
pub fn cmd(program: &str) -> Command {
Command::new(program)
}
}
error_chain!{
foreign_links {
Io(std::io::Error);
}
}
fn program_exists(program: &str) -> Result<()> {
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();
match status {
Ok(ref out) if out.status.success() => Ok(()),
_ => {
bail!("Missing npm package '{0}' install with: 'npm -g install {0}'",
package)
}
}
}
pub enum Resource<'a> {
Program(&'a str),
Package(&'a str),
}
use Resource::{Package, Program};
impl<'a> Resource<'a> {
pub fn exists(&self) -> Result<()> {
match *self {
Program(name) => program_exists(name),
Package(name) => npm_package_exists(name),
}
}
}
fn run() -> Result<()> {
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
// Check dependencies
Program("npm").exists()?;
Program("node").exists().or(Program("nodejs").exists())?;
Package("nib").exists()?;
Package("stylus").exists()?;
// Compile stylus stylesheet to css
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
.chain_err(|| "Please run the script with: 'cargo build'!")?;
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
let stylus_dir = theme_dir.join("stylus/book.styl");
if !Command::new("stylus")
.arg(format!("{}", stylus_dir.to_str().unwrap()))
.arg("--out")
.arg(format!("{}", theme_dir.to_str().unwrap()))
.arg("--use")
.arg("nib")
.status().unwrap()
.success() {
panic!("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(())
}
quick_main!(run);

32
ci/before_deploy.sh Normal file
View File

@@ -0,0 +1,32 @@
# `before_deploy` phase: here we package the build artifacts
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}-${TRAVIS_OS_NAME}.tar.gz *
popd $td
rm -r $td
}
main() {
mk_artifacts
mk_tarball
}
main

View File

@@ -15,7 +15,7 @@ cargo doc
echo -e "${CYAN}Running mdbook build${NC}"
# Run mdbook to generate the book
target/debug/mdbook build book-example/
target/"$TARGET"/debug/mdbook build book-example/
echo -e "${CYAN}Copying book to target/doc${NC}"
# Copy files from rendered book to doc root
@@ -28,7 +28,7 @@ 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 remote add upstream "https://$GH_TOKEN@github.com/rust-lang-nursery/mdBook.git"
git fetch upstream
git reset upstream/gh-pages

7
rustfmt.toml Normal file
View File

@@ -0,0 +1,7 @@
array_layout = "Visual"
chain_indent = "Visual"
fn_args_layout = "Visual"
fn_call_style = "Visual"
format_strings = true
generics_indent = "Visual"

46
src/bin/build.rs Normal file
View File

@@ -0,0 +1,46 @@
use std::path::PathBuf;
use clap::{ArgMatches, SubCommand, App};
use mdbook::MDBook;
use mdbook::errors::Result;
use {get_book_dir, open};
// Create clap subcommand arguments
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 (deprecated: use book.toml instead)'",
)
.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 mut book = MDBook::new(&book_dir).read_config()?;
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = PathBuf::from(dest_dir);
}
// This flag is deprecated in favor of being set via `book.toml`.
if args.is_present("no-create") {
book.config.build.create_missing = false;
}
book.build()?;
if args.is_present("open") {
open(book.get_destination().join("index.html"));
}
Ok(())
}

75
src/bin/init.rs Normal file
View File

@@ -0,0 +1,75 @@
use std::io;
use std::io::Write;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use get_book_dir;
// Create clap subcommand arguments
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("--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()?;
// If flag `--theme` is present, copy theme to src
if args.is_present("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.");
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);
}
}
// 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.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.");
}
}
println!("\nAll done, no errors...");
Ok(())
}
// Simple function that user comfirmation
fn confirm() -> bool {
io::stdout().flush().unwrap();
let mut s = String::new();
io::stdin().read_line(&mut s).ok();
match &*s.trim() {
"Y" | "y" | "yes" | "Yes" => true,
_ => false,
}
}

View File

@@ -1,228 +1,101 @@
#[macro_use]
extern crate mdbook;
#[macro_use]
extern crate clap;
extern crate crossbeam;
// Dependencies for the Watch feature
#[cfg(feature = "watch")]
extern crate notify;
#[cfg(feature = "watch")]
extern crate time;
extern crate env_logger;
extern crate log;
extern crate mdbook;
extern crate open;
use std::env;
use std::error::Error;
use std::ffi::OsStr;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use clap::{App, AppSettings, ArgMatches};
use log::{LogLevelFilter, LogRecord};
use env_logger::LogBuilder;
use clap::{App, ArgMatches, SubCommand};
// Uses for the Watch feature
pub mod build;
pub mod init;
pub mod test;
#[cfg(feature = "serve")]
pub mod serve;
#[cfg(feature = "watch")]
use notify::Watcher;
#[cfg(feature = "watch")]
use std::sync::mpsc::channel;
use mdbook::MDBook;
pub mod watch;
const NAME: &'static str = "mdbook";
fn main() {
init_logger();
// Create a list of valid arguments and sub-commands
let matches = App::new(NAME)
.about("Create a book in form of a static website from markdown files")
.author("Mathieu David <mathieudavid@mathieudavid.org>")
// Get the version from our Cargo.toml using clap's crate_version!() macro
.version(&*format!("v{}", crate_version!()))
.subcommand_required(true)
.after_help("For more information about a specific command, try `mdbook <command> --help`")
.subcommand(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 ommitted)'")
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
.arg_from_usage("--force 'skip confirmation prompts'"))
.subcommand(SubCommand::with_name("build")
.about("Build the book from the markdown files")
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
.subcommand(SubCommand::with_name("watch")
.about("Watch the files for changes")
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
.subcommand(SubCommand::with_name("test")
.about("Test that code samples compile"))
.get_matches();
let app = App::new(NAME)
.about("Create a book in form of a static website from markdown files")
.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`\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());
#[cfg(feature = "watch")]
let app = app.subcommand(watch::make_subcommand());
#[cfg(feature = "serve")]
let app = app.subcommand(serve::make_subcommand());
// Check which subcomamnd the user ran...
let res = match matches.subcommand() {
("init", Some(sub_matches)) => init(sub_matches),
("build", Some(sub_matches)) => build(sub_matches),
let res = match app.get_matches().subcommand() {
("init", Some(sub_matches)) => init::execute(sub_matches),
("build", Some(sub_matches)) => build::execute(sub_matches),
#[cfg(feature = "watch")]
("watch", Some(sub_matches)) => watch(sub_matches),
("test", Some(sub_matches)) => test(sub_matches),
(_, _) => unreachable!()
("watch", Some(sub_matches)) => watch::execute(sub_matches),
#[cfg(feature = "serve")]
("serve", Some(sub_matches)) => serve::execute(sub_matches),
("test", Some(sub_matches)) => test::execute(sub_matches),
(_, _) => unreachable!(),
};
if let Err(e) = res {
writeln!(&mut io::stderr(), "An error occured:\n{}", e).ok();
::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())
};
// Simple function that user comfirmation
fn confirm() -> bool {
io::stdout().flush().unwrap();
let mut s = String::new();
io::stdin().read_line(&mut s).ok();
match &*s.trim() {
"Y" | "y" | "yes" | "Yes" => true,
_ => false
}
}
// Init command implementation
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir);
// Call the function that does the initialization
try!(book.init());
// If flag `--theme` is present, copy theme to src
if args.is_present("theme") {
// Skip this id `--force` is present
if !args.is_present("force") {
// Print warning
print!("\nCopying the default theme to {:?}", book.get_src());
println!("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);
}
}
// Call the function that copies the theme
try!(book.copy_theme());
println!("\nTheme copied.");
let mut builder = LogBuilder::new();
builder.format(format).filter(None, LogLevelFilter::Info);
if let Ok(var) = env::var("RUST_LOG") {
builder.parse(&var);
}
println!("\nAll done, no errors...");
Ok(())
builder.init().unwrap();
}
// Build command implementation
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config();
try!(book.build());
Ok(())
}
// Watch command implementation
#[cfg(feature = "watch")]
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
let book = MDBook::new(&book_dir).read_config();
// Create a channel to receive the events.
let (tx, rx) = channel();
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
match w {
Ok(mut watcher) => {
// Add the source directory to the watcher
if let Err(e) = watcher.watch(book.get_src()) {
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
::std::process::exit(0);
};
// Add the book.json file to the watcher if it exists, because it's not
// located in the source directory
if let Err(_) = watcher.watch(book_dir.join("book.json")) {
// do nothing if book.json is not found
}
let previous_time = time::get_time().sec;
crossbeam::scope(|scope| {
loop {
match rx.recv() {
Ok(event) => {
// Skip the event if an event has already been issued in the last second
if time::get_time().sec - previous_time < 1 { continue }
if let Some(path) = event.path {
// Trigger the build process in a new thread (to keep receiving events)
scope.spawn(move || {
println!("File changed: {:?}\nBuilding book...\n", path);
match build(args) {
Err(e) => println!("Error while building: {:?}", e),
_ => {}
}
println!("");
});
} else {
continue;
}
},
Err(e) => {
println!("An error occured: {:?}", e);
}
}
}
});
},
Err(e) => {
println!("Error while trying to watch the files:\n\n\t{:?}", e);
::std::process::exit(0);
}
}
Ok(())
}
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config();
try!(book.test());
Ok(())
}
fn get_book_dir(args: &ArgMatches) -> PathBuf {
if let Some(dir) = args.value_of("dir") {
// Check if path is relative from current dir, or absolute...
let p = Path::new(dir);
if p.is_relative() {
env::current_dir().unwrap().join(dir)
env::current_dir().unwrap().join(dir)
} else {
p.to_path_buf()
p.to_path_buf()
}
} else {
env::current_dir().unwrap()
}
}
fn open<P: AsRef<OsStr>>(path: P) {
if let Err(e) = open::that(path) {
println!("Error opening web browser: {}", e);
}
}

127
src/bin/serve.rs Normal file
View File

@@ -0,0 +1,127 @@
extern crate iron;
extern crate staticfile;
extern crate ws;
use std;
use std::path::PathBuf;
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 {get_book_dir, open};
#[cfg(feature = "watch")]
use watch;
struct ErrorRecover;
// Create clap subcommand arguments
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("-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("-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 mut book = MDBook::new(&book_dir).read_config()?;
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = PathBuf::from(dest_dir);
}
let port = args.value_of("port").unwrap_or("3000");
let ws_port = args.value_of("websocket-port").unwrap_or("3001");
let interface = args.value_of("interface").unwrap_or("localhost");
let public_address = args.value_of("address").unwrap_or(interface);
let open_browser = args.is_present("open");
let address = format!("{}:{}", interface, port);
let ws_address = format!("{}:{}", interface, ws_port);
book.livereload = Some(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
));
book.build()?;
let mut chain = Chain::new(staticfile::Static::new(book.get_destination()));
chain.link_after(ErrorRecover);
let _iron = Iron::new(chain).http(&*address).unwrap();
let ws_server = ws::WebSocket::new(|_| |_| Ok(())).unwrap();
let broadcaster = ws_server.broadcaster();
std::thread::spawn(move || { ws_server.listen(&*ws_address).unwrap(); });
let serving_url = format!("http://{}", address);
println!("\nServing 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(),
}
println!("");
});
Ok(())
}
impl AfterMiddleware for ErrorRecover {
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> {
match err.response.status {
// each error will result in 404 response
Some(_) => Ok(err.response.set(status::NotFound)),
_ => Err(err),
}
}
}

26
src/bin/test.rs Normal file
View File

@@ -0,0 +1,26 @@
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use get_book_dir;
// Create clap subcommand arguments
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'",
)
}
// 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 book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config()?;
book.test(library_paths)?;
Ok(())
}

111
src/bin/watch.rs Normal file
View File

@@ -0,0 +1,111 @@
extern crate notify;
use std::path::{Path, PathBuf};
use self::notify::Watcher;
use std::time::Duration;
use std::sync::mpsc::channel;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use {get_book_dir, open};
// Create clap subcommand arguments
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(
"[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 mut book = MDBook::new(&book_dir).read_config()?;
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = PathBuf::from(dest_dir);
}
if args.is_present("open") {
book.build()?;
open(book.get_destination().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);
}
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) -> (),
{
use self::notify::RecursiveMode::*;
use self::notify::DebouncedEvent::*;
// Create a channel to receive the events.
let (tx, rx) = channel();
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)
}
};
// 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);
};
// Add the theme directory to the watcher
watcher.watch(book.theme_dir(), Recursive)
.unwrap_or_default();
// 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.root.join("book.json"), NonRecursive)
.is_err()
{
// do nothing if book.json is not found
}
if watcher.watch(book.root.join("book.toml"), NonRecursive)
.is_err()
{
// do nothing if book.toml is not found
}
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);
}
}
}
}

View File

@@ -1,106 +0,0 @@
extern crate rustc_serialize;
use self::rustc_serialize::json::Json;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct BookConfig {
pub title: String,
pub author: String,
root: PathBuf,
dest: PathBuf,
src: PathBuf,
pub indent_spaces: i32,
multilingual: bool,
}
impl BookConfig {
pub fn new(root: &Path) -> Self {
BookConfig {
title: String::new(),
author: String::new(),
root: root.to_owned(),
dest: PathBuf::from("book"),
src: PathBuf::from("src"),
indent_spaces: 4, // indentation used for SUMMARY.md
multilingual: false,
}
}
pub fn read_config(&mut self, root: &Path) -> &mut Self {
debug!("[fn]: read_config");
// If the file does not exist, return early
let mut config_file = match File::open(root.join("book.json")) {
Ok(f) => f,
Err(_) => {
debug!("[*]: Failed to open {:?}", root.join("book.json"));
return self
},
};
debug!("[*]: Reading config");
let mut data = String::new();
// Just return if an error occured.
// I would like to propagate the error, but I have to return `&self`
if let Err(_) = config_file.read_to_string(&mut data) { return self }
// Convert to JSON
if let Ok(config) = Json::from_str(&data) {
// Extract data
debug!("[*]: Extracting data from config");
// Title & author
if let Some(a) = config.find_path(&["title"]) { self.title = a.to_string().replace("\"", "") }
if let Some(a) = config.find_path(&["author"]) { self.author = a.to_string().replace("\"", "") }
// Destination
if let Some(a) = config.find_path(&["dest"]) {
let dest = PathBuf::from(&a.to_string().replace("\"", ""));
// If path is relative make it absolute from the parent directory of src
match dest.is_relative() {
true => {
let dest = self.get_root().join(&dest).to_owned();
self.set_dest(&dest);
},
false => { self.set_dest(&dest); },
}
}
}
self
}
pub fn get_root(&self) -> &Path {
&self.root
}
pub fn set_root(&mut self, root: &Path) -> &mut Self {
self.root = root.to_owned();
self
}
pub fn get_dest(&self) -> &Path {
&self.dest
}
pub fn set_dest(&mut self, dest: &Path) -> &mut Self {
self.dest = dest.to_owned();
self
}
pub fn get_src(&self) -> &Path {
&self.src
}
pub fn set_src(&mut self, src: &Path) -> &mut Self {
self.src = src.to_owned();
self
}
}

View File

@@ -1,8 +1,7 @@
extern crate rustc_serialize;
use self::rustc_serialize::json::{Json, ToJson};
use serde::{Serialize, Serializer};
use serde::ser::SerializeStruct;
use std::path::PathBuf;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub enum BookItem {
@@ -27,9 +26,7 @@ pub struct BookItems<'a> {
impl Chapter {
pub fn new(name: String, path: PathBuf) -> Self {
Chapter {
name: name,
path: path,
@@ -39,15 +36,15 @@ impl Chapter {
}
impl ToJson for Chapter {
fn to_json(&self) -> Json {
let mut m: BTreeMap<String, Json> = BTreeMap::new();
m.insert("name".to_owned(), self.name.to_json());
m.insert("path".to_owned(),self.path.to_str()
.expect("Json conversion failed for path").to_json()
);
m.to_json()
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()
}
}
@@ -69,20 +66,20 @@ impl<'a> Iterator for BookItems<'a> {
}
}
} else {
let cur = self.items.get(self.current_index).unwrap();
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)
return Some(cur);
}
}
}

View File

@@ -1,357 +0,0 @@
use std::path::{Path, PathBuf};
use std::fs::{self, File};
use std::error::Error;
use std::io;
use std::io::Write;
use std::io::ErrorKind;
use std::process::Command;
use {BookConfig, BookItem, theme, parse, utils};
use book::BookItems;
use renderer::{Renderer, HtmlHandlebars};
pub struct MDBook {
config: BookConfig,
pub content: Vec<BookItem>,
renderer: Box<Renderer>,
}
impl MDBook {
/// Create a new `MDBook` struct with root directory `root`
///
/// - The default source directory is set to `root/src`
/// - The default output directory is set to `root/book`
///
/// They can both be changed by using [`set_src()`](#method.set_src) and [`set_dest()`](#method.set_dest)
pub fn new(root: &Path) -> MDBook {
if !root.exists() || !root.is_dir() {
output!("{:?} No directory with that name", root);
}
MDBook {
content: vec![],
config: BookConfig::new(root)
.set_src(&root.join("src"))
.set_dest(&root.join("book"))
.to_owned(),
renderer: Box::new(HtmlHandlebars::new()),
}
}
/// Returns a flat depth-first iterator over the elements of the book, it returns an [BookItem enum](bookitem.html):
/// `(section: String, bookitem: &BookItem)`
///
/// ```no_run
/// # extern crate mdbook;
/// # use mdbook::MDBook;
/// # use mdbook::BookItem;
/// # use std::path::Path;
/// # fn main() {
/// # let mut book = MDBook::new(Path::new("mybook"));
/// for item in book.iter() {
/// match item {
/// &BookItem::Chapter(ref section, ref chapter) => {},
/// &BookItem::Affix(ref chapter) => {},
/// &BookItem::Spacer => {},
/// }
/// }
///
/// // would print something like this:
/// // 1. Chapter 1
/// // 1.1 Sub Chapter
/// // 1.2 Sub Chapter
/// // 2. Chapter 2
/// //
/// // etc.
/// # }
/// ```
pub fn iter(&self) -> BookItems {
BookItems {
items: &self.content[..],
current_index: 0,
stack: Vec::new(),
}
}
/// `init()` creates some boilerplate files and directories to get you started with your book.
///
/// ```text
/// book-test/
/// ├── book
/// └── src
/// ├── chapter_1.md
/// └── 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<(), Box<Error>> {
debug!("[fn]: init");
if !self.config.get_root().exists() {
fs::create_dir_all(self.config.get_root()).unwrap();
output!("{:?} created", self.config.get_root());
}
{
let dest = self.config.get_dest();
let src = self.config.get_src();
if !dest.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", dest);
try!(fs::create_dir(&dest));
}
if !src.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", src);
try!(fs::create_dir(&src));
}
let summary = src.join("SUMMARY.md");
if !summary.exists() {
// Summary does not exist, create it
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", src.join("SUMMARY.md"));
let mut f = try!(File::create(&src.join("SUMMARY.md")));
debug!("[*]: Writing to SUMMARY.md");
try!(writeln!(f, "# Summary"));
try!(writeln!(f, ""));
try!(writeln!(f, "- [Chapter 1](./chapter_1.md)"));
}
}
// parse SUMMARY.md, and create the missing item related file
try!(self.parse_summary());
debug!("[*]: constructing paths for missing files");
for item in self.iter() {
debug!("[*]: item: {:?}", item);
match *item {
BookItem::Spacer => continue,
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
if ch.path != PathBuf::new() {
let path = self.config.get_src().join(&ch.path);
if !path.exists() {
debug!("[*]: {:?} does not exist, trying to create file", path);
try!(::std::fs::create_dir_all(path.parent().unwrap()));
let mut f = try!(File::create(path));
//debug!("[*]: Writing to {:?}", path);
try!(writeln!(f, "# {}", ch.name));
}
}
}
}
}
debug!("[*]: init done");
Ok(())
}
/// 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<(), Box<Error>> {
debug!("[fn]: build");
try!(self.init());
// Clean output directory
try!(utils::remove_dir_content(&self.config.get_dest()));
try!(self.renderer.render(&self));
Ok(())
}
pub fn copy_theme(&self) -> Result<(), Box<Error>> {
debug!("[fn]: copy_theme");
let theme_dir = self.config.get_src().join("theme");
if !theme_dir.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", theme_dir);
try!(fs::create_dir(&theme_dir));
}
// index.hbs
let mut index = try!(File::create(&theme_dir.join("index.hbs")));
try!(index.write_all(theme::INDEX));
// book.css
let mut css = try!(File::create(&theme_dir.join("book.css")));
try!(css.write_all(theme::CSS));
// book.js
let mut js = try!(File::create(&theme_dir.join("book.js")));
try!(js.write_all(theme::JS));
// highlight.css
let mut highlight_css = try!(File::create(&theme_dir.join("highlight.css")));
try!(highlight_css.write_all(theme::HIGHLIGHT_CSS));
// highlight.js
let mut highlight_js = try!(File::create(&theme_dir.join("highlight.js")));
try!(highlight_js.write_all(theme::HIGHLIGHT_JS));
Ok(())
}
/// 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`
///
/// ```no_run
/// # extern crate mdbook;
/// # use mdbook::MDBook;
/// # use std::path::Path;
/// # fn main() {
/// let mut book = MDBook::new(Path::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.
pub fn read_config(mut self) -> Self {
let root = self.config.get_root().to_owned();
self.config.read_config(&root);
self
}
/// 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;
/// # use std::path::Path;
///
/// fn main() {
/// let mut book = MDBook::new(Path::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;
self
}
pub fn test(&mut self) -> Result<(), Box<Error>> {
// read in the chapters
try!(self.parse_summary());
for item in self.iter() {
match *item {
BookItem::Chapter(_, ref ch) => {
if ch.path != PathBuf::new() {
let path = self.get_src().join(&ch.path);
println!("[*]: Testing file: {:?}", path);
let output_result = Command::new("rustdoc")
.arg(&path)
.arg("--test")
.output();
let output = try!(output_result);
if !output.status.success() {
return Err(Box::new(io::Error::new(ErrorKind::Other, format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)))) as Box<Error>);
}
}
}
_ => {}
}
}
Ok(())
}
pub fn set_dest(mut self, dest: &Path) -> Self {
// Handle absolute and relative paths
match dest.is_absolute() {
true => { self.config.set_dest(dest); },
false => {
let dest = self.config.get_root().join(dest).to_owned();
self.config.set_dest(&dest);
}
}
self
}
pub fn get_dest(&self) -> &Path {
self.config.get_dest()
}
pub fn set_src(mut self, src: &Path) -> Self {
// Handle absolute and relative paths
match src.is_absolute() {
true => { self.config.set_src(src); },
false => {
let src = self.config.get_root().join(src).to_owned();
self.config.set_src(&src);
}
}
self
}
pub fn get_src(&self) -> &Path {
self.config.get_src()
}
pub fn set_title(mut self, title: &str) -> Self {
self.config.title = title.to_owned();
self
}
pub fn get_title(&self) -> &str {
&self.config.title
}
pub fn set_author(mut self, author: &str) -> Self {
self.config.author = author.to_owned();
self
}
pub fn get_author(&self) -> &str {
&self.config.author
}
// Construct book
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
// When append becomes stable, use self.content.append() ...
self.content = try!(parse::construct_bookitems(&self.config.get_src().join("SUMMARY.md")));
Ok(())
}
}

View File

@@ -1,7 +1,404 @@
pub mod mdbook;
pub mod bookitem;
pub mod bookconfig;
pub use self::bookitem::{BookItem, BookItems};
pub use self::bookconfig::BookConfig;
pub use self::mdbook::MDBook;
use std::path::{Path, PathBuf};
use std::fs::{self, File};
use std::io::Write;
use std::process::Command;
use tempdir::TempDir;
use {parse, theme, utils};
use renderer::{HtmlHandlebars, Renderer};
use preprocess;
use errors::*;
use config::Config;
pub struct MDBook {
pub root: PathBuf,
pub config: Config,
pub content: Vec<BookItem>,
renderer: Box<Renderer>,
pub livereload: Option<String>,
}
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)
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);
}
MDBook {
root: root,
config: Config::default(),
content: vec![],
renderer: Box::new(HtmlHandlebars::new()),
livereload: None,
}
}
/// Returns a flat depth-first iterator over the elements of the book,
/// it returns an [BookItem enum](bookitem.html):
/// `(section: String, bookitem: &BookItem)`
///
/// ```no_run
/// # extern crate mdbook;
/// # use mdbook::MDBook;
/// # use mdbook::BookItem;
/// # #[allow(unused_variables)]
/// # fn main() {
/// # let book = MDBook::new("mybook");
/// for item in book.iter() {
/// match item {
/// &BookItem::Chapter(ref section, ref chapter) => {},
/// &BookItem::Affix(ref chapter) => {},
/// &BookItem::Spacer => {},
/// }
/// }
///
/// // would print something like this:
/// // 1. Chapter 1
/// // 1.1 Sub Chapter
/// // 1.2 Sub Chapter
/// // 2. Chapter 2
/// //
/// // etc.
/// # }
/// ```
pub fn iter(&self) -> BookItems {
BookItems {
items: &self.content[..],
current_index: 0,
stack: Vec::new(),
}
}
/// `init()` creates some boilerplate files and directories
/// to get you started with your book.
///
/// ```text
/// book-test/
/// ├── book
/// └── src
/// ├── chapter_1.md
/// └── 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.root.exists() {
fs::create_dir_all(&self.root).unwrap();
info!("{:?} created", self.root.display());
}
{
let dest = self.get_destination();
if !dest.exists() {
debug!("[*]: {} does not exist, trying to create directory", dest.display());
fs::create_dir_all(dest)?;
}
let src = self.get_source();
if !src.exists() {
debug!("[*]: {} does not exist, trying to create directory", src.display());
fs::create_dir_all(&src)?;
}
let summary = src.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.get_source().join(&ch.path);
if !path.exists() {
if !self.config.build.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(())
}
pub fn create_gitignore(&self) {
let gitignore = self.get_gitignore();
let destination = self.get_destination();
// 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.root) {
let relative = destination
.strip_prefix(&self.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.get_destination())?;
self.renderer.render(self)
}
pub fn get_gitignore(&self) -> PathBuf {
self.root.join(".gitignore")
}
pub fn copy_theme(&self) -> Result<()> {
debug!("[fn]: copy_theme");
let themedir = self.theme_dir();
if !themedir.exists() {
debug!("[*]: {:?} does not exist, trying to create directory",
themedir);
fs::create_dir(&themedir)?;
}
// index.hbs
let mut index = File::create(themedir.join("index.hbs"))?;
index.write_all(theme::INDEX)?;
// header.hbs
let mut header = File::create(themedir.join("header.hbs"))?;
header.write_all(theme::HEADER)?;
// 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)?;
Ok(())
}
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<()> {
let path = self.get_destination().join(filename);
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 config_path = self.root.join("book.toml");
if config_path.exists() {
debug!("[*] Loading the config from {}", config_path.display());
self.config = Config::from_disk(&config_path)?;
} else {
self.config = Config::default();
}
Ok(self)
}
/// 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;
self
}
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")?;
for item in self.iter() {
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 content = utils::fs::file_to_string(&path)?;
// Parse and expand links
let content = preprocess::links::replace_all(&content, base)?;
println!("[*]: Testing file: {:?}", path);
// write preprocessed file to tempdir
let path = temp_dir.path().join(&ch.path);
let mut tmpf = utils::fs::create_file(&path)?;
tmpf.write_all(content.as_bytes())?;
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));
}
}
}
}
Ok(())
}
// Construct book
fn parse_summary(&mut self) -> Result<()> {
// When append becomes stable, use self.content.append() ...
let summary = self.get_source().join("SUMMARY.md");
self.content = parse::construct_bookitems(&summary)?;
Ok(())
}
pub fn get_destination(&self) -> PathBuf {
self.root.join(&self.config.build.build_dir)
}
pub fn get_source(&self) -> PathBuf {
self.root.join(&self.config.book.src)
}
pub fn theme_dir(&self) -> PathBuf {
match self.config.html_config().and_then(|h| h.theme) {
Some(d) => self.root.join(d),
None => self.root.join("theme"),
}
}
}

425
src/config.rs Normal file
View File

@@ -0,0 +1,425 @@
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Read;
use toml::{self, Value};
use toml::value::Table;
use serde::{Deserialize, Deserializer};
use errors::*;
/// The overall configuration object for MDBook.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Config {
/// Metadata about the book.
pub book: BookConfig,
pub build: BuildConfig,
rest: Table,
}
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)
}
/// 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> {
let pieces: Vec<_> = key.split(".").collect();
recursive_get(&pieces, &self.rest)
}
/// 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> {
let pieces: Vec<_> = key.split(".").collect();
recursive_get_mut(&pieces, &mut self.rest)
}
/// Convenience method for getting the html renderer's configuration.
///
/// # Note
///
/// This is for compatibility only. It will be removed completely once the
/// rendering and plugin system is established.
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)
}
}
fn from_legacy(mut table: Table) -> 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) => {
if let Some(value) = $table.remove($key).and_then(|v| v.try_into().ok()) {
$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);
// This complicated chain of and_then's is so we can move
// "output.html.destination" to "build.build_dir" and parse it into a
// PathBuf.
let destination: Option<PathBuf> = table.get_mut("output")
.and_then(|output| output.as_table_mut())
.and_then(|output| output.get_mut("html"))
.and_then(|html| html.as_table_mut())
.and_then(|html| html.remove("destination"))
.and_then(|dest| dest.try_into().ok());
if let Some(dest) = destination {
cfg.build.build_dir = dest;
}
cfg.rest = table;
cfg
}
}
fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> {
if key.is_empty() {
return None;
} else if key.len() == 1 {
return table.get(key[0]);
}
let first = key[0];
let rest = &key[1..];
if let Some(&Value::Table(ref nested)) = table.get(first) {
recursive_get(rest, nested)
} else {
None
}
}
fn recursive_get_mut<'a>(key: &[&str], table: &'a mut Table) -> Option<&'a mut Value> {
// TODO: Figure out how to abstract over mutability to reduce copy-pasta
if key.is_empty() {
return None;
} else if key.len() == 1 {
return table.get_mut(key[0]);
}
let first = key[0];
let rest = &key[1..];
if let Some(&mut Value::Table(ref mut nested)) = table.get_mut(first) {
recursive_get_mut(rest, nested)
} else {
None
}
}
impl<'de> Deserialize<'de> for Config {
fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
let raw = Value::deserialize(de)?;
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",
));
}
};
if is_legacy_format(&table) {
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(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: table,
})
}
}
fn is_legacy_format(table: &Table) -> bool {
let top_level_items = ["title", "author", "authors"];
top_level_items.iter().any(|key| table.contains_key(&key.to_string()))
}
/// 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,
}
impl Default for BuildConfig {
fn default() -> BuildConfig {
BuildConfig {
build_dir: PathBuf::from("book"),
create_missing: true,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct HtmlConfig {
pub theme: Option<PathBuf>,
pub curly_quotes: bool,
pub mathjax_support: bool,
pub google_analytics: Option<String>,
pub additional_css: Vec<PathBuf>,
pub additional_js: Vec<PathBuf>,
pub playpen: Playpen,
}
/// Configuration for tweaking how the the HTML renderer handles the playpen.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Playpen {
pub editor: PathBuf,
pub editable: bool,
}
#[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
[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,
};
let playpen_should_be = Playpen {
editable: true,
editor: PathBuf::from("ace"),
};
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,
};
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);
}
}

View File

@@ -3,8 +3,8 @@
//! **mdBook** is similar to Gitbook but implemented in Rust.
//! It offers a command line interface, but can also be used as a regular crate.
//!
//! 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.
//! 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.
//!
//! Some reasons why you would want to use the crate (over the cli):
@@ -21,15 +21,17 @@
//! extern crate mdbook;
//!
//! use mdbook::MDBook;
//! use std::path::Path;
//! use std::path::PathBuf;
//!
//! fn main() {
//! let mut book = MDBook::new(Path::new("my-book")) // Path to root
//! .set_src(Path::new("src")) // Path from root to source directory
//! .set_dest(Path::new("book")) // Path from root to output directory
//! .read_config(); // Parse book.json file for configuration
//!
//! book.build().unwrap(); // Render the book
//! let mut md = MDBook::new("my-book");
//!
//! // tweak the book configuration a bit
//! md.config.book.src = PathBuf::from("source");
//! md.config.build.build_dir = PathBuf::from("book");
//!
//! // Render the book
//! md.build().unwrap();
//! }
//! ```
//!
@@ -45,12 +47,12 @@
//! #
//! # use mdbook::MDBook;
//! # use mdbook::renderer::HtmlHandlebars;
//! # use std::path::Path;
//! #
//! # #[allow(unused_variables)]
//! # fn main() {
//! # let your_renderer = HtmlHandlebars::new();
//! #
//! let book = MDBook::new(Path::new("my-book")).set_renderer(Box::new(your_renderer));
//! 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
@@ -60,24 +62,67 @@
//!
//! ## utils
//!
//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the following function
//! 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)
//!
//! ```ignore
//! utils::create_path(path: &Path)
//! ```
//! This function creates all the directories in a given path if they do not exist
//! 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.
#[macro_use]
pub mod macros;
pub mod book;
extern crate error_chain;
extern crate handlebars;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate pulldown_cmark;
extern crate regex;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
extern crate tempdir;
extern crate toml;
mod parse;
mod preprocess;
pub mod book;
pub mod config;
pub mod renderer;
pub mod theme;
pub mod utils;
pub use book::MDBook;
pub use book::BookItem;
pub use book::BookConfig;
pub use renderer::Renderer;
/// The error types used through out this crate.
pub mod errors {
error_chain!{
foreign_links {
Io(::std::io::Error);
HandlebarsRender(::handlebars::RenderError);
HandlebarsTemplate(Box<::handlebars::TemplateError>);
Utf8(::std::string::FromUtf8Error);
}
errors {
Subprocess(message: String, output: ::std::process::Output) {
description("A subprocess failed")
display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
}
}
}
// 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,23 +0,0 @@
#[cfg(feature = "debug")]
macro_rules! debug {
($fmt:expr) => (println!($fmt));
($fmt:expr, $($arg:tt)*) => (println!($fmt, $($arg)*));
}
#[cfg(not(feature = "debug"))]
macro_rules! debug {
($fmt:expr) => ();
($fmt:expr, $($arg:tt)*) => ();
}
#[cfg(feature = "output")]
macro_rules! output {
($fmt:expr) => (println!($fmt));
($fmt:expr, $($arg:tt)*) => (println!($fmt, $($arg)*));
}
#[cfg(not(feature = "output"))]
macro_rules! output {
($fmt:expr) => ();
($fmt:expr, $($arg:tt)*) => ();
}

View File

@@ -1,20 +1,23 @@
use std::path::PathBuf;
use std::fs::File;
use std::io::{Read, Result, Error, ErrorKind};
use std::io::{Error, ErrorKind, Read, Result};
use book::bookitem::{BookItem, Chapter};
pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
debug!("[fn]: construct_bookitems");
let mut summary = String::new();
try!(try!(File::open(path)).read_to_string(&mut summary));
File::open(path)?.read_to_string(&mut summary)?;
debug!("[*]: Parse SUMMARY.md");
let top_items = try!(parse_level(&mut summary.split('\n').collect(), 0, vec![0]));
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>> {
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![];
@@ -22,78 +25,97 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
while !summary.is_empty() {
let item: BookItem;
// Indentation level of the line to parse
let level = try!(level(summary[0], 4));
let level = level(summary[0], 4)?;
// if level < current_level we remove the last digit of section, exit the current function,
// 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 {
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");
let last = items.pop().expect(
"There should be at least one item since this can't be the root level",
);
item = if let BookItem::Chapter(ref s, ref ch) = last {
if let BookItem::Chapter(ref s, ref ch) = last {
let mut ch = ch.clone();
ch.sub_items = try!(parse_level(summary, level, section.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
continue;
} else {
return Err(Error::new( ErrorKind::Other, format!(
"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."
)))
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, format!(
"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."
)))
},
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, format!(
"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."
)))
},
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;
let len = section.len() - 1;
section[len] += 1;
let s = section.iter().fold("".to_owned(), |s, i| s + &i.to_string() + ".");
let s = section.iter()
.fold("".to_owned(), |s, i| s + &i.to_string() + ".");
BookItem::Chapter(s, ch)
}
_ => parsed_item
_ => parsed_item,
}
} else {
// If parse_line does not return Some(_) continue...
summary.remove(0);
@@ -130,12 +152,10 @@ fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
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)
)
)
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)
@@ -146,12 +166,12 @@ 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' });
let line = l.trim_matches(|c: char| c == ' ' || c == '\t');
// Spacers are "------"
if line.starts_with("--") {
debug!("[*]: Line is spacer");
return Some(BookItem::Spacer)
return Some(BookItem::Spacer);
}
if let Some(c) = line.chars().nth(0) {
@@ -161,16 +181,20 @@ fn parse_line(l: &str) -> Option<BookItem> {
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 }
},
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 }
return Some(BookItem::Affix(Chapter::new(name, path)));
} else {
return None;
}
}
_ => {}
}
@@ -185,32 +209,31 @@ fn read_link(line: &str) -> Option<(String, PathBuf)> {
// 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 {
if let Some(i) = line.find('[') {
start_delimitor = i;
} else {
debug!("[*]: '[' not found, this line is not a link. Ignoring...");
return None
return None;
}
if let Some(i) = line[start_delimitor..].find("](") {
end_delimitor = start_delimitor +i;
}
else {
end_delimitor = start_delimitor + i;
} else {
debug!("[*]: '](' not found, this line is not a link. Ignoring...");
return None
return None;
}
let name = line[start_delimitor + 1 .. end_delimitor].to_owned();
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 {
} else {
debug!("[*]: ')' not found, this line is not a link. Ignoring...");
return None
return None;
}
let path = PathBuf::from(line[start_delimitor + 1 .. end_delimitor].to_owned());
let path = PathBuf::from(line[start_delimitor + 1..end_delimitor].to_owned());
Some((name, path))
}

253
src/preprocess/links.rs Normal file
View File

@@ -0,0 +1,253 @@
use std::path::{Path, PathBuf};
use regex::{CaptureMatches, Captures, Regex};
use utils::fs::file_to_string;
use errors::*;
const ESCAPE_CHAR: char = '\\';
pub fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> Result<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 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;
}
replaced.push_str(&s[previous_end_index..]);
Ok(replaced)
}
#[derive(PartialOrd, PartialEq, Debug, Clone)]
enum LinkType<'a> {
Escaped,
Include(PathBuf),
Playpen(PathBuf, Vec<&'a str>),
}
#[derive(PartialOrd, PartialEq, Debug, Clone)]
struct Link<'a> {
start_index: usize,
end_index: usize,
link: LinkType<'a>,
link_text: &'a str,
}
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 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)),
_ => None,
}
}
(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(),
}
})
})
}
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
let base = base.as_ref();
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::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))
}
}
}
}
struct LinkIter<'a>(CaptureMatches<'a, 'a>);
impl<'a> Iterator for LinkIter<'a> {
type Item = Link<'a>;
fn next(&mut self) -> Option<Link<'a>> {
for cap in &mut self.0 {
if let Some(inc) = Link::from_capture(cap) {
return Some(inc);
}
}
None
}
}
fn find_links(contents: &str) -> LinkIter {
// lazily compute following regex
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
lazy_static! {
static ref RE: Regex = Regex::new(r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link
| # or
\{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9]+) # link type
\s+ # separating whitespace
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
\s*\}\} # whitespace and link closing parens
").unwrap();
}
LinkIter(RE.captures_iter(contents))
}
// ---------------------------------------------------------------------------------
// Tests
//
#[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: 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::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}}",
});
}

1
src/preprocess/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod links;

View File

@@ -1,240 +1,394 @@
extern crate handlebars;
extern crate rustc_serialize;
use renderer::html_handlebars::helpers;
use preprocess;
use renderer::Renderer;
use book::MDBook;
use book::bookitem::BookItem;
use book::bookitem::{BookItem, Chapter};
use config::{Config, Playpen, HtmlConfig};
use {utils, theme};
use theme::{Theme, playpen_editor};
use errors::*;
use regex::{Captures, Regex};
use std::ascii::AsciiExt;
use std::path::{Path, PathBuf};
use std::fs::{self, File};
use std::error::Error;
use std::io::{self, Read, Write};
use std::io::{self, Read};
use std::collections::BTreeMap;
use std::collections::HashMap;
use self::handlebars::{Handlebars, JsonRender};
use self::rustc_serialize::json::{Json, ToJson};
use handlebars::Handlebars;
use serde_json;
#[derive(Default)]
pub struct HtmlHandlebars;
impl HtmlHandlebars {
pub fn new() -> Self {
HtmlHandlebars
}
}
impl Renderer for HtmlHandlebars {
fn render(&self, book: &MDBook) -> Result<(), Box<Error>> {
debug!("[fn]: render");
let mut handlebars = Handlebars::new();
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!"))?;
// Load theme
let theme = theme::Theme::new(book.get_src());
// Parse and expand links
let content = preprocess::links::replace_all(&content, base)?;
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
print_content.push_str(&content);
// Register template
debug!("[*]: Register handlebars template");
try!(handlebars.register_template_string("index", try!(String::from_utf8(theme.index))));
// 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")
})?;
// Register helpers
debug!("[*]: Register handlebars helpers");
// Non-lexical lifetimes needed :'(
let title: String;
{
let book_title = ctx.data
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
title = ch.name.clone() + " - " + book_title;
}
ctx.data.insert("path".to_owned(), json!(path));
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
ctx.data.insert("path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)));
// Render the handlebars template with the data
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_else(|| Error::from(
format!("Bad file name: {}", filepath.display()),
))?),
&ctx.book.config.html_config().unwrap_or_default().playpen,
);
// Write to file
info!("[*] Creating {:?} ✓", filepath.display());
ctx.book.write_file(filepath, &rendered.into_bytes())?;
if ctx.is_index {
self.render_index(ctx.book, 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");
let mut content = String::new();
File::open(destination.join(&ch.path.with_extension("html")))?
.read_to_string(&mut content)?;
// 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");
book.write_file("index.html", content.as_bytes())?;
info!("[*] Creating index.html from {:?} ✓",
book.get_destination().join(&ch.path.with_extension("html")));
Ok(())
}
#[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, html_config: &HtmlConfig) -> 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("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME)?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot",
theme::FONT_AWESOME_EOT)?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg",
theme::FONT_AWESOME_SVG)?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf",
theme::FONT_AWESOME_TTF)?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff",
theme::FONT_AWESOME_WOFF)?;
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2",
theme::FONT_AWESOME_WOFF2)?;
book.write_file("_FontAwesome/fonts/FontAwesome.ttf",
theme::FONT_AWESOME_TTF)?;
let playpen_config = &html_config.playpen;
// Ace is a very large dependency, so only load it when requested
if playpen_config.editable {
// Load the editor
let editor = playpen_editor::PlaypenEditor::new(&playpen_config.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)?;
}
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.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) {
// 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"))));
}
fn register_hbs_helpers(&self, handlebars: &mut Handlebars) {
handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc));
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
handlebars.register_helper("next", Box::new(helpers::navigation::next));
}
let mut data = try!(make_data(book));
/// 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 html = book.config.html_config().unwrap_or_default();
// Print version
let mut print_content: String = String::new();
let custom_files = html.additional_css
.iter()
.chain(html.additional_js.iter());
// Check if dest directory exists
debug!("[*]: Check if destination directory exists");
if let Err(_) = fs::create_dir_all(book.get_dest()) {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Unexpected error when constructing destination path")))
for custom_file in custom_files {
self.write_custom_file(custom_file, book)?;
}
// Render a file for every entry in the book
let mut index = true;
for item in book.iter() {
match *item {
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
if ch.path != PathBuf::new() {
let path = book.get_src().join(&ch.path);
debug!("[*]: Opening file: {:?}", path);
let mut f = try!(File::open(&path));
let mut content: String = String::new();
debug!("[*]: Reading file");
try!(f.read_to_string(&mut content));
// Parse for playpen links
if let Some(p) = path.parent() {
content = helpers::playpen::render_playpen(&content, p);
}
// Render markdown using the pulldown-cmark crate
content = utils::render_markdown(&content);
print_content.push_str(&content);
// Remove content from previous file and render content for this one
data.remove("path");
match ch.path.to_str() {
Some(p) => { data.insert("path".to_owned(), p.to_json()); },
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
}
// Remove content from previous file and render content for this one
data.remove("content");
data.insert("content".to_owned(), content.to_json());
// Remove path to root from previous file and render content for this one
data.remove("path_to_root");
data.insert("path_to_root".to_owned(), utils::path_to_root(&ch.path).to_json());
// Rendere the handlebars template with the data
debug!("[*]: Render template");
let rendered = try!(handlebars.render("index", &data));
debug!("[*]: Create file {:?}", &book.get_dest().join(&ch.path).with_extension("html"));
// Write to file
let mut file = try!(utils::create_file(&book.get_dest().join(&ch.path).with_extension("html")));
output!("[*] Creating {:?} ✓", &book.get_dest().join(&ch.path).with_extension("html"));
try!(file.write_all(&rendered.into_bytes()));
// Create an index.html from the first element in SUMMARY.md
if index {
debug!("[*]: index.html");
let mut index_file = try!(File::create(book.get_dest().join("index.html")));
let mut content = String::new();
let _source = try!(File::open(book.get_dest().join(&ch.path.with_extension("html"))))
.read_to_string(&mut content);
// 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");
try!(index_file.write_all(content.as_bytes()));
output!(
"[*] Creating index.html from {:?} ✓",
book.get_dest().join(&ch.path.with_extension("html"))
);
index = false;
}
}
}
_ => {}
}
}
// Print version
// Remove content from previous file and render content for this one
data.remove("path");
data.insert("path".to_owned(), "print.md".to_json());
// Remove content from previous file and render content for this one
data.remove("content");
data.insert("content".to_owned(), print_content.to_json());
// Remove path to root from previous file and render content for this one
data.remove("path_to_root");
data.insert("path_to_root".to_owned(), utils::path_to_root(Path::new("print.md")).to_json());
// Rendere the handlebars template with the data
debug!("[*]: Render template");
let rendered = try!(handlebars.render("index", &data));
let mut file = try!(utils::create_file(&book.get_dest().join("print").with_extension("html")));
try!(file.write_all(&rendered.into_bytes()));
output!("[*] Creating print.html ✓");
// Copy static files (js, css, images, ...)
debug!("[*] Copy static files");
// JavaScript
let mut js_file = if let Ok(f) = File::create(book.get_dest().join("book.js")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create book.js")))
};
try!(js_file.write_all(&theme.js));
// Css
let mut css_file = if let Ok(f) = File::create(book.get_dest().join("book.css")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create book.css")))
};
try!(css_file.write_all(&theme.css));
// JQuery local fallback
let mut jquery = if let Ok(f) = File::create(book.get_dest().join("jquery.js")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create jquery.js")))
};
try!(jquery.write_all(&theme.jquery));
// syntax highlighting
let mut highlight_css = if let Ok(f) = File::create(book.get_dest().join("highlight.css")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create highlight.css")))
};
try!(highlight_css.write_all(&theme.highlight_css));
let mut tomorrow_night_css = if let Ok(f) = File::create(book.get_dest().join("tomorrow-night.css")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create tomorrow-night.css")))
};
try!(tomorrow_night_css.write_all(&theme.tomorrow_night_css));
let mut highlight_js = if let Ok(f) = File::create(book.get_dest().join("highlight.js")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create highlight.js")))
};
try!(highlight_js.write_all(&theme.highlight_js));
// Font Awesome local fallback
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/css/font-awesome.css")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create font-awesome.css")))
};
try!(font_awesome.write_all(theme::FONT_AWESOME));
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.eot")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.eot")))
};
try!(font_awesome.write_all(theme::FONT_AWESOME_EOT));
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.svg")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.svg")))
};
try!(font_awesome.write_all(theme::FONT_AWESOME_SVG));
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.ttf")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.ttf")))
};
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.woff")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff")))
};
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF));
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.woff2")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff2")))
};
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF2));
let mut font_awesome = if let Ok(f) = utils::create_file(&book.get_dest().join("_FontAwesome/fonts/FontAwesome.ttf")) { f } else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create FontAwesome.ttf")))
};
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
// Copy all remaining files
try!(utils::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"]));
Ok(())
}
}
fn make_data(book: &MDBook) -> Result<BTreeMap<String,Json>, Box<Error>> {
debug!("[fn]: make_data");
let mut data = BTreeMap::new();
data.insert("language".to_owned(), "en".to_json());
data.insert("title".to_owned(), book.get_title().to_json());
impl Renderer for HtmlHandlebars {
fn render(&self, book: &MDBook) -> Result<()> {
let html_config = book.config.html_config().unwrap_or_default();
debug!("[fn]: render");
let mut handlebars = Handlebars::new();
let theme_dir = match html_config.theme {
Some(ref theme) => theme,
None => Path::new("theme"),
};
let theme = theme::Theme::new(theme_dir);
debug!("[*]: Register the index handlebars template");
handlebars.register_template_string(
"index",
String::from_utf8(theme.index.clone())?,
)?;
debug!("[*]: Register the header handlebars template");
handlebars.register_partial(
"header",
String::from_utf8(theme.header.clone())?,
)?;
debug!("[*]: Register handlebars helpers");
self.register_hbs_helpers(&mut handlebars);
let mut data = make_data(book, &book.config)?;
// Print version
let mut print_content = String::new();
// TODO: The Renderer trait should really pass in where it wants us to build to...
let destination = book.get_destination();
debug!("[*]: Check if destination directory exists");
fs::create_dir_all(&destination)
.chain_err(|| "Unexpected error when constructing destination path")?;
for (i, item) in book.iter().enumerate() {
let ctx = RenderItemContext {
book: book,
handlebars: &handlebars,
destination: destination.to_path_buf(),
data: data.clone(),
is_index: i == 0,
html_config: html_config.clone(),
};
self.render_item(item, ctx, &mut print_content)?;
}
// Print version
self.configure_print_version(&mut data, &print_content);
if let Some(ref title) = book.config.book.title {
data.insert("title".to_owned(), json!(title));
}
// Render the handlebars template with the data
debug!("[*]: Render template");
let rendered = handlebars.render("index", &data)?;
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 ✓");
// Copy static files (js, css, images, ...)
debug!("[*] Copy static files");
self.copy_static_files(book, &theme, &html_config)?;
self.copy_additional_css_and_js(book)?;
// Copy all remaining files
let src = book.get_source();
utils::fs::copy_files_except_ext(&src, &destination, true, &["md"])?;
Ok(())
}
}
fn make_data(book: &MDBook, config: &Config) -> Result<serde_json::Map<String, serde_json::Value>> {
debug!("[fn]: 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!(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(ref livereload) = book.livereload {
data.insert("livereload".to_owned(), json!(livereload));
}
// Add google analytics tag
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
data.insert("google_analytics".to_owned(), json!(ga));
}
if html.mathjax_support {
data.insert("mathjax_support".to_owned(), json!(true));
}
// Add check to see if there is an additional style
if !html.additional_css.is_empty() {
let mut css = Vec::new();
for style in &html.additional_css {
match style.strip_prefix(&book.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"))
}
}
}
data.insert("additional_css".to_owned(), json!(css));
}
// Add check to see if there is an additional script
if !html.additional_js.is_empty() {
let mut js = Vec::new();
for script in &html.additional_js {
match script.strip_prefix(&book.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"))
}
}
}
data.insert("additional_js".to_owned(), json!(js));
}
if html.playpen.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"));
}
let mut chapters = vec![];
@@ -244,31 +398,286 @@ fn make_data(book: &MDBook) -> Result<BTreeMap<String,Json>, Box<Error>> {
match *item {
BookItem::Affix(ref ch) => {
chapter.insert("name".to_owned(), ch.name.to_json());
match ch.path.to_str() {
Some(p) => { chapter.insert("path".to_owned(), p.to_json()); },
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
}
},
BookItem::Chapter(ref s, ref ch) => {
chapter.insert("section".to_owned(), s.to_json());
chapter.insert("name".to_owned(), ch.name.to_json());
match ch.path.to_str() {
Some(p) => { chapter.insert("path".to_owned(), p.to_json()); },
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
}
},
BookItem::Spacer => {
chapter.insert("spacer".to_owned(), "_spacer_".to_json());
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_"));
}
}
chapters.push(chapter);
}
data.insert("chapters".to_owned(), chapters.to_json());
data.insert("chapters".to_owned(), json!(chapters));
debug!("[*]: JSON constructed");
Ok(data)
}
/// Goes through the rendered HTML, making sure all header tags are wrapped in
/// an anchor so people can link to sections directly.
fn build_header_links(html: &str, 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");
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);
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
let id = match *id_count {
0 => raw_id,
other => format!("{}-{}", raw_id, other),
};
*id_count += 1;
format!(
r##"<a class="header" href="{filepath}#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
level = level,
id = id,
text = content,
filepath = filepath
)
}
/// 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];
format!("<a{before}href=\"{filepath}#{anchor}\"{after}>",
before = before,
filepath = filepath,
anchor = anchor,
after = after)
})
.into_owned()
}
// The rust book uses annotations for rustdoc to test code snippets,
// like the following:
// ```rust,should_panic
// fn main() {
// // Code here
// }
// ```
// 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];
format!(r#"<code{before}class="{classes}"{after}>"#,
before = before,
classes = classes,
after = after)
})
.into_owned()
}
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];
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 {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!("<pre class=\"playpen\"><code class=\"{}\">\n# \
#![allow(unused_variables)]\n\
{}#fn main() {{\n\
{}\
#}}</code></pre>",
classes,
attrs,
code)
}
} else {
// not language-rust, so no-op
text.to_owned()
}
})
.into_owned()
}
fn partition_source(s: &str) -> (String, String) {
let mut after_header = false;
let mut before = String::new();
let mut after = String::new();
for line in s.lines() {
let trimline = line.trim();
let header = trimline.chars().all(|c| c.is_whitespace()) || trimline.starts_with("#![");
if !header || after_header {
after_header = true;
after.push_str(line);
after.push_str("\n");
} else {
before.push_str(line);
before.push_str("\n");
}
}
(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::*;
#[test]
fn original_build_header_links() {
let inputs = vec![
(
"blah blah <h1>Foo</h1>",
r##"blah blah <a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
),
(
"<h1>Foo</h1>",
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
),
(
"<h3>Foo^bar</h3>",
r##"<a class="header" href="./some_chapter/some_section.html#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
),
(
"<h4></h4>",
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>"##,
),
(
"<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>"##,
),
];
for (src, should_be) in inputs {
let filepath = "./some_chapter/some_section.html";
let got = build_header_links(&src, filepath);
assert_eq!(got, should_be);
// This is redundant for most cases
let got = fix_anchor_links(&got, filepath);
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,3 +1,2 @@
pub mod navigation;
pub mod toc;
pub mod playpen;

View File

@@ -1,195 +1,234 @@
extern crate handlebars;
extern crate rustc_serialize;
use std::path::Path;
use std::collections::BTreeMap;
use self::rustc_serialize::json::{self, ToJson};
use self::handlebars::{Handlebars, RenderError, RenderContext, Helper, Context, Renderable};
// Handlebars helper for navigation
pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
debug!("[fn]: previous (handlebars helper)");
debug!("[*]: Get data from context");
// 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 = c.navigate(rc.get_path(), "chapters");
let current = c.navigate(rc.get_path(), "path")
.to_string()
.replace("\"", "");
use serde_json;
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
debug!("[*]: Decode chapters from JSON");
// Decode json format
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) {
Ok(data) => data,
Err(_) => return Err(RenderError{ desc: "Could not decode the JSON data".to_owned()}),
};
let mut previous: Option<BTreeMap<String, String>> = None;
type StringMap = BTreeMap<String, String>;
debug!("[*]: Search for current Chapter");
// Search for current chapter and return previous entry
for item in decoded {
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
match previous.get("name") {
Some(n) => {
debug!("[*]: Inserting title: {}", n);
previous_chapter.insert("title".to_owned(), n.to_json())
},
None => {
debug!("[*]: No title found for chapter");
return Err(RenderError{ desc: "No title found for chapter in JSON data".to_owned() })
}
};
// Chapter link
match previous.get("path") {
Some(p) => {
let path = Path::new(p).with_extension("html");
debug!("[*]: Inserting link: {:?}", path);
match path.to_str() {
Some(p) => { previous_chapter.insert("link".to_owned(), p.to_json()); },
None => return Err(RenderError{ desc: "Link could not be converted to str".to_owned() })
}
},
None => return Err(RenderError{ desc: "No path found for chapter in JSON data".to_owned() })
}
debug!("[*]: Inject in context");
// Inject in current context
let updated_context = c.extend(&previous_chapter);
debug!("[*]: Render template");
// Render template
match _h.template() {
Some(t) => {
try!(t.render(&updated_context, r, rc));
},
None => return Err(RenderError{ desc: "Error with the handlebars template".to_owned() })
}
}
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(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
debug!("[fn]: next (handlebars helper)");
Ok(None)
}
}
fn find_chapter(
rc: &mut RenderContext,
target: Target
) -> Result<Option<StringMap>, RenderError> {
debug!("[*]: Get data from context");
// 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 = c.navigate(rc.get_path(), "chapters");
let current = c.navigate(rc.get_path(), "path")
.to_string()
.replace("\"", "");
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
debug!("[*]: Decode chapters from JSON");
// Decode json format
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) {
Ok(data) => data,
Err(_) => return Err(RenderError{ desc: "Could not decode the JSON data".to_owned() }),
};
let mut previous: Option<BTreeMap<String, String>> = None;
let base_path = rc.evaluate_absolute("path")?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
debug!("[*]: Search for current Chapter");
// Search for current chapter and return previous entry
for item in decoded {
let mut previous: Option<StringMap> = None;
debug!("[*]: Search for chapter");
for item in chapters {
match item.get("path") {
Some(path) if !path.is_empty() => {
if let Some(previous) = previous {
let previous_path = match previous.get("path") {
Some(p) => p,
None => return Err(RenderError{ desc: "No path found for chapter in JSON data".to_owned() })
};
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();
match item.get("name") {
Some(n) => {
debug!("[*]: Inserting title: {}", n);
next_chapter.insert("title".to_owned(), n.to_json());
}
None => return Err(RenderError{ desc: "No title found for chapter in JSON data".to_owned() })
}
let link = Path::new(path).with_extension("html");
debug!("[*]: Inserting link: {:?}", link);
match link.to_str() {
Some(l) => { next_chapter.insert("link".to_owned(), l.to_json()); },
None => return Err(RenderError{ desc: "Link could not converted to str".to_owned() })
}
debug!("[*]: Inject in context");
// Inject in current context
let updated_context = c.extend(&next_chapter);
debug!("[*]: Render template");
// Render template
match _h.template() {
Some(t) => {
try!(t.render(&updated_context, r, rc));
},
None => return Err(RenderError{ desc: "Error with the handlebars template".to_owned() })
}
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> {
debug!("[*]: 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("\\", "/"))))
})?;
debug!("[*]: 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> {
debug!("[fn]: 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> {
debug!("[fn]: 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.template_render(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.template_render(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.template_render(TEMPLATE, &data).unwrap(),
"two: two.html|");
}
}

View File

@@ -1,160 +0,0 @@
extern crate handlebars;
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Read;
pub fn render_playpen(s: &str, path: &Path) -> 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 mut previous_end_index = 0;
let mut replaced = String::new();
for playpen in find_playpens(s, path) {
if playpen.escaped {
replaced.push_str(&s[previous_end_index..playpen.start_index-1]);
replaced.push_str(&s[playpen.start_index..playpen.end_index]);
previous_end_index = playpen.end_index;
continue
}
// Check if the file exists
if !playpen.rust_file.exists() || !playpen.rust_file.is_file() {
output!("[-] No file exists for {{{{#playpen }}}}\n {}", playpen.rust_file.to_str().unwrap());
continue
}
// Open file & read file
let mut file = if let Ok(f) = File::open(&playpen.rust_file) { f } else { continue };
let mut file_content = String::new();
if let Err(_) = file.read_to_string(&mut file_content) { continue };
let replacement = String::new() + "<pre class=\"playpen\"><code class=\"language-rust\">" + &file_content + "</code></pre>";
replaced.push_str(&s[previous_end_index..playpen.start_index]);
replaced.push_str(&replacement);
previous_end_index = playpen.end_index;
//println!("Playpen{{ {}, {}, {:?}, {} }}", playpen.start_index, playpen.end_index, playpen.rust_file, playpen.editable);
}
replaced.push_str(&s[previous_end_index..]);
replaced
}
#[derive(PartialOrd, PartialEq, Debug)]
struct Playpen{
start_index: usize,
end_index: usize,
rust_file: PathBuf,
editable: bool,
escaped: bool,
}
fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
let mut playpens = vec![];
for (i, _) in s.match_indices("{{#playpen") {
debug!("[*]: find_playpen");
let mut escaped = false;
if i > 0 {
if let Some(c) = s[i-1..].chars().nth(0) {
if c == '\\' { escaped = true }
}
}
// DON'T forget the "+ i" else you have an index out of bounds error !!
let end_i = if let Some(n) = s[i..].find("}}") { n } else { continue } + i + 2;
debug!("s[{}..{}] = {}", i, end_i, s[i..end_i].to_string());
// If there is nothing between "{{#playpen" and "}}" skip
if end_i-2 - (i+10) < 1 { continue }
if s[i+10..end_i-2].trim().len() == 0 { continue }
debug!("{}", s[i+10..end_i-2].to_string());
// Split on whitespaces
let params: Vec<&str> = s[i+10..end_i-2].split_whitespace().collect();
let mut editable = false;
if params.len() > 1 {
editable = if let Some(_) = params[1].find("editable") {true} else {false};
}
playpens.push(
Playpen{
start_index: i,
end_index: end_i,
rust_file: base_path.join(PathBuf::from(params[0])),
editable: editable,
escaped: escaped
}
)
}
playpens
}
//
//---------------------------------------------------------------------------------
// Tests
//
#[test]
fn test_find_playpens_no_playpen() {
let s = "Some random text without playpen...";
assert!(find_playpens(s, Path::new("")) == vec![]);
}
#[test]
fn test_find_playpens_partial_playpen() {
let s = "Some random text with {{#playpen...";
assert!(find_playpens(s, Path::new("")) == vec![]);
}
#[test]
fn test_find_playpens_empty_playpen() {
let s = "Some random text with {{#playpen}} and {{#playpen }}...";
assert!(find_playpens(s, Path::new("")) == vec![]);
}
#[test]
fn test_find_playpens_simple_playpen() {
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
assert!(find_playpens(s, Path::new("")) == vec![
Playpen{start_index: 22, end_index: 42, rust_file: PathBuf::from("file.rs"), editable: false, escaped: false},
Playpen{start_index: 47, end_index: 68, rust_file: PathBuf::from("test.rs"), editable: false, escaped: false}
]);
}
#[test]
fn test_find_playpens_complex_playpen() {
let s = "Some random text with {{#playpen file.rs editable}} and {{#playpen test.rs editable }}...";
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("dir")));
assert!(find_playpens(s, Path::new("dir")) == vec![
Playpen{start_index: 22, end_index: 51, rust_file: PathBuf::from("dir/file.rs"), editable: true, escaped: false},
Playpen{start_index: 56, end_index: 86, rust_file: PathBuf::from("dir/test.rs"), editable: true, escaped: false}
]);
}
#[test]
fn test_find_playpens_escaped_playpen() {
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
assert!(find_playpens(s, Path::new("")) == vec![
Playpen{start_index: 39, end_index: 68, rust_file: PathBuf::from("file.rs"), editable: true, escaped: true},
]);
}

View File

@@ -1,130 +1,136 @@
extern crate handlebars;
extern crate rustc_serialize;
extern crate pulldown_cmark;
use std::path::Path;
use std::collections::BTreeMap;
use self::rustc_serialize::json;
use self::handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
use self::pulldown_cmark::{Parser, html, Event, Tag};
use serde_json;
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;
impl HelperDef for RenderToc {
fn call(&self, c: &Context, _h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
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"))?
.replace("\"", "");
// 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 = c.navigate(rc.get_path(), "chapters");
let current = c.navigate(rc.get_path(), "path").to_string().replace("\"", "");
try!(rc.writer.write("<ul class=\"chapter\">".as_bytes()));
rc.writer.write_all(b"<ul class=\"chapter\">")?;
// Decode json format
let decoded: Vec<BTreeMap<String,String>> = json::decode(&chapters.to_string()).unwrap();
let mut current_level = 1;
let mut current_level = 1;
for item in decoded {
// Spacer
if let Some(_) = item.get("spacer") {
try!(rc.writer.write("<li class=\"spacer\"></li>".as_bytes()));
continue
}
let level = if let Some(s) = item.get("section") { s.len() / 2 } else { 1 };
if level > current_level {
try!(rc.writer.write("<li>".as_bytes()));
try!(rc.writer.write("<ul class=\"section\">".as_bytes()));
try!(rc.writer.write("<li>".as_bytes()));
} else if level < current_level {
while level < current_level {
try!(rc.writer.write("</ul>".as_bytes()));
try!(rc.writer.write("</li>".as_bytes()));
current_level = current_level - 1;
for item in chapters {
// Spacer
if item.get("spacer").is_some() {
rc.writer.write_all(b"<li class=\"spacer\"></li>")?;
continue;
}
try!(rc.writer.write("<li>".as_bytes()));
}
else {
try!(rc.writer.write("<li".as_bytes()));
if let None = item.get("section") {
try!(rc.writer.write(" class=\"affix\"".as_bytes()));
}
try!(rc.writer.write(">".as_bytes()));
}
// Link
let path_exists = if let Some(path) = item.get("path") {
if !path.is_empty() {
try!(rc.writer.write("<a href=\"".as_bytes()));
let level = if let Some(s) = item.get("section") {
s.matches('.').count()
} else {
1
};
// Add link
try!(rc.writer.write(
Path::new(
item.get("path")
.expect("Error: path should be Some(_)")
).with_extension("html")
.to_str().unwrap().as_bytes()
));
try!(rc.writer.write("\"".as_bytes()));
if path == &current {
try!(rc.writer.write(" class=\"active\"".as_bytes()));
if level > current_level {
while level > current_level {
rc.writer.write_all(b"<li>")?;
rc.writer.write_all(b"<ul 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"</li>")?;
current_level -= 1;
}
rc.writer.write_all(b"<li>")?;
} else {
rc.writer.write_all(b"<li")?;
if item.get("section").is_none() {
rc.writer.write_all(b" class=\"affix\"")?;
}
rc.writer.write_all(b">")?;
}
try!(rc.writer.write(">".as_bytes()));
true
// Link
let path_exists = if let Some(path) = item.get("path") {
if !path.is_empty() {
rc.writer.write_all(b"<a href=\"")?;
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
.with_extension("html")
.to_str()
.unwrap()
// Hack for windows who tends to use `\` as separator instead of `/`
.replace("\\", "/");
// Add link
rc.writer.write_all(tmp.as_bytes())?;
rc.writer.write_all(b"\"")?;
if path == &current {
rc.writer.write_all(b" class=\"active\"")?;
}
rc.writer.write_all(b">")?;
true
} else {
false
}
} else {
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> ")?;
}
}else {
false
};
// Section does not necessarily exist
if let Some(section) = item.get("section") {
try!(rc.writer.write("<strong>".as_bytes()));
try!(rc.writer.write(section.as_bytes()));
try!(rc.writer.write("</strong> ".as_bytes()));
if let Some(name) = item.get("name") {
// Render only inline code blocks
// 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,
});
// render markdown to html
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
html::push_html(&mut markdown_parsed_name, parser);
// write to the handlebars template
rc.writer.write_all(markdown_parsed_name.as_bytes())?;
}
if path_exists {
rc.writer.write_all(b"</a>")?;
}
rc.writer.write_all(b"</li>")?;
}
while current_level > 1 {
rc.writer.write_all(b"</ul>")?;
rc.writer.write_all(b"</li>")?;
current_level -= 1;
}
if let Some(name) = item.get("name") {
// Render only inline code blocks
// 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) => true,
&Event::InlineHtml(_) => true,
&Event::Text(_) => true,
_ => false,
}
});
// render markdown to html
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
html::push_html(&mut markdown_parsed_name, parser);
// write to the handlebars template
try!(rc.writer.write(markdown_parsed_name.as_bytes()));
}
if path_exists {
try!(rc.writer.write("</a>".as_bytes()));
}
try!(rc.writer.write("</li>".as_bytes()));
current_level = level;
rc.writer.write_all(b"</ul>")?;
Ok(())
}
try!(rc.writer.write("</ul>".as_bytes()));
Ok(())
}
}

View File

@@ -1,5 +1,9 @@
pub use self::renderer::Renderer;
pub use self::html_handlebars::HtmlHandlebars;
pub mod renderer;
mod html_handlebars;
use errors::*;
pub trait Renderer {
fn render(&self, book: &::book::MDBook) -> Result<()>;
}

View File

@@ -1,5 +0,0 @@
use std::error::Error;
pub trait Renderer {
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
}

View File

@@ -0,0 +1,71 @@
/*
Based off of the Ayu theme
Original by Dempfi (https://github.com/dempfi/ayu)
*/
.hljs {
display: block;
overflow-x: auto;
background: #191f26;
color: #e6e1cf;
padding: 0.5em;
}
.hljs-comment,
.hljs-quote,
.hljs-meta {
color: #5c6773;
font-style: italic;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-attr,
.hljs-regexp,
.hljs-link,
.hljs-selector-id,
.hljs-selector-class {
color: #ff7733;
}
.hljs-number,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #ffee99;
}
.hljs-string,
.hljs-bullet {
color: #b8cc52;
}
.hljs-title,
.hljs-built_in,
.hljs-section {
color: #ffb454;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-symbol {
color: #ff7733;
}
.hljs-name {
color: #36a3d9;
}
.hljs-tag {
color: #00568d;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@@ -3,6 +3,14 @@ body {
font-family: "Open Sans", sans-serif;
color: #333;
}
body {
margin: 0;
font-size: 1rem;
}
code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
font-size: 0.875em;
}
.left {
float: left;
}
@@ -12,6 +20,9 @@ body {
.hidden {
display: none;
}
.play-button.hidden {
display: none;
}
h2,
h3 {
margin-top: 2.5em;
@@ -27,15 +38,17 @@ h5 {
}
table {
margin: 0 auto;
border-collapse: collapse;
}
table td {
padding: 3px 20px;
border: 1px solid;
}
table thead td {
font-weight: 700;
}
table td {
padding: 3px 20px;
}
.sidebar {
position: absolute;
position: fixed;
left: 0;
top: 0;
bottom: 0;
@@ -70,7 +83,7 @@ table td {
.chapter {
list-style: none outside none;
padding-left: 0;
line-height: 1.9em;
line-height: 2.2em;
}
.chapter li a {
padding: 5px 0;
@@ -87,7 +100,7 @@ table td {
.section {
list-style: none outside none;
padding-left: 20px;
line-height: 2.5em;
line-height: 1.9em;
}
.section li {
-o-text-overflow: ellipsis;
@@ -96,43 +109,31 @@ table td {
white-space: nowrap;
}
.page-wrapper {
position: absolute;
overflow-y: auto;
left: 315px;
right: 0;
top: 0;
bottom: 0;
padding-left: 300px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
min-height: 100%;
-webkit-transition: left 0.5s;
-moz-transition: left 0.5s;
-o-transition: left 0.5s;
-ms-transition: left 0.5s;
transition: left 0.5s;
-webkit-transition: padding-left 0.5s;
-moz-transition: padding-left 0.5s;
-o-transition: padding-left 0.5s;
-ms-transition: padding-left 0.5s;
transition: padding-left 0.5s;
}
@media only screen and (max-width: 1060px) {
.page-wrapper {
left: 15px;
padding-right: 15px;
padding-left: 0;
}
}
.sidebar-hidden .page-wrapper {
left: 15px;
padding-left: 0;
}
.sidebar-visible .page-wrapper {
left: 315px;
padding-left: 300px;
}
.page {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
padding-right: 15px;
overflow-y: auto;
outline: 0;
padding: 0 15px;
}
.content {
margin-left: auto;
@@ -203,7 +204,7 @@ table td {
font-size: 2.5em;
text-align: center;
text-decoration: none;
position: absolute;
position: fixed;
top: 50px /* Height of menu-bar */;
bottom: 0;
margin: 0;
@@ -243,15 +244,31 @@ table td {
text-decoration: none;
}
.previous {
left: 0;
left: 315px;
-webkit-transition: left 0.5s;
-moz-transition: left 0.5s;
-o-transition: left 0.5s;
-ms-transition: left 0.5s;
transition: left 0.5s;
}
@media only screen and (max-width: 1060px) {
.previous {
left: 15px;
}
}
.next {
right: 15px;
}
.sidebar-hidden .previous {
left: 15px;
}
.sidebar-visible .previous {
left: 315px;
}
.theme-popup {
position: fixed;
left: -40px;
-webkit-border-radius: 4px;
position: absolute;
left: 10px;
z-index: 1000;
border-radius: 4px;
font-size: 0.7em;
}
@@ -260,6 +277,12 @@ table td {
padding: 2px 10px;
line-height: 25px;
white-space: nowrap;
cursor: pointer;
}
.theme-popup .theme:hover:first-child,
.theme-popup .theme:hover:last-child {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
@media only screen and (max-width: 1250px) {
.nav-chapters {
@@ -283,7 +306,6 @@ table td {
position: relative;
display: inline-block;
margin-bottom: 50px;
-webkit-border-radius: 5px;
border-radius: 5px;
}
.next {
@@ -296,14 +318,6 @@ table td {
.light {
color: #333;
background-color: #fff;
/*
table {
thead td {
color: $table-header-fg;
backrgound: $table-header-bg;
}
}
*/
/* Inline code */
}
.light .content .header:link,
@@ -338,7 +352,8 @@ table td {
.light .nav-chapters,
.light .nav-chapters:visited,
.light .mobile-nav-chapters,
.light .mobile-nav-chapters:visited {
.light .mobile-nav-chapters:visited,
.light .menu-bar a i {
color: #ccc;
}
.light .menu-bar i:hover,
@@ -353,16 +368,21 @@ table td {
background-color: #fafafa;
}
.light .content a:link,
.light a:visited {
.light a:visited,
.light a > .hljs {
color: #4183c4;
}
.light .theme-popup {
color: #333;
background: #fafafa;
border: 1px solid #ccc;
}
.light .theme-popup .theme:hover {
background-color: #e6e6e6;
}
.light .theme-popup .default {
color: #ccc;
}
.light blockquote {
margin: 20px 0;
padding: 0 20px;
@@ -371,18 +391,37 @@ table td {
border-top: 0.1em solid #e1edf1;
border-bottom: 0.1em solid #e1edf1;
}
.light table td {
border-color: #f2f2f2;
}
.light table tbody tr:nth-child(2n) {
background: #f7f7f7;
}
.light table thead {
background: #ccc;
}
.light table thead td {
border: none;
}
.light table thead tr {
border: 1px #ccc solid;
}
.light :not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
-webkit-border-radius: 3px;
border-radius: 3px;
color: #6e6b5e;
}
.light a:hover > .hljs {
text-decoration: underline;
}
.light pre {
position: relative;
}
.light pre > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;
color: #364149;
@@ -400,14 +439,6 @@ table td {
.coal {
color: #98a3ad;
background-color: #141617;
/*
table {
thead td {
color: $table-header-fg;
backrgound: $table-header-bg;
}
}
*/
/* Inline code */
}
.coal .content .header:link,
@@ -442,7 +473,8 @@ table td {
.coal .nav-chapters,
.coal .nav-chapters:visited,
.coal .mobile-nav-chapters,
.coal .mobile-nav-chapters:visited {
.coal .mobile-nav-chapters:visited,
.coal .menu-bar a i {
color: #43484d;
}
.coal .menu-bar i:hover,
@@ -457,16 +489,21 @@ table td {
background-color: #292c2f;
}
.coal .content a:link,
.coal a:visited {
.coal a:visited,
.coal a > .hljs {
color: #2b79a2;
}
.coal .theme-popup {
color: #98a3ad;
background: #141617;
border: 1px solid #43484d;
}
.coal .theme-popup .theme:hover {
background-color: #1f2124;
}
.coal .theme-popup .default {
color: #43484d;
}
.coal blockquote {
margin: 20px 0;
padding: 0 20px;
@@ -475,18 +512,37 @@ table td {
border-top: 0.1em solid #2c2f44;
border-bottom: 0.1em solid #2c2f44;
}
.coal table td {
border-color: #1f2223;
}
.coal table tbody tr:nth-child(2n) {
background: #1b1d1e;
}
.coal table thead {
background: #3f4649;
}
.coal table thead td {
border: none;
}
.coal table thead tr {
border: 1px #3f4649 solid;
}
.coal :not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
-webkit-border-radius: 3px;
border-radius: 3px;
color: #c5c8c6;
}
.coal a:hover > .hljs {
text-decoration: underline;
}
.coal pre {
position: relative;
}
.coal pre > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;
color: #a1adb8;
@@ -504,14 +560,6 @@ table td {
.navy {
color: #bcbdd0;
background-color: #161923;
/*
table {
thead td {
color: $table-header-fg;
backrgound: $table-header-bg;
}
}
*/
/* Inline code */
}
.navy .content .header:link,
@@ -546,7 +594,8 @@ table td {
.navy .nav-chapters,
.navy .nav-chapters:visited,
.navy .mobile-nav-chapters,
.navy .mobile-nav-chapters:visited {
.navy .mobile-nav-chapters:visited,
.navy .menu-bar a i {
color: #737480;
}
.navy .menu-bar i:hover,
@@ -561,16 +610,21 @@ table td {
background-color: #282d3f;
}
.navy .content a:link,
.navy a:visited {
.navy a:visited,
.navy a > .hljs {
color: #2b79a2;
}
.navy .theme-popup {
color: #bcbdd0;
background: #161923;
border: 1px solid #737480;
}
.navy .theme-popup .theme:hover {
background-color: #282e40;
}
.navy .theme-popup .default {
color: #737480;
}
.navy blockquote {
margin: 20px 0;
padding: 0 20px;
@@ -579,18 +633,37 @@ table td {
border-top: 0.1em solid #2f333f;
border-bottom: 0.1em solid #2f333f;
}
.navy table td {
border-color: #1f2331;
}
.navy table tbody tr:nth-child(2n) {
background: #1b1f2b;
}
.navy table thead {
background: #39415b;
}
.navy table thead td {
border: none;
}
.navy table thead tr {
border: 1px #39415b solid;
}
.navy :not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
-webkit-border-radius: 3px;
border-radius: 3px;
color: #c5c8c6;
}
.navy a:hover > .hljs {
text-decoration: underline;
}
.navy pre {
position: relative;
}
.navy pre > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;
color: #c8c9db;
@@ -608,14 +681,6 @@ table td {
.rust {
color: #262625;
background-color: #e1e1db;
/*
table {
thead td {
color: $table-header-fg;
backrgound: $table-header-bg;
}
}
*/
/* Inline code */
}
.rust .content .header:link,
@@ -650,7 +715,8 @@ table td {
.rust .nav-chapters,
.rust .nav-chapters:visited,
.rust .mobile-nav-chapters,
.rust .mobile-nav-chapters:visited {
.rust .mobile-nav-chapters:visited,
.rust .menu-bar a i {
color: #737480;
}
.rust .menu-bar i:hover,
@@ -665,16 +731,21 @@ table td {
background-color: #3b2e2a;
}
.rust .content a:link,
.rust a:visited {
.rust a:visited,
.rust a > .hljs {
color: #2b79a2;
}
.rust .theme-popup {
color: #262625;
background: #e1e1db;
border: 1px solid #b38f6b;
}
.rust .theme-popup .theme:hover {
background-color: #99908a;
}
.rust .theme-popup .default {
color: #737480;
}
.rust blockquote {
margin: 20px 0;
padding: 0 20px;
@@ -683,18 +754,37 @@ table td {
border-top: 0.1em solid #b8b8b1;
border-bottom: 0.1em solid #b8b8b1;
}
.rust table td {
border-color: #d7d7cf;
}
.rust table tbody tr:nth-child(2n) {
background: #dbdbd4;
}
.rust table thead {
background: #b3a497;
}
.rust table thead td {
border: none;
}
.rust table thead tr {
border: 1px #b3a497 solid;
}
.rust :not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
-webkit-border-radius: 3px;
border-radius: 3px;
color: #6e6b5e;
}
.rust a:hover > .hljs {
text-decoration: underline;
}
.rust pre {
position: relative;
}
.rust pre > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;
color: #c8c9db;
@@ -709,3 +799,204 @@ table td {
.rust pre > .result {
margin-top: 10px;
}
.ayu {
color: #c5c5c5;
background-color: #0f1419;
/* Inline code */
}
.ayu .content .header:link,
.ayu .content .header:visited {
color: #c5c5c5;
pointer: cursor;
}
.ayu .content .header:link:hover,
.ayu .content .header:visited:hover {
text-decoration: none;
}
.ayu .sidebar {
background-color: #14191f;
color: #c8c9db;
}
.ayu .chapter li {
color: #5c6773;
}
.ayu .chapter li a {
color: #c8c9db;
}
.ayu .chapter li .active,
.ayu .chapter li a:hover {
/* Animate color change */
color: #ffb454;
}
.ayu .chapter .spacer {
background-color: #2d334f;
}
.ayu .menu-bar,
.ayu .menu-bar:visited,
.ayu .nav-chapters,
.ayu .nav-chapters:visited,
.ayu .mobile-nav-chapters,
.ayu .mobile-nav-chapters:visited,
.ayu .menu-bar a i {
color: #737480;
}
.ayu .menu-bar i:hover,
.ayu .nav-chapters:hover,
.ayu .mobile-nav-chapters i:hover {
color: #b7b9cc;
}
.ayu .mobile-nav-chapters i:hover {
color: #c8c9db;
}
.ayu .mobile-nav-chapters {
background-color: #14191f;
}
.ayu .content a:link,
.ayu a:visited,
.ayu a > .hljs {
color: #0096cf;
}
.ayu .theme-popup {
color: #c5c5c5;
background: #14191f;
border: 1px solid #5c6773;
}
.ayu .theme-popup .theme:hover {
background-color: #191f26;
}
.ayu .theme-popup .default {
color: #737480;
}
.ayu blockquote {
margin: 20px 0;
padding: 0 20px;
color: #c5c5c5;
background-color: #262933;
border-top: 0.1em solid #2f333f;
border-bottom: 0.1em solid #2f333f;
}
.ayu table td {
border-color: #182028;
}
.ayu table tbody tr:nth-child(2n) {
background: #141b22;
}
.ayu table thead {
background: #324354;
}
.ayu table thead td {
border: none;
}
.ayu table thead tr {
border: 1px #324354 solid;
}
.ayu :not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
border-radius: 3px;
color: #ffb454;
}
.ayu a:hover > .hljs {
text-decoration: underline;
}
.ayu pre {
position: relative;
}
.ayu pre > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;
color: #c8c9db;
cursor: pointer;
}
.ayu pre > .buttons :hover {
color: #ffb454;
}
.ayu pre > .buttons i {
margin-left: 8px;
}
.ayu pre > .result {
margin-top: 10px;
}
@media only print {
#sidebar,
#menu-bar,
.nav-chapters,
.mobile-nav-chapters {
display: none;
}
#page-wrapper {
left: 0;
overflow-y: initial;
}
#page-wrapper.page-wrapper {
padding-left: 0px;
}
#content {
max-width: none;
margin: 0;
padding: 0;
}
.page {
overflow-y: initial;
}
code {
background-color: #666;
border-radius: 5px;
/* Force background to be printed in Chrome */
-webkit-print-color-adjust: exact;
}
pre > .buttons {
z-index: 2;
}
a,
a:visited,
a:active,
a:hover {
color: #4183c4;
text-decoration: none;
}
h1,
h2,
h3,
h4,
h5,
h6 {
page-break-inside: avoid;
page-break-after: avoid;
/*break-after: avoid*/
}
pre,
code {
page-break-inside: avoid;
white-space: pre-wrap /* CSS 3 */;
white-space: -moz-pre-wrap /* Mozilla, since 1999 */;
white-space: -pre-wrap /* Opera 4-6 */;
white-space: -o-pre-wrap /* Opera 7 */;
word-wrap: break-word /* Internet Explorer 5.5+ */;
}
}
.tooltiptext {
position: absolute;
visibility: hidden;
color: #fff;
background-color: #333;
-webkit-transform: translateX(-50%);
-moz-transform: translateX(-50%);
-o-transform: translateX(-50%);
-ms-transform: translateX(-50%);
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
left: -8px; /* Half of the width of the icon */
top: -35px;
font-size: 0.8em;
text-align: center;
border-radius: 6px;
padding: 5px 8px;
margin: 5px;
z-index: 1000;
}
.tooltipped .tooltiptext {
visibility: visible;
}

View File

@@ -7,21 +7,34 @@ $( document ).ready(function() {
window.onunload = function(){};
// Set theme
var theme = localStorage.getItem('theme');
if (theme === null) { theme = 'light'; }
var theme = store.get('mdbook-theme');
if (theme === null || theme === undefined) { theme = 'light'; }
set_theme(theme);
// Syntax highlighting Configuration
hljs.configure({
tabReplace: ' ', // 4 spaces
languages: [], // Languages used for auto-detection
});
$('code').each(function(i, block) {
hljs.highlightBlock(block);
});
if (window.ace) {
// language-rust class needs to be removed for editable
// blocks or highlightjs will capture events
$('code.editable').removeClass('language-rust');
$('code').not('.editable').each(function(i, block) {
hljs.highlightBlock(block);
});
} else {
$('code').each(function(i, block) {
hljs.highlightBlock(block);
});
}
// Adding the hljs class gives code blocks the color css
// even if highlighting doesn't apply
$('code').addClass('hljs');
var KEY_CODES = {
PREVIOUS_KEY: 37,
@@ -29,6 +42,7 @@ $( document ).ready(function() {
};
$(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();
@@ -46,42 +60,22 @@ $( document ).ready(function() {
});
// Interesting DOM Elements
var html = $("html");
var sidebar = $("#sidebar");
var page_wrapper = $("#page-wrapper");
var content = $("#content");
// Add anchors for all content headers
content.find("h1, h2, h3, h4, h5").wrap(function(){
var wrapper = $("<a class=\"header\">");
wrapper.attr("name", $(this).text());
// Add so that when you click the link actually shows up in the url bar...
wrapper.attr("href", $(location).attr('href') + "#" + $(this).text());
return wrapper;
});
// Help keyboard navigation by always focusing on page content
$(".page").focus();
// Toggle sidebar
$("#sidebar-toggle").click(function(event){
if ( html.hasClass("sidebar-hidden") ) {
html.removeClass("sidebar-hidden").addClass("sidebar-visible");
localStorage.setItem('sidebar', 'visible');
} else if ( html.hasClass("sidebar-visible") ) {
html.removeClass("sidebar-visible").addClass("sidebar-hidden");
localStorage.setItem('sidebar', 'hidden');
} else {
if(sidebar.position().left === 0){
html.addClass("sidebar-hidden");
localStorage.setItem('sidebar', 'hidden');
} else {
html.addClass("sidebar-visible");
localStorage.setItem('sidebar', 'visible');
}
$("#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) {
@@ -89,49 +83,69 @@ $( document ).ready(function() {
}
// 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 (default)<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="navy">Navy</div>'))
.append($('<div class="theme" id="ayu">Ayu</div>'));
$(this).append(popup);
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";
}
localStorage.setItem('theme', theme);
if (window.ace && window.editors) {
window.editors.forEach(function(editor) {
editor.setTheme(ace_theme);
});
}
store.set('mdbook-theme', theme);
$('body').removeClass().addClass(theme);
}
@@ -152,10 +166,10 @@ $( document ).ready(function() {
for(var n = 0; n < lines.length; n++){
if($.trim(lines[n])[0] == hiding_character){
if(first_non_hidden_line){
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)#/, "$1") + "</span>";
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;
}
@@ -172,21 +186,22 @@ $( document ).ready(function() {
if(!lines_hidden) { return; }
// add expand button
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\"></i></div>");
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\" title=\"Show hidden lines\"></i></div>");
pre_block.find("i").click(function(e){
if( $(this).hasClass("fa-expand") ) {
$(this).removeClass("fa-expand").addClass("fa-compress");
$(this).attr("title", "Hide lines");
pre_block.find("span.hidden").removeClass("hidden").addClass("unhidden");
}
else {
$(this).removeClass("fa-compress").addClass("fa-expand");
$(this).attr("title", "Show hidden lines");
pre_block.find("span.unhidden").removeClass("unhidden").addClass("hidden");
}
});
});
// Process playpen code blocks
$(".playpen").each(function(block){
var pre_block = $(this);
@@ -196,16 +211,147 @@ $( document ).ready(function() {
pre_block.prepend("<div class=\"buttons\"></div>");
buttons = pre_block.find(".buttons");
}
buttons.prepend("<i class=\"fa fa-play play-button\"></i>");
buttons.prepend("<i class=\"fa fa-play play-button hidden\" title=\"Run this code\"></i>");
buttons.prepend("<i class=\"fa fa-copy clip-button\" title=\"Copy to clipboard\"><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\" title=\"Undo changes\"></i>");
}
buttons.find(".play-button").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();
});
});
var clipboardSnippets = new Clipboard('.clip-button', {
text: function(trigger) {
hideTooltip(trigger);
let playpen = $(trigger).parents(".playpen");
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!");
});
$.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);
});
},
});
});
function playpen_text(playpen) {
let code_block = playpen.find("code").first();
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;
}
}
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.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);
});
}
}
}
// 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('mdbook-sidebar', 'visible');
} else if ( html.hasClass("sidebar-visible") ) {
html.removeClass("sidebar-visible").addClass("sidebar-hidden");
store.set('mdbook-sidebar', 'hidden');
} else {
if($("#sidebar").position().left === 0){
html.addClass("sidebar-hidden");
store.set('mdbook-sidebar', 'hidden');
} else {
html.addClass("sidebar-visible");
store.set('mdbook-sidebar', 'visible');
}
}
}
function run_rust_code(code_block) {
var result_block = code_block.find(".result");
@@ -214,17 +360,35 @@ function run_rust_code(code_block) {
result_block = code_block.find(".result");
}
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.text("Running...");
$.ajax({
url: "https://play.rust-lang.org/evaluate.json",
url: "https://play.rust-lang.org/execute",
method: "POST",
crossDomain: true,
dataType: "json",
contentType: "application/json",
data: JSON.stringify({version: "stable", optimize: "0", code: code_block.find(".language-rust").text() }),
data: JSON.stringify(params),
timeout: 15000,
success: function(response){
result_block.text(response.result);
}
result_block.text(response.success ? response.stdout : response.stderr);
},
error: function(qXHR, textStatus, errorThrown){
result_block.text("Playground communication " + textStatus);
},
});
}

7
src/theme/clipboard.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
src/theme/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

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

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

View File

@@ -1,5 +1,56 @@
/* Modified Base16 Atelier Dune Light - Theme
/* Original by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */
/* Base16 Atelier Dune Light - Theme */
/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */
/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */
/* Atelier-Dune Comment */
.hljs-comment,
.hljs-quote {
color: #AAA;
}
/* Atelier-Dune Red */
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #d73737;
}
/* Atelier-Dune Orange */
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #b65611;
}
/* Atelier-Dune Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #60ac39;
}
/* Atelier-Dune Blue */
.hljs-title,
.hljs-section {
color: #6684e1;
}
/* Atelier-Dune Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #b854d4;
}
.hljs {
display: block;
@@ -7,101 +58,12 @@
background: #f1f1f1;
color: #6e6b5e;
padding: 0.5em;
-webkit-text-size-adjust: none;
}
/* Atelier-Dune Comment */
.hljs-comment {
color: #AAA;
.hljs-emphasis {
font-style: italic;
}
/* Atelier-Dune Red */
.hljs-variable,
.hljs-tag,
.hljs-regexp,
.hljs-name,
.ruby .hljs-constant,
.xml .hljs-tag .hljs-title,
.xml .hljs-pi,
.xml .hljs-doctype,
.html .hljs-doctype,
.css .hljs-id,
.css .hljs-class,
.css .hljs-pseudo {
color: #d73737;
}
/* Atelier-Dune Orange */
.hljs-number,
.hljs-preprocessor,
.hljs-built_in,
.hljs-literal,
.hljs-params,
.hljs-attribute,
.hljs-constant {
color: #b65611;
}
/* Atelier-Dune Yellow */
.ruby .hljs-class .hljs-title,
.css .hljs-rule .hljs-attribute {
color: #ae9513;
}
/* Atelier-Dune Green */
.hljs-string,
.hljs-value,
.hljs-inheritance,
.ruby .hljs-symbol,
.xml .hljs-cdata {
color: #2a9292;
}
/* Atelier-Dune Aqua */
.hljs-title,
.css .hljs-hexcolor {
color: #1fad83;
}
/* Atelier-Dune Blue */
.hljs-function,
.python .hljs-decorator,
.python .hljs-title,
.ruby .hljs-function .hljs-title,
.ruby .hljs-title .hljs-keyword,
.perl .hljs-sub,
.javascript .hljs-title,
.coffeescript .hljs-title {
color: #6684e1;
}
/* Atelier-Dune Purple */
.hljs-keyword,
.javascript .hljs-function {
color: #b854d4;
}
.coffeescript .javascript,
.javascript .xml,
.tex .hljs-formula,
.xml .javascript,
.xml .vbscript,
.xml .css,
.xml .hljs-cdata {
opacity: 0.5;
}
/* markdown */
.hljs-header {
color: #A30000;
}
.hljs-link_label {
color: #33CCCC;
}
.hljs-link_url {
color: #CC66FF;
.hljs-strong {
font-weight: bold;
}

File diff suppressed because one or more lines are too long

View File

@@ -4,42 +4,65 @@
<meta charset="UTF-8">
<title>{{ title }}</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{% block description %}{% endblock %}">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="{{ path_to_root }}">
<link rel="stylesheet" href="book.css">
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
<link rel="shortcut icon" href="{{ favicon }}">
<!-- Font Awesome -->
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme -->
{{#each additional_css}}
<link rel="stylesheet" href="{{this}}">
{{/each}}
{{#if mathjax_support}}
<!-- MathJax -->
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}}
<!-- Fetch Clipboard.js from CDN but have a local fallback -->
<script src="https://cdn.jsdelivr.net/clipboard.js/1.6.1/clipboard.min.js"></script>
<script>
if (typeof Clipboard == 'undefined') {
document.write(unescape("%3Cscript src='clipboard.min.js'%3E%3C/script%3E"));
}
</script>
<!-- Fetch JQuery from CDN but have a local fallback -->
<script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>
<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>
</head>
<body>
<body class="light">
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme = localStorage.getItem('theme');
if (theme == null) { theme = 'light'; }
var theme = store.get('mdbook-theme');
if (theme === null || theme === undefined) { theme = 'light'; }
$('body').removeClass().addClass(theme);
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
var sidebar = localStorage.getItem('sidebar');
var sidebar = store.get('mdbook-sidebar');
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
</script>
@@ -50,17 +73,20 @@
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div class="page" tabindex="-1">
{{> 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>
<i id="sidebar-toggle" class="fa fa-bars" title="Toggle sidebar"></i>
<i id="theme-toggle" class="fa fa-paint-brush" title="Change theme"></i>
</div>
<h1 class="menu-title">{{ 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>
<a href="print.html">
<i id="print-button" class="fa fa-print" title="Print this book"></i>
</a>
</div>
</div>
@@ -70,13 +96,13 @@
<!-- Mobile navigation buttons -->
{{#previous}}
<a href="{{link}}" class="mobile-nav-chapters previous">
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous" title="Previous chapter">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a href="{{link}}" class="mobile-nav-chapters next">
<a rel="next" href="{{link}}" class="mobile-nav-chapters next" title="Next chapter">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
@@ -105,7 +131,51 @@
}
</script>
<!-- Livereload script (if served using the cli tool) -->
{{{livereload}}}
{{#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 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}}
{{#if is_print}}
<script>
$(document).ready(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>

View File

@@ -1,102 +1,190 @@
pub mod playpen_editor;
use std::path::Path;
use std::fs::File;
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");
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 JQUERY: &'static [u8] = include_bytes!("jquery-2.1.4.min.js");
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");
/// The `Theme` struct should be used instead of the static variables because the `new()` method
/// will look if the user has a theme directory in his source folder and use the users theme instead
/// of the default.
/// The `Theme` struct should be used instead of the static variables because
/// the `new()` method will look if the user has a theme directory in his
/// source folder and use the users theme instead of the default.
///
/// You should exceptionnaly use the static variables only if you need the default theme even if the
/// user has specified another theme.
/// You should only ever use the static variables directly if you want to
/// override the user's theme with the defaults.
#[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>,
pub highlight_css: Vec<u8>,
pub tomorrow_night_css: Vec<u8>,
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 {
pub fn new(src: &Path) -> Self {
pub fn new<P: AsRef<Path>>(theme_dir: P) -> Self {
let theme_dir = theme_dir.as_ref();
let mut theme = Theme::default();
// Default theme
let mut theme = Theme {
index: INDEX.to_owned(),
css: CSS.to_owned(),
js: JS.to_owned(),
highlight_css: HIGHLIGHT_CSS.to_owned(),
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
highlight_js: HIGHLIGHT_JS.to_owned(),
jquery: JQUERY.to_owned(),
};
// Check if the given path exists
if !src.exists() || !src.is_dir() {
return theme
// If the theme directory doesn't exist there's no point continuing...
if !theme_dir.exists() || !theme_dir.is_dir() {
return theme;
}
let src = src.join("theme");
// If src does exist, check if there is a theme directory in it
if !src.exists() || !src.is_dir() {
return theme
}
// Check for individual files, if they exist copy them across
{
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),
];
// Check for individual files if they exist
for (filename, dest) in files {
if !filename.exists() {
continue;
}
// index.hbs
if let Ok(mut f) = File::open(&src.join("index.hbs")) {
theme.index.clear(); // Reset the value, because read_to_string appends...
let _ = f.read_to_end(&mut theme.index);
}
// book.js
if let Ok(mut f) = File::open(&src.join("book.js")) {
theme.js.clear();
let _ = f.read_to_end(&mut theme.js);
}
// book.css
if let Ok(mut f) = File::open(&src.join("book.css")) {
theme.css.clear();
let _ = f.read_to_end(&mut theme.css);
}
// highlight.js
if let Ok(mut f) = File::open(&src.join("highlight.js")) {
theme.highlight_js.clear();
let _ = f.read_to_end(&mut theme.highlight_js);
}
// highlight.css
if let Ok(mut f) = File::open(&src.join("highlight.css")) {
theme.highlight_css.clear();
let _ = f.read_to_end(&mut theme.highlight_css);
}
// tomorrow-night.css
if let Ok(mut f) = File::open(&src.join("tomorrow-night.css")) {
theme.tomorrow_night_css.clear();
let _ = f.read_to_end(&mut theme.tomorrow_night_css);
if let Err(e) = load_file_contents(&filename, dest) {
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
}
}
}
theme
}
}
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(),
highlight_css: HIGHLIGHT_CSS.to_owned(),
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
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(),
}
}
}
/// Checks if a file exists, if so, the destination buffer will be filled with
/// its contents.
fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result<()> {
let filename = filename.as_ref();
let mut buffer = Vec::new();
File::open(filename)?.read_to_end(&mut buffer)?;
// We needed the buffer so we'd only overwrite the existing content if we
// could successfully load the file into memory.
dest.clear();
dest.append(&mut buffer);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempdir::TempDir;
use std::path::PathBuf;
#[test]
fn theme_uses_defaults_with_nonexistent_src_dir() {
let non_existent = PathBuf::from("/non/existent/directory/");
assert!(!non_existent.exists());
let should_be = Theme::default();
let got = Theme::new(&non_existent);
assert_eq!(got, should_be);
}
#[test]
fn theme_dir_overrides_defaults() {
// Get all the non-Rust files in the theme directory
let special_files = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("src/theme")
.read_dir()
.unwrap()
.filter_map(|f| f.ok())
.map(|f| f.path())
.filter(|p| p.is_file() && !p.ends_with(".rs"));
let temp = TempDir::new("mdbook").unwrap();
// "touch" all of the special files so we have empty copies
for special_file in special_files {
let filename = temp.path().join(special_file.file_name().unwrap());
let _ = File::create(&filename);
}
let got = Theme::new(temp.path());
let empty = Theme {
index: Vec::new(),
header: Vec::new(),
css: Vec::new(),
favicon: Vec::new(),
js: Vec::new(),
highlight_css: Vec::new(),
tomorrow_night_css: Vec::new(),
ayu_highlight_css: Vec::new(),
highlight_js: Vec::new(),
clipboard_js: Vec::new(),
store_js: Vec::new(),
jquery: Vec::new(),
};
assert_eq!(got, empty);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
window.editors = [];
(function(editors) {
if (typeof(ace) === 'undefined' || !ace) {
return;
}
$(".editable").each(function() {
let editor = ace.edit(this);
editor.setOptions({
highlightActiveLine: false,
showPrintMargin: false,
showLineNumbers: false,
showGutter: false,
maxLines: Infinity
});
editor.$blockScrolling = Infinity;
editor.getSession().setMode("ace/mode/rust");
editor.originalCode = editor.getValue();
editors.push(editor);
});
})(window.editors);

View File

@@ -0,0 +1,70 @@
use std::path::Path;
use theme::load_file_contents;
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
}
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

2
src/theme/store.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -7,3 +7,5 @@
@import 'nav-icons'
@import 'theme-popup'
@import 'themes'
@import 'print'
@import 'tooltip'

View File

@@ -3,6 +3,16 @@ html, body {
color: #333
}
body {
margin: 0;
font-size: 1rem;
}
code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
font-size: 0.875em;
}
.left {
float: left
}
@@ -15,6 +25,10 @@ html, body {
display: none;
}
.play-button.hidden {
display: none;
}
h2, h3 { margin-top: 2.5em }
h4, h5 { margin-top: 2em }
@@ -22,7 +36,14 @@ h4, h5 { margin-top: 2em }
table {
margin: 0 auto;
border-collapse: collapse;
thead td { font-weight: 700; }
td { padding: 3px 20px; }
td {
padding: 3px 20px;
border: 1px solid;
}
thead {
td { font-weight: 700; }
}
}

View File

@@ -3,7 +3,7 @@
text-align: center
text-decoration: none
position: absolute
position: fixed
top: 50px /* Height of menu-bar */
bottom: 0
margin: 0
@@ -19,5 +19,20 @@
.mobile-nav-chapters { display: none }
.nav-chapters:hover { text-decoration: none }
.previous { left: 0 }
.next { right: 15px }
.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
}
}
.next { right: $page-padding }
.sidebar-hidden .previous {
left: $page-padding
}
.sidebar-visible .previous {
left: $sidebar-width + $page-padding
}

View File

@@ -1,43 +1,30 @@
@require 'variables'
.page-wrapper {
position: absolute
overflow-y: auto
left: $sidebar-width + 15px
right: 0
top: 0
bottom: 0
padding-left: $sidebar-width
box-sizing: border-box
-webkit-overflow-scrolling: touch
min-height: 100%
// Animation: slide away
transition: left 0.5s
transition: padding-left 0.5s
@media only screen and (max-width: 1060px) {
left: 15px;
padding-right: 15px;
@media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
padding-left: 0
}
}
.sidebar-hidden .page-wrapper {
left: 15px
padding-left: 0
}
.sidebar-visible .page-wrapper {
left: $sidebar-width + 15px
padding-left: $sidebar-width
}
.page {
position: absolute
top: 0
right: 0
left: 0
bottom: 0
padding-right: 15px
overflow-y: auto
outline: 0
padding: 0 $page-padding
}
.content {

View File

@@ -1,31 +1,46 @@
@media only print {
.sidebar,
.menu-bar,
#sidebar,
#menu-bar,
.nav-chapters,
.mobile-nav-chapters {
display: none
display: none
}
.page-wrapper {
left: 0
#page-wrapper {
left: 0;
overflow-y: initial;
}
.content {
max-width: 100%
#page-wrapper.page-wrapper {
padding-left: 0px;
}
#content {
max-width: none;
margin: 0;
padding: 0;
}
.page {
overflow-y: initial;
}
code {
background-color: #666666
border-radius: 5px
background-color: #666666
border-radius: 5px
/* Force background to be printed in Chrome */
-webkit-print-color-adjust: exact
/* Force background to be printed in Chrome */
-webkit-print-color-adjust: exact
}
pre > .buttons {
z-index: 2;
}
a, a:visited, a:active, a:hover {
color: #4183c4
text-decoration: none
color: #4183c4
text-decoration: none
}
h1, h2, h3, h4, h5, h6 {

View File

@@ -1,7 +1,7 @@
@require 'variables'
.sidebar {
position: absolute
position: fixed
left: 0
top: 0
bottom: 0
@@ -15,7 +15,7 @@
// Animation: slide away
transition: left 0.5s
@media only screen and (max-width: 1060px) {
@media only screen and (max-width: $max-page-width-with-hidden-sidebar) {
left: - $sidebar-width
}
@@ -35,7 +35,7 @@
.chapter {
list-style: none outside none
padding-left: 0
line-height: 1.9em
line-height: 2.2em
li a {
padding: 5px 0
@@ -54,7 +54,7 @@
.section {
list-style: none outside none
padding-left: 20px
line-height: 2.5em
line-height: 1.9em
li {
text-overflow: ellipsis

View File

@@ -1,6 +1,8 @@
.theme-popup {
position: fixed
left: -40px
position: absolute
left: 10px
z-index: 1000;
border-radius: 4px
font-size: 0.7em
@@ -10,7 +12,15 @@
padding: 2px 10px
line-height: 25px
white-space: nowrap
cursor: pointer
&:hover:first-child,
&:hover:last-child {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
}
}
@media only screen and (max-width: 1250px) {

View File

@@ -0,0 +1,30 @@
$theme-name = 'ayu'
$bg = #0f1419
$fg = #c5c5c5
$sidebar-bg = #14191f
$sidebar-fg = #c8c9db
$sidebar-non-existant = #5c6773
$sidebar-active = #ffb454
$sidebar-spacer = #2d334f
$icons = #737480
$icons-hover = #b7b9cc
$links = #0096cf
$inline-code-color = #ffb454
$theme-popup-bg = #14191f
$theme-popup-border = #5c6773
$theme-hover = #191f26
$quote-bg = #262933
$quote-border = lighten($quote-bg, 5%)
$table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
@import 'base'

View File

@@ -38,7 +38,8 @@
.nav-chapters,
.nav-chapters:visited,
.mobile-nav-chapters,
.mobile-nav-chapters:visited {
.mobile-nav-chapters:visited,
.menu-bar a i {
color: $icons
}
@@ -56,15 +57,18 @@
background-color: $sidebar-bg
}
.content a:link, a:visited {
.content a:link, a:visited, a > .hljs {
color: $links
}
.theme-popup {
color: $fg
background: $theme-popup-bg
border: 1px solid $theme-popup-border
.theme:hover { background-color: $theme-hover }
.default { color: $icons }
}
blockquote {
@@ -76,14 +80,25 @@
border-bottom: .1em solid $quote-border;
}
/*
table {
thead td {
color: $table-header-fg;
backrgound: $table-header-bg;
td {
border-color: $table-border-color;
}
// Alternate background colors for rows
tbody tr:nth-child(2n) {
background: $table-alternate-bg;
}
thead {
background: $table-header-bg;
td { border: none; }
tr { border: 1px $table-header-bg solid; }
}
}
*/
/* Inline code */
:not(pre) > .hljs {
@@ -91,6 +106,11 @@
vertical-align: middle;
padding: 0.1em 0.3em;
border-radius: 3px;
color: $inline-code-color;
}
a:hover > .hljs {
text-decoration: underline;
}
pre {
@@ -98,6 +118,7 @@
& > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;

View File

@@ -14,6 +14,8 @@ $icons-hover = #b3c0cc
$links = #2b79a2
$inline-code-color = #c5c8c6;
$theme-popup-bg = #141617
$theme-popup-border = #43484d
$theme-hover = #1f2124
@@ -21,4 +23,8 @@ $theme-hover = #1f2124
$quote-bg = #242637
$quote-border = lighten($quote-bg, 5%)
$table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
@import 'base'

View File

@@ -2,3 +2,4 @@
@import 'coal'
@import 'navy'
@import 'rust'
@import 'ayu'

View File

@@ -14,6 +14,8 @@ $icons-hover = #333333
$links = #4183c4
$inline-code-color = #6e6b5e;
$theme-popup-bg = #fafafa
$theme-popup-border = #cccccc
$theme-hover = #e6e6e6
@@ -21,4 +23,8 @@ $theme-hover = #e6e6e6
$quote-bg = #f2f7f9
$quote-border = darken($quote-bg, 5%)
$table-border-color = darken($bg, 5%)
$table-header-bg = darken($bg, 20%)
$table-alternate-bg = darken($bg, 3%)
@import 'base'

View File

@@ -14,6 +14,8 @@ $icons-hover = #b7b9cc
$links = #2b79a2
$inline-code-color = #c5c8c6;
$theme-popup-bg = #161923
$theme-popup-border = #737480
$theme-hover = #282e40
@@ -21,4 +23,8 @@ $theme-hover = #282e40
$quote-bg = #262933
$quote-border = lighten($quote-bg, 5%)
$table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
@import 'base'

View File

@@ -14,6 +14,8 @@ $icons-hover = #262625
$links = #2b79a2
$inline-code-color = #6e6b5e;
$theme-popup-bg = #e1e1db
$theme-popup-border = #b38f6b
$theme-hover = #99908a
@@ -21,4 +23,8 @@ $theme-hover = #99908a
$quote-bg = #c1c1bb
$quote-border = darken($quote-bg, 5%)
$table-border-color = darken($bg, 5%)
$table-header-bg = #b3a497
$table-alternate-bg = darken($bg, 3%)
@import 'base'

View File

@@ -0,0 +1,18 @@
.tooltiptext {
position: absolute;
visibility: hidden;
color: #fff;
background-color: #333;
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
left: -8px; /* Half of the width of the icon */
top: -35px;
font-size: 0.8em;
text-align: center;
border-radius: 6px;
padding: 5px 8px;
margin: 5px;
z-index: 1000;
}
.tooltipped .tooltiptext {
visibility: visible;
}

View File

@@ -1 +1,3 @@
$sidebar-width = 300px
$page-padding = 15px
$max-page-width-with-hidden-sidebar = 1060px

241
src/utils/fs.rs Normal file
View File

@@ -0,0 +1,241 @@
use std::path::{Component, Path, PathBuf};
use errors::*;
use std::io::Read;
use std::fs::{self, File};
/// 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);
}
Ok(content)
}
/// Takes a path and returns a path containing just enough `../` to point to
/// the root of the given path.
///
/// This is mostly interesting for a relative path to point back to the
/// directory from where the path starts.
///
/// ```rust
/// # extern crate mdbook;
/// #
/// # use std::path::Path;
/// # use mdbook::utils::fs::path_to_root;
/// #
/// # fn main() {
/// let path = Path::new("some/relative/path");
/// assert_eq!(path_to_root(path), "../../");
/// # }
/// ```
///
/// **note:** it's not very fool-proof, if you find a situation where
/// it doesn't return the correct path.
/// Consider [submitting a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
/// or a [pull-request](https://github.com/rust-lang-nursery/mdBook/pulls) to improve it.
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
debug!("[fn]: path_to_root");
// Remove filename and add "../" for every directory
path.into()
.parent()
.expect("")
.components()
.fold(String::new(), |mut s, c| {
match c {
Component::Normal(_) => s.push_str("../"),
_ => {
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");
// Construct path
if let Some(p) = path.parent() {
debug!("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 {
let item = item.path();
if item.is_dir() {
fs::remove_dir_all(item)?;
} else {
fs::remove_file(item)?;
}
}
}
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!("[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
if metadata.is_dir() && recursive {
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)?;
} 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...")));
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...")))?;
}
}
Ok(())
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
// tests
#[cfg(test)]
mod tests {
extern crate tempdir;
use super::copy_files_except_ext;
use std::fs;
#[test]
fn copy_files_except_ext_test() {
let tmp = match tempdir::TempDir::new("") {
Ok(t) => t,
Err(_) => panic!("Could not create a temp dir"),
};
// Create a couple of files
if let Err(_) = fs::File::create(&tmp.path().join("file.txt")) {
panic!("Could not create file.txt")
}
if let Err(_) = fs::File::create(&tmp.path().join("file.md")) {
panic!("Could not create file.md")
}
if let Err(_) = fs::File::create(&tmp.path().join("file.png")) {
panic!("Could not create file.png")
}
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir")) {
panic!("Could not create sub_dir")
}
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
panic!("Could not create sub_dir/file.png")
}
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
panic!("Could not create sub_dir_exists")
}
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
panic!("Could not create sub_dir_exists/file.txt")
}
// Create output dir
if let Err(_) = fs::create_dir(&tmp.path().join("output")) {
panic!("Could not create output")
}
if let Err(_) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
panic!("Could not create output/sub_dir_exists")
}
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
Err(e) => panic!("Error while executing the function:\n{:?}", e),
Ok(_) => {}
}
// Check if the correct files where created
if !(&tmp.path().join("output/file.txt")).exists() {
panic!("output/file.txt should exist")
}
if (&tmp.path().join("output/file.md")).exists() {
panic!("output/file.md should not exist")
}
if !(&tmp.path().join("output/file.png")).exists() {
panic!("output/file.png should exist")
}
if !(&tmp.path().join("output/sub_dir/file.png")).exists() {
panic!("output/sub_dir/file.png should exist")
}
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
panic!("output/sub_dir/file.png should exist")
}
}
}

View File

@@ -1,196 +1,224 @@
extern crate pulldown_cmark;
pub mod fs;
use std::path::{Path, Component};
use std::error::Error;
use std::io;
use std::fs::{self, metadata, File};
use self::pulldown_cmark::{Parser, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
/// Takes a path and returns a path containing just enough `../` to point to the root of the given path.
///
/// This is mostly interesting for a relative path to point back to the directory from where the
/// path starts.
///
/// ```ignore
/// let mut path = Path::new("some/relative/path");
///
/// println!("{}", path_to_root(&path));
/// ```
///
/// **Outputs**
///
/// ```text
/// "../../"
/// ```
///
/// **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.
pub fn path_to_root(path: &Path) -> String {
debug!("[fn]: path_to_root");
// Remove filename and add "../" for every directory
path.to_path_buf().parent().expect("")
.components().fold(String::new(), |mut s, c| {
match c {
Component::Normal(_) => s.push_str("../"),
_ => {
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, Box<Error>> {
debug!("[fn]: create_file");
// Construct path
if let Some(p) = path.parent() {
debug!("Parent directory is: {:?}", p);
try!(fs::create_dir_all(p));
}
debug!("[*]: Create file: {:?}", path);
let f = match File::create(path) {
Ok(f) => f,
Err(e) => {
debug!("File::create: {}", e);
return Err(Box::new(io::Error::new(io::ErrorKind::Other, format!("{}", e))))
},
};
Ok(f)
}
/// Removes all the content of a directory but not the directory itself
pub fn remove_dir_content(dir: &Path) -> Result<(), Box<Error>> {
for item in try!(fs::read_dir(dir)) {
if let Ok(item) = item {
let item = item.path();
if item.is_dir() { try!(fs::remove_dir_all(item)); } else { try!(fs::remove_file(item)); }
}
}
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<(), Box<Error>> {
debug!("[fn] copy_files_except_ext");
// Check that from and to are different
if from == to { return Ok(()) }
debug!("[*] Loop");
for entry in try!(fs::read_dir(from)) {
let entry = try!(entry);
debug!("[*] {:?}", entry.path());
let metadata = try!(entry.metadata());
// If the entry is a dir and the recursive option is enabled, call itself
if metadata.is_dir() && recursive {
if entry.path() == to.to_path_buf() { continue }
debug!("[*] is dir");
// check if output dir already exists
if !to.join(entry.file_name()).exists() {
try!(fs::create_dir(&to.join(entry.file_name())));
}
try!(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...")));
output!("[*] copying file: {:?}\n to {:?}", entry.path(), &to.join(entry.path().file_name().expect("a file should have a file name...")));
try!(fs::copy(entry.path(), &to.join(entry.path().file_name().expect("a file should have a file name..."))));
}
}
}
Ok(())
}
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
OPTION_ENABLE_TABLES};
use std::borrow::Cow;
///
///
/// Wrapper around the pulldown-cmark parser and renderer to render markdown
pub fn render_markdown(text: &str) -> String {
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
let mut opts = Options::empty();
opts.insert(OPTION_ENABLE_TABLES);
opts.insert(OPTION_ENABLE_FOOTNOTES);
let p = Parser::new_ext(&text, opts);
html::push_html(&mut s, p);
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));
html::push_html(&mut s, events);
s
}
struct EventQuoteConverter {
enabled: bool,
convert_text: bool,
}
impl EventQuoteConverter {
fn new(enabled: bool) -> Self {
EventQuoteConverter {
enabled: enabled,
convert_text: true,
}
}
fn convert<'a>(&mut self, event: Event<'a>) -> Event<'a> {
if !self.enabled {
return event;
}
match event {
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
self.convert_text = false;
event
}
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,
}
}
}
fn clean_codeblock_headers(event: Event) -> Event {
match event {
Event::Start(Tag::CodeBlock(ref info)) => {
let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect();
Event::Start(Tag::CodeBlock(Cow::from(info)))
}
_ => event,
}
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
fn convert_quotes_to_curly(original_text: &str) -> String {
// We'll consider the start to be "whitespace".
let mut preceded_by_whitespace = true;
// tests
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();
converted_char
})
.collect()
}
#[cfg(test)]
mod tests {
extern crate tempdir;
mod render_markdown {
use super::super::render_markdown;
use super::copy_files_except_ext;
use std::fs;
#[test]
fn copy_files_except_ext_test() {
let tmp = match tempdir::TempDir::new("") {
Ok(t) => t,
Err(_) => panic!("Could not create a temp dir"),
};
// Create a couple of files
if let Err(_) = fs::File::create(&tmp.path().join("file.txt")) { panic!("Could not create file.txt") }
if let Err(_) = fs::File::create(&tmp.path().join("file.md")) { panic!("Could not create file.md") }
if let Err(_) = fs::File::create(&tmp.path().join("file.png")) { panic!("Could not create file.png") }
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir")) { panic!("Could not create sub_dir") }
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir/file.png")) { panic!("Could not create sub_dir/file.png") }
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir_exists")) { panic!("Could not create sub_dir_exists") }
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) { panic!("Could not create sub_dir_exists/file.txt") }
// Create output dir
if let Err(_) = fs::create_dir(&tmp.path().join("output")) { panic!("Could not create output") }
if let Err(_) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) { panic!("Could not create output/sub_dir_exists") }
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
Err(e) => panic!("Error while executing the function:\n{:?}", e),
Ok(_) => {},
#[test]
fn it_can_keep_quotes_straight() {
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
}
// Check if the correct files where created
if !(&tmp.path().join("output/file.txt")).exists() { panic!("output/file.txt should exist") }
if (&tmp.path().join("output/file.md")).exists() { panic!("output/file.md should not exist") }
if !(&tmp.path().join("output/file.png")).exists() { panic!("output/file.png should exist") }
if !(&tmp.path().join("output/sub_dir/file.png")).exists() { panic!("output/sub_dir/file.png should exist") }
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() { panic!("output/sub_dir/file.png should exist") }
#[test]
fn it_can_make_quotes_curly_except_when_they_are_in_code() {
let input = r#"
'one'
```
'two'
```
`'three'` 'four'"#;
let expected = r#"<p>one</p>
<pre><code>'two'
</code></pre>
<p><code>'three'</code> four</p>
"#;
assert_eq!(render_markdown(input, true), expected);
}
#[test]
fn whitespace_outside_of_codeblock_header_is_preserved() {
let input = r#"
some text with spaces
```rust
fn main() {
// code inside is unchanged
}
```
more text with spaces
"#;
let expected = r#"<p>some text with spaces</p>
<pre><code class="language-rust">fn main() {
// code inside is unchanged
}
</code></pre>
<p>more text with spaces</p>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}
#[test]
fn rust_code_block_properties_are_passed_as_space_delimited_class() {
let input = r#"
```rust,no_run,should_panic,property_3
```
"#;
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);
}
#[test]
fn rust_code_block_properties_with_whitespace_are_passed_as_space_delimited_class() {
let input = r#"
```rust, no_run,,,should_panic , ,property_3
```
"#;
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);
}
#[test]
fn rust_code_block_without_properties_has_proper_html_class() {
let input = r#"
```rust
```
"#;
let expected = r#"<pre><code class="language-rust"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
let input = r#"
```rust
```
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
}
}
mod convert_quotes_to_curly {
use super::super::convert_quotes_to_curly;
#[test]
fn it_converts_single_quotes() {
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”");
}
#[test]
fn it_treats_tab_as_whitespace() {
assert_eq!(convert_quotes_to_curly("\t'one'"), "\tone");
}
}
}

116
tests/dummy_book/mod.rs Normal file
View File

@@ -0,0 +1,116 @@
//! This will create an entire book in a temporary directory using some
//! dummy contents from the `tests/dummy-book/` directory.
// Not all features are used in all test crates, so...
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
extern crate mdbook;
extern crate tempdir;
extern crate walkdir;
use std::path::Path;
use std::fs::{self, File};
use std::io::{Read, Write};
use mdbook::errors::*;
use mdbook::utils::fs::file_to_string;
// The funny `self::` here is because we've got an `extern crate ...` and are
// in a submodule
use self::tempdir::TempDir;
use self::mdbook::MDBook;
use self::walkdir::WalkDir;
/// Create a dummy book in a temporary directory, using the contents of
/// `SUMMARY_MD` as a guide.
///
/// The "Nested Chapter" file contains a code block with a single
/// `assert!($TEST_STATUS)`. If you want to check MDBook's testing
/// functionality, `$TEST_STATUS` can be substitute for either `true` or
/// `false`. This is done using the `passing_test` parameter.
#[derive(Clone, Debug, PartialEq)]
pub struct DummyBook {
passing_test: bool,
}
impl DummyBook {
/// Create a new `DummyBook` with all the defaults.
pub fn new() -> DummyBook {
DummyBook { passing_test: true }
}
/// Whether the doc-test included in the "Nested Chapter" should pass or
/// fail (it passes by default).
pub fn with_passing_test(&mut self, test_passes: bool) -> &mut DummyBook {
self.passing_test = test_passes;
self
}
/// Write a book to a temporary directory using the provided settings.
pub fn build(&self) -> Result<TempDir> {
let temp = TempDir::new("dummy_book").chain_err(|| "Unable to create temp directory")?;
let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
recursive_copy(&dummy_book_root, temp.path()).chain_err(|| {
"Couldn't copy files into a \
temporary directory"
})?;
let sub_pattern = if self.passing_test { "true" } else { "false" };
let file_containing_test = temp.path().join("src/first/nested.md");
replace_pattern_in_file(&file_containing_test, "$TEST_STATUS", sub_pattern)?;
Ok(temp)
}
}
fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()> {
let contents = file_to_string(filename)?;
File::create(filename)?.write_all(contents.replace(from, to).as_bytes())?;
Ok(())
}
/// Read the contents of the provided file into memory and then iterate through
/// the list of strings asserting that the file contains all of them.
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let filename = filename.as_ref();
let content = file_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(content.contains(s),
"Searching for {:?} in {}\n\n{}",
s,
filename.display(),
content);
}
}
/// Recursively copy an entire directory tree to somewhere else (a la `cp -r`).
fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()> {
let from = from.as_ref();
let to = to.as_ref();
for entry in WalkDir::new(&from) {
let entry = entry.chain_err(|| "Unable to inspect directory entry")?;
let original_location = entry.path();
let relative = original_location.strip_prefix(&from)
.expect("`original_location` is inside the `from` \
directory");
let new_location = to.join(relative);
if original_location.is_file() {
if let Some(parent) = new_location.parent() {
fs::create_dir_all(parent).chain_err(|| "Couldn't create directory")?;
}
fs::copy(&original_location, &new_location).chain_err(|| {
"Unable to copy file contents"
})?;
}
}
Ok(())
}

View File

@@ -0,0 +1,10 @@
# Summary
[Introduction](intro.md)
- [First Chapter](./first/index.md)
- [Nested Chapter](./first/nested.md)
---
- [Second Chapter](./second.md)
[Conclusion](./conclusion.md)

View File

@@ -0,0 +1 @@
# Conclusion

View File

@@ -0,0 +1,5 @@
# First Chapter
more text.
## Some Section

View File

@@ -0,0 +1,9 @@
# Nested Chapter
This file has some testable code.
```rust
assert!($TEST_STATUS);
```
## Some Section

View File

@@ -0,0 +1,3 @@
# Introduction
Here's some interesting text...

View File

@@ -0,0 +1 @@
# Second Chapter

64
tests/init.rs Normal file
View File

@@ -0,0 +1,64 @@
extern crate mdbook;
extern crate tempdir;
use std::path::PathBuf;
use mdbook::MDBook;
use tempdir::TempDir;
/// Run `mdbook init` in an empty directory and make sure the default files
/// are created.
#[test]
fn base_mdbook_init_should_create_default_content() {
let created_files = vec!["book", "src", "src/SUMMARY.md", "src/chapter_1.md"];
let temp = TempDir::new("mdbook").unwrap();
for file in &created_files {
assert!(!temp.path().join(file).exists());
}
let mut md = MDBook::new(temp.path());
md.init().unwrap();
for file in &created_files {
let target = temp.path().join(file);
println!("{}", target.display());
assert!(target.exists(), "{} doesn't exist", file);
}
}
/// Set some custom arguments for where to place the source and destination
/// files, then call `mdbook init`.
#[test]
fn run_mdbook_init_with_custom_book_and_src_locations() {
let created_files = vec!["out", "in", "in/SUMMARY.md", "in/chapter_1.md"];
let temp = TempDir::new("mdbook").unwrap();
for file in &created_files {
assert!(!temp.path().join(file).exists(),
"{} shouldn't exist yet!",
file);
}
let mut md = MDBook::new(temp.path());
md.config.book.src = PathBuf::from("in");
md.config.build.build_dir = PathBuf::from("out");
md.init().unwrap();
for file in &created_files {
let target = temp.path().join(file);
assert!(target.exists(), "{} should have been created by `mdbook init`", file);
}
}
#[test]
fn book_toml_isnt_required() {
let temp = TempDir::new("mdbook").unwrap();
let mut md = MDBook::new(temp.path());
md.init().unwrap();
assert!(!temp.path().join("book.toml").exists());
md.read_config().unwrap().build().unwrap();
}

280
tests/rendered_output.rs Normal file
View File

@@ -0,0 +1,280 @@
extern crate mdbook;
#[macro_use]
extern crate pretty_assertions;
extern crate select;
extern crate tempdir;
extern crate walkdir;
mod dummy_book;
use dummy_book::{assert_contains_strings, DummyBook};
use std::fs::{File, remove_file};
use std::io::Write;
use std::path::Path;
use std::ffi::OsStr;
use walkdir::{DirEntry, WalkDir, WalkDirIterator};
use select::document::Document;
use select::predicate::{Class, Name, Predicate};
use tempdir::TempDir;
use mdbook::errors::*;
use mdbook::utils::fs::file_to_string;
use mdbook::MDBook;
const BOOK_ROOT: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book");
const TOC_TOP_LEVEL: &[&'static str] = &["1. First Chapter",
"2. Second Chapter",
"Conclusion",
"Introduction"];
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter"];
/// Make sure you can load the dummy book and build it without panicking.
#[test]
fn build_the_dummy_book() {
let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path());
md.build().unwrap();
}
#[test]
fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path());
assert!(!temp.path().join("book").exists());
md.build().unwrap();
assert!(temp.path().join("book").exists());
assert!(temp.path().join("book").join("index.html").exists());
}
#[test]
fn make_sure_bottom_level_files_contain_links_to_chapters() {
let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path());
md.build().unwrap();
let dest = temp.path().join("book");
let links = vec![r#"href="intro.html""#,
r#"href="./first/index.html""#,
r#"href="./first/nested.html""#,
r#"href="./second.html""#,
r#"href="./conclusion.html""#];
let files_in_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"];
for filename in files_in_bottom_dir {
assert_contains_strings(dest.join(filename), &links);
}
}
#[test]
fn check_correct_cross_links_in_nested_dir() {
let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path());
md.build().unwrap();
let first = temp.path().join("book").join("first");
let links = vec![r#"<base href="../">"#,
r#"href="intro.html""#,
r#"href="./first/index.html""#,
r#"href="./first/nested.html""#,
r#"href="./second.html""#,
r#"href="./conclusion.html""#];
let files_in_nested_dir = vec!["index.html", "nested.html"];
for filename in files_in_nested_dir {
assert_contains_strings(first.join(filename), &links);
}
assert_contains_strings(first.join("index.html"),
&[r##"href="./first/index.html#some-section" id="some-section""##]);
assert_contains_strings(first.join("nested.html"),
&[r##"href="./first/nested.html#some-section" id="some-section""##]);
}
#[test]
fn rendered_code_has_playpen_stuff() {
let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path());
md.build().unwrap();
let nested = temp.path().join("book/first/nested.html");
let playpen_class = vec![r#"class="playpen""#];
assert_contains_strings(nested, &playpen_class);
let book_js = temp.path().join("book/book.js");
assert_contains_strings(book_js, &[".playpen"]);
}
#[test]
fn chapter_content_appears_in_rendered_document() {
let content = vec![("index.html", "Here's some interesting text"),
("second.html", "Second Chapter"),
("first/nested.html", "testable code"),
("first/index.html", "more text"),
("conclusion.html", "Conclusion")];
let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path());
md.build().unwrap();
let destination = temp.path().join("book");
for (filename, text) in content {
let path = destination.join(filename);
assert_contains_strings(path, &[text]);
}
}
/// Apply a series of predicates to some root predicate, where each
/// successive predicate is the descendant of the last one. Similar to how you
/// might do `ul.foo li a` in CSS to access all anchor tags in the `foo` list.
macro_rules! descendants {
($root:expr, $($child:expr),*) => {
$root
$(
.descendant($child)
)*
};
}
/// Make sure that all `*.md` files (excluding `SUMMARY.md`) were rendered
/// and placed in the `book` directory with their extensions set to `*.html`.
#[test]
fn chapter_files_were_rendered_to_html() {
let temp = DummyBook::new().build().unwrap();
let src = Path::new(BOOK_ROOT).join("src");
let chapter_files = WalkDir::new(&src).into_iter()
.filter_entry(|entry| entry_ends_with(entry, ".md"))
.filter_map(|entry| entry.ok())
.map(|entry| entry.path().to_path_buf())
.filter(|path| {
path.file_name().and_then(OsStr::to_str)
!= Some("SUMMARY.md")
});
for chapter in chapter_files {
let rendered_location = temp.path().join(chapter.strip_prefix(&src).unwrap())
.with_extension("html");
assert!(rendered_location.exists(),
"{} doesn't exits",
rendered_location.display());
}
}
fn entry_ends_with(entry: &DirEntry, ending: &str) -> bool {
entry.file_name().to_string_lossy().ends_with(ending)
}
/// Read the main page (`book/index.html`) and expose it as a DOM which we
/// can search with the `select` crate
fn root_index_html() -> Result<Document> {
let temp = DummyBook::new().build()
.chain_err(|| "Couldn't create the dummy book")?;
MDBook::new(temp.path()).build()
.chain_err(|| "Book building failed")?;
let index_page = temp.path().join("book").join("index.html");
let html = file_to_string(&index_page).chain_err(|| "Unable to read index.html")?;
Ok(Document::from(html.as_str()))
}
#[test]
fn check_second_toc_level() {
let doc = root_index_html().unwrap();
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
should_be.sort();
let pred = descendants!(Class("chapter"), Name("li"), Name("li"), Name("a"));
let mut children_of_children: Vec<_> =
doc.find(pred).map(|elem| elem.text().trim().to_string())
.collect();
children_of_children.sort();
assert_eq!(children_of_children, should_be);
}
#[test]
fn check_first_toc_level() {
let doc = root_index_html().unwrap();
let mut should_be = Vec::from(TOC_TOP_LEVEL);
should_be.extend(TOC_SECOND_LEVEL);
should_be.sort();
let pred = descendants!(Class("chapter"), Name("li"), Name("a"));
let mut children: Vec<_> = doc.find(pred).map(|elem| elem.text().trim().to_string())
.collect();
children.sort();
assert_eq!(children, should_be);
}
#[test]
fn check_spacers() {
let doc = root_index_html().unwrap();
let should_be = 1;
let num_spacers =
doc.find(Class("chapter").descendant(Name("li").and(Class("spacer")))).count();
assert_eq!(num_spacers, should_be);
}
/// Ensure building fails if `create-missing` is false and one of the files does
/// not exist.
#[test]
fn failure_on_missing_file() {
let (md, _temp) = create_missing_setup(Some(false));
// On failure, `build()` does not return a specific error, so assume
// any error is a failure due to a missing file.
assert!(md.read_config().unwrap().build().is_err());
}
/// Ensure a missing file is created if `create-missing` is true.
#[test]
fn create_missing_file_with_config() {
let (md, temp) = create_missing_setup(Some(true));
md.read_config().unwrap().build().unwrap();
assert!(temp.path().join("src").join("intro.md").exists());
}
/// Ensure a missing file is created if `create-missing` is not set (the default
/// is true).
#[test]
fn create_missing_file_without_config() {
let (md, temp) = create_missing_setup(None);
md.read_config().unwrap().build().unwrap();
assert!(temp.path().join("src").join("intro.md").exists());
}
fn create_missing_setup(create_missing: Option<bool>) -> (MDBook, TempDir) {
let temp = DummyBook::new().build().unwrap();
let md = MDBook::new(temp.path());
let mut file = File::create(temp.path().join("book.toml")).unwrap();
match create_missing {
Some(true) => file.write_all(b"[build]\ncreate-missing = true\n").unwrap(),
Some(false) => file.write_all(b"[build]\ncreate-missing = false\n").unwrap(),
None => (),
}
file.flush().unwrap();
remove_file(temp.path().join("src").join("intro.md")).unwrap();
(md, temp)
}

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