Compare commits

..

295 Commits

Author SHA1 Message Date
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
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
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
Mathieu David
3fd1d4606c Fix tests after removing PathExt from utils 2016-01-03 14:08:17 +01:00
Mathieu David
78b6148463 Basic formatting for tables + Styling for blockquotes
Added basic formatting for tables so that they have some padding and are aligned in the center of the page.
I did not add color or borders because I am not sure how tables should look like.

A lot of people in IntermezzOS want asides, blockquotes are probably the easiest way to do that. I have thus styled blockquotes for all the color themes.
2016-01-03 13:47:59 +01:00
Mathieu David
78e1897b47 Remove code that has better equivalent in std
Path_Ext has been stabilized in the Standard Library, the temporary copy I had can go.

I found a fs::create_dir_all method that does exactly what create_path was doing, but better... create_path is thus replaced with that.
2016-01-03 13:02:04 +01:00
Mathieu David
d000fc8bac Updated pulldown-cmark to version 0.0.5
Version 0.0.5 contains table and footnotes support, both options are now enabled in mdBook.
2016-01-03 12:02:39 +01:00
Mathieu David
5170e6b675 Fix #89, bug introduced earlier where all headers are black in all color themes 2016-01-01 11:02:24 +01:00
Mathieu David
a7f329d337 Add href to heading anchors so that the url for the anchor is displayed in the url bar when clicking the header 2016-01-01 02:17:40 +01:00
Mathieu David
bb0c878e06 #29 update doc with an example of runnable rust code 2016-01-01 01:57:21 +01:00
Mathieu David
2a7463c45b #29 Add a way to escape {{#playpen ... } using a backslash in front: \{{#playpen ... }} 2016-01-01 01:40:37 +01:00
Mathieu David
db7424e947 Continue #29, playpens are now runnable 2016-01-01 00:32:12 +01:00
Mathieu David
0ac0301d72 Continue #29, Rust files can now be loaded with {{#playpen file.rs}}, they will be displayed as other code snippets included with markdown backticks except they have a playpen css class 2015-12-31 19:25:02 +01:00
Mathieu David
38b2dee17e Continue #29 Check that the rust file exists and read to string 2015-12-31 14:14:56 +01:00
Mathieu David
0cb234de5d Add tests for find_playpens 2015-12-31 12:02:25 +01:00
Mathieu David
ee4a7fb35c Start implementing #29 support for embedding playpen, implemented the function that parses the markdown to find playpen links 2015-12-30 22:40:23 +01:00
Mathieu David
9c5c8a6804 Bump version to 0.0.6, v0.0.5 has been published to Crates.io 2015-12-30 17:26:04 +01:00
Mathieu David
ae6334f358 Fix bug where we would not check if there was actually a page to navigate to when using arrow keys 2015-12-30 17:19:43 +01:00
Mathieu David
98cd2f0c27 Version bump to 0.0.5, v0.0.4 has been published to crates.io 2015-12-30 17:05:09 +01:00
Mathieu David
600eb02fee Fix bug introduced earlier, where navigation arrows would become blue when visited + make the tooltip on nvigation arrows a little clearer 2015-12-30 16:48:46 +01:00
Mathieu David
41462e8b2d Merge pull request #87 from funkill/keys_navigation
add navigation by arrows
2015-12-30 16:37:09 +01:00
Istratov D. S
43eef7637a add navigation by arrows 2015-12-30 18:30:08 +03:00
Mathieu David
dc8f6cd5e9 Add contributors to the doc + set visisted links to the same color as normal links 2015-12-30 15:59:18 +01:00
Mathieu David
5b9d8ee6ac Fix #83, spacing is reduced between two consecutive headings 2015-12-30 15:41:49 +01:00
Mathieu David
8a4d744dc1 Merge branch 'master' of https://github.com/azerupi/mdBook 2015-12-30 15:04:43 +01:00
Mathieu David
4f583dfea9 Update documentation, Closes #80 2015-12-30 15:04:24 +01:00
Mathieu David
678a0906db Merge pull request #86 from azerupi/revert-85-keys_navigation
Revert "Add navigation by keyboard using alt + left/right arrows"
2015-12-30 14:39:21 +01:00
Mathieu David
f47d420811 Revert "Add navigation by keyboard using alt + left/right arrows" 2015-12-30 14:38:00 +01:00
Mathieu David
5ffee8144b Merge pull request #85 from funkill/keys_navigation
Add navigation by keyboard using alt + left/right arrows
2015-12-30 14:32:54 +01:00
Istratov D. S
e9e8b4239e add navigation by keyboard using alt + left/right arrows 2015-12-30 16:09:59 +03:00
Mathieu David
0a0a96808d Merge branch 'master' of https://github.com/azerupi/mdBook 2015-12-30 00:51:05 +01:00
Mathieu David
2d00f40a24 Tweak css for inline code blocks in sidebar 2015-12-30 00:50:22 +01:00
Mathieu David
e40b293336 Fix #70 render inline code blocks in the sidebar 2015-12-30 00:46:55 +01:00
Mathieu David
6c94ba8a88 Merge pull request #82 from steveklabnik/docs
mdbook test documentation
2015-12-30 00:12:21 +01:00
Steve Klabnik
eed440ef5a fix mdbook test for the book 2015-12-29 17:59:08 -05:00
Steve Klabnik
e8436d2fd2 Add some documentation for mdbook test 2015-12-29 17:58:56 -05:00
Mathieu David
b40688c880 Merge branch 'master' into watch-command 2015-12-29 13:40:13 +01:00
Mathieu David
71213f40da Add expand/collapse button to show and hide the hidden code lines 2015-12-29 13:08:25 +01:00
Mathieu David
0620ef1f47 Hides rust code lines prepended with # 2015-12-29 12:26:32 +01:00
Mathieu David
d6d0979ecf The code on the lines prepended with a # are hidden, the space of the line remains because of the '\n' in <pre> tag 2015-12-28 23:52:05 +01:00
Mathieu David
159b300067 Merge branch 'master' into hide-rust-js 2015-12-28 16:40:56 +01:00
Mathieu David
0dd6a17187 Fix some small things in javascript 2015-12-28 16:39:14 +01:00
Mathieu David
f9b6e09c26 Merge pull request #79 from asolove/72-auto-anchor
Add anchors around all headers in the content.
2015-12-28 16:29:55 +01:00
asolove
4dfa15cffa Update .styl file. Ran the compile and it results in exactly what I did by hand, d'oh. 2015-12-27 21:13:31 -07:00
Mathieu David
7762475b33 Merge pull request #78 from asolove/76-newlines-in-index
Add newlines back in to generated index.html files.
2015-12-27 23:56:56 +01:00
asolove
0ab8a73ba2 Add anchors around all headers in the content.
- Just uses the header's text as its anchor name. Spaces work. Scrolling to the anchor works even when the anchor is added after the dom loads.
- Adjust theme css to only style links, not <a> tags used as anchors.
2015-12-27 15:17:59 -07:00
asolove
5b289c1303 Fix 0ffd638 with smarter way to join with linebreaks. 2015-12-27 14:24:42 -07:00
asolove
0ffd638904 Add newlines back in to generated index.html files. 2015-12-27 14:10:13 -07:00
Mathieu David
50504282fb Merge pull request #77 from asolove/scroll-sidebar-to-active-section
On page load, scroll sidebar to active section. Resolves #21.
2015-12-27 10:50:04 +01:00
asolove
1de00f9cd7 On page load, scroll sidebar to active section. 2015-12-26 20:45:50 -07:00
Mathieu David
a2b25232d3 Merge pull request #75 from mdinger/non_pre
Generalize inline code to all themes
2015-12-23 00:50:58 +01:00
mdinger
b1265862c7 Generalize inline code to all themes 2015-12-22 16:30:05 -05:00
Mathieu David
f1cd9f54c2 Fixes rust-lang/book#29 where the navigation arrow for next chapter was displayed on top of the scroll bar making it unusable 2015-12-17 17:34:24 +01:00
Mathieu David
95d82a924f Merge pull request #71 from steveklabnik/mdbook_test
Add initial support for running rustdoc to test rust code snippets, until a more generic test "system" is implemented
2015-12-15 22:00:36 +01:00
Steve Klabnik
6bcc592ed9 Implement 'mdbook test'
Fixes #69
2015-12-15 13:56:24 -05:00
Mathieu David
4ca6693a48 Update handlebars from 0.11.x to 0.12.x 2015-12-15 18:58:34 +01:00
Mathieu David
d376b0663a Bumped version to 0.0.4 (after publishing 0.0.3 to crates.io) + updated README to use cargo install for the installation 2015-12-11 22:17:05 +01:00
Mathieu David
04c7020168 Merge pull request #65 from kbknapp/cargo-install
adds version for deps to make cargo installable
2015-11-22 20:36:43 +01:00
Kevin K
274c22c702 adds version for deps to make cargo installable 2015-11-20 08:41:16 -05:00
Mathieu David
22b6448381 Merge branch 'master' into watch-command 2015-11-10 16:33:25 +01:00
Mathieu David
6dcb411f6a Bumped version that was still set to 0.0.1 + added a bigger top margin for h2 and h3 elements 2015-11-10 16:26:39 +01:00
Mathieu David
cdbb2ee5fd Watch builds are now spawned in new threads (using crossbeam) and there is a timelock, preventing multiple builds being triggered in less than a second 2015-11-09 14:31:00 +01:00
Mathieu David
aae23f46aa Include Cargo.lock in versioning 2015-10-10 13:35:32 +02:00
Mathieu David
522eef9296 first implementation of the watch sub-command. #61 Needs refining, bug in notify made me use recursion, afraid of hitting the max recursion limit... 2015-09-27 14:38:37 +02:00
68 changed files with 3624 additions and 1199 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

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
target
Cargo.lock
target
book-test
book-example/book

View File

@@ -1,20 +1,101 @@
language: rust
rust:
- stable
- beta
- nightly
matrix:
allow_failure:
- rust: nightly
sudo: false
language: generic
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
- PROJECT_NAME=mdBook
- secure: l3/qEC4krRerllLQzni8j5AjngFi6pluWvBWj//1mJLoIEYwxlQ9mYxEdd9BqccWWFn3K0bVYCVC/64+tP6sRfLkZCe2gPUtwe7ITwCDbapUxmkiRObVJCs5yMQZt6idyhHUDKAXKgNCrusfI2BM3tKGBfRK7Cnn/R/7p/U9+q7D1sgJtUKp6ypVzK6A3jLNp3dFLFI19a5KmbZMVsaa7tOhtdDJjjr7ebsc9z7HMW5/OItiWU3FSauVQQlUMaCiEgFuIG7H7OnBAYWB/gNEtLuwfLqU9UjtWk/njNNRnmJ7m3y5HbQhv5H5F5mJUOq9XFlPLwPwyTeVztSGdQm6k8Pp2pgKBUjY27afBl9BWU+msmN6k0oXfhvIebiBPe/x2udiKeFik1xqOOEU1q9dF0sZiuPxCSM1n7tgWklJ8epgaRQaMPPQw9pO/2H5/ynHCJqBlw6WcdiqWtwAyyr/GEx62u/cg5IVkqb7KLmYsWzjS8wYG4CYs1eIxCw2xPZxP0FGuUXvxTBUPipFze6Z7FqxVauXtVe2D7c1P4738HZP660rmR0GYtHtKLny1QxCCK9sxd9JmcezFCSz4YeQ1od9xc0OzGJ2ullKNGizmGfYmgL6X8faNylLIEdaiHAcY16xV3L0g3fXL1Qg360UHQyj7GIv+0nqQnf+H9xRTTU=
matrix:
include:
# Stable channel
- os: osx
env: TARGET=i686-apple-darwin CHANNEL=stable
- os: linux
env: TARGET=i686-unknown-linux-gnu CHANNEL=stable
addons:
apt:
packages: &i686_unknown_linux_gnu
- gcc-multilib
- os: osx
env: TARGET=x86_64-apple-darwin CHANNEL=stable
- os: linux
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=stable
addons:
apt:
packages:
- nodejs
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=stable
# Beta channel
- os: osx
env: TARGET=i686-apple-darwin CHANNEL=beta
- os: linux
env: TARGET=i686-unknown-linux-gnu CHANNEL=beta
addons:
apt:
packages: *i686_unknown_linux_gnu
- os: osx
env: TARGET=x86_64-apple-darwin CHANNEL=beta
- os: linux
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=beta
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=beta
# Nightly channel
- os: osx
env: TARGET=i686-apple-darwin CHANNEL=nightly
- os: linux
env: TARGET=i686-unknown-linux-gnu CHANNEL=nightly
addons:
apt:
packages: *i686_unknown_linux_gnu
- os: osx
env: TARGET=x86_64-apple-darwin CHANNEL=nightly
- os: linux
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=nightly
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
# Musl builds fail due to a bug in Rust (https://github.com/azerupi/mdBook/issues/158)
allow_failures:
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=stable
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=beta
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
install:
- npm install stylus nib
- export PATH="$PATH:$HOME/.cargo/bin"
- bash ci/install.sh
script:
- bash ci/script.sh
after_success:
- test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && bash deploy.sh
- test "$TRAVIS_PULL_REQUEST" == "false" &&
test "$TRAVIS_BRANCH" == "master" &&
test "$TARGET" == "x86_64-unknown-linux-gnu" &&
test "$CHANNEL" = "stable" &&
npm install stylus nib &&
bash deploy.sh
before_deploy:
- 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}-${TARGET}.tar.gz
# don't delete the artifacts from previous phases
skip_cleanup: true
# deploy when a new tag is pushed
on:
condition: $CHANNEL = stable
tags: true
notifications:
email:
on_success: never

View File

@@ -1,6 +1,6 @@
[package]
name = "mdbook"
version = "0.0.1"
version = "0.0.21"
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
description = "create books from markdown files (like Gitbook)"
documentation = "http://azerupi.github.io/mdBook/index.html"
@@ -11,22 +11,42 @@ readme = "README.md"
build = "build.rs"
exclude = [
"book-example/*",
"src/theme/stylus",
]
[dependencies]
clap = "*"
handlebars = "*"
rustc-serialize = "*"
pulldown-cmark = "*"
clap = "2.19.2"
handlebars = { version = "0.25.0", features = ["serde_type"] }
serde = "0.9"
serde_json = "0.9"
pulldown-cmark = "0.0.8"
log = "0.3"
env_logger = "0.4.0"
toml = { version = "0.3", features = ["serde"] }
open = "1.1"
regex = "0.2.1"
# Watch feature
notify = { version = "4.0", optional = true }
time = { version = "0.1.34", optional = true }
crossbeam = { version = "0.2.8", optional = true }
# Serve feature
iron = { version = "0.5", optional = true }
staticfile = { version = "0.4", optional = true }
ws = { version = "0.6", optional = true}
# Tests
[dev-dependencies]
tempdir = "*"
tempdir = "0.3.4"
[features]
default = ["output"]
default = ["output", "watch", "serve"]
debug = []
output = []
regenerate-css = []
watch = ["notify", "time", "crossbeam"]
serve = ["iron", "staticfile", "ws"]
[[bin]]
doc = false

140
README.md
View File

@@ -1,84 +1,126 @@
# 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/azerupi/mdBook"><img src="https://travis-ci.org/azerupi/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/crates/l/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.
**This project is still evolving.**
See [#90](https://github.com/azerupi/mdBook/issues/90)
## 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 [**Documentation**](http://azerupi.github.io/mdBook/) for mdBook has been written in Markdown and is using mdBook to generate the online book-like website you can read. The documentation uses the latest version on GitHub and showcases the available features.
## Installation
```
git clone --depth=1 https://github.com/azerupi/mdBook.git
cd mdBook
cargo build --release
```
There are multiple ways to install mdBook.
The executable `mdbook` will be in the `./target/release` folder, this should be added to the path.
1. **Binaries**
Binaries are available for download [here](https://github.com/azerupi/mdBook/releases). Make sure to put the path to the binary into your `PATH`.
If you want to regenerate the css (stylesheet), make sure that you installed `stylus` and `nib` from `npm` because it is used to compile the stylesheets
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
```
Install [node.js](https://nodejs.org/en/)
This will download and compile mdBook for you, the only thing left to do is to add the Cargo bin directory to your `PATH`.
```
npm install -g stylus nib
```
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***!
Then build with the `regenerate-css` feature:
```
cargo install --git https://github.com/azerupi/mdBook.git
```
Again, make sure to add the Cargo bin directory to your `PATH`.
```
cargo build --release --features="regenerate-css"
```
4. **For Contributions**
If you want to contribute to mdBook you will have to clone the repository on your local machine:
## Structure
```
git clone https://github.com/azerupi/mdBook.git
```
`cd` into `mdBook/` and run
There are two main parts of this project:
```
cargo build
```
- **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.
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
### Command line interface
#### init
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.
## Usage
Please, take a look at the [**Documentation**](http://azerupi.github.io/mdBook/cli/init.html) for more information.
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.
#### build
Here are the main commands you will want to run. For a more exhaustive explanation, check out the [documentation](http://azerupi.github.io/mdBook/).
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)
- `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://azerupi.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 [Documentation](http://azerupi.github.io/mdBook/lib/lib.html) and the [API docs](http://azerupi.github.io/mdBook/mdbook/index.html) 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 not very confident with Rust, **I will be glad to mentor as best as I can if you decide to tackle an issue or new feature.**
People who are not familiar with the code can look at [issues that are tagged **easy**](https://github.com/azerupi/mdBook/labels/Easy). A lot of issues are also related to web development, so people that are not comfortable with Rust can also participate! :wink:
You can pick any issue you want to work on. Usually it's a good idea to ask if someone is already working on it and if not to claim the issue.
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 is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE](LICENSE) file.

60
appveyor.yml Normal file
View File

@@ -0,0 +1,60 @@
environment:
global:
PROJECT_NAME: mdBook
matrix:
# Stable channel
- TARGET: i686-pc-windows-msvc
RUST_CHANNEL: stable
- TARGET: x86_64-pc-windows-msvc
RUST_CHANNEL: stable
# Beta channel
- TARGET: i686-pc-windows-msvc
RUST_CHANNEL: beta
- TARGET: x86_64-pc-windows-msvc
RUST_CHANNEL: beta
# Nightly channel
- TARGET: i686-pc-windows-msvc
RUST_CHANNEL: nightly
- TARGET: x86_64-pc-windows-msvc
RUST_CHANNEL: nightly
# Install Rust and Cargo
install:
- ps: Start-FileDownload "https://static.rust-lang.org/dist/channel-rust-stable"
- ps: $env:RUST_VERSION = Get-Content channel-rust-stable | select -first 1 | %{$_.split('-')[1]}
- if NOT "%RUST_CHANNEL%" == "stable" set RUST_VERSION=%RUST_CHANNEL%
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-${env:RUST_VERSION}-${env:TARGET}.exe"
- rust-%RUST_VERSION%-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust"
- SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin
- rustc -V
- cargo -V
build: false
# Equivalent to Travis' `script` phase
test_script:
- cargo build --verbose
- 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"
}

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

@@ -0,0 +1,3 @@
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
author = "Mathieu David"

View File

@@ -1,15 +1,22 @@
# Summary
[Introduction](misc/introduction.md)
- [mdBook](README.md)
- [Command Line Tool](cli/cli-tool.md)
- [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)
- [Configuration](format/config.md)
- [Theme](format/theme/theme.md)
- [index.hbs](format/theme/index-hbs.md)
- [Syntax highlighting](format/theme/syntax-highlighting.md)
- [MathJax Support](format/mathjax.md)
- [Rust code specific features](format/rust.md)
- [Rust Library](lib/lib.md)
-----------
[Contributors](misc/contributors.md)

View File

@@ -2,7 +2,7 @@
The build command is used to render your book:
```
```bash
mdbook build
```
@@ -17,10 +17,19 @@ convenience. Large books will therefore remain structured when rendered.
Like `init`, the `build` command can take a directory as argument to use instead of the
current working directory.
```
```bash
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

@@ -5,14 +5,28 @@ Let's focus on the command line tool capabilities first.
## Install
At the moment, the only way to install mdBook is by downloading the source code from Github and building it yourself. Fortunately
this is made very easy with Cargo.
### Pre-requisite
If you haven't already, you should begin by installing [Rust](https://www.rust-lang.org/install.html) and [Git](https://git-scm.com/downloads)
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs to be compiled with **Cargo**, because we don't yet offer ready-to-go binaries. If you haven't already installed Rust, please go ahead and [install it](https://www.rust-lang.org/downloads.html) now.
Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.
### Install Crates.io version
Installing mdBook is relatively easy if you already have Rust and Cargo installed. You just have to type this snippet in your terminal:
```bash
cargo install mdbook
```
This will fetch the source code from [Crates.io](https://crates.io/) and compile it. You will have to add Cargo's `bin` directory to your `PATH`.
Run `mdbook help` in your terminal to verify if it works. Congratulations, you have installed mdBook!
### 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.
```bash
git clone --depth=1 https://github.com/azerupi/mdBook.git
cd mdBook
cargo build --release

View File

@@ -1,17 +1,14 @@
# The init command
There is some minimal boilerplate that is the same for every new book. It's for this purpose that mdBook includes an `init` command.
The `init` command is used like this:
```
```bash
mdbook init
```
It will create a couple of files and directories in the working directory so that you can
spend more time writing your book and less setting it up.
The files set up for you are the following:
```
When using the `init` command for the first time, a couple of files will be set up for you:
```bash
book-test/
├── book
└── src
@@ -19,22 +16,23 @@ book-test/
└── SUMMARY.md
```
The `src` directory is were you write your book in markdown. It contains all the source files,
- The `src` directory is were you write your book in markdown. It contains all the source files,
configuration files, etc.
The `book` directory is where your book is rendered. All the output is ready to be uploaded
to a server to be seen by the internet.
- 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).
When a `SUMMARY.md` file already exists, the `init` command will generate the files according to the paths used in the `SUMMARY.md`
#### 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.
#### Specify a directory
When using the `init` command, you can also specify a directory, instead of using the current working directory,
by appending a path to the command:
```
```bash
mdbook init path/to/book
```

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/azerupi/mdBook/issues)*

View File

@@ -0,0 +1,19 @@
# The test command
When writing a book, you sometimes need to automate some tests. For example, [The Rust Programming Book](https://doc.rust-lang.org/stable/book/) uses a lot of code examples that could get outdated.
Therefore it is very important for them to be able to automatically test these code examples.
mdBook supports a `test` command that will run all available tests in mdBook. At the moment, only one test is available:
*"Test Rust code examples using Rustdoc"*, but I hope this will be expanded in the future to include more tests like:
- checking for broken links
- checking for unused files
- ...
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
$ mdbook test
[*]: Testing file: "/mdBook/book-example/src/README.md”
```

View File

@@ -0,0 +1,26 @@
# The watch command
The `watch` command is useful when you want your book to be rendered on every file change.
You could repeatedly issue `mdbook build` every time a file is changed. But using `mdbook watch` once will watch your files and will trigger a build automatically whenever you modify a file.
#### Specify a directory
Like `init` and `build`, `watch` can take a directory as argument to use instead of the
current working directory.
```bash
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)*

View File

@@ -1,21 +1,28 @@
# 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:
We encourage using the TOML format, but JSON is also recognized and parsed.
```json
{
"title": "Example book",
"author": "Name",
"dest": "output/my-book"
}
Here is an example of what a ***book.toml*** file might look like:
```toml
title = "Example book"
author = "Name"
description = "The example book covers examples."
dest = "output/my-book"
```
#### Supported variables
- **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
If relative paths are given, they will be relative to the book's root, i.e. the
parent directory of the source directory.
- **title:** The title of the book.
- **author:** The author of the book.
- **description:** The description, which is added as meta in the html head of each page.
- **src:** The path to the book's source files (chapters in Markdown, SUMMARY.md, etc.). Defaults to `root/src`.
- **dest:** The path to the directory where you want your book to be rendered. Defaults to `root/book`.
- **theme_path:** The path to a custom theme directory. Defaults to `root/theme`.
***note:*** *the supported configurable parameters are scarce at the moment, but more will be added in the future*

View File

@@ -0,0 +1,6 @@
fn main() {
println!("Hello World!");
#
# // You can even hide lines! :D
# println!("I am hidden! Expand the code snippet to see me");
}

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

@@ -0,0 +1,21 @@
# MathJax Support
mdBook supports math equations through [MathJax](https://www.mathjax.org/).
**However the normal method for indication math equations with `$$` does not work (yet?).**
To indicate an inline equation \\( \int x = \frac{x^2}{2} \\) use
```
\\( \int x = \frac{x^2}{2} \\)
```
To indicate a block equation
\\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\]
use
```bash
\\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\]
```

View File

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

View File

@@ -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.

View File

@@ -5,12 +5,51 @@ For syntax highlighting I use [Highlight.js](https://highlightjs.org) with a cus
Automatic language detection has been turned off, so you will probably want to
specify the programming language you use like this
<pre class="language-markdown"><code class="language-markdown">```rust
<pre><code class="language-markdown">```rust
fn main() {
// Some code
}
```</code></pre>
## Custom theme
Like the rest of the theme, the files used for syntax highlighting can be overridden with your own.
- ***highlight.js*** normally you shouldn't have to overwrite this file, unless you want to use a more recent version.
- ***highlight.css*** theme used by highlight.js for syntax highlighting.
If you want to use another theme for `highlight.js` download it from their website, or make it yourself,
rename it to `highlight.css` and put it in `src/theme` (or the equivalent if you changed your source folder)
Now your theme will be used instead of the default theme.
## Hiding code lines
There is a feature in mdBook that let's you hide code lines by prepending them with a `#`.
```bash
# fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
# }
```
Will render as
```rust
# fn main() {
let x = 5;
let y = 7;
println!("{}", x + y);
# }
```
**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.
@@ -19,15 +58,3 @@ Feel free to [submit a new issue](https://github.com/azerupi/mdBook/issues) expl
You could also create a pull-request with the proposed improvements.
Overall the theme should be light and sober, without to many flashy colors.
## Custom theme
Like the rest of the theme, the files used for syntax highlighting can be overwritten with your own.
- ***highlight.js*** normally you shouldn't have to overwrite this file. But if you need to, you can.
- ***highlight.css*** theme used by highlight.js for syntax highlighting.
If you want to use another theme for `highlight.js` download it from their website, or make it yourself,
rename it to `highlight.css` and put it in `src/theme` (or the equivalent if you changed your source folder)
Now your theme will be used instead of the default theme.

View File

@@ -1,18 +1,22 @@
# Theme
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to render your markdown files in and comes with a default theme
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to render your markdown files and comes with a default theme
included in the mdBook binary.
But the theme is totally customizable, you can 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 overwrite
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
and now that file will be used instead of the default file.
Here are the files you can overwrite:
Here are the files you can override:
- ***index.hbs*** is the handlebars template.
- ***book.css*** is the style used in the output. If you want to change the design of your book, this is probably the file you want to modify. Sometimes in conjunction with `index.hbs` when you want to radically change the layout.
- ***book.js*** is mostly used to add client side functionality, like hiding / 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
***Note:*** *When you overwrite a file, it is possible that you break some functionality. Therefore I recommend to use the file from the default theme as template and only add / modify what you need. You can copy the default theme into your source directory automatically by using `mdbook init --theme`.*
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.
**Note:** When you override a file, it is possible that you break some functionality. Therefore I recommend to use the file from the default theme as template and only add / modify what you need. You can copy the default theme into your source directory automatically by using `mdbook init --theme` just remove the files you don't want to override.

View File

@@ -3,7 +3,7 @@
mdBook is not only a command line tool, it can be used as a crate. You can extend it,
integrate it in current projects. Here is a short example:
```rust
```rust,ignore
extern crate mdbook;
use mdbook::MDBook;
@@ -13,7 +13,7 @@ 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
.read_config(); // Parse book.toml or book.json file for configuration
book.build().unwrap(); // Render the book
}

View File

@@ -1 +1,13 @@
# Contributors
Here is a list of the contributors who have helped improving mdBook. Big shout-out to them!
If you have contributed to mdBook and I forgot to add you, don't hesitate to add yourself to the list. If you are in the list, feel free to add your real name & contact information if you wish.
- [mdinger](https://github.com/mdinger)
- Kevin ([kbknapp](https://github.com/kbknapp))
- Steve Klabnik ([steveklabnik](https://github.com/steveklabnik))
- Adam Solove ([asolove](https://github.com/asolove))
- Wayne Nilsen ([waynenilsen](https://github.com/waynenilsen))
- [funnkill](https://github.com/funkill)
- Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang))

View File

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

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}-${TARGET}.tar.gz *
popd $td
rm -r $td
}
main() {
mk_artifacts
mk_tarball
}
main

58
ci/install.sh Normal file
View File

@@ -0,0 +1,58 @@
# `install` phase: install stuff needed for the `script` phase
set -ex
case "$TRAVIS_OS_NAME" in
linux)
host=x86_64-unknown-linux-gnu
;;
osx)
host=x86_64-apple-darwin
;;
esac
mktempd() {
echo $(mktemp -d 2>/dev/null || mktemp -d -t tmp)
}
install_rustup() {
local td=$(mktempd)
pushd $td
curl -O https://static.rust-lang.org/rustup/dist/$host/rustup-setup
chmod +x rustup-setup
./rustup-setup -y
popd
rm -r $td
rustup default $CHANNEL
rustc -V
cargo -V
}
install_standard_crates() {
if [ "$host" != "$TARGET" ]; then
if [ ! "$CHANNEL" = "stable" ]; then
rustup target add $TARGET
else
local version=$(rustc -V | cut -d' ' -f2)
local tarball=rust-std-${version}-${TARGET}
local td=$(mktempd)
curl -s https://static.rust-lang.org/dist/${tarball}.tar.gz | \
tar --strip-components 1 -C $td -xz
$td/install.sh --prefix=$(rustc --print sysroot)
rm -r $td
fi
fi
}
main() {
install_rustup
install_standard_crates
}
main

45
ci/script.sh Normal file
View File

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

View File

@@ -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

15
rustfmt.toml Normal file
View File

@@ -0,0 +1,15 @@
write_mode = "Overwrite"
max_width = 120
ideal_width = 120
fn_call_width = 100
fn_args_density = "Compressed"
enum_trailing_comma = true
match_block_trailing_comma = true
struct_trailing_comma = "Always"
wrap_comments = true
report_todo = "Always"
report_fixme = "Always"

View File

@@ -1,63 +1,120 @@
extern crate mdbook;
#[macro_use]
extern crate clap;
extern crate log;
extern crate env_logger;
extern crate open;
// Dependencies for the Watch feature
#[cfg(feature = "watch")]
extern crate notify;
#[cfg(feature = "watch")]
extern crate time;
#[cfg(feature = "watch")]
extern crate crossbeam;
// Dependencies for the Serve feature
#[cfg(feature = "serve")]
extern crate iron;
#[cfg(feature = "serve")]
extern crate staticfile;
#[cfg(feature = "serve")]
extern crate ws;
use std::env;
use std::error::Error;
use std::ffi::OsStr;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use clap::{App, ArgMatches, SubCommand};
use clap::{App, ArgMatches, SubCommand, AppSettings};
// Uses for the Watch feature
#[cfg(feature = "watch")]
use notify::Watcher;
#[cfg(feature = "watch")]
use std::time::Duration;
#[cfg(feature = "watch")]
use std::sync::mpsc::channel;
use mdbook::MDBook;
const NAME: &'static str = "mdbook";
fn main() {
env_logger::init().unwrap();
// 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`")
.setting(AppSettings::SubcommandRequired)
.after_help("For more information about a specific command, try `mdbook <command> --help`\nSource code for mdbook available at: https://github.com/azerupi/mdBook")
.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("[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'"))
.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)'"))
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
.arg_from_usage("--no-create 'Will not create non-existent files linked from SUMMARY.md'")
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'"))
.subcommand(SubCommand::with_name("watch")
.about("Watch the files for changes"))
.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)'"))
.subcommand(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'"))
.subcommand(SubCommand::with_name("test")
.about("Test that code samples compile"))
.get_matches();
// Check which subcomamnd the user ran...
let res = match matches.subcommand() {
("init", Some(sub_matches)) => init(sub_matches),
("init", Some(sub_matches)) => init(sub_matches),
("build", Some(sub_matches)) => build(sub_matches),
("watch", _) => unimplemented!(),
(_, _) => unreachable!()
#[cfg(feature = "watch")]
("watch", Some(sub_matches)) => watch(sub_matches),
#[cfg(feature = "serve")]
("serve", Some(sub_matches)) => serve(sub_matches),
("test", Some(sub_matches)) => test(sub_matches),
(_, _) => unreachable!(),
};
if let Err(e) = res {
writeln!(&mut io::stderr(), "An error occured:\n{}", e).ok();
::std::process::exit(101);
}
}
// 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
_ => false,
}
}
// Init command implementation
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
let book_dir = get_book_dir(args);
@@ -69,7 +126,7 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
// If flag `--theme` is present, copy theme to src
if args.is_present("theme") {
// Skip this id `--force` is present
// Skip this if `--force` is present
if !args.is_present("force") {
// Print warning
print!("\nCopying the default theme to {:?}", book.get_src());
@@ -90,30 +147,234 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
}
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
let is_dest_inside_root = book.get_dest().starts_with(book.get_root());
if !args.is_present("force") && is_dest_inside_root {
println!("\nDo you want a .gitignore to be created? (y/n)");
if confirm() {
book.create_gitignore();
println!("\n.gitignore created.");
}
}
println!("\nAll done, no errors...");
Ok(())
}
// 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();
let book = MDBook::new(&book_dir).read_config();
let mut book = match args.value_of("dest-dir") {
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
None => book
};
if args.is_present("no-create") {
book.create_missing = false;
}
try!(book.build());
if args.is_present("open") {
open(book.get_dest().join("index.html"));
}
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();
let mut book = match args.value_of("dest-dir") {
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
None => book
};
if args.is_present("open") {
try!(book.build());
open(book.get_dest().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(())
}
// Watch command implementation
#[cfg(feature = "serve")]
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
const RELOAD_COMMAND: &'static str = "reload";
let book_dir = get_book_dir(args);
let book = MDBook::new(&book_dir).read_config();
let mut book = match args.value_of("dest-dir") {
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
None => book
};
let port = args.value_of("port").unwrap_or("3000");
let ws_port = args.value_of("ws-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.set_livereload(format!(r#"
<script type="text/javascript">
var socket = new WebSocket("ws://{}:{}");
socket.onmessage = function (event) {{
if (event.data === "{}") {{
socket.close();
location.reload(true); // force reload from server (not from cache)
}}
}};
window.onbeforeunload = function() {{
socket.close();
}}
</script>
"#, public_address, ws_port, RELOAD_COMMAND).to_owned());
try!(book.build());
let staticfile = staticfile::Static::new(book.get_dest());
let iron = iron::Iron::new(staticfile);
let _iron = iron.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();
});
println!("\nServing on {}", address);
if open_browser {
open(format!("http://{}", address));
}
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(())
}
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);
}
}
// Calls the closure when a book source file is changed. This is blocking!
#[cfg(feature = "watch")]
fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
where F: Fn(&Path, &mut MDBook) -> ()
{
use notify::RecursiveMode::*;
use 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_src(), Recursive) {
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
::std::process::exit(0);
};
// Add the book.{json,toml} file to the watcher if it exists, because it's not
// located in the source directory
if watcher.watch(book.get_root().join("book.json"), NonRecursive).is_err() {
// do nothing if book.json is not found
}
if watcher.watch(book.get_root().join("book.toml"), NonRecursive).is_err() {
// do nothing if book.toml is not found
}
println!("\nListening for changes...\n");
loop {
match rx.recv() {
Ok(event) => match event {
NoticeWrite(path) |
NoticeRemove(path) |
Create(path) |
Write(path) |
Remove(path) |
Rename(_, path) => {
closure(&path, book);
}
_ => {}
},
Err(e) => {
println!("An error occured: {:?}", e);
},
}
}
}

View File

@@ -1,30 +1,41 @@
extern crate rustc_serialize;
use self::rustc_serialize::json::Json;
extern crate toml;
use std::process::exit;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::collections::BTreeMap;
use std::str::FromStr;
use serde_json;
#[derive(Debug, Clone)]
pub struct BookConfig {
root: PathBuf,
pub dest: PathBuf,
pub src: PathBuf,
pub theme_path: PathBuf,
pub title: String,
pub author: String,
root: PathBuf,
dest: PathBuf,
src: PathBuf,
pub description: String,
pub indent_spaces: i32,
multilingual: bool,
}
impl BookConfig {
pub fn new(root: &Path) -> Self {
BookConfig {
root: root.to_owned(),
dest: root.join("book"),
src: root.join("src"),
theme_path: root.join("theme"),
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
description: String::new(),
indent_spaces: 4, // indentation used for SUMMARY.md
multilingual: false,
}
}
@@ -33,44 +44,114 @@ impl BookConfig {
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
},
let read_file = |path: PathBuf| -> String {
let mut data = String::new();
let mut f: File = match File::open(&path) {
Ok(x) => x,
Err(_) => {
error!("[*]: Failed to open {:?}", &path);
exit(2);
}
};
if f.read_to_string(&mut data).is_err() {
error!("[*]: Failed to read {:?}", &path);
exit(2);
}
data
};
debug!("[*]: Reading config");
let mut data = String::new();
// Read book.toml or book.json if exists
// 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 }
if root.join("book.toml").exists() {
// Convert to JSON
if let Ok(config) = Json::from_str(&data) {
// Extract data
debug!("[*]: Reading config");
let data = read_file(root.join("book.toml"));
self.parse_from_toml_string(&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("\"", "") }
} else if root.join("book.json").exists() {
// Destination
if let Some(a) = config.find_path(&["dest"]) {
let dest = PathBuf::from(&a.to_string().replace("\"", ""));
debug!("[*]: Reading config");
let data = read_file(root.join("book.json"));
self.parse_from_json_string(&data);
// 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); },
}
} else {
debug!("[*]: No book.toml or book.json was found, using defaults.");
}
self
}
pub fn parse_from_toml_string(&mut self, data: &str) -> &mut Self {
let config = match toml::from_str(data) {
Ok(x) => {x},
Err(e) => {
error!("[*]: Toml parse errors in book.toml: {:?}", e);
exit(2);
}
};
self.parse_from_btreemap(&config);
self
}
/// Parses the string to JSON and converts it to BTreeMap<String, toml::Value>.
pub fn parse_from_json_string(&mut self, data: &str) -> &mut Self {
let c: serde_json::Value = match serde_json::from_str(data) {
Ok(x) => x,
Err(e) => {
error!("[*]: JSON parse errors in book.json: {:?}", e);
exit(2);
}
};
let config = json_object_to_btreemap(c.as_object().unwrap());
self.parse_from_btreemap(&config);
self
}
pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &mut Self {
// Title, author, description
if let Some(a) = config.get("title") {
self.title = a.to_string().replace("\"", "");
}
if let Some(a) = config.get("author") {
self.author = a.to_string().replace("\"", "");
}
if let Some(a) = config.get("description") {
self.description = a.to_string().replace("\"", "");
}
// Destination folder
if let Some(a) = config.get("dest") {
let mut dest = PathBuf::from(&a.to_string().replace("\"", ""));
// If path is relative make it absolute from the parent directory of src
if dest.is_relative() {
dest = self.get_root().join(&dest);
}
self.set_dest(&dest);
}
// Source folder
if let Some(a) = config.get("src") {
let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
if src.is_relative() {
src = self.get_root().join(&src);
}
self.set_src(&src);
}
// Theme path folder
if let Some(a) = config.get("theme_path") {
let mut theme_path = PathBuf::from(&a.to_string().replace("\"", ""));
if theme_path.is_relative() {
theme_path = self.get_root().join(&theme_path);
}
self.set_theme_path(&theme_path);
}
self
@@ -103,4 +184,42 @@ impl BookConfig {
self
}
pub fn get_theme_path(&self) -> &Path {
&self.theme_path
}
pub fn set_theme_path(&mut self, theme_path: &Path) -> &mut Self {
self.theme_path = theme_path.to_owned();
self
}
}
pub fn json_object_to_btreemap(json: &serde_json::Map<String, serde_json::Value>) -> BTreeMap<String, toml::Value> {
let mut config: BTreeMap<String, toml::Value> = BTreeMap::new();
for (key, value) in json.iter() {
config.insert(
String::from_str(key).unwrap(),
json_value_to_toml_value(value.to_owned())
);
}
config
}
pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value {
match json {
serde_json::Value::Null => toml::Value::String("".to_string()),
serde_json::Value::Bool(x) => toml::Value::Boolean(x),
serde_json::Value::Number(ref x) if x.is_i64() => toml::Value::Integer(x.as_i64().unwrap()),
serde_json::Value::Number(ref x) if x.is_u64() => toml::Value::Integer(x.as_i64().unwrap()),
serde_json::Value::Number(x) => toml::Value::Float(x.as_f64().unwrap()),
serde_json::Value::String(x) => toml::Value::String(x),
serde_json::Value::Array(x) => {
toml::Value::Array(x.iter().map(|v| json_value_to_toml_value(v.to_owned())).collect())
},
serde_json::Value::Object(x) => {
toml::Value::Table(json_object_to_btreemap(&x))
},
}
}

349
src/book/bookconfig_test.rs Normal file
View File

@@ -0,0 +1,349 @@
#![cfg(test)]
use std::path::Path;
use serde_json;
use book::bookconfig::*;
#[test]
fn it_parses_json_config() {
let text = r#"
{
"title": "mdBook Documentation",
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
"author": "Mathieu David"
}"#;
// TODO don't require path argument, take pwd
let mut config = BookConfig::new(Path::new("."));
config.parse_from_json_string(&text.to_string());
let mut expected = BookConfig::new(Path::new("."));
expected.title = "mdBook Documentation".to_string();
expected.author = "Mathieu David".to_string();
expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
}
#[test]
fn it_parses_toml_config() {
let text = r#"
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
author = "Mathieu David"
"#;
// TODO don't require path argument, take pwd
let mut config = BookConfig::new(Path::new("."));
config.parse_from_toml_string(&text.to_string());
let mut expected = BookConfig::new(Path::new("."));
expected.title = "mdBook Documentation".to_string();
expected.author = "Mathieu David".to_string();
expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
}
#[test]
fn it_parses_json_nested_array_to_toml() {
// Example from:
// toml-0.2.1/tests/valid/arrays-nested.json
let text = r#"
{
"nest": {
"type": "array",
"value": [
{"type": "array", "value": [
{"type": "string", "value": "a"}
]},
{"type": "array", "value": [
{"type": "string", "value": "b"}
]}
]
}
}"#;
let c: serde_json::Value = serde_json::from_str(&text).unwrap();
let result = json_object_to_btreemap(&c.as_object().unwrap());
let expected = r#"{
"nest": Table(
{
"type": String(
"array"
),
"value": Array(
[
Table(
{
"type": String(
"array"
),
"value": Array(
[
Table(
{
"type": String(
"string"
),
"value": String(
"a"
)
}
)
]
)
}
),
Table(
{
"type": String(
"array"
),
"value": Array(
[
Table(
{
"type": String(
"string"
),
"value": String(
"b"
)
}
)
]
)
}
)
]
)
}
)
}"#;
assert_eq!(format!("{:#?}", result), expected);
}
#[test]
fn it_parses_json_arrays_to_toml() {
// Example from:
// toml-0.2.1/tests/valid/arrays.json
let text = r#"
{
"ints": {
"type": "array",
"value": [
{"type": "integer", "value": "1"},
{"type": "integer", "value": "2"},
{"type": "integer", "value": "3"}
]
},
"floats": {
"type": "array",
"value": [
{"type": "float", "value": "1.1"},
{"type": "float", "value": "2.1"},
{"type": "float", "value": "3.1"}
]
},
"strings": {
"type": "array",
"value": [
{"type": "string", "value": "a"},
{"type": "string", "value": "b"},
{"type": "string", "value": "c"}
]
},
"dates": {
"type": "array",
"value": [
{"type": "datetime", "value": "1987-07-05T17:45:00Z"},
{"type": "datetime", "value": "1979-05-27T07:32:00Z"},
{"type": "datetime", "value": "2006-06-01T11:00:00Z"}
]
}
}"#;
let c: serde_json::Value = serde_json::from_str(&text).unwrap();
let result = json_object_to_btreemap(&c.as_object().unwrap());
let expected = r#"{
"dates": Table(
{
"type": String(
"array"
),
"value": Array(
[
Table(
{
"type": String(
"datetime"
),
"value": String(
"1987-07-05T17:45:00Z"
)
}
),
Table(
{
"type": String(
"datetime"
),
"value": String(
"1979-05-27T07:32:00Z"
)
}
),
Table(
{
"type": String(
"datetime"
),
"value": String(
"2006-06-01T11:00:00Z"
)
}
)
]
)
}
),
"floats": Table(
{
"type": String(
"array"
),
"value": Array(
[
Table(
{
"type": String(
"float"
),
"value": String(
"1.1"
)
}
),
Table(
{
"type": String(
"float"
),
"value": String(
"2.1"
)
}
),
Table(
{
"type": String(
"float"
),
"value": String(
"3.1"
)
}
)
]
)
}
),
"ints": Table(
{
"type": String(
"array"
),
"value": Array(
[
Table(
{
"type": String(
"integer"
),
"value": String(
"1"
)
}
),
Table(
{
"type": String(
"integer"
),
"value": String(
"2"
)
}
),
Table(
{
"type": String(
"integer"
),
"value": String(
"3"
)
}
)
]
)
}
),
"strings": Table(
{
"type": String(
"array"
),
"value": Array(
[
Table(
{
"type": String(
"string"
),
"value": String(
"a"
)
}
),
Table(
{
"type": String(
"string"
),
"value": String(
"b"
)
}
),
Table(
{
"type": String(
"string"
),
"value": String(
"c"
)
}
)
]
)
}
)
}"#;
assert_eq!(format!("{:#?}", result), expected);
}

View File

@@ -1,8 +1,6 @@
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,7 +25,6 @@ pub struct BookItems<'a> {
impl Chapter {
pub fn new(name: String, path: PathBuf) -> Self {
Chapter {
@@ -39,15 +36,12 @@ 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) -> Result<S::Ok, S::Error> where S: Serializer {
let mut struct_ = try!(serializer.serialize_struct("Chapter", 2));
try!(struct_.serialize_field("name", &self.name));
try!(struct_.serialize_field("path", &self.path));
struct_.end()
}
}
@@ -66,10 +60,10 @@ impl<'a> Iterator for BookItems<'a> {
Some((parent_items, parent_idx)) => {
self.items = parent_items;
self.current_index = parent_idx + 1;
}
},
}
} 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) => {
@@ -79,10 +73,10 @@ impl<'a> Iterator for BookItems<'a> {
},
BookItem::Spacer => {
self.current_index += 1;
}
},
}
return Some(cur)
return Some(cur);
}
}
}

View File

@@ -1,321 +0,0 @@
use std::path::{Path, PathBuf};
use std::fs::{self, File};
use std::io::Write;
use std::error::Error;
use {BookConfig, BookItem, theme, parse, utils};
use book::BookItems;
use renderer::{Renderer, HtmlHandlebars};
use utils::{PathExt, create_path};
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() {
create_path(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 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,483 @@
pub mod mdbook;
pub mod bookitem;
pub mod bookconfig;
pub mod bookconfig_test;
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::error::Error;
use std::io;
use std::io::Write;
use std::io::ErrorKind;
use std::process::Command;
use {theme, parse, utils};
use renderer::{Renderer, HtmlHandlebars};
pub struct MDBook {
root: PathBuf,
dest: PathBuf,
src: PathBuf,
theme_path: PathBuf,
pub title: String,
pub author: String,
pub description: String,
pub content: Vec<BookItem>,
renderer: Box<Renderer>,
livereload: Option<String>,
/// Should `mdbook build` create files referenced from SUMMARY.md if they
/// don't exist
pub create_missing: bool,
}
impl MDBook {
/// Create a new `MDBook` struct with root directory `root`
///
/// # Examples
///
/// ```no_run
/// # extern crate mdbook;
/// # use mdbook::MDBook;
/// # use std::path::Path;
/// # fn main() {
/// let 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.
///
/// 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(root: &Path) -> MDBook {
if !root.exists() || !root.is_dir() {
warn!("{:?} No directory with that name", root);
}
MDBook {
root: root.to_owned(),
dest: root.join("book"),
src: root.join("src"),
theme_path: root.join("theme"),
title: String::new(),
author: String::new(),
description: String::new(),
content: vec![],
renderer: Box::new(HtmlHandlebars::new()),
livereload: None,
create_missing: true,
}
}
/// 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.root.exists() {
fs::create_dir_all(&self.root).unwrap();
info!("{:?} created", &self.root);
}
{
if !self.dest.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", self.dest);
try!(fs::create_dir_all(&self.dest));
}
if !self.src.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", self.src);
try!(fs::create_dir_all(&self.src));
}
let summary = self.src.join("SUMMARY.md");
if !summary.exists() {
// Summary does not exist, create it
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", self.src.join("SUMMARY.md"));
let mut f = try!(File::create(&self.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);
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.src.join(&ch.path);
if !path.exists() {
if !self.create_missing {
return Err(format!(
"'{}' referenced from SUMMARY.md does not exist.",
path.to_string_lossy()).into());
}
debug!("[*]: {:?} does not exist, trying to create file", path);
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(())
}
pub fn create_gitignore(&self) {
let gitignore = self.get_gitignore();
if !gitignore.exists() {
// Gitignore does not exist, create it
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`. If it
// is not, `strip_prefix` will return an Error.
if !self.get_dest().starts_with(&self.root) {
return;
}
let relative = self.get_dest()
.strip_prefix(&self.root)
.expect("Destination is not relative to root.");
let relative = relative.to_str()
.expect("Path could not be yielded into a string slice.");
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<(), Box<Error>> {
debug!("[fn]: build");
try!(self.init());
// Clean output directory
try!(utils::fs::remove_dir_content(&self.dest));
try!(self.renderer.render(&self));
Ok(())
}
pub fn get_gitignore(&self) -> PathBuf {
self.root.join(".gitignore")
}
pub fn copy_theme(&self) -> Result<(), Box<Error>> {
debug!("[fn]: copy_theme");
let theme_dir = self.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));
// favicon.png
let mut favicon = try!(File::create(&theme_dir.join("favicon.png")));
try!(favicon.write_all(theme::FAVICON));
// 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(())
}
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<(), Box<Error>> {
let path = self.get_dest().join(filename);
try!(utils::fs::create_file(&path).and_then(|mut file| {
file.write_all(content)
}).map_err(|e| {
io::Error::new(io::ErrorKind::Other, format!("Could not create {}: {}", path.display(), e))
}));
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`
pub fn read_config(mut self) -> Self {
let config = BookConfig::new(&self.root)
.read_config(&self.root)
.to_owned();
self.title = config.title;
self.description = config.description;
self.author = config.author;
self.dest = config.dest;
self.src = config.src;
self.theme_path = config.theme_path;
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() {
if let BookItem::Chapter(_, ref ch) = *item {
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 get_root(&self) -> &Path {
&self.root
}
pub fn set_dest(mut self, dest: &Path) -> Self {
// Handle absolute and relative paths
if dest.is_absolute() {
self.dest = dest.to_owned();
} else {
let dest = self.root.join(dest).to_owned();
self.dest = dest;
}
self
}
pub fn get_dest(&self) -> &Path {
&self.dest
}
pub fn set_src(mut self, src: &Path) -> Self {
// Handle absolute and relative paths
if src.is_absolute() {
self.src = src.to_owned();
} else {
let src = self.root.join(src).to_owned();
self.src = src;
}
self
}
pub fn get_src(&self) -> &Path {
&self.src
}
pub fn set_title(mut self, title: &str) -> Self {
self.title = title.to_owned();
self
}
pub fn get_title(&self) -> &str {
&self.title
}
pub fn set_author(mut self, author: &str) -> Self {
self.author = author.to_owned();
self
}
pub fn get_author(&self) -> &str {
&self.author
}
pub fn set_description(mut self, description: &str) -> Self {
self.description = description.to_owned();
self
}
pub fn get_description(&self) -> &str {
&self.description
}
pub fn set_livereload(&mut self, livereload: String) -> &mut Self {
self.livereload = Some(livereload);
self
}
pub fn unset_livereload(&mut self) -> &Self {
self.livereload = None;
self
}
pub fn get_livereload(&self) -> Option<&String> {
self.livereload.as_ref()
}
pub fn set_theme_path(mut self, theme_path: &Path) -> Self {
self.theme_path = if theme_path.is_absolute() {
theme_path.to_owned()
} else {
self.root.join(theme_path).to_owned()
};
self
}
pub fn get_theme_path(&self) -> &Path {
&self.theme_path
}
// 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.src.join("SUMMARY.md")));
Ok(())
}
}

View File

@@ -63,14 +63,20 @@
//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the following function
//!
//! ```ignore
//! utils::create_path(path: &Path)
//! utils::fs::create_path(path: &Path)
//! ```
//! This function creates all the directories in a given path if they do not exist
//!
//! Make sure to take a look at it.
extern crate serde;
#[macro_use]
pub mod macros;
extern crate serde_json;
extern crate handlebars;
extern crate pulldown_cmark;
extern crate regex;
#[macro_use] extern crate log;
pub mod book;
mod parse;
pub mod renderer;

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

@@ -26,7 +26,9 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
// 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 {
@@ -35,20 +37,22 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
section.push(0);
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()));
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 {
@@ -59,26 +63,32 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
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;
}
},
_ => {},
}
@@ -86,12 +96,12 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
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() + ".");
BookItem::Chapter(s, ch)
}
_ => parsed_item
},
_ => parsed_item,
}
} else {
@@ -131,11 +141,7 @@ fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
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)
)
)
return Err(Error::new(ErrorKind::Other, format!("Indentation error on line:\n\n{}", line)));
}
Ok(level)
@@ -146,12 +152,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,18 +167,22 @@ 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 +195,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))
}

View File

@@ -1,23 +1,24 @@
extern crate handlebars;
extern crate rustc_serialize;
extern crate pulldown_cmark;
use renderer::html_handlebars::helpers;
use renderer::Renderer;
use book::MDBook;
use book::bookitem::BookItem;
use {utils, theme};
use regex::{Regex, Captures};
use std::ascii::AsciiExt;
use std::path::{Path, PathBuf};
use std::fs::File;
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 self::pulldown_cmark::{Parser, html};
use handlebars::Handlebars;
use serde_json;
#[derive(Default)]
pub struct HtmlHandlebars;
impl HtmlHandlebars {
@@ -32,7 +33,7 @@ impl Renderer for HtmlHandlebars {
let mut handlebars = Handlebars::new();
// Load theme
let theme = theme::Theme::new(book.get_src());
let theme = theme::Theme::new(book.get_theme_path());
// Register template
debug!("[*]: Register handlebars template");
@@ -51,8 +52,9 @@ impl Renderer for HtmlHandlebars {
// Check if dest directory exists
debug!("[*]: Check if destination directory exists");
if let Err(_) = utils::create_path(book.get_dest()) {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Unexpected error when constructing destination path")))
if fs::create_dir_all(book.get_dest()).is_err() {
return Err(Box::new(io::Error::new(io::ErrorKind::Other,
"Unexpected error when constructing destination path")));
}
// Render a file for every entry in the book
@@ -60,7 +62,8 @@ impl Renderer for HtmlHandlebars {
for item in book.iter() {
match *item {
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
BookItem::Chapter(_, ref ch) |
BookItem::Affix(ref ch) => {
if ch.path != PathBuf::new() {
let path = book.get_src().join(&ch.path);
@@ -72,138 +75,123 @@ impl Renderer for HtmlHandlebars {
debug!("[*]: Reading file");
try!(f.read_to_string(&mut content));
// Render markdown using the pulldown-cmark crate
content = render_html(&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"))),
// 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("content");
data.insert("content".to_owned(), content.to_json());
// 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"))?;
data.insert("path".to_owned(), json!(path));
data.insert("content".to_owned(), json!(content));
data.insert("chapter_title".to_owned(), json!(ch.name));
data.insert("path_to_root".to_owned(), json!(utils::fs::path_to_root(&ch.path)));
// 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
// Render 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"));
let filename = Path::new(&ch.path).with_extension("html");
try!(file.write_all(&rendered.into_bytes()));
// Do several kinds of post-processing
let rendered = build_header_links(rendered, filename.to_str().unwrap_or(""));
let rendered = fix_anchor_links(rendered, filename.to_str().unwrap_or(""));
let rendered = fix_code_blocks(rendered);
let rendered = add_playpen_pre(rendered);
// Write to file
info!("[*] Creating {:?} ✓", filename.display());
try!(book.write_file(filename, &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);
.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();
content = content.lines()
.filter(|line| !line.contains("<base href="))
.collect::<Vec<&str>>()
.join("\n");
try!(index_file.write_all(content.as_bytes()));
try!(book.write_file("index.html", content.as_bytes()));
output!(
"[*] Creating index.html from {:?} ✓",
book.get_dest().join(&ch.path.with_extension("html"))
);
info!("[*] 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());
// Update the context with data for this file
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"))));
// 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
// Render 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");
// do several kinds of post-processing
let rendered = build_header_links(rendered, "print.html");
let rendered = fix_anchor_links(rendered, "print.html");
let rendered = fix_code_blocks(rendered);
let rendered = add_playpen_pre(rendered);
try!(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");
// JavaScript
let mut js_file = try!(File::create(book.get_dest().join("book.js")));
try!(js_file.write_all(&theme.js));
// Css
let mut css_file = try!(File::create(book.get_dest().join("book.css")));
try!(css_file.write_all(&theme.css));
// JQuery local fallback
let mut jquery = try!(File::create(book.get_dest().join("jquery.js")));
try!(jquery.write_all(&theme.jquery));
// Font Awesome local fallback
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/css/font-awesome").with_extension("css")));
try!(font_awesome.write_all(theme::FONT_AWESOME));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.eot")));
try!(font_awesome.write_all(theme::FONT_AWESOME_EOT));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.svg")));
try!(font_awesome.write_all(theme::FONT_AWESOME_SVG));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.ttf")));
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.woff")));
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.woff2")));
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF2));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/FontAwesome.ttf")));
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
// syntax highlighting
let mut highlight_css = try!(File::create(book.get_dest().join("highlight.css")));
try!(highlight_css.write_all(&theme.highlight_css));
let mut tomorrow_night_css = try!(File::create(book.get_dest().join("tomorrow-night.css")));
try!(tomorrow_night_css.write_all(&theme.tomorrow_night_css));
let mut highlight_js = try!(File::create(book.get_dest().join("highlight.js")));
try!(highlight_js.write_all(&theme.highlight_js));
try!(book.write_file("book.js", &theme.js));
try!(book.write_file("book.css", &theme.css));
try!(book.write_file("favicon.png", &theme.favicon));
try!(book.write_file("jquery.js", &theme.jquery));
try!(book.write_file("highlight.css", &theme.highlight_css));
try!(book.write_file("tomorrow-night.css", &theme.tomorrow_night_css));
try!(book.write_file("highlight.js", &theme.highlight_js));
try!(book.write_file("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME));
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot", theme::FONT_AWESOME_EOT));
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg", theme::FONT_AWESOME_SVG));
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf", theme::FONT_AWESOME_TTF));
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff", theme::FONT_AWESOME_WOFF));
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2", theme::FONT_AWESOME_WOFF2));
try!(book.write_file("_FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF));
// Copy all remaining files
try!(utils::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"]));
try!(utils::fs::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"]));
Ok(())
}
}
fn make_data(book: &MDBook) -> Result<BTreeMap<String,Json>, Box<Error>> {
fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>, 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());
let mut data = serde_json::Map::new();
data.insert("language".to_owned(), json!("en"));
data.insert("title".to_owned(), json!(book.get_title()));
data.insert("description".to_owned(), json!(book.get_description()));
data.insert("favicon".to_owned(), json!("favicon.png"));
if let Some(livereload) = book.get_livereload() {
data.insert("livereload".to_owned(), json!(livereload));
}
let mut chapters = vec![];
@@ -213,38 +201,154 @@ 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"))),
}
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(), 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"))),
}
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(), "_spacer_".to_json());
}
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)
}
fn render_html(text: &str) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
let p = Parser::new(&text);
html::push_html(&mut s, p);
s
fn build_header_links(html: String, filename: &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];
let text = &caps[2];
let mut id = text.to_string();
let repl_sub = vec!["<em>", "</em>", "<code>", "</code>",
"<strong>", "</strong>",
"&lt;", "&gt;", "&amp;", "&#39;", "&quot;"];
for sub in repl_sub {
id = id.replace(sub, "");
}
let id = id.chars().filter_map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
if c.is_ascii() {
Some(c.to_ascii_lowercase())
} else {
Some(c)
}
} else if c.is_whitespace() && c.is_ascii() {
Some('-')
} else {
None
}
}).collect::<String>();
let id_count = *id_counter.get(&id).unwrap_or(&0);
id_counter.insert(id.clone(), id_count + 1);
let id = if id_count > 0 {
format!("{}-{}", id, id_count)
} else {
id
};
format!("<a class=\"header\" href=\"{filename}#{id}\" id=\"{id}\"><h{level}>{text}</h{level}></a>",
level=level, id=id, text=text, filename=filename)
}).into_owned()
}
// 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: String, filename: &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=\"{filename}#{anchor}\"{after}>",
before=before, filename=filename, 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: String) -> 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!("<code{before}class=\"{classes}\"{after}>", before=before, classes=classes, after=after)
}).into_owned()
}
fn add_playpen_pre(html: String) -> 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") {
// wrap the contents in an external pre block
if text.contains("fn 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=\"{}\"># #![allow(unused_variables)]
{}#fn main() {{
{}
#}}</code></pre>", classes, attrs, code)
}
} else {
// not language-rust, so no-op
format!("{}", text)
}
}).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)
}

View File

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

View File

@@ -1,33 +1,31 @@
extern crate handlebars;
extern crate rustc_serialize;
use std::path::Path;
use std::collections::BTreeMap;
use std::collections::{VecDeque, BTreeMap};
use serde_json;
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable};
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> {
pub fn previous(_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 chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
let current = c.navigate(rc.get_path(), "path")
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path")
.to_string()
.replace("\"", "");
debug!("[*]: Decode chapters from JSON");
// Decode json format
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) {
let decoded: Vec<BTreeMap<String, String>> = match serde_json::from_str(&chapters.to_string()) {
Ok(data) => data,
Err(_) => return Err(RenderError{ desc: "Could not decode the JSON data"}),
Err(_) => return Err(RenderError::new("Could not decode the JSON data")),
};
let mut previous: Option<BTreeMap<String, String>> = None;
@@ -41,7 +39,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
if path == &current {
debug!("[*]: Found current chapter");
if let Some(previous) = previous{
if let Some(previous) = previous {
debug!("[*]: Creating BTreeMap to inject in context");
// Create new BTreeMap to extend the context: 'title' and 'link'
@@ -51,47 +49,50 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
match previous.get("name") {
Some(n) => {
debug!("[*]: Inserting title: {}", n);
previous_chapter.insert("title".to_owned(), n.to_json())
previous_chapter.insert("title".to_owned(), json!(n))
},
None => {
debug!("[*]: No title found for chapter");
return Err(RenderError{ desc: "No title found for chapter in JSON data" })
}
return Err(RenderError::new("No title found for chapter in JSON data"));
},
};
// Chapter link
match previous.get("path") {
Some(p) => {
// Hack for windows who tends to use `\` as separator instead of `/`
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" })
Some(p) => {
previous_chapter.insert("link".to_owned(), json!(p.replace("\\", "/")));
},
None => return Err(RenderError::new("Link could not be converted to str")),
}
},
None => return Err(RenderError{ desc: "No path found for chapter in JSON data" })
None => return Err(RenderError::new("No path found for chapter in JSON data")),
}
debug!("[*]: Inject in context");
// Inject in current context
let updated_context = c.extend(&previous_chapter);
let updated_context = rc.context().extend(&previous_chapter);
debug!("[*]: Render template");
// Render template
match _h.template() {
Some(t) => {
try!(t.render(&updated_context, r, rc));
*rc.context_mut() = updated_context;
try!(t.render(r, rc));
},
None => return Err(RenderError{ desc: "Error with the handlebars template" })
None => return Err(RenderError::new("Error with the handlebars template")),
}
}
break;
}
else {
} else {
previous = Some(item.clone());
}
},
@@ -107,24 +108,24 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
debug!("[fn]: next (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 chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
let current = c.navigate(rc.get_path(), "path")
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path")
.to_string()
.replace("\"", "");
debug!("[*]: Decode chapters from JSON");
// Decode json format
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) {
let decoded: Vec<BTreeMap<String, String>> = match serde_json::from_str(&chapters.to_string()) {
Ok(data) => data,
Err(_) => return Err(RenderError{ desc: "Could not decode the JSON data"}),
Err(_) => return Err(RenderError::new("Could not decode the JSON data")),
};
let mut previous: Option<BTreeMap<String, String>> = None;
@@ -140,7 +141,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
let previous_path = match previous.get("path") {
Some(p) => p,
None => return Err(RenderError{ desc: "No path found for chapter in JSON data"})
None => return Err(RenderError::new("No path found for chapter in JSON data")),
};
if previous_path == &current {
@@ -153,9 +154,9 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
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"})
next_chapter.insert("title".to_owned(), json!(n));
},
None => return Err(RenderError::new("No title found for chapter in JSON data")),
}
@@ -163,25 +164,29 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
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"})
Some(l) => {
// Hack for windows who tends to use `\` as separator instead of `/`
next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/")));
},
None => return Err(RenderError::new("Link could not converted to str")),
}
debug!("[*]: Inject in context");
// Inject in current context
let updated_context = c.extend(&next_chapter);
let updated_context = rc.context().extend(&next_chapter);
debug!("[*]: Render template");
// Render template
match _h.template() {
Some(t) => {
try!(t.render(&updated_context, r, rc));
*rc.context_mut() = updated_context;
try!(t.render(r, rc));
},
None => return Err(RenderError{ desc: "Error with the handlebars template" })
None => return Err(RenderError::new("Error with the handlebars template")),
}
break
break;
}
}

View File

@@ -0,0 +1,195 @@
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() {
warn!("[-] 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 file.read_to_string(&mut file_content).is_err() {
continue;
};
let replacement = String::new() + "<pre><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().is_empty() {
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 editable = params
.get(1)
.map(|p| p.find("editable").is_some())
.unwrap_or(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,111 +1,138 @@
extern crate handlebars;
extern crate rustc_serialize;
use std::path::Path;
use std::collections::BTreeMap;
use std::collections::{VecDeque, BTreeMap};
use self::rustc_serialize::json;
use self::handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
use serde_json;
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper};
use pulldown_cmark::{Parser, html, Event, 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 = 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()));
// 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.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path").to_string().replace("\"", "");
try!(rc.writer.write_all("<ul class=\"chapter\">".as_bytes()));
// Decode json format
let decoded: Vec<BTreeMap<String,String>> = json::decode(&chapters.to_string()).unwrap();
// Decode json format
let decoded: Vec<BTreeMap<String, String>> = serde_json::from_str(&chapters.to_string()).unwrap();
let mut current_level = 1;
let mut current_level = 1;
for item in decoded {
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;
// Spacer
if item.get("spacer").is_some() {
try!(rc.writer.write_all("<li class=\"spacer\"></li>".as_bytes()));
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 {
try!(rc.writer.write_all("<li>".as_bytes()));
try!(rc.writer.write_all("<ul class=\"section\">".as_bytes()));
current_level += 1;
}
try!(rc.writer.write_all("<li>".as_bytes()));
} else if level < current_level {
while level < current_level {
try!(rc.writer.write_all("</ul>".as_bytes()));
try!(rc.writer.write_all("</li>".as_bytes()));
current_level -= 1;
}
try!(rc.writer.write_all("<li>".as_bytes()));
} else {
try!(rc.writer.write_all("<li".as_bytes()));
if item.get("section").is_none() {
try!(rc.writer.write_all(" class=\"affix\"".as_bytes()));
}
try!(rc.writer.write_all(">".as_bytes()));
}
try!(rc.writer.write(">".as_bytes()));
true
// Link
let path_exists = if let Some(path) = item.get("path") {
if !path.is_empty() {
try!(rc.writer.write_all("<a href=\"".as_bytes()));
// Add link
try!(rc.writer.write_all(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("\\", "/")
.as_bytes()));
try!(rc.writer.write_all("\"".as_bytes()));
if path == &current {
try!(rc.writer.write_all(" class=\"active\"".as_bytes()));
}
try!(rc.writer.write_all(">".as_bytes()));
true
} else {
false
}
} else {
false
};
// Section does not necessarily exist
if let Some(section) = item.get("section") {
try!(rc.writer.write_all("<strong>".as_bytes()));
try!(rc.writer.write_all(section.as_bytes()));
try!(rc.writer.write_all("</strong> ".as_bytes()));
}
}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
try!(rc.writer.write_all(markdown_parsed_name.as_bytes()));
}
if path_exists {
try!(rc.writer.write_all("</a>".as_bytes()));
}
try!(rc.writer.write_all("</li>".as_bytes()));
}
while current_level > 1 {
try!(rc.writer.write_all("</ul>".as_bytes()));
try!(rc.writer.write_all("</li>".as_bytes()));
current_level -= 1;
}
if let Some(name) = item.get("name") {
try!(rc.writer.write(name.as_bytes()));
}
if path_exists {
try!(rc.writer.write("</a>".as_bytes()));
}
try!(rc.writer.write("</li>".as_bytes()));
current_level = level;
try!(rc.writer.write_all("</ul>".as_bytes()));
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 std::error::Error;
pub trait Renderer {
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
}

View File

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

View File

@@ -3,12 +3,43 @@ body {
font-family: "Open Sans", sans-serif;
color: #333;
}
code {
font-family: "Source Code Pro", "Menlo", "DejaVu Sans Mono", monospace;
font-size: 0.875em;
}
.left {
float: left;
}
.right {
float: right;
}
.hidden {
display: none;
}
h2,
h3 {
margin-top: 2.5em;
}
h4,
h5 {
margin-top: 2em;
}
.header + .header h3,
.header + .header h4,
.header + .header h5 {
margin-top: 1em;
}
table {
margin: 0 auto;
border-collapse: collapse;
}
table td {
padding: 3px 20px;
border: 1px solid;
}
table thead td {
font-weight: 700;
}
.sidebar {
position: absolute;
left: 0;
@@ -33,6 +64,9 @@ body {
left: -300px;
}
}
.sidebar code {
line-height: 2em;
}
.sidebar-hidden .sidebar {
left: -300px;
}
@@ -42,7 +76,7 @@ body {
.chapter {
list-style: none outside none;
padding-left: 0;
line-height: 1.9em;
line-height: 2.2em;
}
.chapter li a {
padding: 5px 0;
@@ -59,7 +93,7 @@ body {
.section {
list-style: none outside none;
padding-left: 20px;
line-height: 2.5em;
line-height: 1.9em;
}
.section li {
-o-text-overflow: ellipsis;
@@ -218,11 +252,12 @@ body {
left: 0;
}
.next {
right: 0;
right: 15px;
}
.theme-popup {
position: fixed;
left: -40px;
position: relative;
left: 10px;
z-index: 1000;
-webkit-border-radius: 4px;
border-radius: 4px;
font-size: 0.7em;
@@ -233,6 +268,11 @@ body {
line-height: 25px;
white-space: nowrap;
}
.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 {
display: none;
@@ -268,6 +308,16 @@ body {
.light {
color: #333;
background-color: #fff;
/* Inline code */
}
.light .content .header:link,
.light .content .header:visited {
color: #333;
pointer: cursor;
}
.light .content .header:link:hover,
.light .content .header:visited:hover {
text-decoration: none;
}
.light .sidebar {
background-color: #fafafa;
@@ -288,8 +338,11 @@ body {
background-color: #f4f4f4;
}
.light .menu-bar,
.light .menu-bar:visited,
.light .nav-chapters,
.light .mobile-nav-chapters {
.light .nav-chapters:visited,
.light .mobile-nav-chapters,
.light .mobile-nav-chapters:visited {
color: #ccc;
}
.light .menu-bar i:hover,
@@ -303,19 +356,83 @@ body {
.light .mobile-nav-chapters {
background-color: #fafafa;
}
.light .content a {
.light .content a:link,
.light a:visited {
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;
color: #333;
background-color: #f2f7f9;
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;
}
.light pre {
position: relative;
}
.light pre > .buttons {
position: absolute;
right: 5px;
top: 5px;
color: #364149;
cursor: pointer;
}
.light pre > .buttons :hover {
color: #008cff;
}
.light pre > .buttons i {
margin-left: 8px;
}
.light pre > .result {
margin-top: 10px;
}
.coal {
color: #98a3ad;
background-color: #141617;
/* Inline code */
}
.coal .content .header:link,
.coal .content .header:visited {
color: #98a3ad;
pointer: cursor;
}
.coal .content .header:link:hover,
.coal .content .header:visited:hover {
text-decoration: none;
}
.coal .sidebar {
background-color: #292c2f;
@@ -336,8 +453,11 @@ body {
background-color: #393939;
}
.coal .menu-bar,
.coal .menu-bar:visited,
.coal .nav-chapters,
.coal .mobile-nav-chapters {
.coal .nav-chapters:visited,
.coal .mobile-nav-chapters,
.coal .mobile-nav-chapters:visited {
color: #43484d;
}
.coal .menu-bar i:hover,
@@ -351,19 +471,83 @@ body {
.coal .mobile-nav-chapters {
background-color: #292c2f;
}
.coal .content a {
.coal .content a:link,
.coal a:visited {
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;
color: #98a3ad;
background-color: #242637;
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;
}
.coal pre {
position: relative;
}
.coal pre > .buttons {
position: absolute;
right: 5px;
top: 5px;
color: #a1adb8;
cursor: pointer;
}
.coal pre > .buttons :hover {
color: #3473ad;
}
.coal pre > .buttons i {
margin-left: 8px;
}
.coal pre > .result {
margin-top: 10px;
}
.navy {
color: #bcbdd0;
background-color: #161923;
/* Inline code */
}
.navy .content .header:link,
.navy .content .header:visited {
color: #bcbdd0;
pointer: cursor;
}
.navy .content .header:link:hover,
.navy .content .header:visited:hover {
text-decoration: none;
}
.navy .sidebar {
background-color: #282d3f;
@@ -384,8 +568,11 @@ body {
background-color: #2d334f;
}
.navy .menu-bar,
.navy .menu-bar:visited,
.navy .nav-chapters,
.navy .mobile-nav-chapters {
.navy .nav-chapters:visited,
.navy .mobile-nav-chapters,
.navy .mobile-nav-chapters:visited {
color: #737480;
}
.navy .menu-bar i:hover,
@@ -399,19 +586,83 @@ body {
.navy .mobile-nav-chapters {
background-color: #282d3f;
}
.navy .content a {
.navy .content a:link,
.navy a:visited {
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;
color: #bcbdd0;
background-color: #262933;
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;
}
.navy pre {
position: relative;
}
.navy pre > .buttons {
position: absolute;
right: 5px;
top: 5px;
color: #c8c9db;
cursor: pointer;
}
.navy pre > .buttons :hover {
color: #2b79a2;
}
.navy pre > .buttons i {
margin-left: 8px;
}
.navy pre > .result {
margin-top: 10px;
}
.rust {
color: #262625;
background-color: #e1e1db;
/* Inline code */
}
.rust .content .header:link,
.rust .content .header:visited {
color: #262625;
pointer: cursor;
}
.rust .content .header:link:hover,
.rust .content .header:visited:hover {
text-decoration: none;
}
.rust .sidebar {
background-color: #3b2e2a;
@@ -432,8 +683,11 @@ body {
background-color: #45373a;
}
.rust .menu-bar,
.rust .menu-bar:visited,
.rust .nav-chapters,
.rust .mobile-nav-chapters {
.rust .nav-chapters:visited,
.rust .mobile-nav-chapters,
.rust .mobile-nav-chapters:visited {
color: #737480;
}
.rust .menu-bar i:hover,
@@ -447,13 +701,120 @@ body {
.rust .mobile-nav-chapters {
background-color: #3b2e2a;
}
.rust .content a {
.rust .content a:link,
.rust a:visited {
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;
color: #262625;
background-color: #c1c1bb;
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;
}
.rust pre {
position: relative;
}
.rust pre > .buttons {
position: absolute;
right: 5px;
top: 5px;
color: #c8c9db;
cursor: pointer;
}
.rust pre > .buttons :hover {
color: #e69f67;
}
.rust pre > .buttons i {
margin-left: 8px;
}
.rust 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;
}
#content {
max-width: none;
margin: 0;
padding: 0;
}
.page {
overflow-y: initial;
}
code {
background-color: #666;
-webkit-border-radius: 5px;
border-radius: 5px;
/* Force background to be printed in Chrome */
-webkit-print-color-adjust: exact;
}
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+ */;
}
}

View File

@@ -8,7 +8,7 @@ $( document ).ready(function() {
// Set theme
var theme = localStorage.getItem('theme');
if (theme == null) { theme = 'light'; }
if (theme === null) { theme = 'light'; }
set_theme(theme);
@@ -22,13 +22,39 @@ $( document ).ready(function() {
$('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,
NEXT_KEY: 39
};
$(document).on('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
switch (e.keyCode) {
case KEY_CODES.NEXT_KEY:
e.preventDefault();
if($('.nav-chapters.next').length) {
window.location.href = $('.nav-chapters.next').attr('href');
}
break;
case KEY_CODES.PREVIOUS_KEY:
e.preventDefault();
if($('.nav-chapters.previous').length) {
window.location.href = $('.nav-chapters.previous').attr('href');
}
break;
}
});
// Interesting DOM Elements
var html = $("html");
var sidebar = $("#sidebar");
var page_wrapper = $("#page-wrapper");
var content = $("#content");
// Toggle sidebar
$("#sidebar-toggle").click(function(event){
@@ -50,6 +76,13 @@ $( document ).ready(function() {
});
// Scroll sidebar to current active section
var activeSection = sidebar.find(".active");
if(activeSection.length) {
sidebar.scrollTop(activeSection.offset().top);
}
// Print button
$("#print-button").click(function(){
var printWindow = window.open("print.html");
@@ -66,18 +99,18 @@ $( document ).ready(function() {
$('.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>'));
$(this).append(popup);
popup.insertAfter(this);
$('.theme').click(function(){
var theme = $(this).attr('id');
set_theme(theme)
set_theme(theme);
});
}
@@ -96,4 +129,108 @@ $( document ).ready(function() {
$('body').removeClass().addClass(theme);
}
// Hide Rust code lines prepended with a specific character
var hiding_character = "#";
$("code.language-rust").each(function(i, block){
var code_block = $(this);
var pre_block = $(this).parent();
// hide lines
var lines = code_block.html().split("\n");
var first_non_hidden_line = false;
var lines_hidden = false;
for(var n = 0; n < lines.length; n++){
if($.trim(lines[n])[0] == hiding_character){
if(first_non_hidden_line){
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)#/, "$1") + "</span>";
}
else {
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)#/, "$1") + "\n" + "</span>";
}
lines_hidden = true;
}
else if(first_non_hidden_line) {
lines[n] = "\n" + lines[n];
}
else {
first_non_hidden_line = true;
}
}
code_block.html(lines.join(""));
// If no lines were hidden, return
if(!lines_hidden) { return; }
// add expand button
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\"></i></div>");
pre_block.find("i").click(function(e){
if( $(this).hasClass("fa-expand") ) {
$(this).removeClass("fa-expand").addClass("fa-compress");
pre_block.find("span.hidden").removeClass("hidden").addClass("unhidden");
}
else {
$(this).removeClass("fa-compress").addClass("fa-expand");
pre_block.find("span.unhidden").removeClass("unhidden").addClass("hidden");
}
});
});
// Process playpen code blocks
$(".playpen").each(function(block){
var pre_block = $(this);
// Add play button
var buttons = pre_block.find(".buttons");
if( buttons.length === 0 ) {
pre_block.prepend("<div class=\"buttons\"></div>");
buttons = pre_block.find(".buttons");
}
buttons.prepend("<i class=\"fa fa-play play-button\"></i>");
buttons.find(".play-button").click(function(e){
run_rust_code(pre_block);
});
});
});
function run_rust_code(code_block) {
var result_block = code_block.find(".result");
if(result_block.length === 0) {
code_block.append("<code class=\"result hljs language-bash\"></code>");
result_block = code_block.find(".result");
}
let text = code_block.find(".language-rust").text();
let params = {
version: "stable",
optimize: "0",
code: text,
};
if(text.includes("#![feature")) {
params.version = "nightly";
}
result_block.text("Running...");
$.ajax({
url: "https://play.rust-lang.org/evaluate.json",
method: "POST",
crossDomain: true,
dataType: "json",
contentType: "application/json",
data: JSON.stringify(params),
success: function(response){
result_block.text(response.result);
}
});
}

BIN
src/theme/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -10,106 +10,61 @@
-webkit-text-size-adjust: none;
}
/* Inline code */
:not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
border-radius: 3px;
}
/* Atelier-Dune Comment */
.hljs-comment {
.hljs-comment,
.hljs-quote {
color: #AAA;
}
/* Atelier-Dune Red */
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.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 {
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #d73737;
}
/* Atelier-Dune Orange */
.hljs-number,
.hljs-preprocessor,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-params,
.hljs-attribute,
.hljs-constant {
.hljs-type,
.hljs-params {
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;
.hljs-symbol,
.hljs-bullet {
color: #60ac39;
}
/* 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 {
.hljs-title,
.hljs-section {
color: #6684e1;
}
/* Atelier-Dune Purple */
.hljs-keyword,
.javascript .hljs-function {
.hljs-selector-tag {
color: #b854d4;
}
.coffeescript .javascript,
.javascript .xml,
.tex .hljs-formula,
.xml .javascript,
.xml .vbscript,
.xml .css,
.xml .hljs-cdata {
opacity: 0.5;
.hljs-emphasis {
font-style: italic;
}
/* 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

@@ -2,18 +2,21 @@
<html lang="{{ language }}">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<title>{{ chapter_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">
@@ -22,14 +25,14 @@
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></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>
</head>
<body>
<body class="light">
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
var theme = localStorage.getItem('theme');
@@ -84,13 +87,13 @@
</div>
{{#previous}}
<a href="{{link}}" class="nav-chapters previous">
<a href="{{link}}" class="nav-chapters previous" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a href="{{link}}" class="nav-chapters next">
<a href="{{link}}" class="nav-chapters next" title="You can navigate through the chapters using the arrow keys">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
@@ -105,6 +108,9 @@
}
</script>
<!-- Livereload script (if served using the cli tool) -->
{{{livereload}}}
<script src="highlight.js"></script>
<script src="book.js"></script>
</body>

View File

@@ -2,10 +2,10 @@ use std::path::Path;
use std::fs::File;
use std::io::Read;
use utils::{PathExt};
pub static INDEX: &'static [u8] = include_bytes!("index.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");
@@ -28,6 +28,7 @@ pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/
pub struct Theme {
pub index: 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>,
@@ -42,6 +43,7 @@ impl Theme {
let mut theme = Theme {
index: INDEX.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(),
@@ -51,13 +53,7 @@ impl Theme {
// Check if the given path exists
if !src.exists() || !src.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
return theme;
}
// Check for individual files if they exist
@@ -71,7 +67,7 @@ impl Theme {
// book.js
if let Ok(mut f) = File::open(&src.join("book.js")) {
theme.js.clear();
let _ = f.read_to_end(&mut theme.js);
let _ = f.read_to_end(&mut theme.js);
}
// book.css
@@ -80,6 +76,12 @@ impl Theme {
let _ = f.read_to_end(&mut theme.css);
}
// favicon.png
if let Ok(mut f) = File::open(&src.join("favicon.png")) {
theme.favicon.clear();
let _ = f.read_to_end(&mut theme.favicon);
}
// highlight.js
if let Ok(mut f) = File::open(&src.join("highlight.js")) {
theme.highlight_js.clear();

View File

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

View File

@@ -1,12 +1,40 @@
html, body {
font-family: "Open Sans", sans-serif;
color: #333;
font-family: "Open Sans", sans-serif
color: #333
}
code {
font-family: "Source Code Pro", "Menlo", "DejaVu Sans Mono", monospace;
font-size: 0.875em;
}
.left {
float: left;
float: left
}
.right {
float: right;
float: right
}
.hidden {
display: none;
}
h2, h3 { margin-top: 2.5em }
h4, h5 { margin-top: 2em }
.header + .header h3, .header + .header h4, .header + .header h5 { margin-top: 1em }
table {
margin: 0 auto;
border-collapse: collapse;
td {
padding: 3px 20px;
border: 1px solid;
}
thead {
td { font-weight: 700; }
}
}

View File

@@ -20,4 +20,4 @@
.mobile-nav-chapters { display: none }
.nav-chapters:hover { text-decoration: none }
.previous { left: 0 }
.next { right: 0 }
.next { right: 15px }

View File

@@ -1,31 +1,38 @@
@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%
#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
}
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

@@ -18,6 +18,10 @@
@media only screen and (max-width: 1060px) {
left: - $sidebar-width
}
code {
line-height: 2em;
}
}
.sidebar-hidden .sidebar {
@@ -31,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
@@ -50,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: relative
left: 10px
z-index: 1000;
border-radius: 4px
font-size: 0.7em
@@ -10,7 +12,14 @@
padding: 2px 10px
line-height: 25px
white-space: nowrap
&: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

@@ -1,7 +1,17 @@
.{unquote($theme-name)} {
color: $fg
background-color: $bg
.content .header:link, .content .header:visited {
color: $fg;
pointer: cursor;
&:hover {
text-decoration: none;
}
}
.sidebar {
background-color: $sidebar-bg
color: $sidebar-fg
@@ -13,7 +23,7 @@
a { color: $sidebar-fg }
.active,
a:hover {
a:hover, {
/* Animate color change */
color: $sidebar-active
}
@@ -24,8 +34,11 @@
}
.menu-bar,
.menu-bar:visited,
.nav-chapters,
.mobile-nav-chapters {
.nav-chapters:visited,
.mobile-nav-chapters,
.mobile-nav-chapters:visited {
color: $icons
}
@@ -43,14 +56,73 @@
background-color: $sidebar-bg
}
.content a {
.content a:link, a:visited {
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 {
margin: 20px 0;
padding: 0 20px;
color: $fg;
background-color: $quote-bg;
border-top: .1em solid $quote-border;
border-bottom: .1em solid $quote-border;
}
table {
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 {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
border-radius: 3px;
}
pre {
position: relative;
& > .buttons {
position: absolute;
right: 5px;
top: 5px;
color: $sidebar-fg;
cursor: pointer;
:hover { color: $sidebar-active; }
i { margin-left: 8px; }
}
& > .result { margin-top: 10px; }
}
}

View File

@@ -18,4 +18,11 @@ $theme-popup-bg = #141617
$theme-popup-border = #43484d
$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

@@ -18,4 +18,11 @@ $theme-popup-bg = #fafafa
$theme-popup-border = #cccccc
$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

@@ -18,4 +18,11 @@ $theme-popup-bg = #161923
$theme-popup-border = #737480
$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

@@ -18,4 +18,11 @@ $theme-popup-bg = #e1e1db
$theme-popup-border = #b38f6b
$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'

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

@@ -0,0 +1,230 @@
use std::path::{Path, Component};
use std::error::Error;
use std::io::{self, 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(path: &Path) -> Result<String, Box<Error>> {
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) => {
debug!("[*]: Failed to open {:?}", path);
return Err(Box::new(e));
},
};
let mut content = String::new();
if let Err(e) = file.read_to_string(&mut content) {
debug!("[*]: Failed to read {:?}", path);
return Err(Box::new(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.
///
/// ```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) -> io::Result<File> {
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);
File::create(path)
}
/// 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...")));
info!("[*] 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(())
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
// 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,228 +1,20 @@
use std::path::{Path, PathBuf, Component};
use std::error::Error;
use std::fs::{self, metadata, File};
pub mod fs;
/// This is copied from the rust source code until Path_ Ext stabilizes.
/// You can use it, but be aware that it will be removed when those features go to rust stable
pub trait PathExt {
fn exists(&self) -> bool;
fn is_file(&self) -> bool;
fn is_dir(&self) -> bool;
}
use pulldown_cmark::{Parser, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
impl PathExt for Path {
fn exists(&self) -> bool {
metadata(self).is_ok()
}
fn is_file(&self) -> bool {
metadata(self).map(|s| s.is_file()).unwrap_or(false)
}
fn is_dir(&self) -> bool {
metadata(self).map(|s| s.is_dir()).unwrap_or(false)
}
}
/// 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.
/// Wrapper around the pulldown-cmark parser and renderer to render markdown
pub fn path_to_root(path: &Path) -> String {
debug!("[fn]: path_to_root");
// Remove filename and add "../" for every directory
pub fn render_markdown(text: &str) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
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 checks for every component in a path if the directory exists,
/// if it does not it is created.
pub fn create_path(path: &Path) -> Result<(), Box<Error>> {
debug!("[fn]: create_path");
// Create directories if they do not exist
let mut constructed_path = PathBuf::new();
for component in path.components() {
let dir;
match component {
Component::Normal(_) => { dir = PathBuf::from(component.as_os_str()); },
Component::RootDir => {
debug!("[*]: Root directory");
// This doesn't look very compatible with Windows...
constructed_path.push("/");
continue
},
_ => continue,
}
constructed_path.push(&dir);
debug!("[*]: {:?}", constructed_path);
if !constructed_path.exists() || !constructed_path.is_dir() {
try!(fs::create_dir(&constructed_path));
debug!("[*]: Directory created {:?}", constructed_path);
} else {
debug!("[*]: Directory exists {:?}", constructed_path);
continue
}
}
debug!("[*]: Constructed path: {:?}", constructed_path);
Ok(())
}
/// 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() {
try!(create_path(p));
}
debug!("[*]: Create file: {:?}", path);
let f = try!(File::create(path));
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(())
}
/// **Untested!**
///
/// 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...")));
//try!(create_path(&to.join(entry.path())));
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(())
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
// tests
#[cfg(test)]
mod tests {
extern crate tempdir;
use super::copy_files_except_ext;
use super::PathExt;
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") }
}
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);
s
}