Compare commits
1242 Commits
update-rea
...
master
Author | SHA1 | Date |
---|---|---|
Eric Huss | 2420919ca8 | |
Eric Huss | c671c2e904 | |
Janik H. | c9df8dd1f3 | |
Eric Huss | 8ae86d4310 | |
Johannes Gloeckle | c144c26dcf | |
Eric Huss | 481f6b1531 | |
dependabot[bot] | b267d56ba7 | |
Eric Huss | dd139f8228 | |
dependabot[bot] | be4756e4bf | |
Eric Huss | bd323fb930 | |
goodmost | aff1070f43 | |
Eric Huss | b6742e90b1 | |
Max Heller | 95b6ed7965 | |
Eric Huss | 5a35144d4f | |
Eric Huss | 5f5f9d6fd5 | |
Eric Huss | c602a2fcd6 | |
_ | 821d3c423c | |
Eric Huss | 6b89f5dad8 | |
Eric Huss | d28cf53009 | |
Eric Huss | 504900d7bd | |
Eric Huss | 0cc439eee3 | |
Eric Huss | e8b8f34f2b | |
Wil Wade | 58a23e06a1 | |
Eric Huss | 5a4ac03c0d | |
Eric Huss | c5a506e240 | |
Eric Huss | bc5cd13c16 | |
sspaeti | d406c7c09b | |
Eric Huss | 9cf3117636 | |
Eric Huss | 61786ddcdf | |
Eric Huss | f33281fae2 | |
Eric Huss | 93bd457a54 | |
Eric Huss | 600824bed2 | |
Eric Huss | 42e635bb9e | |
Eric Huss | d48810f045 | |
Eric Huss | 3387cf373d | |
Eric Huss | 7825bd6c5a | |
Jvst Me | ba14f4ad53 | |
Eric Huss | 02bbc3f777 | |
gibbz00 | 45a2d0b40e | |
Eric Huss | 53eccf7047 | |
Joe Taber | 63000bc122 | |
Eric Huss | 220cb4f0c8 | |
GeckoEidechse | 7ce3a41184 | |
Eric Huss | 51efaf2e81 | |
dependabot[bot] | f0d6d428dc | |
Eric Huss | 01778fc90a | |
dependabot[bot] | d9928ad3f9 | |
Eric Huss | 77b7876986 | |
klensy | 745f7c7313 | |
Eric Huss | 0a96d0e3fa | |
klensy | e3ad9d097e | |
klensy | 573b6522f9 | |
Eric Huss | 59d3717159 | |
Ning Sun | a42eafc316 | |
Eric Huss | 11f839b9e5 | |
Max Heller | 721274239a | |
Eric Huss | 090eba0db5 | |
klensy | 88be4ac417 | |
Dylan DPC | c1d622e56e | |
Jonathan Hult | 91af1c3b54 | |
Stephen Checkoway | 32687e64fe | |
Eric Huss | b7f46213c7 | |
Eric Huss | aa8982bdb4 | |
Eric Huss | 14826db606 | |
Eric Huss | 847a582022 | |
Eric Huss | 97cd00faeb | |
Eric Huss | 8d4193fb46 | |
leonzchang | 8d4ae388fa | |
leonzchang | 7082689866 | |
leonzchang | 40c034ed3f | |
Dylan DPC | 208d5ea7ab | |
DuckDuckWhale | ed51438c8b | |
Eric Huss | 49fce6673a | |
Eric Huss | a016ac0d2b | |
Eric Huss | ad55f5367e | |
Eric Huss | 660cbfa6ce | |
Joseph Perez | 982608246e | |
Dylan DPC | 6f6de2cf05 | |
Szymon Pilkowski | ae3e3f8269 | |
Eric Huss | dc21f1497b | |
Jacek Sieka | 5c8941ba16 | |
leonzchang | b0a001c6a4 | |
leonzchang | 722c55f85f | |
leonzchang | 3ab19f3295 | |
leonzchang | 621ffc46c0 | |
leonzchang | fbb629c02e | |
Evgeny Chaban | 80d3a86468 | |
Evgeny Chaban | 8e8fd2717e | |
Evgeny Chaban | f92d24e89c | |
Eric Huss | 94e0a44e15 | |
Eric Huss | f25181f68d | |
Eric Huss | cf19eb1386 | |
Eric Huss | 0583119698 | |
Dmitry Luschan | 3389f3db7f | |
Eric Huss | c642f5f8a3 | |
Michael Howell | ceb8b509e2 | |
Michael Howell | 65dae11e47 | |
Dylan DPC | d5b1676216 | |
Eric Huss | 09f222baf7 | |
Eric Huss | 802e7bffc3 | |
Eric Huss | fb272d1afa | |
Eric Huss | b871676def | |
Eric Huss | 869fe2f50d | |
Eric Huss | db877b1c9b | |
Eric Huss | 4749f9d97a | |
cN3rd | 8564a7fb51 | |
cN3rd | 6be98e0bbd | |
cN3rd | 5e0c68c45e | |
cN3rd | 7717b9dcf2 | |
cN3rd | 819a108f07 | |
Tim Crawford | 3a99899114 | |
Tim Crawford | 1088066c69 | |
Tim Crawford | 73d44503fd | |
Eric Huss | 25aaff0bd6 | |
cui fliter | 29691461c5 | |
Dylan DPC | a74e4dcec8 | |
Tshepang Mbambo | 0b0b548d7a | |
Dylan DPC | 02f3823e4c | |
qaqland | 36327efe9d | |
Aron Heinecke | 079f52a191 | |
Ivan Tham | c9f1d01346 | |
Eldred Habert | 9bc68bdd93 | |
Eric Huss | 56c225bd34 | |
Eric Huss | 55c017cad1 | |
Eric Huss | 7849d55b99 | |
Eric Huss | c903cc8827 | |
Eric Huss | 4a797b9565 | |
Eric Huss | 57b487eaa3 | |
Eric Huss | 891b7c06f2 | |
Eric Huss | f7e212ec9c | |
Pavel Roskin | 228538ea62 | |
Eric Huss | 347e7886e1 | |
Eric Huss | bfa5fb8844 | |
Eric Huss | a8fd6038f1 | |
Eric Huss | fbfe887084 | |
Eric Huss | aed991f75f | |
Eric Huss | ab2cb71c00 | |
Giorgio Reale | fcfde083e7 | |
Eric Huss | 4614a3637a | |
Eric Huss | d450544d6b | |
Eric Huss | 9340e6a78d | |
riverbl | e00b8835cc | |
Eric Huss | 429ca06289 | |
Eric Huss | 0fbfc90bea | |
Eric Huss | 581e5025a2 | |
Tshepang Mbambo | e57fce290b | |
Eric Huss | d5a3682de9 | |
Eric Huss | 75f5862218 | |
Eric Huss | aed518f945 | |
Eric Huss | e942d41c1d | |
Eric Huss | 38fcfd8732 | |
Eric Huss | 82ec68128d | |
Eric Huss | 9497354cfd | |
Eric Huss | baa936439d | |
Eric Huss | 394061d28d | |
Eric Huss | 0f25db67dc | |
Eric Huss | 49ba91961f | |
Eric Huss | 28ce772ae9 | |
Eric Huss | 424c2d9f6b | |
Eric Huss | 89797064b8 | |
Eric Huss | 7824aed878 | |
Eric Huss | 8236c43c90 | |
Eric Huss | 6df89fbe94 | |
Eric Huss | b423bf7ddd | |
Eric Huss | cdbdb8248c | |
Eric Huss | db45052d7e | |
Eric Huss | 804bbf6564 | |
Eric Huss | bd3b9bacf6 | |
Eric Huss | 5505d57066 | |
Eric Huss | cf88c4e720 | |
Eric Huss | 9911e86039 | |
zica | 9eba0f6ab2 | |
Antoine | 6d265c1cce | |
Eric Huss | 904aa530b5 | |
Eric Huss | fa316f3edc | |
Eric Huss | 41d19e7338 | |
Eric Huss | 4f15a3f85c | |
Eric Huss | 222166ca5a | |
Eric Huss | ab3eb81e52 | |
Eric Huss | f37486a74f | |
Eric Huss | a38b854338 | |
Eric Huss | e18113a746 | |
Dylan DPC | d4edbd1acf | |
Caleb Robson | 056e45a003 | |
Em Zhan | 72b3227824 | |
Eric Huss | a51f8a6b8e | |
Em Zhan | 1ef8d70ac4 | |
Eric Huss | a204946d39 | |
Tshepang Mbambo | 3c7795cf44 | |
Eric Huss | 9349204636 | |
Eric Huss | d2bcd04133 | |
Eric Huss | 61708ad0bd | |
Eric Huss | c9cfe22fd6 | |
Eric Huss | 5572d3d4de | |
Eric Huss | 1441fe0b91 | |
Jannik Obermann | 7df1d8c838 | |
Eric Huss | 3a51abfcad | |
Eric Huss | 870e9086dc | |
Eric Huss | 1db52ff531 | |
Eric Huss | e3be293420 | |
Eric Huss | bbc32dff82 | |
Eric Huss | 861197e61c | |
Eric Huss | 34e5ef22a0 | |
Eric Huss | b141297651 | |
Uriel | 0cb977e603 | |
ImUrX | c8a5adcee9 | |
ImUrX | ecdb411711 | |
ImUrX | a4e206168d | |
Dylan DPC | 4f1b5eae54 | |
zjj | 54f14e89cf | |
Eric Huss | 1b3922d466 | |
Eric Huss | 00a30a9984 | |
Eric Huss | db6699dae2 | |
Eric Huss | 4d229d7b94 | |
Eric Huss | d94c5f8380 | |
Eric Huss | 099217390e | |
Eric Huss | 4c4ab8a57d | |
Eric Huss | d746b23749 | |
Eric Huss | f77c597e01 | |
Eric Huss | 3c54a4d33b | |
Eric Huss | cf9de82c2a | |
Eric Huss | c3155e2642 | |
Eric Huss | d8f171a996 | |
Eric Huss | 0ef3bb1cc6 | |
Eric Huss | 54df8234ed | |
Eric Huss | dc08e37320 | |
Eric Huss | 45a8575b95 | |
Eric Huss | be966cfe1f | |
Eric Huss | f4507aeb9b | |
Eric Huss | 0985691fbd | |
Eric Huss | 01047846a9 | |
Eric Huss | 75a6d65e5a | |
Eric Huss | 71ea92bbec | |
liutailin | aac6de01de | |
Eric Huss | af036d9f45 | |
Tetsuya Morimoto | 04016f3be6 | |
Dylan DPC | 41567b0456 | |
Eric Huss | 9db3a601ca | |
Eric Huss | 35fdd00203 | |
expikr | 7a435be018 | |
Eric Huss | dec0e24275 | |
dependabot[bot] | c624fc078b | |
Sean Poulter | b9c6b326b7 | |
Sean Poulter | 0003072623 | |
Sean Poulter | bffdb0b03d | |
Eric Huss | b5ffc734a2 | |
Andreas Deininger | a2c88ae0f1 | |
Eric Huss | efb671aaf2 | |
Eric Huss | a4b4b8f649 | |
Eric Huss | 4c59405e5c | |
Eric Huss | 703a215ef8 | |
Skwodo | f5f96bc4f4 | |
Eric Huss | 1668ab7877 | |
Tshepang Mbambo | 26fc0da9a9 | |
Eric Huss | c15220d1a1 | |
Eric Huss | 7c4562a8b3 | |
Eric Huss | 6e3176f726 | |
Eric Huss | 958b456873 | |
Eric Huss | a43b5b69ab | |
Eric Huss | 1517435441 | |
Eric Huss | 7abb28cb2e | |
Eric Huss | 112fd4aac3 | |
Eric Huss | 90fbe112af | |
Martin Geisler | c150529c7c | |
Roy Wellington Ⅳ | fa6aa2ced8 | |
Eric Huss | 39664985ba | |
Eric Huss | ab1e9694bc | |
Eric Huss | 2c710d3b7d | |
Eric Huss | 581ab2c945 | |
Eric Huss | 274b48c82f | |
Eric Huss | e352e4f59c | |
Eric Huss | 734936d819 | |
Eric Huss | 0e1384b4d2 | |
Daniel Eades | 2160613c6a | |
Eric Huss | 69bb5c7fba | |
Felix Uhl | f32e1a7773 | |
Eric Huss | 703c2f214b | |
dalance | 6de831778a | |
Skwodo | ca46086e79 | |
Eric Huss | 0079184c16 | |
Noritada Kobayashi | dcc9efea0a | |
Dylan DPC | a3b508fab9 | |
Eric Huss | 5359b487f2 | |
Eric Huss | c2d973997a | |
Martin Geisler | b09aa0e65c | |
Eric Huss | 41a6f0d43e | |
Eric Huss | 9764f8886b | |
Noritada Kobayashi | 1ba2c063e0 | |
Eric Huss | c640294dbf | |
Eric Huss | dec487c62b | |
Eric Huss | 1ba74a30fc | |
Eric Huss | fcf0cebf6c | |
Eric Huss | e14d38194f | |
Eric Huss | 294aad092e | |
Eric Huss | 8767ebf835 | |
Eric Huss | cd907f2edf | |
Eric Huss | eb77083d23 | |
Eric Huss | 219362318c | |
Eric Huss | 68a75dae48 | |
David | 87a381e0a7 | |
Eric Huss | 0b2520f84a | |
Eric Huss | 21ab85cd03 | |
Eric Huss | 486bf32ac7 | |
Eric Huss | 4f6610716a | |
Eric Huss | 6db4ca71da | |
Eric Huss | d5319e2b4f | |
Eric Huss | cda44480b7 | |
Eric Huss | fb0af12433 | |
Eric Huss | b5f858da4e | |
Eric Huss | 59bd5db556 | |
armandocumate | cf1557e454 | |
Dylan DPC | 36e1f01091 | |
LePichu | e3c484af01 | |
Dylan DPC | 4deb5c7cee | |
klensy | 21fb329d56 | |
Eric Huss | 678b469835 | |
Eric Huss | ded48ddac7 | |
Eric Huss | 8a02fc755f | |
Dylan DPC | 4844f72b96 | |
Eric Huss | f32bd6f945 | |
Eric Huss | f64fcbc07d | |
Eric Huss | c34c3bf730 | |
Benji Smith | de4c551363 | |
Eric Huss | d45f02d38c | |
Eric Huss | 666975a1ef | |
Will Crichton | 144a1e4009 | |
Will Crichton | 8b486dfc71 | |
Aman Verma | db092a404e | |
Aman Verma | edda3d1b51 | |
Aman Verma | 27a11e7b35 | |
Aman Verma | cfd4c93d88 | |
Will Crichton | b1ca805d2a | |
Will Crichton | 852a882fab | |
Will Crichton | fb0cbc90e3 | |
Dylan DPC | 3a24f10d7c | |
armandocumate | 3fc036e01a | |
Eric Huss | 056a46cc97 | |
Eric Huss | f8df8ed72d | |
Brett Chalupa | 79c159d123 | |
Dylan DPC | a8c37ceace | |
Kian-Meng Ang | cb01f11ad1 | |
Noritada Kobayashi | 7aaa84853d | |
David | 75857fbf73 | |
Eric Huss | c8db0c8ec6 | |
Martin Geisler | 3958260353 | |
Eric Huss | 8cdb8d0367 | |
Dylan DPC | 66bf85b14f | |
Dylan DPC | 1a0892745e | |
Guillaume Gomez | 76b0493fb0 | |
Chris Lovett | 74eb4059d6 | |
Dylan DPC | 13f53eb64f | |
Joep Meindertsma | b3941526cb | |
Eric Huss | fff067b2a8 | |
Martin Geisler | 217546c2a0 | |
Eric Huss | 40c06f5e77 | |
Eric Huss | bb09caa9a3 | |
gifnksm | 4ebefeb43a | |
Eric Huss | 8f01d0234f | |
mitchmindtree | 13035baeae | |
Eric Huss | 92afe9bd3c | |
Eric Huss | 4c1aca0abb | |
Eric Huss | da166e051d | |
Steven Engler | 6a4ba95926 | |
Eric Huss | 6688bd8d7b | |
Dylan DPC | 01313a39cc | |
Joseph Priou | f92911b8aa | |
Dylan DPC | 42d6fd5804 | |
Fauconfan | a8a45a5fbe | |
Fauconfan | 11781f0c1b | |
Eric Huss | 53055e0345 | |
Eric Huss | 1af6d4b0ec | |
Eric Huss | 3e311dc975 | |
Eric Huss | 04e31eb07b | |
Eric Huss | eb82ddca0b | |
Eric Huss | 1d2b720ebe | |
Eric Huss | 4c303c3b1d | |
ISSOtm | 42129c6181 | |
Eric Huss | a10a57e67d | |
ISSOtm | fa5f32c7fd | |
ISSOtm | a91e888575 | |
ISSOtm | 8571883923 | |
Eric Huss | 4cf005d4bd | |
Eric Huss | b38792c166 | |
ISSOtm | 248863addf | |
Eric Huss | 7e2752e71f | |
Eric Huss | cbf0ca027d | |
Eric Huss | 2c2ba636a9 | |
Eric Huss | 494e6722b2 | |
Eric Huss | ddf71222c5 | |
Eric Huss | 1d89127d8f | |
Eric Huss | 5f00625c14 | |
Matthew Woodcraft | 000a93dc77 | |
Matthew Woodcraft | 1f8c090a5f | |
Eric Huss | 0547868d4d | |
Dylan DPC | 1056b8361c | |
Dylan DPC | a5f861bf2b | |
Dylan DPC | 93aee6419e | |
Josh Rotenberg | 4b1a7e9ae7 | |
Eric Huss | 2f5e89f3ec | |
Eric Huss | 2b903ad057 | |
Matt Ickstadt | fb397e6fa0 | |
Matthew Woodcraft | 00a55b35a8 | |
Matthew Woodcraft | d65ce55453 | |
Eric Huss | 37d756ae75 | |
Eric Huss | f8782666ba | |
josh rotenberg | c74c682939 | |
josh rotenberg | 8b49600673 | |
josh rotenberg | 29c729fd23 | |
josh rotenberg | 5d65967448 | |
Eric Huss | bf258eeb9b | |
klensy | af6237015a | |
Eric Huss | a462fb63c3 | |
klensy | f3332fb0da | |
Eric Huss | 5bea83114b | |
Clark | fe8bb38ec1 | |
Clark | a60571321a | |
Eric Huss | e1c2e1a753 | |
Maximilian Roos | 800dbf2929 | |
Eric Huss | 1880447dce | |
Eric Huss | 268dbb099f | |
Eric Huss | ae275ad1b1 | |
Eric Huss | ceff050bb4 | |
josh rotenberg | 8357811d96 | |
josh rotenberg | 860a17d85a | |
Eric Huss | ba324cddb6 | |
Eric Huss | a5dcd78393 | |
sgoudham | 8e1195322a | |
Sergei Trofimovich | 2a2b51c8ab | |
Eric Huss | eb5ec2a314 | |
Eric Huss | 445529a68f | |
Eric Huss | 981b79b3b3 | |
Eric Huss | 78bcda02cb | |
Eric Huss | cdfa5ad990 | |
Eric Huss | 720f502e9d | |
Eric Huss | adf6129769 | |
Eric Huss | a5fddfa468 | |
Eric Huss | 7c37dd5e85 | |
Ed Page | 857ca19fe4 | |
Ed Page | a19d91ef37 | |
Ed Page | ac8526688a | |
Ed Page | 1e1c99bbdb | |
Ed Page | 0c89293029 | |
Ed Page | 39eb78c88b | |
Ed Page | 372842aac6 | |
Ed Page | 7934e06668 | |
Ed Page | 44f982f8e5 | |
Eric Huss | 1f04a62648 | |
Eric Huss | 675c8c3f4e | |
rsapkf | 97cb77bbdd | |
Eric Huss | 46345b8e49 | |
Eric Huss | 566451e9a7 | |
Eric Huss | 15626294b0 | |
Eric Huss | 6cab04554e | |
Shogo Takata | 2ae7f007cc | |
Shogo Takata | 89e37a7751 | |
Shogo Takata | 0dca4d9b9f | |
Shogo Takata | b85c3035fe | |
Clark | 6899d94027 | |
Eric Huss | fa0f9df497 | |
Dylan DPC | 917df6e97d | |
Eric Huss | 5921f59817 | |
Pi Delport | 50bad7f983 | |
Tom Milligan | 972c61fa76 | |
ilslv | b73d02fb8c | |
ilslv | 6c4974b5c6 | |
ilslv | 81d661c4f1 | |
Eric Huss | 2213312938 | |
Jade Lovelace | 4ae7ab5e87 | |
Jade | 59569984e2 | |
Jade | 89b580ab52 | |
Jade | 85df785cd3 | |
Jade | fde88c22a8 | |
Eric Huss | 0ec4b692f4 | |
David Tolnay | c5c8f1a6d3 | |
Eric Huss | 336640633b | |
Guillaume Gomez | 4206739492 | |
Daniel Morawetz | 9e6217871e | |
Daniel Morawetz | 7b1241d0f2 | |
Eric Huss | 9acc0debec | |
Caleb Cartwright | a226de38b6 | |
josh rotenberg | f5b0b1934a | |
Eric Huss | 64838ce07d | |
klensy | 526a0394b0 | |
Eric Huss | 6ed570940d | |
Eric Huss | b0511f408d | |
Eric Huss | 68a5c09fdf | |
Eric Huss | 97b6a35afc | |
josh rotenberg | 1cacef025d | |
Guillaume Gomez | ddb0d2396f | |
Eric Huss | f2fba30786 | |
Eric Huss | f3e5fce6bf | |
josh rotenberg | 2d36cd9263 | |
josh rotenberg | 09087097b5 | |
josh rotenberg | 18b9f42fba | |
josh rotenberg | b38949a408 | |
josh rotenberg | c32869cf10 | |
Eric Huss | e3e170715e | |
Eric Huss | a387f482d8 | |
Sven Wick | 4e03818f3e | |
Alessandro Coglio | 3f268cb0df | |
Eric Huss | 25829926e5 | |
Eric Huss | c828002b70 | |
Eric Huss | cdbfb4a5b9 | |
josh rotenberg | 191dccab10 | |
Eric Huss | 5eb7d46a99 | |
Eric Huss | dffcedf031 | |
Eric Huss | c9b6be8660 | |
Eric Huss | 23af80c506 | |
Eric Huss | 857acb9759 | |
Eric Huss | 2ddcb43899 | |
josh rotenberg | 1c0983b811 | |
josh rotenberg | 1be69af553 | |
josh rotenberg | c63000f365 | |
Ingu Kang | bbaa0ea1fa | |
Abdo | 58bc92d380 | |
Eric Huss | 17d1ed3716 | |
Yashodhan Joshi | 8df8ce063d | |
Eric Huss | c3ff4a5129 | |
Eric Huss | 4d20fa578b | |
Eric Huss | 9e47498458 | |
josh rotenberg | 903469a45f | |
Yashodhan Joshi | b8ef89db62 | |
Yashodhan Joshi | c283211a37 | |
Yashodhan Joshi | d5af051d0e | |
Yashodhan Joshi | 68f9afe64b | |
Eric Huss | ffa8284743 | |
Eric Huss | 3e91f9cd5d | |
Eric Huss | f55028b61a | |
Eric Huss | 0d887505af | |
Eric Huss | 6c20736a55 | |
Ivan Tham | e4a46c9477 | |
Ivan Tham | 6ae5c686d9 | |
Lucas Betschart | b862080006 | |
ISSOtm | 6b790b83ec | |
ISSOtm | d8ad68c947 | |
ISSOtm | 6b784be616 | |
Eric Huss | f5598b2eee | |
Johannes Stoelp | ff4b8e7a8d | |
ISSOtm | 9c34e602bd | |
Eric Huss | a306da3ad7 | |
Michael Howell | 9bede85efa | |
Michael Howell | 11b1e86187 | |
Michael Howell | 10d30a2dc0 | |
Eric Huss | 601ebc5499 | |
Michael Howell | 4251d7a838 | |
Eric Huss | 93008cf20b | |
apogeeoak | 3f9f681b9e | |
Eric Huss | 63680d0786 | |
Eric Huss | 656a1825cc | |
pauliyobo | 1a2fa29209 | |
Eric Huss | 6be81214b1 | |
Eric Huss | d22299d998 | |
Eric Huss | 0af417085f | |
Eric Huss | 9634798eb7 | |
Michael Howell | 2a8af1c21d | |
Wojciech Kępka | 981f8695ff | |
Eric Huss | 48b5e52f62 | |
Klim Tsoutsman | c4fec94c4c | |
Ben Armstead | ab0c338c08 | |
Ben Armstead | 8a82f6336a | |
Ben Armstead | 1700783594 | |
Eric Huss | e6629cd75b | |
Eric Huss | 5a077b9ff4 | |
Eric Huss | 8b4e488de1 | |
Eric Huss | 68d8ceec47 | |
Eric Huss | db337d4a6f | |
josh rotenberg | 5e277140be | |
Eric Huss | 14add9c290 | |
Eric Huss | 87877a9dae | |
Eric Huss | 2cf00d0880 | |
Eric Huss | 8c7af3c767 | |
Eric Huss | 6dd785ea6c | |
Eric Huss | 8d131b4310 | |
Eric Huss | 97b38063b1 | |
josh rotenberg | d23734f82e | |
Eric Huss | 2ccfaadd1d | |
Eric Huss | 3d04e5c7ff | |
FWYongxing | 49ef7b6f02 | |
josh rotenberg | da7026190c | |
josh rotenberg | 92377013cc | |
josh rotenberg | 34b586ab32 | |
josh rotenberg | a79065b0d3 | |
josh rotenberg | b3ab93a4b3 | |
josh rotenberg | 49b75810fa | |
josh rotenberg | b85d5eb455 | |
Eric Huss | 27faa54ae8 | |
Eric Huss | fae0759626 | |
Tuyen Pham | cc74ca2e6e | |
Tuyen Pham | 7cae3a058d | |
Eric Huss | 8fb6ac7987 | |
Michael Howell | 82d32ee761 | |
Eric Huss | fe9b534ad7 | |
Eric Huss | 56652e8fa6 | |
Eric Huss | c3a1e41ed7 | |
Eric Huss | 3976c9d8f0 | |
Eric Huss | 96b6f02834 | |
Eric Huss | b571511737 | |
josh rotenberg | ebdab38a32 | |
josh rotenberg | c06f450e7d | |
josh rotenberg | b87c231fc3 | |
josh rotenberg | 8024b08f93 | |
josh rotenberg | 8ec0bf6e30 | |
kana | a8926d5392 | |
kana | 00473d8420 | |
josh rotenberg | 86d390032b | |
Eric Huss | b3c0b01350 | |
Eric Huss | e33192753d | |
Frits Stegmann | 7932e13512 | |
josh rotenberg | 9fd2509c0d | |
josh rotenberg | 5dec8508c7 | |
Eric Huss | 4d2dc6f482 | |
Tuyen Pham | efb13d7bc1 | |
kana | 27b1e05c87 | |
Eric Huss | e440094b37 | |
Eric Huss | 15cae10ca8 | |
Eric Huss | dc2062ab36 | |
Eric Huss | d9ce98d710 | |
josh rotenberg | b59aab56f2 | |
Eric Huss | b899c48019 | |
josh rotenberg | 515a253e97 | |
Eric Huss | 7ddc3df945 | |
Eric Huss | 2f7293af5c | |
Atharva Raykar | fa3ae53d46 | |
Eric Huss | 6425c29893 | |
Eric Huss | d0bb830491 | |
Eric Huss | d325c601bb | |
Eric Huss | e9e889f523 | |
josh rotenberg | e5e10c681a | |
josh rotenberg | 05edc4421b | |
Eric Huss | 22ea5fe335 | |
Riccardo Magliocchetti | 714c5fb81e | |
Riccardo Magliocchetti | 56ceb627b8 | |
Riccardo Magliocchetti | c1b2bec7d7 | |
Eric Huss | 8201b411ab | |
Eric Huss | 836546cf0d | |
Ning Sun | 9813802b3e | |
josh rotenberg | fcf8f938d2 | |
josh rotenberg | 60aaa7ae31 | |
Ning Sun | 1b584d1746 | |
josh rotenberg | aa4cb9465f | |
Eldred Habert | 89a2e39b80 | |
Eric Huss | 3c2b8cd10f | |
josh rotenberg | 6b0b42ebcc | |
josh rotenberg | 7a3513200f | |
Eric Huss | 3db0c0b9a1 | |
Eric Huss | 2c7aac6d7a | |
Eldred Habert | 3ee22fb430 | |
Eric Huss | 16c5ec4d74 | |
Eric Huss | 7e7e779ef7 | |
Andrea Gelmini | b364e8ea2c | |
josh rotenberg | 78325aaccb | |
Eric Huss | 1411ea967a | |
josh rotenberg | d147a85006 | |
Eric Huss | 0f0dce8d6c | |
Andrew Morgan | 379574dc61 | |
Eric Huss | 6a7de13c6f | |
Eric Huss | 331aad1597 | |
Eric Huss | 7e01cf9e18 | |
josh rotenberg | c922b8aae6 | |
apatniv | b21446898a | |
josh rotenberg | f4b4a331d7 | |
josh rotenberg | aa349e0b7c | |
Eric Huss | b592b10633 | |
Eric Huss | d62cf8e883 | |
Vivek Bharath Akupatni | c6844dd771 | |
Tshepang Lekhonkhobe | 009247be01 | |
Eric Huss | 84b3b7218e | |
Eric Huss | 71ba6c9eb8 | |
Simon Cruanes | 9d4ee689db | |
Simon Cruanes | ffe88d7e29 | |
Simon Cruanes | 9f930706bb | |
Simon Cruanes | 24fa615149 | |
Eric Huss | a72d6002b7 | |
josh rotenberg | 5b7abf4714 | |
josh rotenberg | d0ef70e574 | |
Flavio Castelli | 7525b35383 | |
Eric Huss | b54e73e3b6 | |
josh rotenberg | 59c76fa665 | |
Guillaume Gomez | c1d982d92b | |
syntezoid | 3db275d68a | |
Flavio Castelli | 94e797fba0 | |
Flavio Castelli | c3beecc96a | |
Flavio Castelli | 7aff98a859 | |
Jonas Berlin | bbf54d7459 | |
Jonas Berlin | dcc642e66d | |
Jonas Berlin | 2b738d4425 | |
Jonas Berlin | b3670ece0e | |
Tatsuya Kawano | 30ce7e79ac | |
David Tolnay | 94f7578576 | |
Eric Huss | e6568a70eb | |
Evian-Zhang | 0eb23efd44 | |
Evian-Zhang | e78a8471c7 | |
Paul | dcccd3289d | |
pauliyobo | 5637a66459 | |
Eric Huss | 536873ca26 | |
Camelid | d6ea4e3f7a | |
mbartlett21 | fcceee4761 | |
Eric Huss | 3f39ba82f9 | |
Eric Huss | 7da38715c1 | |
Eric Huss | c83bbd6319 | |
Eric Huss | fad3c663f4 | |
Eric Huss | f8b9054265 | |
Fenhl | f26116a491 | |
Tim Small | 7f59fdd9bd | |
Eric Huss | 45d41eac5f | |
Eric Huss | 2b5890e2ed | |
Eric Huss | 0b9570b160 | |
Daniel Eades | 90396c5b76 | |
Eric Huss | 24b76dd879 | |
Eric Huss | 9a9eb0124a | |
Eric Huss | 257374d76b | |
Eric Huss | 1a0c296532 | |
Eric Huss | 9b4ab72a80 | |
Eric Huss | b1c2e466e7 | |
Eric Huss | cdea0f6b61 | |
pierwill | e9b0be7090 | |
Tatsuya Kawano | d402a12e88 | |
Tatsuya Kawano | 218e200117 | |
Eric Huss | 3d55375f61 | |
Eric Huss | 77e7cfd22b | |
Eric Huss | 76cd39e5e2 | |
Eric Huss | 09e7bb76dc | |
Eric Huss | 28387130c0 | |
Eric Huss | 33d3d9c3ec | |
Alexandru Macovei | beec17e55d | |
Eric Huss | e651f4d734 | |
Eric Huss | 87d2cd9845 | |
Pietro Albini | 32abeef088 | |
Eric Huss | 5de9b6841e | |
apatniv | 95e0743bc0 | |
apatniv | 3c97525743 | |
Eric Huss | 9a65c8ab92 | |
Eric Huss | a64a7b7470 | |
francis-du | fd4137a9ea | |
Vivek Bharath Akupatni | a3d4febe3e | |
Maxime BORGES | 7af4b1dfe8 | |
Eric Huss | ba6bffac5a | |
Maxime BORGES | 6201e577fe | |
Eric Huss | cf2459f730 | |
Eric Huss | 45a481049e | |
Kousuke Takaki | 6bcabcbb6b | |
apatniv | ef993e8cc2 | |
apatniv | a3a5386da0 | |
Eric Huss | 3ab911afa1 | |
Spencer Burris | 4615ce2f8c | |
Lucas Zanini | 7cb8087469 | |
Spencer Burris | d1721667b6 | |
avitex | 1038f0b7f5 | |
avitex | 942cc12a74 | |
Eric Huss | 59f2a9bf4e | |
A Ho | 75d0f1efd4 | |
Eric Huss | 552e3378cf | |
Sergey Golovin | 7c0ddff96a | |
Eric Huss | 07e72757d3 | |
Eric Huss | 58f66a146d | |
Eric Huss | 643d5ecc5c | |
David Tolnay | c712ba7aab | |
David Tolnay | 1450070f73 | |
A Ho | e310dfc605 | |
A Ho | cbfd75a821 | |
Eric Huss | eaa6914205 | |
Eric Huss | a76557a678 | |
Eric Huss | 01836ba5d4 | |
Eric Huss | 46ce077de6 | |
Eric Huss | f7c9180d80 | |
FrankHB | 9e9cf49c50 | |
Camelid | 4c951d530d | |
FrankHB | 780fb979a0 | |
Camelid | b77942d3c8 | |
Eric Huss | d0deee90b0 | |
Eric Huss | e6ac8ecdd9 | |
Eric Huss | d1f5ecc103 | |
Ross MacArthur | e0b247e9d6 | |
Eric Huss | db8a2821ea | |
Eric Huss | 39d7130019 | |
Camelid | 2eccb457d2 | |
Camelid | d1682d27fb | |
Eric Huss | a94a940ff7 | |
Eric Huss | daf402e1dc | |
Eric Huss | 5ebd2c0527 | |
ifeanyi | b349e8abc9 | |
ifeanyi | e225586953 | |
Eric Huss | cf7663f800 | |
Eric Huss | 3155c63e88 | |
Eric Huss | 4df9ec90af | |
Camelid | 73cabeb904 | |
Eric Huss | 4b773024ae | |
Eric Huss | 33ea661350 | |
Eric Huss | 1b18740b56 | |
Eric Huss | 6fed9e52f9 | |
Eric Huss | fd59dc73e5 | |
Eric Huss | 146bea48c6 | |
Camelid | efb5bc285d | |
Gus Wynn | 5ea8e55aea | |
David Tolnay | 1acf23ff73 | |
David Tolnay | 69cc1fa005 | |
Søren Mortensen | 2fb489137b | |
Camelid | 4d9eb9b4b4 | |
Eric Huss | f6768b816c | |
Eric Huss | 8f7e030ac3 | |
Eric Huss | 9180dd1659 | |
Eric Huss | 9278b838a8 | |
Fabien Tregan | 2674347768 | |
Eric Huss | 3d44553671 | |
Evian-Zhang | 9d5c454e47 | |
Eric Huss | a00e7d1769 | |
Evian-Zhang | 60be20a783 | |
Eric Huss | 8746206060 | |
Eric Huss | f5ae7c4f13 | |
Érico Rolim | dcf9462d1e | |
Érico Rolim | 78aa2a16f8 | |
FrankHB | 65d9eb6f7e | |
Eric Huss | 303db0ddec | |
Eric Huss | a884c2574e | |
maniyar1 | 60029e4e15 | |
Yuki Okushi | 4e16d96ed5 | |
Owen Nelson | 0eefd63a13 | |
Eric Huss | 89c2743cc6 | |
Michael P. Jung | a825427722 | |
maniyar1 | c99047bbda | |
Eric Huss | 20a0b99c3d | |
Eric Huss | ec495a7823 | |
Eric Huss | e38fb1ecc6 | |
Eric Huss | f37ea9a4e7 | |
Eric Huss | 8f74804c70 | |
Eric Huss | 649f3555e5 | |
Eric Huss | 8432df1e80 | |
Igor Matuszewski | 9eba9ed93a | |
Eric Huss | b0c6f2d7a3 | |
Eric Huss | 6e0688afef | |
Eric Huss | e9951af73e | |
Eric Huss | 138dc696b7 | |
Eric Huss | 91b2fb86bf | |
Eric Huss | d4df7e7cdd | |
Eric Huss | 4699269e49 | |
Eric Huss | c1ed6ee108 | |
Eric Huss | f59cfe7e2f | |
Eric Huss | 9268884b17 | |
Eric Huss | 4f435c62e6 | |
Eric Huss | 9a97f0a096 | |
Eric Huss | bc23d08fa5 | |
ThePuzzlemaker | 84d848f292 | |
Manuel Woelker | d7df832cce | |
Manuel Woelker | 406b325c54 | |
Manuel Woelker | 6d6e5407a3 | |
Manuel Woelker | 06efa7a675 | |
Manuel Woelker | bff36e7229 | |
Manuel Woelker | cda28bb618 | |
Eric Huss | fe1ba71d45 | |
Eric Huss | 23f5ffd6d6 | |
Eric Huss | 484e5c0b8f | |
Eric Huss | a80febd318 | |
Nihaal Sangha | 16010ee28b | |
Bryce Fisher-Fleig | fb1476d1e3 | |
Eric Huss | b375f4e3d5 | |
Eric Huss | 25ec7ace1a | |
Michael-F-Bryan | ebc01dbb71 | |
Michael-F-Bryan | 7b3e945a27 | |
Michael-F-Bryan | 964a10ff29 | |
Michael-F-Bryan | 5907caa732 | |
Michael-F-Bryan | da55cf273f | |
Michael-F-Bryan | a6ab4d8402 | |
Michael-F-Bryan | 4c2318922f | |
Michael-F-Bryan | b2d50392ea | |
Michael-F-Bryan | a5086a1e58 | |
Eric Huss | 6c4c3448e3 | |
Eric Huss | 5d5c55e619 | |
Eric Huss | e2023fd72d | |
Eric Huss | e677b72eb8 | |
Aphek | 7e090ca42f | |
Aphek | 122c988477 | |
mark | d0fe9bd41c | |
mark | b1ccb30220 | |
mark | 91e3aa4b55 | |
mark | 2d63286c63 | |
mark | 5dd2a5bff4 | |
Eric Huss | 1b3b10d2ae | |
Aphek | 2c26c65f4d | |
Eric Huss | e8d4bc52e1 | |
Eric Huss | 6038af292f | |
Eric Huss | 578e4da5b6 | |
Mathieu David | 43008ef2ef | |
Mathieu David | d605938886 | |
Eric Huss | 7e11d37e49 | |
Eric Huss | 50bcf67f2b | |
Gilles Rasigade | c2d58158da | |
Gilles Rasigade | 1731779a8d | |
Gilles Rasigade | f7e349d37f | |
Gilles Rasigade | 61c8413138 | |
Eric Huss | 8ee950e3de | |
Eric Huss | c44ef1b2f0 | |
Eric Huss | 07dfc4b89a | |
toyboot4e | 282e55122e | |
Eric Huss | 17210b058f | |
Tomasz Kurcz | b1cf3f117d | |
Tomasz Kurcz | d665732056 | |
toyboot4e | 2f59dbf1ef | |
toyboot4e | 3a63276727 | |
toyboot4e | 4c64f23089 | |
toyboot4e | 683d2b2240 | |
Eric Huss | 11f95f76e6 | |
Eric Huss | 2732c5e8f7 | |
Kim Hermansson | 6b550cb4bb | |
Eric Huss | 712362f9e7 | |
Eric Huss | 28ce8f5ac0 | |
kngwyu | 255756cfee | |
Gabriel Majeri | 53d821bf6d | |
Gabriel Majeri | d39d4517aa | |
Eric Huss | bd0f434225 | |
Eric Huss | 3806d7b6ea | |
Eric Huss | d1b484ff35 | |
Eric Huss | 04c04dfc88 | |
Mike English | 1d265fd143 | |
Eric Huss | 8e673c96c2 | |
Eric Huss | 99ecd4f87c | |
Eric Huss | e839ef0866 | |
Eric Huss | 769cc0a7c1 | |
Andrew Mackenzie | c2686a817a | |
Andrew Mackenzie | bd14d0910a | |
Eric Huss | 6eb597a556 | |
Zdenek Crha | 5c91041dad | |
Dylan DPC | 59568208ff | |
Eric Huss | 21a16c9b75 | |
Eric Huss | 4e8e1e1408 | |
Martin Carton | 2baed040c2 | |
Mathieu David | 101063093b | |
Eric Huss | f7ffffbd1e | |
Eric Huss | 760c9b0767 | |
Eric Huss | 6016e12b90 | |
Eric Huss | 88684d843d | |
dalance | b82562fe8a | |
Eric Huss | 44c3213f5d | |
Dylan DPC | fd56a53e76 | |
Dylan DPC | ca4b85b815 | |
Sebastian Thiel | d7a2b29f06 | |
Eric Huss | 4039c72fd3 | |
Evan Carroll | 2bd8bdf798 | |
Sergey Pedan | 0da7ba4abe | |
Eric Huss | d6cfa21fff | |
Tomasz Różański | 95fba3f357 | |
Eric Huss | d5999849d9 | |
Will Kahn-Greene | 8b2659e0f4 | |
Eric Huss | c4a64ab599 | |
Eric Huss | 6b4e3584b4 | |
Avery Harnish | b8fc7a1b2d | |
Eric Huss | 2ee083dfbe | |
Ning Sun | 1947f8ca65 | |
Eric Huss | 2f59943c04 | |
Eric Huss | 980f943179 | |
Josh | 5e998788e9 | |
Arashmidos | 6a94492238 | |
Aleksey Kladov | e3717ad47b | |
nickelc | 49b7f08164 | |
Eric Huss | 7def6d70e8 | |
Eric Huss | 554f29703f | |
Michael Bryan | 730d7f8410 | |
Dylan Owen | b6603468d6 | |
Eric Huss | 441a10bdd7 | |
Eric Huss | efdb83266a | |
Eric Huss | ac9c12334a | |
Marcus Klaas de Vries | 2a3088422a | |
Dylan DPC | 1f505c2b2e | |
Gabriel Majeri | a7b3aa0444 | |
Eric Huss | a9160acd64 | |
Benedikt Werner | 4c1bca1684 | |
Benedikt Werner | 8fffb2a704 | |
Eric Huss | ba37cc8462 | |
Ricky | 3ea0f9b745 | |
boyned//Kampfkarren | 29d8747e01 | |
Benedikt Werner | f5549f2267 | |
Benedikt Werner | e2a8600712 | |
Eric Huss | f2cb601c11 | |
Steve Klabnik | 6e0d0facff | |
Steve Klabnik | f79d5d4582 | |
Rostislav | 820714a560 | |
Eric Huss | d5535d1226 | |
Eric Huss | e5f77aaaf2 | |
Matthew Woodcraft | 86a368b726 | |
Matthew Woodcraft | 1dc482b00d | |
Eric Huss | 21d8f394ae | |
Benedikt Werner | c9dae170f3 | |
Eric Huss | fcf2d7a03b | |
Eric Huss | 2498887dfc | |
Eric Huss | f04d7b802d | |
Eric Huss | bfcddf2680 | |
Eric Huss | 2b649fe94f | |
Eric Huss | fc4236eaa7 | |
rnitta | a592da33bb | |
Weihang Lo | 6af6219e5b | |
Andrew Pritchard | e5f74b6c86 | |
Benedikt Werner | 84a2ab0dba | |
David Omar Flores Chávez | d63ef8330d | |
rnitta | 01e50303a2 | |
Dylan DPC | 2b3304cb8b | |
Adrian Heine né Lang | 4448f3fc4b | |
Chris Ladd | 859659f197 | |
Eric Huss | 4a93eddae2 | |
Eric Huss | 0173451b67 | |
Carol (Nichols || Goulding) | ac1749ff2f | |
Eric Huss | 8cdeb121c5 | |
Amanjeev Sethi | 74313bb701 | |
Amanjeev Sethi | 3c25dba9b4 | |
Amanjeev Sethi | 2387942588 | |
Eric Huss | 93c9ae5700 | |
Eric Huss | 9efa9fd1c4 | |
Eric Huss | 8a33407cc5 | |
morphologue | 699844a5c3 | |
Flying-Toast | 9bdec5e7cc | |
Kim Hermansson | 930f730361 | |
Eric Huss | 09c738468f | |
Kim Hermansson | a3d1afdd1f | |
Kim Hå | 8e8e53ae15 | |
rnitta | 5fe801a7d1 | |
Eric Huss | a6f317e352 | |
Eric Huss | ed95252f05 | |
Eric Huss | a058da8b74 | |
Eric Huss | 73be1292ab | |
Eric Huss | 98ecd1178b | |
Eric Huss | 996ac382c1 | |
Eric Huss | b88839cc25 | |
Flying-Toast | 1ef94c2a7e | |
Flying-Toast | f0ac13e3e2 | |
Flying-Toast | b0ae14a2c7 | |
Andrew Pritchard | 81ab2eb7db | |
Tjeu Kayim | 213171591a | |
Tjeu Kayim | db13d8e561 | |
Eric Huss | b4bb44292d | |
Lzu Tao | bb7a863d3e | |
Lzu Tao | e62a9dba87 | |
Lzu Tao | 4a94b656cd | |
Carol (Nichols || Goulding) | a873d46871 | |
Carol (Nichols || Goulding) | ce0c5f1d07 | |
Eric Huss | 33d7e86fb6 | |
Carol (Nichols || Goulding) | f9f9785839 | |
Eric Huss | 0c37b912ba | |
Stephan Druskat | e880fb6339 | |
Eric Huss | a8d6337ac6 | |
Eric Huss | f37a89cd4c | |
Eric Huss | aaeb3e2852 | |
Carol (Nichols || Goulding) | 8c4b292d58 | |
Carol (Nichols || Goulding) | 40159362c0 | |
Carol (Nichols || Goulding) | aa67245743 | |
Carol (Nichols || Goulding) | d968443074 | |
Carol (Nichols || Goulding) | 3716123e10 | |
Carol (Nichols || Goulding) | 50a2ec3cf1 | |
Carol (Nichols || Goulding) | 07459aef60 | |
Eric Huss | 0f56c09d3a | |
Lzu Tao | 63ad3d9340 | |
WofWca | 1c5dc1e310 | |
Steve Klabnik | 77af889a2e | |
Liam Beckman | e48fed74bf | |
Sorin Davidoi | e512850c13 | |
Michael Bryan | bb412edf53 | |
Michael Bryan | 5b0a23ebab | |
Michael Bryan | e56c41a1c2 | |
Michael Bryan | d1b5a8f982 | |
Eric Huss | f396623b63 | |
Matthias Eichstedt | 9ec43b6c6d | |
Eric Huss | 7c4d2070f7 | |
Eric Huss | 50d5917530 | |
Eric Huss | 9cd47eb80f | |
Eric Huss | 4932df2570 | |
Eric Huss | 11d31c989c | |
Eric Huss | e5ace6d6a4 | |
Eric Huss | e7c3d02c61 | |
Benoît CORTIER | d8a68ba3f6 | |
Benoît CORTIER | d29a79349c | |
Eric Huss | d6088c8a57 | |
Eric Huss | b91e5c8807 | |
Eric Huss | 6199e4df79 | |
Ning Sun | 2d11eb05fe | |
Carol (Nichols || Goulding) | 3d45e40693 | |
Eric Huss | 228e99ba11 | |
Eric Huss | 4b569edadd | |
Eric Huss | 3e652b5bfc | |
Lzu Tao | ba41d73dc3 | |
Lzu Tao | 1ce1401263 | |
Lzu Tao | 00b3d9cf86 | |
Eric Huss | bb3398bdbb | |
Eric Huss | 19c26217c0 | |
Eric Huss | a2029f0a78 | |
Eric Huss | 7c33ac800c | |
Eric Huss | d371001ab8 | |
Eric Huss | d73504eb23 | |
Carol (Nichols || Goulding) | abddd7c6f7 | |
Carol (Nichols || Goulding) | 31e36f85e7 | |
Jeremy Stucki | 92a7b0cdcd | |
Jeremy Stucki | 592140db5b | |
Jeremy Stucki | 3a0eeb4bbb | |
Jeremy Stucki | a9dae326fa | |
Jeremy Stucki | abba959add | |
Jeremy Stucki | ea15e55829 | |
j143-bot | d07bd9fed4 | |
Carol (Nichols || Goulding) | b83c55f7ef | |
Eric Huss | 69a08ef390 | |
Eric Huss | 1cd1151790 | |
Eric Zubriski | 84d4063e4a | |
Eric Huss | 07830f7f11 | |
Eric Huss | 828b7d05c5 | |
Eric Huss | 379004efcb | |
Eric Huss | 2497e77bf1 | |
Eric Huss | 0c2292b9aa | |
lzutao | 4386a10e87 | |
Eric Huss | 3cfed10098 | |
rnitta | a655d5d241 | |
Eric Huss | f8c3a2deea | |
Eric Huss | b226d2fc55 | |
lzutao | 53ba0d6655 | |
Eric Huss | 43ead86ecc | |
Eric Huss | 1d3ec7e0c7 | |
Eric Huss | f4017376a9 | |
Lzu Tao | 672cf456eb | |
Lzu Tao | 8dce00d54d | |
rnitta | 4f7c299de7 | |
Eric Huss | 04e74bfa1b | |
Eric Huss | 4026a586a1 | |
lzutao | 71281bff10 | |
lzutao | 8542f7f29d | |
Eric Huss | fe492d1cb9 | |
Eric Huss | 481c2f2194 | |
lzutao | 882014860c | |
Bas Bossink | e3ec751a3f | |
Eric Huss | fc565df86b | |
Eric Huss | ec8e63145c | |
Bas Bossink | 2752c88c46 | |
Eric Huss | e7befd19bc | |
Eric Huss | 644b8e132c | |
Eric Huss | 8e82ae534a | |
Eric Huss | 6a8a5b7642 | |
Roman Proskuryakov | c3284a2ae9 | |
Allen | df12cc55c8 | |
Eric Huss | cb4a3e0711 | |
lzutao | 9194a40acd | |
Bas Bossink | 506996808b | |
Philipp Hansch | 5163c5ab75 | |
Stefanie Jäger | ecfaed1e02 | |
Eric Huss | 8bb5426441 | |
Eric Huss | a674c9eff1 | |
lzutao | 7ab939f8f2 | |
lzutao | 581187098c | |
lzutao | ab7802a9a9 | |
Dylan DPC | 345acb8597 | |
Dylan DPC | 6380526e93 | |
Lzu Tao | 4560bdeb47 | |
Lzu Tao | 0aa3a9045a | |
Dylan DPC | b30b58b565 | |
Dylan DPC | c6220fba83 | |
Dylan DPC | 652eab6e7e | |
Dylan DPC | 5726a8afd6 | |
Dylan DPC | 7e26a8430d | |
Dylan DPC | 07a64b110a | |
Eric Huss | dd69e03ff5 | |
Dylan DPC | 7f3a0ff6a0 | |
Dylan DPC | aea317e173 | |
Dylan DPC | f9454615b1 | |
Dylan DPC | 39211291d9 | |
Dylan DPC | f01fe854fa | |
Dylan DPC | 6eeaaaa44d | |
Dylan DPC | 357ebcf7ce | |
Dylan DPC | 1a4f38eace | |
rfm | 1d3f83eede | |
Dylan DPC | 9712347b9c | |
Dylan DPC | f73d42d994 | |
Dylan DPC | a647017e4b | |
Dylan DPC | a66d44190e | |
Felix Rabe | 01fd7a76f0 | |
Dylan DPC | 99dc62f9c3 | |
Dylan DPC | b891fd5a12 | |
Federico Fissore | 02fa7b0a11 | |
Dylan DPC | 8b2e1c2daa | |
Dylan DPC | 88d2f69138 | |
Dylan DPC | cb94053779 | |
Dylan DPC | 0a8707b1e6 | |
Donald Pinckney | 0dc2728fa9 | |
Dylan DPC | 9b02cd7496 | |
ji.zhou | 11f86f4511 | |
Carol (Nichols || Goulding) | 4abac12c04 | |
Carol (Nichols || Goulding) | d7c7d91005 | |
Sebastian Köln | 9243cf9d95 | |
Sebastian Köln | d2470730fc | |
Sebastian Köln | 6a2e2461fb | |
Sebastian Köln | 3faa3e42f0 | |
Sebastian Köln | 9c8fae4704 | |
Sebastian Köln | 9b6f5a9840 | |
Benjamin Fry | 62af2367bb | |
Adam Perry | 37808b7e08 | |
Rongsong Shen | b37f21a09b | |
Eric Huss | 966632a724 | |
Cauê Baasch de Souza | c7281459f9 | |
nasa | ae3f87ad0c | |
Steve Klabnik | c068703028 | |
Steve Klabnik | 6cbc41d413 | |
Steve Klabnik | 25c1ca1275 | |
Steve Klabnik | acbb951240 | |
Steve Klabnik | 9e96165d8f | |
Steve Klabnik | 5c5ef2f86b | |
Steve Klabnik | 23ac06e2eb | |
Steve Klabnik | 2ddbb37f49 | |
Steve Klabnik | a481735fa2 | |
Stéphane Derosiaux | 954cfa86e5 | |
k-nasa | 7e52da3c1b | |
k-nasa | 4e8d051bd1 | |
k-nasa | 78ee8e43bb | |
Shawn | b675b91980 | |
gentoo90 | 3d8db7f25c | |
gentoo90 | 3d37e24c14 | |
Philipp Hansch | eb19d2d654 | |
Michael Bryan | 1052ee92e1 | |
Bas Bossink | 3598e905aa | |
Bas Bossink | 3f002979c4 | |
Bas Bossink | 742dbbc917 | |
Bas Bossink | 991a725c26 | |
Donald Pinckney | 317c7731da | |
Donald Pinckney | 4c17b11ed0 | |
Michael Bryan | 005dfc55bf | |
Desmond | 8c86031384 | |
Stefanie Jäger | 5151aae07e | |
Michael Bryan | 42b87e0fbc | |
Matt Ickstadt | 33add4b532 | |
Michael Bryan | b0513ee771 | |
Michael Bryan | b4538da9c3 | |
Michael Bryan | 7ac3e50b37 | |
yoshimura masataka | 13a9aab2b2 | |
Jason Liquorish | eccec9bb52 | |
Michael Bryan | e63f53fe47 | |
Michael Bryan | 2c20c99d4a | |
Michael Bryan | c6125b184f | |
Michael Bryan | dfb6e3cb10 | |
Michael Bryan | 41071a5dd9 | |
Michael Bryan | f6a7432569 | |
Michael Bryan | 89ea60e7a5 | |
Jason Liquorish | 10b69e60c8 | |
Jason Liquorish | 336e08fe50 | |
Jason Liquorish | 5bfdf9fcc8 | |
Jason Liquorish | d2565af000 | |
Jason Liquorish | 599e47f1f1 | |
Jason Liquorish | 0c31ab2953 | |
Jan-Erik Rediger | f654c42426 | |
Weihang Lo | d729a762fe | |
Weihang Lo | 43b3d157d9 | |
Matt Ickstadt | a9f3be6f44 | |
Matt Ickstadt | 34356b87a0 |
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
*.rs rust
|
*.rs rust
|
||||||
*.woff -text
|
*.woff binary
|
||||||
*.ttf -text
|
*.ttf binary
|
||||||
*.otf -text
|
*.otf binary
|
||||||
*.png -text
|
*.png binary
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
name: Bug Report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels: ["C-bug"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: Thanks for filing a 🐛 bug report 😄!
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Problem
|
||||||
|
description: >
|
||||||
|
Please provide a clear and concise description of what the bug is,
|
||||||
|
including what currently happens and what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: Steps
|
||||||
|
description: Please list the steps to reproduce the bug.
|
||||||
|
placeholder: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
- type: textarea
|
||||||
|
id: possible-solutions
|
||||||
|
attributes:
|
||||||
|
label: Possible Solution(s)
|
||||||
|
description: >
|
||||||
|
Not obligatory, but suggest a fix/reason for the bug,
|
||||||
|
or ideas how to implement the addition or change.
|
||||||
|
- type: textarea
|
||||||
|
id: notes
|
||||||
|
attributes:
|
||||||
|
label: Notes
|
||||||
|
description: Provide any additional notes that might be helpful.
|
||||||
|
- type: textarea
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: >
|
||||||
|
Please paste the output of running `mdbook --version` or which version
|
||||||
|
of the library you are using.
|
||||||
|
render: text
|
|
@ -0,0 +1,28 @@
|
||||||
|
name: Enhancement
|
||||||
|
description: Suggest an idea for enhancing mdBook
|
||||||
|
labels: ["C-enhancement"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for filing a 🙋 feature request 😄!
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: Problem
|
||||||
|
description: >
|
||||||
|
Please provide a clear description of your use case and the problem
|
||||||
|
this feature request is trying to solve.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: Proposed Solution
|
||||||
|
description: >
|
||||||
|
Please provide a clear and concise description of what you want to happen.
|
||||||
|
- type: textarea
|
||||||
|
id: notes
|
||||||
|
attributes:
|
||||||
|
label: Notes
|
||||||
|
description: Provide any additional context or information that might be helpful.
|
|
@ -0,0 +1,24 @@
|
||||||
|
name: Question
|
||||||
|
description: Have a question on how to use mdBook?
|
||||||
|
labels: ["C-question"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Got a question on how to do something with mdBook?
|
||||||
|
- type: textarea
|
||||||
|
id: question
|
||||||
|
attributes:
|
||||||
|
label: Question
|
||||||
|
description: >
|
||||||
|
Enter your question here. Please try to provide as much detail as possible.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: >
|
||||||
|
Please paste the output of running `mdbook --version` or which version
|
||||||
|
of the library you are using.
|
||||||
|
render: text
|
|
@ -0,0 +1,73 @@
|
||||||
|
name: Deploy
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Deploy Release
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target:
|
||||||
|
- aarch64-unknown-linux-musl
|
||||||
|
- x86_64-unknown-linux-gnu
|
||||||
|
- x86_64-unknown-linux-musl
|
||||||
|
- x86_64-apple-darwin
|
||||||
|
- x86_64-pc-windows-msvc
|
||||||
|
include:
|
||||||
|
- target: aarch64-unknown-linux-musl
|
||||||
|
os: ubuntu-20.04
|
||||||
|
- target: x86_64-unknown-linux-gnu
|
||||||
|
os: ubuntu-20.04
|
||||||
|
- target: x86_64-unknown-linux-musl
|
||||||
|
os: ubuntu-20.04
|
||||||
|
- target: x86_64-apple-darwin
|
||||||
|
os: macos-latest
|
||||||
|
- target: x86_64-pc-windows-msvc
|
||||||
|
os: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- name: Install Rust
|
||||||
|
run: ci/install-rust.sh stable ${{ matrix.target }}
|
||||||
|
- name: Build asset
|
||||||
|
run: ci/make-release-asset.sh ${{ matrix.os }} ${{ matrix.target }}
|
||||||
|
- name: Update release with new asset
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: gh release upload $MDBOOK_TAG $MDBOOK_ASSET
|
||||||
|
pages:
|
||||||
|
name: GitHub Pages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- name: Install Rust (rustup)
|
||||||
|
run: rustup update stable --no-self-update && rustup default stable
|
||||||
|
- name: Build book
|
||||||
|
run: cargo run -- build guide
|
||||||
|
- name: Deploy to GitHub
|
||||||
|
env:
|
||||||
|
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
|
||||||
|
run: |
|
||||||
|
touch guide/book/.nojekyll
|
||||||
|
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
|
||||||
|
cd guide/book
|
||||||
|
/tmp/deploy
|
||||||
|
publish:
|
||||||
|
name: Publish to crates.io
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- name: Install Rust (rustup)
|
||||||
|
run: rustup update stable --no-self-update && rustup default stable
|
||||||
|
- name: Publish
|
||||||
|
env:
|
||||||
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
|
run: cargo publish --no-verify
|
|
@ -0,0 +1,66 @@
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
merge_group:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
build: [stable, beta, nightly, macos, windows, msrv]
|
||||||
|
include:
|
||||||
|
- build: stable
|
||||||
|
os: ubuntu-latest
|
||||||
|
rust: stable
|
||||||
|
- build: beta
|
||||||
|
os: ubuntu-latest
|
||||||
|
rust: beta
|
||||||
|
- build: nightly
|
||||||
|
os: ubuntu-latest
|
||||||
|
rust: nightly
|
||||||
|
- build: macos
|
||||||
|
os: macos-latest
|
||||||
|
rust: stable
|
||||||
|
- build: windows
|
||||||
|
os: windows-latest
|
||||||
|
rust: stable
|
||||||
|
- build: msrv
|
||||||
|
os: ubuntu-20.04
|
||||||
|
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
|
||||||
|
rust: 1.71.0
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Install Rust
|
||||||
|
run: bash ci/install-rust.sh ${{ matrix.rust }}
|
||||||
|
- name: Build and run tests
|
||||||
|
run: cargo test --locked
|
||||||
|
- name: Test no default
|
||||||
|
run: cargo test --no-default-features
|
||||||
|
|
||||||
|
rustfmt:
|
||||||
|
name: Rustfmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Install Rust
|
||||||
|
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||||
|
- run: cargo fmt --check
|
||||||
|
|
||||||
|
# The success job is here to consolidate the total success/failure state of
|
||||||
|
# all other jobs. This job is then included in the GitHub branch protection
|
||||||
|
# rule which prevents merges unless all other jobs are passing. This makes
|
||||||
|
# it easier to manage the list of jobs via this yml file and to prevent
|
||||||
|
# accidentally adding new jobs without also updating the branch protections.
|
||||||
|
success:
|
||||||
|
name: Success gate
|
||||||
|
if: always()
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
- rustfmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'
|
||||||
|
- name: Done
|
||||||
|
run: exit 0
|
|
@ -4,8 +4,15 @@ target
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
book-test
|
book-test
|
||||||
book-example/book
|
guide/book
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
tests/dummy_book/book/
|
tests/dummy_book/book/
|
||||||
|
test_book/book/
|
||||||
|
|
||||||
|
# Ignore Jetbrains specific files.
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Ignore Vim temporary and swap files.
|
||||||
|
*.sw?
|
||||||
|
*~
|
||||||
|
|
48
.travis.yml
48
.travis.yml
|
@ -1,48 +0,0 @@
|
||||||
language: rust
|
|
||||||
|
|
||||||
rust:
|
|
||||||
- stable
|
|
||||||
- beta
|
|
||||||
- nightly
|
|
||||||
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
- osx
|
|
||||||
|
|
||||||
cache:
|
|
||||||
timeout: 360
|
|
||||||
cargo: true
|
|
||||||
|
|
||||||
before_cache:
|
|
||||||
- chmod -R a+r $HOME/.cargo
|
|
||||||
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- CRATE_NAME=mdbook
|
|
||||||
|
|
||||||
script:
|
|
||||||
- cargo test --all
|
|
||||||
- cargo test --all --no-default-features
|
|
||||||
|
|
||||||
before_deploy:
|
|
||||||
- sh ci/before_deploy.sh
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
provider: releases
|
|
||||||
api_key:
|
|
||||||
- secure: cURRWBr034iqBz/ifD7uOunBfNR30YxIXfgLX0osWz+iafkVbhDGYYz9sBmRraqO2P7L2koEXMADVb/md1kI2+ykiq/ml+l9zuEAZPVmvSGUN7ZD+7s+lu3l5OBPG5z175T+b2q2q2m8XVR7TW20ra4QbE0bq06KAoOyjSgQVBTSCYsL9uTsGwiVRMEqqJT/BmKhKJNkpGsTKyBSKkOXvfeAAbE260vXUDEN9TYdJ3fvteRrpwLX56ee64gIZUq0RjDc4SKIEqilM6iUtNMvurqaewYNGkiXKRruV6BPCHxEHo6NNT46kOJLBJTf7gZw//dWhSoWpg9P0gdAnPWm407kSa3F7aJ1eRShAFQ4BLyfz9efTqm+jP3fOp7Mm7igSh9w6caSRuOnSsUf5+raRQ8E5Y9HsWGzzpZQk24Fx9EGZ04EeDSdpZAFz+jcbMpHf8t2p4CEx0CCNwYvKx6EydMKbMF5QteQ8SQkXNLhv7Rz2OgtXWYZPRVCMfQfOplsi2InsLCrQxTgwh+6u654SqVSgaHG+IncEAxBrdWy4rHcg7qereUcKfcY3k96vaDxdn/T2c00Ig0aNFR91YnixGMd6J6tQgDcRK9jh6fUm1CCBE9hT+pNUmtgYKuWBoLZexUZFFnfuBed0WciBot1bGDDamndqKq0jJiAzg+GMHk=
|
|
||||||
file_glob: true
|
|
||||||
file: "$CRATE_NAME-$TRAVIS_TAG-$TARGET.*"
|
|
||||||
on:
|
|
||||||
condition: "$TRAVIS_RUST_VERSION = stable"
|
|
||||||
tags: true
|
|
||||||
skip_cleanup: true
|
|
||||||
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- /^v\d+\.\d+\.\d+.*$/
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email:
|
|
||||||
on_success: never
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
||||||
|
# The Rust Code of Conduct
|
||||||
|
|
||||||
|
The Code of Conduct for this repository [can be found online](https://www.rust-lang.org/conduct.html).
|
134
CONTRIBUTING.md
134
CONTRIBUTING.md
|
@ -5,33 +5,41 @@ Welcome stranger!
|
||||||
If you have come here to learn how to contribute to mdBook, we have some tips for you!
|
If you have come here to learn how to contribute to mdBook, we have some tips for you!
|
||||||
|
|
||||||
First of all, don't hesitate to ask questions!
|
First of all, don't hesitate to ask questions!
|
||||||
Use the [issue tracker](https://github.com/rust-lang-nursery/mdBook/issues), no question is too simple.
|
Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple.
|
||||||
If we don't respond in a couple of days, ping us @Michael-F-Bryan, @budziq, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
|
|
||||||
|
### Issue assignment
|
||||||
|
|
||||||
|
**:warning: Important :warning:**
|
||||||
|
|
||||||
|
Before working on pull request, please ping us on the corresponding issue.
|
||||||
|
The current PR backlog is beyond what we can process at this time.
|
||||||
|
Only issues that have an [`E-Help-wanted`](https://github.com/rust-lang/mdBook/labels/E-Help-wanted) or [`Feature accepted`](https://github.com/rust-lang/mdBook/labels/Feature%20accepted) label will likely receive reviews.
|
||||||
|
If there isn't already an open issue for what you want to work on, please open one first to see if it is something we would be available to review.
|
||||||
|
|
||||||
### Issues to work on
|
### Issues to work on
|
||||||
|
|
||||||
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
|
If you are starting out, you might be interested in the
|
||||||
[E-Easy issues](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
[E-Easy issues](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||||
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
|
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
|
||||||
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
|
These issues can be a good launching pad for more involved issues.
|
||||||
include documentation improvements, new tests, examples, updating dependencies, etc.
|
Easy tasks for a first time contribution include documentation improvements, new tests, examples, updating dependencies, etc.
|
||||||
|
|
||||||
If you come from a web development background, you might be interested in issues related to web technologies tagged
|
If you come from a web development background, you might be interested in issues related to web technologies tagged
|
||||||
[A-JavaScript](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
[A-JavaScript](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||||
[A-Style](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
[A-Style](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||||
[A-HTML](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
[A-HTML](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||||
[A-Mobile](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
[A-Mobile](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||||
|
|
||||||
When you decide you want to work on a specific issue, ping us on that issue so that we can assign it to you.
|
When you decide you want to work on a specific issue, and it isn't already assigned to someone else, assign the issue to yourself by leaving a comment with the text `@rustbot claim`.
|
||||||
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
|
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
|
||||||
|
|
||||||
Issues on the issue tracker are categorized with the following labels:
|
Issues on the issue tracker are categorized with the following labels:
|
||||||
|
|
||||||
- **A**-prefixed labels state which area of the project an issue relates to.
|
- **A**-prefixed labels state which area of the project an issue relates to.
|
||||||
- **E**-prefixed labels show an estimate of the experience necessary to fix the issue.
|
- **E**-prefixed labels show an estimate of the experience necessary to fix the issue.
|
||||||
- **M**-prefixed labels are meta-issues used for questions, discussions, or tracking issues
|
- **M**-prefixed labels are meta-issues regarding the management of the mdBook project itself
|
||||||
- **S**-prefixed labels show the status of the issue
|
- **S**-prefixed labels show the status of the issue
|
||||||
- **T**-prefixed labels show the type of issue
|
- **C**-prefixed labels show the category of issue
|
||||||
|
|
||||||
### Building mdBook
|
### Building mdBook
|
||||||
|
|
||||||
|
@ -41,12 +49,12 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||||
0. Clone this repository with git.
|
0. Clone this repository with git.
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
git clone https://github.com/rust-lang/mdBook.git
|
||||||
```
|
```
|
||||||
0. Navigate into the newly created `mdBook` directory
|
0. Navigate into the newly created `mdBook` directory
|
||||||
0. Run `cargo build`
|
0. Run `cargo build`
|
||||||
|
|
||||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
The resulting binary can be found in `mdBook/target/debug/` under the name `mdbook` or `mdbook.exe`.
|
||||||
|
|
||||||
### Code Quality
|
### Code Quality
|
||||||
|
|
||||||
|
@ -57,12 +65,12 @@ We love code quality and Rust has some excellent tools to assist you with contri
|
||||||
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
|
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
|
||||||
This will ensure we have good quality source code that is better for us all to maintain.
|
This will ensure we have good quality source code that is better for us all to maintain.
|
||||||
|
|
||||||
[rustfmt](https://github.com/rust-lang-nursery/rustfmt) has a lot more information on the project.
|
[rustfmt](https://github.com/rust-lang/rustfmt) has a lot more information on the project.
|
||||||
The quick guide is
|
The quick guide is
|
||||||
|
|
||||||
1. Install it
|
1. Install it (`rustfmt` is usually installed by default via [rustup](https://rustup.rs/)):
|
||||||
```
|
```
|
||||||
rustup component add rustfmt-preview
|
rustup component add rustfmt
|
||||||
```
|
```
|
||||||
1. You can now run `rustfmt` on a single file simply by...
|
1. You can now run `rustfmt` on a single file simply by...
|
||||||
```
|
```
|
||||||
|
@ -72,40 +80,96 @@ The quick guide is
|
||||||
```
|
```
|
||||||
cargo fmt
|
cargo fmt
|
||||||
```
|
```
|
||||||
When run through `cargo` it will format all bin and lib files in the current crate.
|
When run through `cargo` it will format all bin and lib files in the current package.
|
||||||
|
|
||||||
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang-nursery/rustfmt)
|
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang/rustfmt)
|
||||||
|
|
||||||
|
|
||||||
#### Finding Issues with Clippy
|
#### Finding Issues with Clippy
|
||||||
|
|
||||||
Clippy is a code analyser/linter detecting mistakes, and therfore helps to improve your code.
|
[Clippy](https://doc.rust-lang.org/clippy/) is a code analyser/linter detecting mistakes, and therefore helps to improve your code.
|
||||||
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
|
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will help us maintain awesome code.
|
||||||
help us maintain awesome code.
|
|
||||||
|
|
||||||
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang-nursery/rust-clippy)
|
|
||||||
|
|
||||||
1. To install
|
1. To install
|
||||||
```
|
```
|
||||||
rustup update
|
rustup component add clippy
|
||||||
rustup install nightly
|
|
||||||
rustup component add clippy-preview --toolchain=nightly
|
|
||||||
```
|
```
|
||||||
2. Running clippy
|
2. Running clippy
|
||||||
As you may notice from the previous step, Clippy is on the nightly branch, so running it is like
|
|
||||||
```
|
```
|
||||||
cargo +nightly clippy
|
cargo clippy
|
||||||
```
|
```
|
||||||
|
|
||||||
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang-nursery.github.io/rust-clippy/master/index.html).
|
### Change requirements
|
||||||
|
|
||||||
|
Please consider the following when making a change:
|
||||||
|
|
||||||
|
* Almost all changes that modify the Rust code must be accompanied with a test.
|
||||||
|
|
||||||
|
* Almost all features and changes must update the documentation.
|
||||||
|
mdBook has the [mdBook Guide](https://rust-lang.github.io/mdBook/) whose source is at <https://github.com/rust-lang/mdBook/tree/master/guide>.
|
||||||
|
|
||||||
|
* Almost all Rust items should be documented with doc comments.
|
||||||
|
See the [Rustdoc Book](https://doc.rust-lang.org/rustdoc/) for more information on writing doc comments.
|
||||||
|
|
||||||
|
* Breaking the API can only be done in major SemVer releases.
|
||||||
|
These are done very infrequently, so it is preferred to avoid these when possible.
|
||||||
|
See [SemVer Compatibility](https://doc.rust-lang.org/cargo/reference/semver.html) for more information on what a SemVer breaking change is.
|
||||||
|
|
||||||
|
(Note: At this time, some SemVer breaking changes are inevitable due to the current code structure.
|
||||||
|
An example is adding new fields to the config structures.
|
||||||
|
These are intended to be fixed in the next major release.)
|
||||||
|
|
||||||
|
* Similarly, the CLI interface is considered to be stable.
|
||||||
|
Care should be taken to avoid breaking existing workflows.
|
||||||
|
|
||||||
|
* Check out the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) for guidelines on designing the API.
|
||||||
|
|
||||||
### Making a pull-request
|
### Making a pull-request
|
||||||
|
|
||||||
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||||
One of the core maintainers will then approve the changes or request some changes before it gets merged.
|
One of the core maintainers will then approve the changes or request some changes before it gets merged.
|
||||||
|
|
||||||
If you want to make your pull-request even better, you might want to run [Clippy](https://github.com/Manishearth/rust-clippy)
|
|
||||||
and [rustfmt](https://github.com/rust-lang-nursery/rustfmt) on the code first.
|
|
||||||
This is not a requirement though and will never block a pull-request from being merged.
|
|
||||||
|
|
||||||
That's it, happy contributions! :tada: :tada: :tada:
|
That's it, happy contributions! :tada: :tada: :tada:
|
||||||
|
|
||||||
|
## Browser compatibility and testing
|
||||||
|
|
||||||
|
Currently we don't have a strict browser compatibility matrix due to our limited resources.
|
||||||
|
We generally strive to keep mdBook compatible with a relatively recent browser on all of the most major platforms.
|
||||||
|
That is, supporting Chrome, Safari, Firefox, Edge on Windows, macOS, Linux, iOS, and Android.
|
||||||
|
If possible, do your best to avoid breaking older browser releases.
|
||||||
|
|
||||||
|
Any change to the HTML or styling is encouraged to manually check on as many browsers and platforms that you can.
|
||||||
|
Unfortunately at this time we don't have any automated UI or browser testing, so your assistance in testing is appreciated.
|
||||||
|
|
||||||
|
## Updating highlight.js
|
||||||
|
|
||||||
|
The following are instructions for updating [highlight.js](https://highlightjs.org/).
|
||||||
|
|
||||||
|
1. Clone the repository at <https://github.com/highlightjs/highlight.js>
|
||||||
|
1. Check out a tagged release (like `10.1.1`).
|
||||||
|
1. Run `npm install`
|
||||||
|
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx nim nix properties r scala x86asm yaml`
|
||||||
|
1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`.
|
||||||
|
1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js).
|
||||||
|
1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release.
|
||||||
|
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [test_book](https://github.com/rust-lang/mdBook/tree/master/test_book) contains a chapter with many languages to examine.
|
||||||
|
|
||||||
|
## Publishing new releases
|
||||||
|
|
||||||
|
Instructions for mdBook maintainers to publish a new release:
|
||||||
|
|
||||||
|
1. Create a PR to update the version and update the CHANGELOG:
|
||||||
|
1. Update the version in `Cargo.toml`
|
||||||
|
2. Run `cargo test` to verify that everything is passing, and to update `Cargo.lock`.
|
||||||
|
3. Double-check for any SemVer breaking changes.
|
||||||
|
Try [`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks), though beware that the current version of mdBook isn't properly adhering to SemVer due to the lack of `#[non_exhaustive]` and other issues. See https://github.com/rust-lang/mdBook/issues/1835.
|
||||||
|
4. Update `CHANGELOG.md` with any changes that users may be interested in.
|
||||||
|
5. Update `continuous-integration.md` to update the version number for the installation instructions.
|
||||||
|
6. Commit the changes, and open a PR.
|
||||||
|
2. After the PR has been merged, create a release in GitHub. This can either be done in the GitHub web UI, or on the command-line:
|
||||||
|
```bash
|
||||||
|
MDBOOK_VERS="`cargo read-manifest | jq -r .version`" ; \
|
||||||
|
gh release create -R rust-lang/mdbook v$MDBOOK_VERS \
|
||||||
|
--title v$MDBOOK_VERS \
|
||||||
|
--notes "See https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md#mdbook-${MDBOOK_VERS//.} for a complete list of changes."
|
||||||
|
```
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
88
Cargo.toml
88
Cargo.toml
|
@ -1,66 +1,74 @@
|
||||||
[package]
|
[package]
|
||||||
name = "mdbook"
|
name = "mdbook"
|
||||||
version = "0.2.2-alpha.0"
|
version = "0.4.37"
|
||||||
authors = [
|
authors = [
|
||||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||||
"Matt Ickstadt <mattico8@gmail.com>"
|
"Matt Ickstadt <mattico8@gmail.com>"
|
||||||
]
|
]
|
||||||
description = "Create books from markdown files"
|
documentation = "https://rust-lang.github.io/mdBook/index.html"
|
||||||
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
|
edition = "2021"
|
||||||
repository = "https://github.com/rust-lang-nursery/mdBook"
|
exclude = ["/guide/*"]
|
||||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
exclude = ["book-example/*"]
|
repository = "https://github.com/rust-lang/mdBook"
|
||||||
|
description = "Creates a book from markdown files"
|
||||||
|
rust-version = "1.71"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.24"
|
anyhow = "1.0.71"
|
||||||
chrono = "0.4"
|
chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
|
||||||
handlebars = "1.0"
|
clap = { version = "4.3.12", features = ["cargo", "wrap_help"] }
|
||||||
serde = "1.0"
|
clap_complete = "4.3.2"
|
||||||
serde_derive = "1.0"
|
once_cell = "1.17.1"
|
||||||
error-chain = "0.12"
|
env_logger = "0.11.1"
|
||||||
serde_json = "1.0"
|
handlebars = "5.0"
|
||||||
pulldown-cmark = "0.1.2"
|
log = "0.4.17"
|
||||||
lazy_static = "1.0"
|
memchr = "2.5.0"
|
||||||
log = "0.4"
|
opener = "0.6.1"
|
||||||
env_logger = "0.5"
|
pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] }
|
||||||
toml = "0.4"
|
regex = "1.8.1"
|
||||||
memchr = "2.0"
|
serde = { version = "1.0.163", features = ["derive"] }
|
||||||
open = "1.1"
|
serde_json = "1.0.96"
|
||||||
regex = "1.0.0"
|
shlex = "1.3.0"
|
||||||
tempfile = "3.0"
|
tempfile = "3.4.0"
|
||||||
itertools = "0.7"
|
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
|
||||||
shlex = "0.1"
|
topological-sort = "0.2.2"
|
||||||
toml-query = "0.7"
|
|
||||||
|
|
||||||
# Watch feature
|
# Watch feature
|
||||||
notify = { version = "4.0", optional = true }
|
notify = { version = "6.1.1", optional = true }
|
||||||
|
notify-debouncer-mini = { version = "0.4.1", optional = true }
|
||||||
|
ignore = { version = "0.4.20", optional = true }
|
||||||
|
pathdiff = { version = "0.2.1", optional = true }
|
||||||
|
|
||||||
# Serve feature
|
# Serve feature
|
||||||
iron = { version = "0.6", optional = true }
|
futures-util = { version = "0.3.28", optional = true }
|
||||||
staticfile = { version = "0.5", optional = true }
|
tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||||
ws = { version = "0.7", optional = true}
|
warp = { version = "0.3.6", default-features = false, features = ["websocket"], optional = true }
|
||||||
|
|
||||||
# Search feature
|
# Search feature
|
||||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
elasticlunr-rs = { version = "3.0.2", optional = true }
|
||||||
ammonia = { version = "1.1", optional = true }
|
ammonia = { version = "3.3.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
select = "0.4"
|
assert_cmd = "2.0.11"
|
||||||
pretty_assertions = "0.5"
|
predicates = "3.0.3"
|
||||||
walkdir = "2.0"
|
select = "0.6.0"
|
||||||
pulldown-cmark-to-cmark = "1.1.0"
|
semver = "1.0.17"
|
||||||
|
pretty_assertions = "1.3.0"
|
||||||
|
walkdir = "2.3.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["output", "watch", "serve", "search"]
|
default = ["watch", "serve", "search"]
|
||||||
debug = []
|
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff"]
|
||||||
output = []
|
serve = ["dep:futures-util", "dep:tokio", "dep:warp"]
|
||||||
watch = ["notify"]
|
search = ["dep:elasticlunr-rs", "dep:ammonia"]
|
||||||
serve = ["iron", "staticfile", "ws"]
|
|
||||||
search = ["elasticlunr-rs", "ammonia"]
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
doc = false
|
doc = false
|
||||||
name = "mdbook"
|
name = "mdbook"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "nop-preprocessor"
|
||||||
|
test = true
|
||||||
|
|
242
README.md
242
README.md
|
@ -1,244 +1,20 @@
|
||||||
# mdBook
|
# mdBook
|
||||||
|
|
||||||
<table>
|
[![Build Status](https://github.com/rust-lang/mdBook/workflows/CI/badge.svg?event=push)](https://github.com/rust-lang/mdBook/actions?workflow=CI)
|
||||||
<tr>
|
[![crates.io](https://img.shields.io/crates/v/mdbook.svg)](https://crates.io/crates/mdbook)
|
||||||
<td><strong>Linux / OS X</strong></td>
|
[![LICENSE](https://img.shields.io/github/license/rust-lang/mdBook.svg)](LICENSE)
|
||||||
<td>
|
|
||||||
<a href="https://travis-ci.org/rust-lang-nursery/mdBook"><img src="https://travis-ci.org/rust-lang-nursery/mdBook.svg?branch=master"></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Windows</strong></td>
|
|
||||||
<td>
|
|
||||||
<a href="https://ci.appveyor.com/project/rust-lang-libs/mdbook"><img src="https://ci.appveyor.com/api/projects/status/ysyke2rvo85sni55?svg=true"></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2">
|
|
||||||
<a href="https://crates.io/crates/mdbook"><img src="https://img.shields.io/crates/v/mdbook.svg"></a>
|
|
||||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg"></a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
mdBook is a utility to create modern online books from Markdown files.
|
mdBook is a utility to create modern online books from Markdown files.
|
||||||
|
|
||||||
|
Check out the **[User Guide]** for a list of features and installation and usage information.
|
||||||
|
The User Guide also serves as a demonstration to showcase what a book looks like.
|
||||||
|
|
||||||
## What does it look like?
|
If you are interested in contributing to the development of mdBook, check out the [Contribution Guide].
|
||||||
|
|
||||||
The [User Guide] for mdBook has been written in Markdown and is using mdBook to
|
|
||||||
generate the online book-like website you can read. The documentation uses the
|
|
||||||
latest version on GitHub and showcases the available features.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
There are multiple ways to install mdBook.
|
|
||||||
|
|
||||||
1. **Binaries**
|
|
||||||
|
|
||||||
Binaries are available for download [here][releases]. Make sure to put the
|
|
||||||
path to the binary into your `PATH`.
|
|
||||||
|
|
||||||
2. **From Crates.io**
|
|
||||||
|
|
||||||
This requires at least [Rust] 1.20 and Cargo to be installed. Once you have installed
|
|
||||||
Rust, type the following in the terminal:
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo install mdbook
|
|
||||||
```
|
|
||||||
|
|
||||||
This will download and compile mdBook for you, the only thing left to do is
|
|
||||||
to add the Cargo bin directory to your `PATH`.
|
|
||||||
|
|
||||||
**Note for automatic deployment**
|
|
||||||
|
|
||||||
If you are using a script to do automatic deployments using Travis or
|
|
||||||
another CI server, we recommend that you specify a semver version range for
|
|
||||||
mdBook when you install it through your script!
|
|
||||||
|
|
||||||
This will constrain the server to install the latests **non-breaking**
|
|
||||||
version of mdBook and will prevent your books from failing to build because
|
|
||||||
we released a new version. For example:
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo install mdbook --vers "^0.1.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **From Git**
|
|
||||||
|
|
||||||
The version published to crates.io will ever so slightly be behind the
|
|
||||||
version hosted here on GitHub. If you need the latest version you can build
|
|
||||||
the git version of mdBook yourself. Cargo makes this ***super easy***!
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo install --git https://github.com/rust-lang-nursery/mdBook.git mdbook
|
|
||||||
```
|
|
||||||
|
|
||||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
|
||||||
|
|
||||||
4. **For Contributions**
|
|
||||||
|
|
||||||
If you want to contribute to mdBook you will have to clone the repository on
|
|
||||||
your local machine:
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
|
||||||
```
|
|
||||||
|
|
||||||
`cd` into `mdBook/` and run
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo build
|
|
||||||
```
|
|
||||||
|
|
||||||
The resulting binary can be found in `mdBook/target/debug/` under the name
|
|
||||||
`mdBook` or `mdBook.exe`.
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
mdBook will primarily be used as a command line tool, even though it exposes
|
|
||||||
all its functionality as a Rust crate for integration in other projects.
|
|
||||||
|
|
||||||
Here are the main commands you will want to run. For a more exhaustive
|
|
||||||
explanation, check out the [User Guide].
|
|
||||||
|
|
||||||
- `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 [CLI docs] for more information and some neat tricks.
|
|
||||||
|
|
||||||
- `mdbook build`
|
|
||||||
|
|
||||||
This is the command you will run to render your book, it reads the
|
|
||||||
`SUMMARY.md` file to understand the structure of your book, takes the
|
|
||||||
markdown files in the source directory as input and outputs static html
|
|
||||||
pages that you can upload to a server.
|
|
||||||
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
- `mdbook clean`
|
|
||||||
|
|
||||||
Delete directory in which generated book is located.
|
|
||||||
|
|
||||||
### 3rd Party Plugins
|
|
||||||
|
|
||||||
The way a book is loaded and rendered can be configured by the user via third
|
|
||||||
party plugins. These plugins are just programs which will be invoked during the
|
|
||||||
build process and are split into roughly two categories, *preprocessors* and
|
|
||||||
*renderers*.
|
|
||||||
|
|
||||||
Preprocessors are used to transform a book before it is sent to a renderer.
|
|
||||||
One example would be to replace all occurrences of
|
|
||||||
`{{#include some_file.ext}}` with the contents of that file. Some existing
|
|
||||||
preprocessors are:
|
|
||||||
|
|
||||||
- `index` - a built-in preprocessor (enabled by default) which will transform
|
|
||||||
all `README.md` chapters to `index.md` so `foo/README.md` can be accessed via
|
|
||||||
the url `foo/` when published to a browser
|
|
||||||
- `links` - a built-in preprocessor (enabled by default) for expanding the
|
|
||||||
`{{# playpen}}` and `{{# include}}` helpers in a chapter.
|
|
||||||
|
|
||||||
Renderers are given the final book so they can do something with it. This is
|
|
||||||
typically used for, as the name suggests, rendering the document in a particular
|
|
||||||
format, however there's nothing stopping a renderer from doing static analysis
|
|
||||||
of a book in order to validate links or run tests. Some existing renderers are:
|
|
||||||
|
|
||||||
- `html` - the built-in renderer which will generate a HTML version of the book
|
|
||||||
- [`linkcheck`] - a backend which will check that all links are valid
|
|
||||||
- [`epub`] - an experimental EPUB generator
|
|
||||||
|
|
||||||
> **Note for Developers:** Feel free to send us a PR if you've developed your
|
|
||||||
> own plugin and want it mentioned here.
|
|
||||||
|
|
||||||
A preprocessor or renderer is enabled by installing the appropriate program and
|
|
||||||
then mentioning it in the book's `book.toml` file.
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ cargo install mdbook-linkcheck
|
|
||||||
$ edit book.toml && cat book.toml
|
|
||||||
[book]
|
|
||||||
title = "My Awesome Book"
|
|
||||||
authors = ["Michael-F-Bryan"]
|
|
||||||
|
|
||||||
[output.html]
|
|
||||||
|
|
||||||
[output.linkcheck] # enable the "mdbook-linkcheck" renderer
|
|
||||||
|
|
||||||
$ mdbook build
|
|
||||||
2018-10-20 13:57:51 [INFO] (mdbook::book): Book building has started
|
|
||||||
2018-10-20 13:57:51 [INFO] (mdbook::book): Running the html backend
|
|
||||||
2018-10-20 13:57:53 [INFO] (mdbook::book): Running the linkcheck backend
|
|
||||||
```
|
|
||||||
|
|
||||||
For more information on the plugin system, consult the [User Guide].
|
|
||||||
|
|
||||||
### As a library
|
|
||||||
|
|
||||||
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 [User Guide] and the [API docs] for more information.
|
|
||||||
|
|
||||||
## Contributions
|
|
||||||
|
|
||||||
Contributions are highly appreciated and encouraged! Don't hesitate to
|
|
||||||
participate to discussions in the issues, propose new features and ask for
|
|
||||||
help.
|
|
||||||
|
|
||||||
If you are just starting out with Rust, there are a series of issus that are
|
|
||||||
tagged [E-Easy] and **we will gladly mentor you** so that you can successfully
|
|
||||||
go through the process of fixing a bug or adding a new feature! Let us know if
|
|
||||||
you need any help.
|
|
||||||
|
|
||||||
For more info about contributing, check out our [contribution guide] who helps
|
|
||||||
you go through the build and contribution process!
|
|
||||||
|
|
||||||
There is also a [rendered version][master-docs] of the latest API docs
|
|
||||||
available, for those hacking on `master`.
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE] file.
|
All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE] file.
|
||||||
|
|
||||||
|
[User Guide]: https://rust-lang.github.io/mdBook/
|
||||||
[User Guide]: https://rust-lang-nursery.github.io/mdBook/
|
[contribution guide]: https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md
|
||||||
[API docs]: https://docs.rs/mdbook/*/mdbook/
|
[LICENSE]: https://github.com/rust-lang/mdBook/blob/master/LICENSE
|
||||||
[E-Easy]: https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy
|
|
||||||
[contribution guide]: https://github.com/rust-lang-nursery/mdBook/blob/master/CONTRIBUTING.md
|
|
||||||
[LICENSE]: https://github.com/rust-lang-nursery/mdBook/blob/master/LICENSE
|
|
||||||
[releases]: https://github.com/rust-lang-nursery/mdBook/releases
|
|
||||||
[Rust]: https://www.rust-lang.org/
|
|
||||||
[CLI docs]: http://rust-lang-nursery.github.io/mdBook/cli/init.html
|
|
||||||
[master-docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
|
|
||||||
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
|
|
||||||
[`epub`]: https://crates.io/crates/mdbook-epub
|
|
||||||
|
|
64
appveyor.yml
64
appveyor.yml
|
@ -1,64 +0,0 @@
|
||||||
environment:
|
|
||||||
global:
|
|
||||||
PROJECT_NAME: mdBook
|
|
||||||
matrix:
|
|
||||||
# Stable channel
|
|
||||||
- TARGET: i686-pc-windows-msvc
|
|
||||||
RUST_CHANNEL: stable
|
|
||||||
- TARGET: x86_64-pc-windows-msvc
|
|
||||||
RUST_CHANNEL: stable
|
|
||||||
# Beta channel
|
|
||||||
- TARGET: i686-pc-windows-msvc
|
|
||||||
RUST_CHANNEL: beta
|
|
||||||
- TARGET: x86_64-pc-windows-msvc
|
|
||||||
RUST_CHANNEL: beta
|
|
||||||
# Nightly channel
|
|
||||||
- TARGET: i686-pc-windows-msvc
|
|
||||||
RUST_CHANNEL: nightly
|
|
||||||
- TARGET: x86_64-pc-windows-msvc
|
|
||||||
RUST_CHANNEL: nightly
|
|
||||||
|
|
||||||
# Install Rust and Cargo
|
|
||||||
install:
|
|
||||||
- ps: >-
|
|
||||||
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
|
|
||||||
$Env:PATH += ';C:\msys64\mingw64\bin'
|
|
||||||
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
|
|
||||||
$Env:PATH += ';C:\msys64\mingw32\bin'
|
|
||||||
}
|
|
||||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
|
|
||||||
- rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_CHANNEL%
|
|
||||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
|
||||||
- rustc -Vv
|
|
||||||
- cargo -V
|
|
||||||
|
|
||||||
build: false
|
|
||||||
|
|
||||||
# Equivalent to Travis' `script` phase
|
|
||||||
test_script:
|
|
||||||
- cargo test --all
|
|
||||||
- cargo test --all --no-default-features
|
|
||||||
|
|
||||||
before_deploy:
|
|
||||||
# Generate artifacts for release
|
|
||||||
- cargo rustc --bin mdbook --release -- -C lto
|
|
||||||
- mkdir staging
|
|
||||||
- copy target\release\mdbook.exe staging
|
|
||||||
- cd staging
|
|
||||||
- 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip *
|
|
||||||
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
description: 'Windows release'
|
|
||||||
artifact: /.*\.zip/
|
|
||||||
auth_token:
|
|
||||||
secure: QQhjKVyz7mpjlyGhlXytbFQQfKFQWTahHkD+B0NzIUoEVqO7ZLWjnoWasvLqW4nE
|
|
||||||
provider: GitHub
|
|
||||||
on:
|
|
||||||
RUST_CHANNEL: stable
|
|
||||||
appveyor_repo_tag: true
|
|
||||||
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- /^v\d+\.\d+\.\d+.*$/
|
|
|
@ -1,19 +0,0 @@
|
||||||
[book]
|
|
||||||
title = "mdBook Documentation"
|
|
||||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
|
||||||
authors = ["Mathieu David", "Michael-F-Bryan"]
|
|
||||||
|
|
||||||
[output.html]
|
|
||||||
mathjax-support = true
|
|
||||||
|
|
||||||
[output.html.playpen]
|
|
||||||
editable = true
|
|
||||||
|
|
||||||
[output.html.search]
|
|
||||||
limit-results = 20
|
|
||||||
use-boolean-and = true
|
|
||||||
boost-title = 2
|
|
||||||
boost-hierarchy = 2
|
|
||||||
boost-paragraph = 1
|
|
||||||
expand = true
|
|
||||||
heading-split-level = 2
|
|
|
@ -1,25 +0,0 @@
|
||||||
# mdBook
|
|
||||||
|
|
||||||
**mdBook** is a command line tool and Rust crate to create books using Markdown
|
|
||||||
files. It's very similar to Gitbook but written in
|
|
||||||
[Rust](http://www.rust-lang.org).
|
|
||||||
|
|
||||||
What you are reading serves as an example of the output of mdBook and at the
|
|
||||||
same time as a high-level documentation.
|
|
||||||
|
|
||||||
mdBook is free and open source, you can find the source code on
|
|
||||||
[GitHub](https://github.com/rust-lang-nursery/mdBook). Issues and feature
|
|
||||||
requests can be posted on the [GitHub issue
|
|
||||||
tracker](https://github.com/rust-lang-nursery/mdBook/issues).
|
|
||||||
|
|
||||||
## API docs
|
|
||||||
|
|
||||||
Alongside this book you can also read the [API
|
|
||||||
docs](https://docs.rs/mdbook/*/mdbook/) generated by Rustdoc if you would like
|
|
||||||
to use mdBook as a crate or write a new renderer and need a more low-level
|
|
||||||
overview.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
mdBook, all the source code, is released under the [Mozilla Public License
|
|
||||||
v2.0](https://www.mozilla.org/MPL/2.0/).
|
|
|
@ -1,55 +0,0 @@
|
||||||
# Command Line Tool
|
|
||||||
|
|
||||||
mdBook can be used either as a command line tool or a [Rust
|
|
||||||
crate](https://crates.io/crates/mdbook). Let's focus on the command line tool
|
|
||||||
capabilities first.
|
|
||||||
|
|
||||||
## Install From Binaries
|
|
||||||
|
|
||||||
Precompiled binaries are provided for major platforms on a best-effort basis.
|
|
||||||
Visit [the releases page](https://github.com/rust-lang-nursery/mdBook/releases)
|
|
||||||
to download the appropriate version for your platform.
|
|
||||||
|
|
||||||
## Install From Source
|
|
||||||
|
|
||||||
mdBook can also be installed from source
|
|
||||||
|
|
||||||
### Pre-requisite
|
|
||||||
|
|
||||||
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs
|
|
||||||
to be compiled with **Cargo**. If you haven't already installed Rust, please go
|
|
||||||
ahead and [install it](https://www.rust-lang.org/downloads.html) now.
|
|
||||||
|
|
||||||
### 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 for the latest release 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/rust-lang-nursery/mdBook)** contains all
|
|
||||||
the latest bug-fixes and features, that will be released in the next version on
|
|
||||||
**Crates.io**, if you can't wait until the next release. You can build the git
|
|
||||||
version yourself. Open your terminal and navigate to the directory of you
|
|
||||||
choice. We need to clone the git repository and then build it with Cargo.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone --depth=1 https://github.com/rust-lang-nursery/mdBook.git
|
|
||||||
cd mdBook
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
The executable `mdbook` will be in the `./target/release` folder, this should be
|
|
||||||
added to the path.
|
|
|
@ -1,49 +0,0 @@
|
||||||
# The serve command
|
|
||||||
|
|
||||||
The serve command is used to preview a book by serving it over HTTP at
|
|
||||||
`localhost:3000` by default. Additionally it watches the book's directory for
|
|
||||||
changes, rebuilding the book and refreshing clients for each change. A websocket
|
|
||||||
connection is used to trigger the client-side refresh.
|
|
||||||
|
|
||||||
#### Specify a directory
|
|
||||||
|
|
||||||
The `serve` command can take a directory as an argument to use as the book's
|
|
||||||
root 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 HTTP hostname
|
|
||||||
to listen on, and the hostname for the browser to connect to for WebSockets.
|
|
||||||
|
|
||||||
For example: suppose you have 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 serve path/to/book -p 8000 -n 127.0.0.1 --websocket-hostname 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`) flag, 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 the
|
|
||||||
book. If not specified it will default to the value of the `build.build-dir` key
|
|
||||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
|
||||||
|
|
||||||
-----
|
|
||||||
|
|
||||||
***Note:*** *The `serve` command is for testing, and is not intended to be a
|
|
||||||
complete HTTP server for a website.*
|
|
|
@ -1,26 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
||||||
The `watch` command can take a directory as an argument to use as the book's
|
|
||||||
root 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 the
|
|
||||||
book. If not specified it will default to the value of the `build.build-dir` key
|
|
||||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
|
|
@ -1,56 +0,0 @@
|
||||||
# Running `mdbook` in Continuous Integration
|
|
||||||
|
|
||||||
While the following examples use Travis CI, their principles should
|
|
||||||
straightforwardly transfer to other continuous integration providers as well.
|
|
||||||
|
|
||||||
## Ensuring Your Book Builds and Tests Pass
|
|
||||||
|
|
||||||
Here is a sample Travis CI `.travis.yml` configuration that ensures `mdbook
|
|
||||||
build` and `mdbook test` run successfully. The key to fast CI turnaround times
|
|
||||||
is caching `mdbook` installs, so that you aren't compiling `mdbook` on every CI
|
|
||||||
run.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
language: rust
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
cache:
|
|
||||||
- cargo
|
|
||||||
|
|
||||||
rust:
|
|
||||||
- stable
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
|
||||||
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.1" mdbook)
|
|
||||||
- cargo install-update -a
|
|
||||||
|
|
||||||
script:
|
|
||||||
- mdbook build path/to/mybook && mdbook test path/to/mybook
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deploying Your Book to GitHub Pages
|
|
||||||
|
|
||||||
Following these instructions will result in your book being published to GitHub
|
|
||||||
pages after a successful CI run on your repository's `master` branch.
|
|
||||||
|
|
||||||
First, create a new GitHub "Personal Access Token" with the "public_repo"
|
|
||||||
permissions (or "repo" for private repositories). Go to your repository's Travis
|
|
||||||
CI settings page and add an environment variable named `GITHUB_TOKEN` that is
|
|
||||||
marked secure and *not* shown in the logs.
|
|
||||||
|
|
||||||
Then, append this snippet to your `.travis.yml` and update the path to the
|
|
||||||
`book` directory:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
deploy:
|
|
||||||
provider: pages
|
|
||||||
skip-cleanup: true
|
|
||||||
github-token: $GITHUB_TOKEN
|
|
||||||
local-dir: path/to/mybook/book
|
|
||||||
keep-history: false
|
|
||||||
on:
|
|
||||||
branch: master
|
|
||||||
```
|
|
||||||
|
|
||||||
That's it!
|
|
|
@ -1,284 +0,0 @@
|
||||||
# Configuration
|
|
||||||
|
|
||||||
You can configure the parameters for your book in the ***book.toml*** file.
|
|
||||||
|
|
||||||
Here is an example of what a ***book.toml*** file might look like:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[book]
|
|
||||||
title = "Example book"
|
|
||||||
author = "John Doe"
|
|
||||||
description = "The example book covers examples."
|
|
||||||
|
|
||||||
[build]
|
|
||||||
build-dir = "my-example-book"
|
|
||||||
create-missing = false
|
|
||||||
|
|
||||||
[preprocess.index]
|
|
||||||
|
|
||||||
[preprocess.links]
|
|
||||||
|
|
||||||
[output.html]
|
|
||||||
additional-css = ["custom.css"]
|
|
||||||
|
|
||||||
[output.html.search]
|
|
||||||
limit-results = 15
|
|
||||||
```
|
|
||||||
|
|
||||||
## Supported configuration options
|
|
||||||
|
|
||||||
It is important to note that **any** relative path specified in the in the
|
|
||||||
configuration will always be taken relative from the root of the book where the
|
|
||||||
configuration file is located.
|
|
||||||
|
|
||||||
### General metadata
|
|
||||||
|
|
||||||
This is general information about your book.
|
|
||||||
|
|
||||||
- **title:** The title of the book
|
|
||||||
- **authors:** The author(s) of the book
|
|
||||||
- **description:** A description for the book, which is added as meta
|
|
||||||
information in the html `<head>` of each page
|
|
||||||
- **src:** By default, the source directory is found in the directory named
|
|
||||||
`src` directly under the root folder. But this is configurable with the `src`
|
|
||||||
key in the configuration file.
|
|
||||||
|
|
||||||
**book.toml**
|
|
||||||
```toml
|
|
||||||
[book]
|
|
||||||
title = "Example book"
|
|
||||||
authors = ["John Doe", "Jane Doe"]
|
|
||||||
description = "The example book covers examples."
|
|
||||||
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build options
|
|
||||||
|
|
||||||
This controls the build process of your book.
|
|
||||||
|
|
||||||
- **build-dir:** The directory to put the rendered book in. By default this is
|
|
||||||
`book/` in the book's root directory.
|
|
||||||
- **create-missing:** By default, any missing files specified in `SUMMARY.md`
|
|
||||||
will be created when the book is built (i.e. `create-missing = true`). If this
|
|
||||||
is `false` then the build process will instead exit with an error if any files
|
|
||||||
do not exist.
|
|
||||||
- **use-default-preprocessors:** Disable the default preprocessors of (`links` &
|
|
||||||
`index`) by setting this option to `false`.
|
|
||||||
|
|
||||||
If you have the same, and/or other preprocessors declared via their table
|
|
||||||
of configuration, they will run instead.
|
|
||||||
|
|
||||||
- For clarity, with no preprocessor configuration, the default `links` and
|
|
||||||
`index` will run.
|
|
||||||
- Setting `use-default-preprocessors = false` will disable these
|
|
||||||
default preprocessors from running.
|
|
||||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
|
||||||
`use-default-preprocessors` that `links` it will run.
|
|
||||||
|
|
||||||
## Configuring Preprocessors
|
|
||||||
|
|
||||||
The following preprocessors are available and included by default:
|
|
||||||
|
|
||||||
- `links`: Expand the `{{ #playpen }}` and `{{ #include }}` handlebars
|
|
||||||
helpers in a chapter to include the contents of a file.
|
|
||||||
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
|
|
||||||
to say, all `README.md` would be rendered to an index file `index.html` in the
|
|
||||||
rendered book.
|
|
||||||
|
|
||||||
|
|
||||||
**book.toml**
|
|
||||||
```toml
|
|
||||||
[build]
|
|
||||||
build-dir = "build"
|
|
||||||
create-missing = false
|
|
||||||
|
|
||||||
[preprocess.links]
|
|
||||||
|
|
||||||
[preprocess.index]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Preprocessor Configuration
|
|
||||||
|
|
||||||
Like renderers, preprocessor will need to be given its own table (e.g.
|
|
||||||
`[preprocessor.mathjax]`). In the section, you may then pass extra
|
|
||||||
configuration to the preprocessor by adding key-value pairs to the table.
|
|
||||||
|
|
||||||
For example
|
|
||||||
|
|
||||||
```
|
|
||||||
[preprocess.links]
|
|
||||||
# set the renderers this preprocessor will run for
|
|
||||||
renderers = ["html"]
|
|
||||||
some_extra_feature = true
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Locking a Preprocessor dependency to a renderer
|
|
||||||
|
|
||||||
You can explicitly specify that a preprocessor should run for a renderer by
|
|
||||||
binding the two together.
|
|
||||||
|
|
||||||
```
|
|
||||||
[preprocessor.mathjax]
|
|
||||||
renderers = ["html"] # mathjax only makes sense with the HTML renderer
|
|
||||||
```
|
|
||||||
|
|
||||||
### Provide Your Own Command
|
|
||||||
|
|
||||||
By default when you add a `[preprocessor.foo]` table to your `book.toml` file,
|
|
||||||
`mdbook` will try to invoke the `mdbook-foo` executa`ble. If you want to use a
|
|
||||||
different program name or pass in command-line arguments, this behaviour can
|
|
||||||
be overridden by adding a `command` field.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[preprocessor.random]
|
|
||||||
command = "python random.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuring Renderers
|
|
||||||
|
|
||||||
### HTML renderer options
|
|
||||||
|
|
||||||
The HTML renderer has a couple of options as well. All the options for the
|
|
||||||
renderer need to be specified under the TOML table `[output.html]`.
|
|
||||||
|
|
||||||
The following configuration options are available:
|
|
||||||
|
|
||||||
- **theme:** mdBook comes with a default theme and all the resource files needed
|
|
||||||
for it. But if this option is set, mdBook will selectively overwrite the theme
|
|
||||||
files with the ones found in the specified folder.
|
|
||||||
- **curly-quotes:** Convert straight quotes to curly quotes, except for those
|
|
||||||
that occur in code blocks and code spans. Defaults to `false`.
|
|
||||||
- **google-analytics:** If you use Google Analytics, this option lets you enable
|
|
||||||
it by simply specifying your ID in the configuration file.
|
|
||||||
- **additional-css:** If you need to slightly change the appearance of your book
|
|
||||||
without overwriting the whole style, you can specify a set of stylesheets that
|
|
||||||
will be loaded after the default ones where you can surgically change the
|
|
||||||
style.
|
|
||||||
- **additional-js:** If you need to add some behaviour to your book without
|
|
||||||
removing the current behaviour, you can specify a set of JavaScript files that
|
|
||||||
will be loaded alongside the default one.
|
|
||||||
- **no-section-label:** mdBook by defaults adds section label in table of
|
|
||||||
contents column. For example, "1.", "2.1". Set this option to true to disable
|
|
||||||
those labels. Defaults to `false`.
|
|
||||||
- **playpen:** A subtable for configuring various playpen settings.
|
|
||||||
- **search:** A subtable for configuring the in-browser search functionality.
|
|
||||||
mdBook must be compiled with the `search` feature enabled (on by default).
|
|
||||||
|
|
||||||
Available configuration options for the `[output.html.playpen]` table:
|
|
||||||
|
|
||||||
- **editable:** Allow editing the source code. Defaults to `false`.
|
|
||||||
- **copy-js:** Copy JavaScript files for the editor to the output directory.
|
|
||||||
Defaults to `true`.
|
|
||||||
|
|
||||||
[Ace]: https://ace.c9.io/
|
|
||||||
|
|
||||||
Available configuration options for the `[output.html.search]` table:
|
|
||||||
|
|
||||||
- **enable:** Enables the search feature. Defaults to `true`.
|
|
||||||
- **limit-results:** The maximum number of search results. Defaults to `30`.
|
|
||||||
- **teaser-word-count:** The number of words used for a search result teaser.
|
|
||||||
Defaults to `30`.
|
|
||||||
- **use-boolean-and:** Define the logical link between multiple search words. If
|
|
||||||
true, all search words must appear in each result. Defaults to `true`.
|
|
||||||
- **boost-title:** Boost factor for the search result score if a search word
|
|
||||||
appears in the header. Defaults to `2`.
|
|
||||||
- **boost-hierarchy:** Boost factor for the search result score if a search word
|
|
||||||
appears in the hierarchy. The hierarchy contains all titles of the parent
|
|
||||||
documents and all parent headings. Defaults to `1`.
|
|
||||||
- **boost-paragraph:** Boost factor for the search result score if a search word
|
|
||||||
appears in the text. Defaults to `1`.
|
|
||||||
- **expand:** True if search should match longer results e.g. search `micro`
|
|
||||||
should match `microwave`. Defaults to `true`.
|
|
||||||
- **heading-split-level:** Search results will link to a section of the document
|
|
||||||
which contains the result. Documents are split into sections by headings this
|
|
||||||
level or less. Defaults to `3`. (`### This is a level 3 heading`)
|
|
||||||
- **copy-js:** Copy JavaScript files for the search implementation to the output
|
|
||||||
directory. Defaults to `true`.
|
|
||||||
|
|
||||||
This shows all available HTML output options in the **book.toml**:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[book]
|
|
||||||
title = "Example book"
|
|
||||||
authors = ["John Doe", "Jane Doe"]
|
|
||||||
description = "The example book covers examples."
|
|
||||||
|
|
||||||
[build]
|
|
||||||
build-dir = "book"
|
|
||||||
create-missing = true
|
|
||||||
preprocess = ["links", "index"]
|
|
||||||
|
|
||||||
[output.html]
|
|
||||||
theme = "my-theme"
|
|
||||||
curly-quotes = true
|
|
||||||
google-analytics = "123456"
|
|
||||||
additional-css = ["custom.css", "custom2.css"]
|
|
||||||
additional-js = ["custom.js"]
|
|
||||||
|
|
||||||
[output.html.playpen]
|
|
||||||
editor = "./path/to/editor"
|
|
||||||
editable = false
|
|
||||||
|
|
||||||
[output.html.search]
|
|
||||||
enable = true
|
|
||||||
searcher = "./path/to/searcher"
|
|
||||||
limit-results = 30
|
|
||||||
teaser-word-count = 30
|
|
||||||
use-boolean-and = true
|
|
||||||
boost-title = 2
|
|
||||||
boost-hierarchy = 1
|
|
||||||
boost-paragraph = 1
|
|
||||||
expand = true
|
|
||||||
heading-split-level = 3
|
|
||||||
copy-js = true
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Renderers
|
|
||||||
|
|
||||||
A custom renderer can be enabled by adding a `[output.foo]` table to your
|
|
||||||
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will
|
|
||||||
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
|
|
||||||
rendering.
|
|
||||||
|
|
||||||
Custom renderers will have access to all configuration within their table
|
|
||||||
(i.e. anything under `[output.foo]`), and the command to be invoked can be
|
|
||||||
manually specified with the `command` field.
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
All configuration values can be overridden from the command line by setting the
|
|
||||||
corresponding environment variable. Because many operating systems restrict
|
|
||||||
environment variables to be alphanumeric characters or `_`, the configuration
|
|
||||||
key needs to be formatted slightly differently to the normal `foo.bar.baz` form.
|
|
||||||
|
|
||||||
Variables starting with `MDBOOK_` are used for configuration. The key is created
|
|
||||||
by removing the `MDBOOK_` prefix and turning the resulting string into
|
|
||||||
`kebab-case`. Double underscores (`__`) separate nested keys, while a single
|
|
||||||
underscore (`_`) is replaced with a dash (`-`).
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
- `MDBOOK_foo` -> `foo`
|
|
||||||
- `MDBOOK_FOO` -> `foo`
|
|
||||||
- `MDBOOK_FOO__BAR` -> `foo.bar`
|
|
||||||
- `MDBOOK_FOO_BAR` -> `foo-bar`
|
|
||||||
- `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
|
|
||||||
|
|
||||||
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can override the
|
|
||||||
book's title without needing to touch your `book.toml`.
|
|
||||||
|
|
||||||
> **Note:** To facilitate setting more complex config items, the value of an
|
|
||||||
> environment variable is first parsed as JSON, falling back to a string if the
|
|
||||||
> parse fails.
|
|
||||||
>
|
|
||||||
> This means, if you so desired, you could override all book metadata when
|
|
||||||
> building the book with something like
|
|
||||||
>
|
|
||||||
> ```text
|
|
||||||
> $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
|
|
||||||
> $ mdbook build
|
|
||||||
> ```
|
|
||||||
|
|
||||||
The latter case may be useful in situations where `mdbook` is invoked from a
|
|
||||||
script or CI, where it sometimes isn't possible to update the `book.toml` before
|
|
||||||
building.
|
|
|
@ -1,6 +0,0 @@
|
||||||
fn main() {
|
|
||||||
println!("Hello World!");
|
|
||||||
#
|
|
||||||
# // You can even hide lines! :D
|
|
||||||
# println!("I am hidden! Expand the code snippet to see me");
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
# mdBook-specific markdown
|
|
||||||
|
|
||||||
## Hiding code lines
|
|
||||||
|
|
||||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
|
||||||
with a `#`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# fn main() {
|
|
||||||
let x = 5;
|
|
||||||
let y = 6;
|
|
||||||
|
|
||||||
println!("{}", x + y);
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
Will render as
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# fn main() {
|
|
||||||
let x = 5;
|
|
||||||
let y = 7;
|
|
||||||
|
|
||||||
println!("{}", x + y);
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Including files
|
|
||||||
|
|
||||||
With the following syntax, you can include files into your book:
|
|
||||||
|
|
||||||
```hbs
|
|
||||||
\{{#include file.rs}}
|
|
||||||
```
|
|
||||||
|
|
||||||
The path to the file has to be relative from the current source file.
|
|
||||||
|
|
||||||
Usually, this command is used for including code snippets and examples. In this
|
|
||||||
case, oftens one would include a specific part of the file e.g. which only
|
|
||||||
contains the relevant lines for the example. We support four different modes of
|
|
||||||
partial includes:
|
|
||||||
|
|
||||||
```hbs
|
|
||||||
\{{#include file.rs:2}}
|
|
||||||
\{{#include file.rs::10}}
|
|
||||||
\{{#include file.rs:2:}}
|
|
||||||
\{{#include file.rs:2:10}}
|
|
||||||
```
|
|
||||||
|
|
||||||
The first command only includes the second line from file `file.rs`. The second
|
|
||||||
command includes all lines up to line 10, i.e. the lines from 11 till the end of
|
|
||||||
the file are omitted. The third command includes all lines from line 2, i.e. the
|
|
||||||
first line is omitted. The last command includes the excerpt of `file.rs`
|
|
||||||
consisting of lines 2 to 10.
|
|
||||||
|
|
||||||
## Inserting runnable Rust files
|
|
||||||
|
|
||||||
With the following syntax, you can insert runnable Rust files into your book:
|
|
||||||
|
|
||||||
```hbs
|
|
||||||
\{{#playpen file.rs}}
|
|
||||||
```
|
|
||||||
|
|
||||||
The path to the Rust file has to be relative from the current source file.
|
|
||||||
|
|
||||||
When play is clicked, the code snippet will be sent to the [Rust Playpen] to be
|
|
||||||
compiled and run. The result is sent back and displayed directly underneath the
|
|
||||||
code.
|
|
||||||
|
|
||||||
Here is what a rendered code snippet looks like:
|
|
||||||
|
|
||||||
{{#playpen example.rs}}
|
|
||||||
|
|
||||||
[Rust Playpen]: https://play.rust-lang.org/
|
|
|
@ -1,38 +0,0 @@
|
||||||
# SUMMARY.md
|
|
||||||
|
|
||||||
The summary file is used by mdBook to know what chapters to include, in what
|
|
||||||
order they should appear, what their hierarchy is and where the source files
|
|
||||||
are. Without this file, there is no book.
|
|
||||||
|
|
||||||
Even though `SUMMARY.md` is a markdown file, the formatting is very strict to
|
|
||||||
allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
|
|
||||||
|
|
||||||
#### Allowed elements
|
|
||||||
|
|
||||||
1. ***Title*** It's common practice to begin with a title, generally <code
|
|
||||||
class="language-markdown"># Summary</code>. But it is not mandatory, the
|
|
||||||
parser just ignores it. So you can too if you feel like it.
|
|
||||||
|
|
||||||
2. ***Prefix Chapter*** Before the main numbered chapters you can add a couple
|
|
||||||
of elements that will not be numbered. This is useful for forewords,
|
|
||||||
introductions, etc. There are however some constraints. You can not nest
|
|
||||||
prefix chapters, they should all be on the root level. And you can not add
|
|
||||||
prefix chapters once you have added numbered chapters.
|
|
||||||
```markdown
|
|
||||||
[Title of prefix element](relative/path/to/markdown.md)
|
|
||||||
```
|
|
||||||
|
|
||||||
3. ***Numbered Chapter*** Numbered chapters are the main content of the book,
|
|
||||||
they will be numbered and can be nested, resulting in a nice hierarchy
|
|
||||||
(chapters, sub-chapters, etc.)
|
|
||||||
```markdown
|
|
||||||
- [Title of the Chapter](relative/path/to/markdown.md)
|
|
||||||
```
|
|
||||||
You can either use `-` or `*` to indicate a numbered chapter.
|
|
||||||
|
|
||||||
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.
|
|
|
@ -1,34 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
The theme is totally customizable, you can selectively replace every file from
|
|
||||||
the theme by your own by adding a `theme` directory next to `src` folder in your
|
|
||||||
project root. Create a new file with the name of the file you want to override
|
|
||||||
and now that file will be used instead of the default file.
|
|
||||||
|
|
||||||
Here are the files you can override:
|
|
||||||
|
|
||||||
- ***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
|
|
||||||
|
|
||||||
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.
|
|
|
@ -1,70 +0,0 @@
|
||||||
# Syntax Highlighting
|
|
||||||
|
|
||||||
For syntax highlighting I use [Highlight.js](https://highlightjs.org) with a
|
|
||||||
custom theme.
|
|
||||||
|
|
||||||
Automatic language detection has been turned off, so you will probably want to
|
|
||||||
specify the programming language you use like this
|
|
||||||
|
|
||||||
<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 lets you hide code lines by prepending them
|
|
||||||
with a `#`.
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# fn main() {
|
|
||||||
let x = 5;
|
|
||||||
let y = 6;
|
|
||||||
|
|
||||||
println!("{}", x + y);
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
Will render as
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# fn main() {
|
|
||||||
let x = 5;
|
|
||||||
let y = 7;
|
|
||||||
|
|
||||||
println!("{}", x + y);
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
**At the moment, this only works for code examples that are annotated with
|
|
||||||
`rust`. Because it would collide with semantics of some programming languages.
|
|
||||||
In the future, we want to make this configurable through the `book.toml` so that
|
|
||||||
everyone can benefit from it.**
|
|
||||||
|
|
||||||
|
|
||||||
## Improve default theme
|
|
||||||
|
|
||||||
If you think the default theme doesn't look quite right for a specific language,
|
|
||||||
or could be improved. Feel free to [submit a new
|
|
||||||
issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you
|
|
||||||
have in mind and I will take a look at it.
|
|
||||||
|
|
||||||
You could also create a pull-request with the proposed improvements.
|
|
||||||
|
|
||||||
Overall the theme should be light and sober, without to many flashy colors.
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Introduction
|
|
||||||
|
|
||||||
A frontmatter chapter.
|
|
|
@ -1,32 +0,0 @@
|
||||||
# This script takes care of building your crate and packaging it for release
|
|
||||||
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local src=$(pwd) \
|
|
||||||
stage=
|
|
||||||
|
|
||||||
case $TRAVIS_OS_NAME in
|
|
||||||
linux)
|
|
||||||
stage=$(mktemp -d)
|
|
||||||
;;
|
|
||||||
osx)
|
|
||||||
stage=$(mktemp -d -t tmp)
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# This will slow down the build, but is necessary to not run out of disk space
|
|
||||||
cargo clean
|
|
||||||
|
|
||||||
cargo rustc --bin mdbook --target $TARGET --release -- -C lto
|
|
||||||
|
|
||||||
cp target/$TARGET/release/mdbook $stage/
|
|
||||||
|
|
||||||
cd $stage
|
|
||||||
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
|
|
||||||
cd $src
|
|
||||||
|
|
||||||
rm -rf $stage
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install/update rust.
|
||||||
|
# The first argument should be the toolchain to install.
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
if [ -z "$1" ]
|
||||||
|
then
|
||||||
|
echo "First parameter must be toolchain to install."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TOOLCHAIN="$1"
|
||||||
|
|
||||||
|
rustup set profile minimal
|
||||||
|
rustup component remove --toolchain=$TOOLCHAIN rust-docs || echo "already removed"
|
||||||
|
rustup update --no-self-update $TOOLCHAIN
|
||||||
|
if [ -n "$2" ]
|
||||||
|
then
|
||||||
|
TARGET="$2"
|
||||||
|
HOST=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
|
||||||
|
if [ "$HOST" != "$TARGET" ]
|
||||||
|
then
|
||||||
|
rustup component add llvm-tools-preview --toolchain=$TOOLCHAIN
|
||||||
|
rustup component add rust-std-$TARGET --toolchain=$TOOLCHAIN
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
rustup default $TOOLCHAIN
|
||||||
|
rustup -V
|
||||||
|
rustc -Vv
|
||||||
|
cargo -V
|
|
@ -0,0 +1,53 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Builds the release and creates an archive and optionally deploys to GitHub.
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
if [[ -z "$GITHUB_REF" ]]
|
||||||
|
then
|
||||||
|
echo "GITHUB_REF must be set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Strip mdbook-refs/tags/ from the start of the ref.
|
||||||
|
TAG=${GITHUB_REF#*/tags/}
|
||||||
|
|
||||||
|
host=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
|
||||||
|
target=$2
|
||||||
|
if [ "$host" != "$target" ]
|
||||||
|
then
|
||||||
|
export "CARGO_TARGET_$(echo $target | tr a-z- A-Z_)_LINKER"=rust-lld
|
||||||
|
fi
|
||||||
|
export CARGO_PROFILE_RELEASE_LTO=true
|
||||||
|
cargo build --locked --bin mdbook --release --target $target
|
||||||
|
cd target/$target/release
|
||||||
|
case $1 in
|
||||||
|
ubuntu*)
|
||||||
|
asset="mdbook-$TAG-$target.tar.gz"
|
||||||
|
tar czf ../../$asset mdbook
|
||||||
|
;;
|
||||||
|
macos*)
|
||||||
|
asset="mdbook-$TAG-$target.tar.gz"
|
||||||
|
# There is a bug with BSD tar on macOS where the first 8MB of the file are
|
||||||
|
# sometimes all NUL bytes. See https://github.com/actions/cache/issues/403
|
||||||
|
# and https://github.com/rust-lang/cargo/issues/8603 for some more
|
||||||
|
# information. An alternative solution here is to install GNU tar, but
|
||||||
|
# flushing the disk cache seems to work, too.
|
||||||
|
sudo /usr/sbin/purge
|
||||||
|
tar czf ../../$asset mdbook
|
||||||
|
;;
|
||||||
|
windows*)
|
||||||
|
asset="mdbook-$TAG-$target.zip"
|
||||||
|
7z a ../../$asset mdbook.exe
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "OS should be first parameter, was: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
if [[ -z "$GITHUB_ENV" ]]
|
||||||
|
then
|
||||||
|
echo "GITHUB_ENV not set, run: gh release upload $TAG target/$asset"
|
||||||
|
else
|
||||||
|
echo "MDBOOK_TAG=$TAG" >> $GITHUB_ENV
|
||||||
|
echo "MDBOOK_ASSET=target/$asset" >> $GITHUB_ENV
|
||||||
|
fi
|
|
@ -1,82 +0,0 @@
|
||||||
//! An example preprocessor for removing all forms of emphasis from a markdown
|
|
||||||
//! book.
|
|
||||||
|
|
||||||
extern crate mdbook;
|
|
||||||
extern crate pulldown_cmark;
|
|
||||||
extern crate pulldown_cmark_to_cmark;
|
|
||||||
|
|
||||||
use mdbook::book::{Book, BookItem, Chapter};
|
|
||||||
use mdbook::errors::{Error, Result};
|
|
||||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
|
||||||
use pulldown_cmark::{Event, Parser, Tag};
|
|
||||||
use pulldown_cmark_to_cmark::fmt::cmark;
|
|
||||||
|
|
||||||
const NAME: &str = "md-links-to-html-links";
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
panic!("This example is intended to be part of a library");
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Deemphasize;
|
|
||||||
|
|
||||||
impl Preprocessor for Deemphasize {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
NAME
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
|
||||||
eprintln!("Running '{}' preprocessor", self.name());
|
|
||||||
let mut num_removed_items = 0;
|
|
||||||
|
|
||||||
process(&mut book.sections, &mut num_removed_items)?;
|
|
||||||
|
|
||||||
eprintln!(
|
|
||||||
"{}: removed {} events from markdown stream.",
|
|
||||||
self.name(),
|
|
||||||
num_removed_items
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(book)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process<'a, I>(items: I, num_removed_items: &mut usize) -> Result<()>
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = &'a mut BookItem> + 'a,
|
|
||||||
{
|
|
||||||
for item in items {
|
|
||||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
|
||||||
eprintln!("{}: processing chapter '{}'", NAME, chapter.name);
|
|
||||||
|
|
||||||
let md = remove_emphasis(num_removed_items, chapter)?;
|
|
||||||
chapter.content = md;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_emphasis(
|
|
||||||
num_removed_items: &mut usize,
|
|
||||||
chapter: &mut Chapter,
|
|
||||||
) -> Result<String> {
|
|
||||||
let mut buf = String::with_capacity(chapter.content.len());
|
|
||||||
|
|
||||||
let events = Parser::new(&chapter.content).filter(|e| {
|
|
||||||
let should_keep = match *e {
|
|
||||||
Event::Start(Tag::Emphasis)
|
|
||||||
| Event::Start(Tag::Strong)
|
|
||||||
| Event::End(Tag::Emphasis)
|
|
||||||
| Event::End(Tag::Strong) => false,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
if !should_keep {
|
|
||||||
*num_removed_items += 1;
|
|
||||||
}
|
|
||||||
should_keep
|
|
||||||
});
|
|
||||||
|
|
||||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
|
||||||
Error::from(format!("Markdown serialization failed: {}", err))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,22 +1,20 @@
|
||||||
extern crate clap;
|
use crate::nop_lib::Nop;
|
||||||
extern crate mdbook;
|
use clap::{Arg, ArgMatches, Command};
|
||||||
extern crate serde_json;
|
|
||||||
|
|
||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
|
||||||
use mdbook::book::Book;
|
use mdbook::book::Book;
|
||||||
use mdbook::errors::Error;
|
use mdbook::errors::Error;
|
||||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||||
|
use semver::{Version, VersionReq};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::process;
|
use std::process;
|
||||||
use nop_lib::Nop;
|
|
||||||
|
|
||||||
pub fn make_app() -> App<'static, 'static> {
|
pub fn make_app() -> Command {
|
||||||
App::new("nop-preprocessor")
|
Command::new("nop-preprocessor")
|
||||||
.about("A mdbook preprocessor which does precisely nothing")
|
.about("A mdbook preprocessor which does precisely nothing")
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("supports")
|
Command::new("supports")
|
||||||
.arg(Arg::with_name("renderer").required(true))
|
.arg(Arg::new("renderer").required(true))
|
||||||
.about("Check whether a renderer is supported by this preprocessor"))
|
.about("Check whether a renderer is supported by this preprocessor"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -27,20 +25,19 @@ fn main() {
|
||||||
|
|
||||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||||
handle_supports(&preprocessor, sub_args);
|
handle_supports(&preprocessor, sub_args);
|
||||||
} else {
|
} else if let Err(e) = handle_preprocessing(&preprocessor) {
|
||||||
if let Err(e) = handle_preprocessing(&preprocessor) {
|
|
||||||
eprintln!("{}", e);
|
eprintln!("{}", e);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
||||||
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
||||||
|
|
||||||
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
|
let book_version = Version::parse(&ctx.mdbook_version)?;
|
||||||
// We should probably use the `semver` crate to check compatibility
|
let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
|
||||||
// here...
|
|
||||||
|
if !version_req.matches(&book_version) {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Warning: The {} plugin was built against version {} of mdbook, \
|
"Warning: The {} plugin was built against version {} of mdbook, \
|
||||||
but we're being called from version {}",
|
but we're being called from version {}",
|
||||||
|
@ -57,8 +54,10 @@ fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
|
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
|
||||||
let renderer = sub_args.value_of("renderer").expect("Required argument");
|
let renderer = sub_args
|
||||||
let supported = pre.supports_renderer(&renderer);
|
.get_one::<String>("renderer")
|
||||||
|
.expect("Required argument");
|
||||||
|
let supported = pre.supports_renderer(renderer);
|
||||||
|
|
||||||
// Signal whether the renderer is supported by exiting with 1 or 0.
|
// Signal whether the renderer is supported by exiting with 1 or 0.
|
||||||
if supported {
|
if supported {
|
||||||
|
@ -87,16 +86,12 @@ mod nop_lib {
|
||||||
"nop-preprocessor"
|
"nop-preprocessor"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book, Error> {
|
||||||
&self,
|
|
||||||
ctx: &PreprocessorContext,
|
|
||||||
book: Book,
|
|
||||||
) -> Result<Book, Error> {
|
|
||||||
// In testing we want to tell the preprocessor to blow up by setting a
|
// In testing we want to tell the preprocessor to blow up by setting a
|
||||||
// particular config value
|
// particular config value
|
||||||
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
|
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
|
||||||
if nop_cfg.contains_key("blow-up") {
|
if nop_cfg.contains_key("blow-up") {
|
||||||
return Err("Boom!!1!".into());
|
anyhow::bail!("Boom!!1!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,5 +103,58 @@ mod nop_lib {
|
||||||
renderer != "not-supported"
|
renderer != "not-supported"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nop_preprocessor_run() {
|
||||||
|
let input_json = r##"[
|
||||||
|
{
|
||||||
|
"root": "/path/to/book",
|
||||||
|
"config": {
|
||||||
|
"book": {
|
||||||
|
"authors": ["AUTHOR"],
|
||||||
|
"language": "en",
|
||||||
|
"multilingual": false,
|
||||||
|
"src": "src",
|
||||||
|
"title": "TITLE"
|
||||||
|
},
|
||||||
|
"preprocessor": {
|
||||||
|
"nop": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"renderer": "html",
|
||||||
|
"mdbook_version": "0.4.21"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"Chapter": {
|
||||||
|
"name": "Chapter 1",
|
||||||
|
"content": "# Chapter 1\n",
|
||||||
|
"number": [1],
|
||||||
|
"sub_items": [],
|
||||||
|
"path": "chapter_1.md",
|
||||||
|
"source_path": "chapter_1.md",
|
||||||
|
"parent_names": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"__non_exhaustive": null
|
||||||
|
}
|
||||||
|
]"##;
|
||||||
|
let input_json = input_json.as_bytes();
|
||||||
|
|
||||||
|
let (ctx, book) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap();
|
||||||
|
let expected_book = book.clone();
|
||||||
|
let result = Nop::new().run(&ctx, book);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
// The nop-preprocessor should not have made any changes to the book content.
|
||||||
|
let actual_book = result.unwrap();
|
||||||
|
assert_eq!(actual_book, expected_book);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
[book]
|
||||||
|
title = "mdBook Documentation"
|
||||||
|
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||||
|
authors = ["Mathieu David", "Michael-F-Bryan"]
|
||||||
|
language = "en"
|
||||||
|
|
||||||
|
[rust]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[output.html]
|
||||||
|
mathjax-support = true
|
||||||
|
site-url = "/mdBook/"
|
||||||
|
git-repository-url = "https://github.com/rust-lang/mdBook/tree/master/guide"
|
||||||
|
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||||
|
|
||||||
|
[output.html.playground]
|
||||||
|
editable = true
|
||||||
|
line-numbers = true
|
||||||
|
|
||||||
|
[output.html.code.hidelines]
|
||||||
|
python = "~"
|
||||||
|
|
||||||
|
[output.html.search]
|
||||||
|
limit-results = 20
|
||||||
|
use-boolean-and = true
|
||||||
|
boost-title = 2
|
||||||
|
boost-hierarchy = 2
|
||||||
|
boost-paragraph = 1
|
||||||
|
expand = true
|
||||||
|
heading-split-level = 2
|
||||||
|
|
||||||
|
[output.html.redirect]
|
||||||
|
"/format/config.html" = "configuration/index.html"
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Document not found (404)
|
||||||
|
|
||||||
|
This URL is invalid, sorry. Try the search instead!
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
**mdBook** is a command line tool to create books with Markdown.
|
||||||
|
It is ideal for creating product or API documentation, tutorials, course materials or anything that requires a clean,
|
||||||
|
easily navigable and customizable presentation.
|
||||||
|
|
||||||
|
* Lightweight [Markdown] syntax helps you focus more on your content
|
||||||
|
* Integrated [search] support
|
||||||
|
* Color [syntax highlighting] for code blocks for many different languages
|
||||||
|
* [Theme] files allow customizing the formatting of the output
|
||||||
|
* [Preprocessors] can provide extensions for custom syntax and modifying content
|
||||||
|
* [Backends] can render the output to multiple formats
|
||||||
|
* Written in [Rust] for speed, safety, and simplicity
|
||||||
|
* Automated testing of [Rust code samples]
|
||||||
|
|
||||||
|
This guide is an example of what mdBook produces.
|
||||||
|
mdBook is used by the Rust programming language project, and [The Rust Programming Language][trpl] book is another fine example of mdBook in action.
|
||||||
|
|
||||||
|
[Markdown]: format/markdown.md
|
||||||
|
[search]: guide/reading.md#search
|
||||||
|
[syntax highlighting]: format/theme/syntax-highlighting.md
|
||||||
|
[theme]: format/theme/index.html
|
||||||
|
[preprocessors]: format/configuration/preprocessors.md
|
||||||
|
[backends]: format/configuration/renderers.md
|
||||||
|
[Rust]: https://www.rust-lang.org/
|
||||||
|
[trpl]: https://doc.rust-lang.org/book/
|
||||||
|
[Rust code samples]: cli/test.md
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
mdBook is free and open source. You can find the source code on
|
||||||
|
[GitHub](https://github.com/rust-lang/mdBook) and issues and feature requests can be posted on
|
||||||
|
the [GitHub issue tracker](https://github.com/rust-lang/mdBook/issues). mdBook relies on the community to fix bugs and
|
||||||
|
add features: if you'd like to contribute, please read
|
||||||
|
the [CONTRIBUTING](https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md) guide and consider opening
|
||||||
|
a [pull request](https://github.com/rust-lang/mdBook/pulls).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The mdBook source and documentation are released under
|
||||||
|
the [Mozilla Public License v2.0](https://www.mozilla.org/MPL/2.0/).
|
|
@ -1,6 +1,15 @@
|
||||||
# Summary
|
# Summary
|
||||||
|
|
||||||
- [mdBook](README.md)
|
[Introduction](README.md)
|
||||||
|
|
||||||
|
# User Guide
|
||||||
|
|
||||||
|
- [Installation](guide/installation.md)
|
||||||
|
- [Reading Books](guide/reading.md)
|
||||||
|
- [Creating a Book](guide/creating.md)
|
||||||
|
|
||||||
|
# Reference Guide
|
||||||
|
|
||||||
- [Command Line Tool](cli/README.md)
|
- [Command Line Tool](cli/README.md)
|
||||||
- [init](cli/init.md)
|
- [init](cli/init.md)
|
||||||
- [build](cli/build.md)
|
- [build](cli/build.md)
|
||||||
|
@ -8,19 +17,26 @@
|
||||||
- [serve](cli/serve.md)
|
- [serve](cli/serve.md)
|
||||||
- [test](cli/test.md)
|
- [test](cli/test.md)
|
||||||
- [clean](cli/clean.md)
|
- [clean](cli/clean.md)
|
||||||
|
- [completions](cli/completions.md)
|
||||||
- [Format](format/README.md)
|
- [Format](format/README.md)
|
||||||
- [SUMMARY.md](format/summary.md)
|
- [SUMMARY.md](format/summary.md)
|
||||||
- [Configuration](format/config.md)
|
- [Draft chapter]()
|
||||||
|
- [Configuration](format/configuration/README.md)
|
||||||
|
- [General](format/configuration/general.md)
|
||||||
|
- [Preprocessors](format/configuration/preprocessors.md)
|
||||||
|
- [Renderers](format/configuration/renderers.md)
|
||||||
|
- [Environment Variables](format/configuration/environment-variables.md)
|
||||||
- [Theme](format/theme/README.md)
|
- [Theme](format/theme/README.md)
|
||||||
- [index.hbs](format/theme/index-hbs.md)
|
- [index.hbs](format/theme/index-hbs.md)
|
||||||
- [Syntax highlighting](format/theme/syntax-highlighting.md)
|
- [Syntax highlighting](format/theme/syntax-highlighting.md)
|
||||||
- [Editor](format/theme/editor.md)
|
- [Editor](format/theme/editor.md)
|
||||||
- [MathJax Support](format/mathjax.md)
|
- [MathJax Support](format/mathjax.md)
|
||||||
- [mdBook specific features](format/mdbook.md)
|
- [mdBook-specific features](format/mdbook.md)
|
||||||
|
- [Markdown](format/markdown.md)
|
||||||
- [Continuous Integration](continuous-integration.md)
|
- [Continuous Integration](continuous-integration.md)
|
||||||
- [For Developers](for_developers/README.md)
|
- [For Developers](for_developers/README.md)
|
||||||
- [Preprocessors](for_developers/preprocessors.md)
|
- [Preprocessors](for_developers/preprocessors.md)
|
||||||
- [Alternate Backends](for_developers/backends.md)
|
- [Alternative Backends](for_developers/backends.md)
|
||||||
|
|
||||||
-----------
|
-----------
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Command Line Tool
|
||||||
|
|
||||||
|
The `mdbook` command-line tool is used to create and build books.
|
||||||
|
After you have [installed](../guide/installation.md) `mdbook`, you can run the `mdbook help` command in your terminal to view the available commands.
|
||||||
|
|
||||||
|
This following sections provide in-depth information on the different commands available.
|
||||||
|
|
||||||
|
* [`mdbook init <directory>`](init.md) — Creates a new book with minimal boilerplate to start with.
|
||||||
|
* [`mdbook build`](build.md) — Renders the book.
|
||||||
|
* [`mdbook watch`](watch.md) — Rebuilds the book any time a source file changes.
|
||||||
|
* [`mdbook serve`](serve.md) — Runs a web server to view the book, and rebuilds on changes.
|
||||||
|
* [`mdbook test`](test.md) — Tests Rust code samples.
|
||||||
|
* [`mdbook clean`](clean.md) — Deletes the rendered output.
|
||||||
|
* [`mdbook completions`](completions.md) — Support for shell auto-completion.
|
|
@ -7,7 +7,8 @@ mdbook build
|
||||||
```
|
```
|
||||||
|
|
||||||
It will try to parse your `SUMMARY.md` file to understand the structure of your
|
It will try to parse your `SUMMARY.md` file to understand the structure of your
|
||||||
book and fetch the corresponding files.
|
book and fetch the corresponding files. Note that this will also create files
|
||||||
|
mentioned in `SUMMARY.md` which are not yet present.
|
||||||
|
|
||||||
The rendered output will maintain the same directory structure as the source for
|
The rendered output will maintain the same directory structure as the source for
|
||||||
convenience. Large books will therefore remain structured when rendered.
|
convenience. Large books will therefore remain structured when rendered.
|
||||||
|
@ -29,10 +30,11 @@ your default web browser after building it.
|
||||||
#### --dest-dir
|
#### --dest-dir
|
||||||
|
|
||||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||||
book. If not specified it will default to the value of the `build.build-dir` key
|
book. Relative paths are interpreted relative to the book's root directory. If
|
||||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
not specified it will default to the value of the `build.build-dir` key in
|
||||||
|
`book.toml`, or to `./book`.
|
||||||
|
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
***Note:*** *Make sure to run the build command in the root directory and not in
|
***Note:*** *The build command copies all files (excluding files with `.md` extension) from the source directory
|
||||||
the source directory*
|
into the build directory.*
|
|
@ -19,9 +19,9 @@ mdbook clean path/to/book
|
||||||
#### --dest-dir
|
#### --dest-dir
|
||||||
|
|
||||||
The `--dest-dir` (`-d`) option allows you to override the book's output
|
The `--dest-dir` (`-d`) option allows you to override the book's output
|
||||||
directory, which will be deleted by this command. If not specified it will
|
directory, which will be deleted by this command. Relative paths are interpreted
|
||||||
default to the value of the `build.build-dir` key in `book.toml`, or to `./book`
|
relative to the book's root directory. If not specified it will default to the
|
||||||
relative to the book's root directory.
|
value of the `build.build-dir` key in `book.toml`, or to `./book`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mdbook clean --dest-dir=path/to/book
|
mdbook clean --dest-dir=path/to/book
|
|
@ -0,0 +1,20 @@
|
||||||
|
# The completions command
|
||||||
|
|
||||||
|
The completions command is used to generate auto-completions for some common shells.
|
||||||
|
This means when you type `mdbook` in your shell, you can then press your shell's auto-complete key (usually the Tab key) and it may display what the valid options are, or finish partial input.
|
||||||
|
|
||||||
|
The completions first need to be installed for your shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# bash
|
||||||
|
mdbook completions bash > ~/.local/share/bash-completion/completions/mdbook
|
||||||
|
# oh-my-zsh
|
||||||
|
mdbook completions zsh > ~/.oh-my-zsh/completions/_mdbook
|
||||||
|
autoload -U compinit && compinit
|
||||||
|
```
|
||||||
|
|
||||||
|
The command prints a completion script for the given shell.
|
||||||
|
Run `mdbook completions --help` for a list of supported shells.
|
||||||
|
|
||||||
|
Where to place the completions depend on which shell you are using and your operating system.
|
||||||
|
Consult your shell's documentation for more information one where to place the script.
|
|
@ -19,15 +19,15 @@ book-test/
|
||||||
└── SUMMARY.md
|
└── SUMMARY.md
|
||||||
```
|
```
|
||||||
|
|
||||||
- The `src` directory is were you write your book in markdown. It contains all
|
- The `src` directory is where you write your book in markdown. It contains all
|
||||||
the source files, configuration files, etc.
|
the source files, configuration files, etc.
|
||||||
|
|
||||||
- The `book` directory is where your book is rendered. All the output is ready
|
- 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.
|
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
|
- The `SUMMARY.md` is the skeleton of your
|
||||||
book and is discussed in more detail [in another
|
book, and is discussed in more detail [in another
|
||||||
chapter](../format/summary.md)
|
chapter](../format/summary.md).
|
||||||
|
|
||||||
#### Tip: Generate chapters from SUMMARY.md
|
#### Tip: Generate chapters from SUMMARY.md
|
||||||
|
|
||||||
|
@ -52,3 +52,31 @@ directory called `theme` in your source directory so that you can modify it.
|
||||||
|
|
||||||
The theme is selectively overwritten, this means that if you don't want to
|
The theme is selectively overwritten, this means that if you don't want to
|
||||||
overwrite a specific file, just delete it and the default file will be used.
|
overwrite a specific file, just delete it and the default file will be used.
|
||||||
|
|
||||||
|
#### --title
|
||||||
|
|
||||||
|
Specify a title for the book. If not supplied, an interactive prompt will ask for
|
||||||
|
a title.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mdbook init --title="my amazing book"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### --ignore
|
||||||
|
|
||||||
|
Create a `.gitignore` file configured to ignore the `book` directory created when [building] a book.
|
||||||
|
If not supplied, an interactive prompt will ask whether it should be created.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mdbook init --ignore=none
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mdbook init --ignore=git
|
||||||
|
```
|
||||||
|
|
||||||
|
[building]: build.md
|
||||||
|
|
||||||
|
#### --force
|
||||||
|
|
||||||
|
Skip the prompts to create a `.gitignore` and for the title for the book.
|
|
@ -0,0 +1,56 @@
|
||||||
|
# The serve command
|
||||||
|
|
||||||
|
The serve command is used to preview a book by serving it via HTTP at
|
||||||
|
`localhost:3000` by default:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mdbook serve
|
||||||
|
```
|
||||||
|
|
||||||
|
The `serve` command watches the book's `src` directory for
|
||||||
|
changes, rebuilding the book and refreshing clients for each change; this includes
|
||||||
|
re-creating deleted files still mentioned in `SUMMARY.md`! A websocket
|
||||||
|
connection is used to trigger the client-side refresh.
|
||||||
|
|
||||||
|
***Note:*** *The `serve` command is for testing a book's HTML output, and is not
|
||||||
|
intended to be a complete HTTP server for a website.*
|
||||||
|
|
||||||
|
#### Specify a directory
|
||||||
|
|
||||||
|
The `serve` command can take a directory as an argument to use as the book's
|
||||||
|
root instead of the current working directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mdbook serve path/to/book
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server options
|
||||||
|
|
||||||
|
The `serve` hostname defaults to `localhost`, and the port defaults to `3000`. Either option can be specified on the command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mdbook serve path/to/book -p 8000 -n 127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### --open
|
||||||
|
|
||||||
|
When you use the `--open` (`-o`) flag, mdbook will open the book in your
|
||||||
|
default web browser after starting the server.
|
||||||
|
|
||||||
|
#### --dest-dir
|
||||||
|
|
||||||
|
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||||
|
book. Relative paths are interpreted relative to the book's root directory. If
|
||||||
|
not specified it will default to the value of the `build.build-dir` key in
|
||||||
|
`book.toml`, or to `./book`.
|
||||||
|
|
||||||
|
#### Specify exclude patterns
|
||||||
|
|
||||||
|
The `serve` command will not automatically trigger a build for files listed in
|
||||||
|
the `.gitignore` file in the book root directory. The `.gitignore` file may
|
||||||
|
contain file patterns described in the [gitignore
|
||||||
|
documentation](https://git-scm.com/docs/gitignore). This can be useful for
|
||||||
|
ignoring temporary files created by some editors.
|
||||||
|
|
||||||
|
***Note:*** *Only the `.gitignore` from the book root directory is used. Global
|
||||||
|
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used.*
|
|
@ -6,8 +6,7 @@ of code examples that could get outdated. Therefore it is very important for
|
||||||
them to be able to automatically test these code examples.
|
them to be able to automatically test these code examples.
|
||||||
|
|
||||||
mdBook supports a `test` command that will run all available tests in a book. At
|
mdBook supports a `test` command that will run all available tests in a book. At
|
||||||
the moment, only rustdoc tests are supported, but this may be expanded upon in
|
the moment, only Rust tests are supported.
|
||||||
the future.
|
|
||||||
|
|
||||||
#### Disable tests on a code block
|
#### Disable tests on a code block
|
||||||
|
|
||||||
|
@ -43,10 +42,26 @@ mdbook test path/to/book
|
||||||
The `--library-path` (`-L`) option allows you to add directories to the library
|
The `--library-path` (`-L`) option allows you to add directories to the library
|
||||||
search path used by `rustdoc` when it builds and tests the examples. Multiple
|
search path used by `rustdoc` when it builds and tests the examples. Multiple
|
||||||
directories can be specified with multiple options (`-L foo -L bar`) or with a
|
directories can be specified with multiple options (`-L foo -L bar`) or with a
|
||||||
comma-delimited list (`-L foo,bar`).
|
comma-delimited list (`-L foo,bar`). The path should point to the Cargo
|
||||||
|
[build cache](https://doc.rust-lang.org/cargo/guide/build-cache.html) `deps` directory that
|
||||||
|
contains the build output of your project. For example, if your Rust project's book is in a directory
|
||||||
|
named `my-book`, the following command would include the crate's dependencies when running `test`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mdbook test my-book -L target/debug/deps/
|
||||||
|
```
|
||||||
|
|
||||||
|
See the `rustdoc` command-line [documentation](https://doc.rust-lang.org/rustdoc/command-line-arguments.html#-l--library-path-where-to-look-for-dependencies)
|
||||||
|
for more information.
|
||||||
|
|
||||||
#### --dest-dir
|
#### --dest-dir
|
||||||
|
|
||||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||||
book. If not specified it will default to the value of the `build.build-dir` key
|
book. Relative paths are interpreted relative to the book's root directory. If
|
||||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
not specified it will default to the value of the `build.build-dir` key in
|
||||||
|
`book.toml`, or to `./book`.
|
||||||
|
|
||||||
|
#### --chapter
|
||||||
|
|
||||||
|
The `--chapter` (`-c`) option allows you to test a specific chapter of the
|
||||||
|
book using the chapter name or the relative path to the chapter.
|
|
@ -0,0 +1,40 @@
|
||||||
|
# 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; this includes re-creating
|
||||||
|
deleted files still mentioned in `SUMMARY.md`!
|
||||||
|
|
||||||
|
#### Specify a directory
|
||||||
|
|
||||||
|
The `watch` command can take a directory as an argument to use as the book's
|
||||||
|
root 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 the
|
||||||
|
book. Relative paths are interpreted relative to the book's root directory. If
|
||||||
|
not specified it will default to the value of the `build.build-dir` key in
|
||||||
|
`book.toml`, or to `./book`.
|
||||||
|
|
||||||
|
|
||||||
|
#### Specify exclude patterns
|
||||||
|
|
||||||
|
The `watch` command will not automatically trigger a build for files listed in
|
||||||
|
the `.gitignore` file in the book root directory. The `.gitignore` file may
|
||||||
|
contain file patterns described in the [gitignore
|
||||||
|
documentation](https://git-scm.com/docs/gitignore). This can be useful for
|
||||||
|
ignoring temporary files created by some editors.
|
||||||
|
|
||||||
|
_Note: Only `.gitignore` from book root directory is used. Global
|
||||||
|
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used._
|
|
@ -0,0 +1,121 @@
|
||||||
|
# Running `mdbook` in Continuous Integration
|
||||||
|
|
||||||
|
There are a variety of services such as [GitHub Actions] or [GitLab CI/CD] which can be used to test and deploy your book automatically.
|
||||||
|
|
||||||
|
The following provides some general guidelines on how to configure your service to run mdBook.
|
||||||
|
Specific recipes can be found at the [Automated Deployment] wiki page.
|
||||||
|
|
||||||
|
[GitHub Actions]: https://docs.github.com/en/actions
|
||||||
|
[GitLab CI/CD]: https://docs.gitlab.com/ee/ci/
|
||||||
|
[Automated Deployment]: https://github.com/rust-lang/mdBook/wiki/Automated-Deployment
|
||||||
|
|
||||||
|
## Installing mdBook
|
||||||
|
|
||||||
|
There are several different strategies for installing mdBook.
|
||||||
|
The particular method depends on your needs and preferences.
|
||||||
|
|
||||||
|
### Pre-compiled binaries
|
||||||
|
|
||||||
|
Perhaps the easiest method is to use the pre-compiled binaries found on the [GitHub Releases page][releases].
|
||||||
|
A simple approach would be to use the popular `curl` CLI tool to download the executable:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir bin
|
||||||
|
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.37/mdbook-v0.4.37-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||||
|
bin/mdbook build
|
||||||
|
```
|
||||||
|
|
||||||
|
Some considerations for this approach:
|
||||||
|
|
||||||
|
* This is relatively fast, and does not necessarily require dealing with caching.
|
||||||
|
* This does not require installing Rust.
|
||||||
|
* Specifying a specific URL means you have to manually update your script to get a new version.
|
||||||
|
This may be a benefit if you want to lock to a specific version.
|
||||||
|
However, some users prefer to automatically get a newer version when they are published.
|
||||||
|
* You are reliant on the GitHub CDN being available.
|
||||||
|
|
||||||
|
[releases]: https://github.com/rust-lang/mdBook/releases
|
||||||
|
|
||||||
|
### Building from source
|
||||||
|
|
||||||
|
Building from source will require having Rust installed.
|
||||||
|
Some services have Rust pre-installed, but if your service does not, you will need to add a step to install it.
|
||||||
|
|
||||||
|
After Rust is installed, `cargo install` can be used to build and install mdBook.
|
||||||
|
We recommend using a SemVer version specifier so that you get the latest **non-breaking** version of mdBook.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install mdbook --no-default-features --features search --vers "^0.4" --locked
|
||||||
|
```
|
||||||
|
|
||||||
|
This includes several recommended options:
|
||||||
|
|
||||||
|
* `--no-default-features` — Disables features like the HTTP server used by `mdbook serve` that is likely not needed on CI.
|
||||||
|
This will speed up the build time significantly.
|
||||||
|
* `--features search` — Disabling default features means you should then manually enable features that you want, such as the built-in [search] capability.
|
||||||
|
* `--vers "^0.4"` — This will install the most recent version of the `0.4` series.
|
||||||
|
However, versions after like `0.5.0` won't be installed, as they may break your build.
|
||||||
|
Cargo will automatically upgrade mdBook if you have an older version already installed.
|
||||||
|
* `--locked` — This will use the dependencies that were used when mdBook was released.
|
||||||
|
Without `--locked`, it will use the latest version of all dependencies, which may include some fixes since the last release, but may also (rarely) cause build problems.
|
||||||
|
|
||||||
|
You will likely want to investigate caching options, as building mdBook can be somewhat slow.
|
||||||
|
|
||||||
|
[search]: guide/reading.md#search
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
You may want to run tests using [`mdbook test`] every time you push a change or create a pull request.
|
||||||
|
This can be used to validate Rust code examples in the book.
|
||||||
|
|
||||||
|
This will require having Rust installed.
|
||||||
|
Some services have Rust pre-installed, but if your service does not, you will need to add a step to install it.
|
||||||
|
|
||||||
|
Other than making sure the appropriate version of Rust is installed, there's not much more than just running `mdbook test` from the book directory.
|
||||||
|
|
||||||
|
You may also want to consider running other kinds of tests, like [mdbook-linkcheck] which will check for broken links.
|
||||||
|
Or if you have your own style checks, spell checker, or any other tests it might be good to run them in CI.
|
||||||
|
|
||||||
|
[`mdbook test`]: cli/test.md
|
||||||
|
[mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck#continuous-integration
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
You may want to automatically deploy your book.
|
||||||
|
Some may want to do this every time a change is pushed, and others may want to only deploy when a specific release is tagged.
|
||||||
|
|
||||||
|
You'll also need to understand the specifics on how to push a change to your web service.
|
||||||
|
For example, [GitHub Pages] just requires committing the output onto a specific git branch.
|
||||||
|
Other services may require using something like SSH to connect to a remote server.
|
||||||
|
|
||||||
|
The basic outline is that you need to run `mdbook build` to generate the output, and then transfer the files (which are in the `book` directory) to the correct location.
|
||||||
|
|
||||||
|
You may then want to consider if you need to invalidate any caches on your web service.
|
||||||
|
|
||||||
|
See the [Automated Deployment] wiki page for examples of various different services.
|
||||||
|
|
||||||
|
[GitHub Pages]: https://docs.github.com/en/pages
|
||||||
|
|
||||||
|
### 404 handling
|
||||||
|
|
||||||
|
mdBook automatically generates a 404 page to be used for broken links.
|
||||||
|
The default output is a file named `404.html` at the root of the book.
|
||||||
|
Some services like [GitHub Pages] will automatically use this page for broken links.
|
||||||
|
For other services, you may want to consider configuring the web server to use this page as it will provide the reader navigation to get back to the book.
|
||||||
|
|
||||||
|
If your book is not deployed at the root of the domain, then you should set the [`output.html.site-url`] setting so that the 404 page works correctly.
|
||||||
|
It needs to know where the book is deployed in order to load the static files (like CSS) correctly.
|
||||||
|
For example, this guide is deployed at <https://rust-lang.github.io/mdBook/>, and the `site-url` setting is configured like this:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# book.toml
|
||||||
|
[output.html]
|
||||||
|
site-url = "/mdBook/"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can customize the look of the 404 page by creating a file named `src/404.md` in your book.
|
||||||
|
If you want to use a different filename, you can set [`output.html.input-404`] to a different filename.
|
||||||
|
|
||||||
|
[`output.html.site-url`]: format/configuration/renderers.md#html-renderer-options
|
||||||
|
[`output.html.input-404`]: format/configuration/renderers.md#html-renderer-options
|
|
@ -12,7 +12,7 @@ The *For Developers* chapters are here to show you the more advanced usage of
|
||||||
The two main ways a developer can hook into the book's build process is via,
|
The two main ways a developer can hook into the book's build process is via,
|
||||||
|
|
||||||
- [Preprocessors](preprocessors.md)
|
- [Preprocessors](preprocessors.md)
|
||||||
- [Alternate Backends](backends.md)
|
- [Alternative Backends](backends.md)
|
||||||
|
|
||||||
|
|
||||||
## The Build Process
|
## The Build Process
|
||||||
|
@ -24,8 +24,9 @@ The process of rendering a book project goes through several steps.
|
||||||
exist
|
exist
|
||||||
- Load the book chapters into memory
|
- Load the book chapters into memory
|
||||||
- Discover which preprocessors/backends should be used
|
- Discover which preprocessors/backends should be used
|
||||||
2. Run the preprocessors
|
2. For each backend:
|
||||||
3. Call each backend in turn
|
1. Run all the preprocessors.
|
||||||
|
2. Call the backend to render the processed result.
|
||||||
|
|
||||||
|
|
||||||
## Using `mdbook` as a Library
|
## Using `mdbook` as a Library
|
||||||
|
@ -41,6 +42,6 @@ The easiest way to find out how to use the `mdbook` crate is by looking at the
|
||||||
explanation on the configuration system.
|
explanation on the configuration system.
|
||||||
|
|
||||||
|
|
||||||
[`MDBook`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.MDBook.html
|
[`MDBook`]: https://docs.rs/mdbook/*/mdbook/book/struct.MDBook.html
|
||||||
[API Docs]: https://docs.rs/mdbook/*/mdbook/
|
[API Docs]: https://docs.rs/mdbook/*/mdbook/
|
||||||
[config]: file:///home/michael/Documents/forks/mdBook/target/doc/mdbook/config/index.html
|
[config]: https://docs.rs/mdbook/*/mdbook/config/index.html
|
|
@ -1,30 +1,25 @@
|
||||||
# Alternate Backends
|
# Alternative Backends
|
||||||
|
|
||||||
A "backend" is simply a program which `mdbook` will invoke during the book
|
A "backend" is simply a program which `mdbook` will invoke during the book
|
||||||
rendering process. This program is passed a JSON representation of the book and
|
rendering process. This program is passed a JSON representation of the book and
|
||||||
configuration information via `stdin`. Once the backend receives this
|
configuration information via `stdin`. Once the backend receives this
|
||||||
information it is free to do whatever it wants.
|
information it is free to do whatever it wants.
|
||||||
|
|
||||||
There are already several alternate backends on GitHub which can be used as a
|
See [Configuring Renderers](../format/configuration/renderers.md) for more information about using backends.
|
||||||
rough example of how this is accomplished in practice.
|
|
||||||
|
|
||||||
- [mdbook-linkcheck] - a simple program for verifying the book doesn't contain
|
|
||||||
any broken links
|
|
||||||
- [mdbook-epub] - an EPUB renderer
|
|
||||||
- [mdbook-test] - a program to run the book's contents through [rust-skeptic] to
|
|
||||||
verify everything compiles and runs correctly (similar to `rustdoc --test`)
|
|
||||||
|
|
||||||
This page will step you through creating your own alternate backend in the form
|
|
||||||
of a simple word counting program. Although it will be written in Rust, there's
|
|
||||||
no reason why it couldn't be accomplished using something like Python or Ruby.
|
|
||||||
|
|
||||||
|
The community has developed several backends.
|
||||||
|
See the [Third Party Plugins] wiki page for a list of available backends.
|
||||||
|
|
||||||
## Setting Up
|
## Setting Up
|
||||||
|
|
||||||
|
This page will step you through creating your own alternative backend in the form
|
||||||
|
of a simple word counting program. Although it will be written in Rust, there's
|
||||||
|
no reason why it couldn't be accomplished using something like Python or Ruby.
|
||||||
|
|
||||||
First you'll want to create a new binary program and add `mdbook` as a
|
First you'll want to create a new binary program and add `mdbook` as a
|
||||||
dependency.
|
dependency.
|
||||||
|
|
||||||
```
|
```shell
|
||||||
$ cargo new --bin mdbook-wordcount
|
$ cargo new --bin mdbook-wordcount
|
||||||
$ cd mdbook-wordcount
|
$ cd mdbook-wordcount
|
||||||
$ cargo add mdbook
|
$ cargo add mdbook
|
||||||
|
@ -92,8 +87,8 @@ fn count_words(ch: &Chapter) -> usize {
|
||||||
Now we've got the basics running, we want to actually use it. First, install the
|
Now we've got the basics running, we want to actually use it. First, install the
|
||||||
program.
|
program.
|
||||||
|
|
||||||
```
|
```shell
|
||||||
$ cargo install
|
$ cargo install --path .
|
||||||
```
|
```
|
||||||
|
|
||||||
Then `cd` to the particular book you'd like to count the words of and update its
|
Then `cd` to the particular book you'd like to count the words of and update its
|
||||||
|
@ -120,7 +115,7 @@ to make sure to add the HTML backend, even if its table just stays empty.
|
||||||
Now you just need to build your book like normal, and everything should *Just
|
Now you just need to build your book like normal, and everything should *Just
|
||||||
Work*.
|
Work*.
|
||||||
|
|
||||||
```
|
```shell
|
||||||
$ mdbook build
|
$ mdbook build
|
||||||
...
|
...
|
||||||
2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
|
2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
|
||||||
|
@ -140,7 +135,7 @@ Syntax highlighting: 314
|
||||||
MathJax Support: 153
|
MathJax Support: 153
|
||||||
Rust code specific features: 148
|
Rust code specific features: 148
|
||||||
For Developers: 788
|
For Developers: 788
|
||||||
Alternate Backends: 710
|
Alternative Backends: 710
|
||||||
Contributors: 85
|
Contributors: 85
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -261,6 +256,10 @@ in [`RenderContext`].
|
||||||
> **Note:** There is no guarantee that the destination directory exists or is
|
> **Note:** There is no guarantee that the destination directory exists or is
|
||||||
> empty (`mdbook` may leave the previous contents to let backends do caching),
|
> empty (`mdbook` may leave the previous contents to let backends do caching),
|
||||||
> so it's always a good idea to create it with `fs::create_dir_all()`.
|
> so it's always a good idea to create it with `fs::create_dir_all()`.
|
||||||
|
>
|
||||||
|
> If the destination directory already exists, don't assume it will be empty.
|
||||||
|
> To allow backends to cache the results from previous runs, `mdbook` may leave
|
||||||
|
> old content in the directory.
|
||||||
|
|
||||||
There's always the possibility that an error will occur while processing a book
|
There's always the possibility that an error will occur while processing a book
|
||||||
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
|
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
|
||||||
|
@ -288,7 +287,7 @@ like this:
|
||||||
+ if cfg.deny_odds && num_words % 2 == 1 {
|
+ if cfg.deny_odds && num_words % 2 == 1 {
|
||||||
+ eprintln!("{} has an odd number of words!", ch.name);
|
+ eprintln!("{} has an odd number of words!", ch.name);
|
||||||
+ process::exit(1);
|
+ process::exit(1);
|
||||||
}
|
+ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -303,8 +302,8 @@ like this:
|
||||||
|
|
||||||
Now, if we reinstall the backend and build a book,
|
Now, if we reinstall the backend and build a book,
|
||||||
|
|
||||||
```
|
```shell
|
||||||
$ cargo install --force
|
$ cargo install --path . --force
|
||||||
$ mdbook build /path/to/book
|
$ mdbook build /path/to/book
|
||||||
...
|
...
|
||||||
2018-01-16 21:21:39 [INFO] (mdbook::renderer): Invoking the "wordcount" renderer
|
2018-01-16 21:21:39 [INFO] (mdbook::renderer): Invoking the "wordcount" renderer
|
||||||
|
@ -325,11 +324,10 @@ generation or a warning).
|
||||||
All environment variables are passed through to the backend, allowing you to use
|
All environment variables are passed through to the backend, allowing you to use
|
||||||
the usual `RUST_LOG` to control logging verbosity.
|
the usual `RUST_LOG` to control logging verbosity.
|
||||||
|
|
||||||
|
|
||||||
## Wrapping Up
|
## Wrapping Up
|
||||||
|
|
||||||
Although contrived, hopefully this example was enough to show how you'd create
|
Although contrived, hopefully this example was enough to show how you'd create
|
||||||
an alternate backend for `mdbook`. If you feel it's missing something, don't
|
an alternative backend for `mdbook`. If you feel it's missing something, don't
|
||||||
hesitate to create an issue in the [issue tracker] so we can improve the user
|
hesitate to create an issue in the [issue tracker] so we can improve the user
|
||||||
guide.
|
guide.
|
||||||
|
|
||||||
|
@ -338,14 +336,11 @@ as a good example of how it's done in real life, so feel free to skim through
|
||||||
the source code or ask questions.
|
the source code or ask questions.
|
||||||
|
|
||||||
|
|
||||||
[mdbook-linkcheck]: https://github.com/Michael-F-Bryan/mdbook-linkcheck
|
[Third Party Plugins]: https://github.com/rust-lang/mdBook/wiki/Third-party-plugins
|
||||||
[mdbook-epub]: https://github.com/Michael-F-Bryan/mdbook-epub
|
[`RenderContext`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
|
||||||
[mdbook-test]: https://github.com/Michael-F-Bryan/mdbook-test
|
[`RenderContext::from_json()`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json
|
||||||
[rust-skeptic]: https://github.com/budziq/rust-skeptic
|
|
||||||
[`RenderContext`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html
|
|
||||||
[`RenderContext::from_json()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html#method.from_json
|
|
||||||
[`semver`]: https://crates.io/crates/semver
|
[`semver`]: https://crates.io/crates/semver
|
||||||
[`Book`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html
|
[`Book`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html
|
||||||
[`Book::iter()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html#method.iter
|
[`Book::iter()`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html#method.iter
|
||||||
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
|
[`Config`]: https://docs.rs/mdbook/*/mdbook/config/struct.Config.html
|
||||||
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues
|
[issue tracker]: https://github.com/rust-lang/mdBook/issues
|
|
@ -5,37 +5,27 @@ book is loaded and before it gets rendered, allowing you to update and mutate
|
||||||
the book. Possible use cases are:
|
the book. Possible use cases are:
|
||||||
|
|
||||||
- Creating custom helpers like `\{{#include /path/to/file.md}}`
|
- Creating custom helpers like `\{{#include /path/to/file.md}}`
|
||||||
- Updating links so `[some chapter](some_chapter.md)` is automatically changed
|
|
||||||
to `[some chapter](some_chapter.html)` for the HTML renderer
|
|
||||||
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
|
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
|
||||||
mathjax equivalents
|
mathjax equivalents
|
||||||
|
|
||||||
|
See [Configuring Preprocessors](../format/configuration/preprocessors.md) for more information about using preprocessors.
|
||||||
|
|
||||||
## Hooking Into MDBook
|
## Hooking Into MDBook
|
||||||
|
|
||||||
MDBook uses a fairly simple mechanism for discovering third party plugins.
|
MDBook uses a fairly simple mechanism for discovering third party plugins.
|
||||||
A new table is added to `book.toml` (e.g. `preprocessor.foo` for the `foo`
|
A new table is added to `book.toml` (e.g. `[preprocessor.foo]` for the `foo`
|
||||||
preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
|
preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
|
||||||
part of the build process.
|
part of the build process.
|
||||||
|
|
||||||
While preprocessors can be hard-coded to specify which backend it should be run
|
Once the preprocessor has been defined and the build process starts, mdBook executes the command defined in the `preprocessor.foo.command` key twice.
|
||||||
for (e.g. it doesn't make sense for MathJax to be used for non-HTML renderers)
|
The first time it runs the preprocessor to determine if it supports the given renderer.
|
||||||
with the `preprocessor.foo.renderer` key.
|
mdBook passes two arguments to the process: the first argument is the string `supports` and the second argument is the renderer name.
|
||||||
|
The preprocessor should exit with a status code 0 if it supports the given renderer, or return a non-zero exit code if it does not.
|
||||||
|
|
||||||
```toml
|
If the preprocessor supports the renderer, then mdbook runs it a second time, passing JSON data into stdin.
|
||||||
[book]
|
The JSON consists of an array of `[context, book]` where `context` is the serialized object [`PreprocessorContext`] and `book` is a [`Book`] object containing the content of the book.
|
||||||
title = "My Book"
|
|
||||||
authors = ["Michael-F-Bryan"]
|
|
||||||
|
|
||||||
[preprocessor.foo]
|
The preprocessor should return the JSON format of the [`Book`] object to stdout, with any modifications it wishes to perform.
|
||||||
# The command can also be specified manually
|
|
||||||
command = "python3 /path/to/foo.py"
|
|
||||||
# Only run the `foo` preprocessor for the HTML and EPUB renderer
|
|
||||||
renderer = ["html", "epub"]
|
|
||||||
```
|
|
||||||
|
|
||||||
In typical unix style, all inputs to the plugin will be written to `stdin` as
|
|
||||||
JSON and `mdbook` will read from `stdout` if it is expecting output.
|
|
||||||
|
|
||||||
The easiest way to get started is by creating your own implementation of the
|
The easiest way to get started is by creating your own implementation of the
|
||||||
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
|
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
|
||||||
|
@ -71,7 +61,7 @@ The `chapter.content` is just a string which happens to be markdown. While it's
|
||||||
entirely possible to use regular expressions or do a manual find & replace,
|
entirely possible to use regular expressions or do a manual find & replace,
|
||||||
you'll probably want to process the input into something more computer-friendly.
|
you'll probably want to process the input into something more computer-friendly.
|
||||||
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
|
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
|
||||||
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to
|
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] crate allowing you to
|
||||||
translate events back into markdown text.
|
translate events back into markdown text.
|
||||||
|
|
||||||
The following code block shows how to remove all emphasis from markdown,
|
The following code block shows how to remove all emphasis from markdown,
|
||||||
|
@ -106,10 +96,39 @@ fn remove_emphasis(
|
||||||
|
|
||||||
For everything else, have a look [at the complete example][example].
|
For everything else, have a look [at the complete example][example].
|
||||||
|
|
||||||
|
## Implementing a preprocessor with a different language
|
||||||
|
|
||||||
|
The fact that mdBook utilizes stdin and stdout to communicate with the preprocessors makes it easy to implement them in a language other than Rust.
|
||||||
|
The following code shows how to implement a simple preprocessor in Python, which will modify the content of the first chapter.
|
||||||
|
The example below follows the configuration shown above with `preprocessor.foo.command` actually pointing to a Python script.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) > 1: # we check if we received any argument
|
||||||
|
if sys.argv[1] == "supports":
|
||||||
|
# then we are good to return an exit status code of 0, since the other argument will just be the renderer's name
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# load both the context and the book representations from stdin
|
||||||
|
context, book = json.load(sys.stdin)
|
||||||
|
# and now, we can just modify the content of the first chapter
|
||||||
|
book['sections'][0]['Chapter']['content'] = '# Hello'
|
||||||
|
# we are done with the book's modification, we can just print it to stdout,
|
||||||
|
print(json.dumps(book))
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
|
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
|
||||||
[pc]: https://crates.io/crates/pulldown-cmark
|
[pc]: https://crates.io/crates/pulldown-cmark
|
||||||
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
||||||
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs
|
[example]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||||
[an example no-op preprocessor]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/nop-preprocessor.rs
|
[an example no-op preprocessor]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||||
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
|
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
|
||||||
[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut
|
[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut
|
||||||
|
[`PreprocessorContext`]: https://docs.rs/mdbook/latest/mdbook/preprocess/struct.PreprocessorContext.html
|
||||||
|
[`Book`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
This section details the configuration options available in the ***book.toml***:
|
||||||
|
- **[General]** configuration including the `book`, `rust`, `build` sections
|
||||||
|
- **[Preprocessor]** configuration for default and custom book preprocessors
|
||||||
|
- **[Renderer]** configuration for the HTML, Markdown and custom renderers
|
||||||
|
- **[Environment Variable]** configuration for overriding configuration options in your environment
|
||||||
|
|
||||||
|
[General]: general.md
|
||||||
|
[Preprocessor]: preprocessors.md
|
||||||
|
[Renderer]: renderers.md
|
||||||
|
[Environment Variable]: environment-variables.md
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Environment Variables
|
||||||
|
|
||||||
|
All configuration values can be overridden from the command line by setting the
|
||||||
|
corresponding environment variable. Because many operating systems restrict
|
||||||
|
environment variables to be alphanumeric characters or `_`, the configuration
|
||||||
|
key needs to be formatted slightly differently to the normal `foo.bar.baz` form.
|
||||||
|
|
||||||
|
Variables starting with `MDBOOK_` are used for configuration. The key is created
|
||||||
|
by removing the `MDBOOK_` prefix and turning the resulting string into
|
||||||
|
`kebab-case`. Double underscores (`__`) separate nested keys, while a single
|
||||||
|
underscore (`_`) is replaced with a dash (`-`).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
- `MDBOOK_foo` -> `foo`
|
||||||
|
- `MDBOOK_FOO` -> `foo`
|
||||||
|
- `MDBOOK_FOO__BAR` -> `foo.bar`
|
||||||
|
- `MDBOOK_FOO_BAR` -> `foo-bar`
|
||||||
|
- `MDBOOK_FOO_bar__baz` -> `foo-bar.baz`
|
||||||
|
|
||||||
|
So by setting the `MDBOOK_BOOK__TITLE` environment variable you can override the
|
||||||
|
book's title without needing to touch your `book.toml`.
|
||||||
|
|
||||||
|
> **Note:** To facilitate setting more complex config items, the value of an
|
||||||
|
> environment variable is first parsed as JSON, falling back to a string if the
|
||||||
|
> parse fails.
|
||||||
|
>
|
||||||
|
> This means, if you so desired, you could override all book metadata when
|
||||||
|
> building the book with something like
|
||||||
|
>
|
||||||
|
> ```shell
|
||||||
|
> $ export MDBOOK_BOOK='{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}'
|
||||||
|
> $ mdbook build
|
||||||
|
> ```
|
||||||
|
|
||||||
|
The latter case may be useful in situations where `mdbook` is invoked from a
|
||||||
|
script or CI, where it sometimes isn't possible to update the `book.toml` before
|
||||||
|
building.
|
|
@ -0,0 +1,118 @@
|
||||||
|
# General Configuration
|
||||||
|
|
||||||
|
You can configure the parameters for your book in the ***book.toml*** file.
|
||||||
|
|
||||||
|
Here is an example of what a ***book.toml*** file might look like:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[book]
|
||||||
|
title = "Example book"
|
||||||
|
authors = ["John Doe"]
|
||||||
|
description = "The example book covers examples."
|
||||||
|
|
||||||
|
[rust]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
build-dir = "my-example-book"
|
||||||
|
create-missing = false
|
||||||
|
|
||||||
|
[preprocessor.index]
|
||||||
|
|
||||||
|
[preprocessor.links]
|
||||||
|
|
||||||
|
[output.html]
|
||||||
|
additional-css = ["custom.css"]
|
||||||
|
|
||||||
|
[output.html.search]
|
||||||
|
limit-results = 15
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported configuration options
|
||||||
|
|
||||||
|
It is important to note that **any** relative path specified in the
|
||||||
|
configuration will always be taken relative from the root of the book where the
|
||||||
|
configuration file is located.
|
||||||
|
|
||||||
|
### General metadata
|
||||||
|
|
||||||
|
This is general information about your book.
|
||||||
|
|
||||||
|
- **title:** The title of the book
|
||||||
|
- **authors:** The author(s) of the book
|
||||||
|
- **description:** A description for the book, which is added as meta
|
||||||
|
information in the html `<head>` of each page
|
||||||
|
- **src:** By default, the source directory is found in the directory named
|
||||||
|
`src` directly under the root folder. But this is configurable with the `src`
|
||||||
|
key in the configuration file.
|
||||||
|
- **language:** The main language of the book, which is used as a language attribute `<html lang="en">` for example.
|
||||||
|
This is also used to derive the direction of text (RTL, LTR) within the book.
|
||||||
|
- **text-direction**: The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL). Possible values: `ltr`, `rtl`.
|
||||||
|
When not specified, the text direction is derived from the book's `language` attribute.
|
||||||
|
|
||||||
|
**book.toml**
|
||||||
|
```toml
|
||||||
|
[book]
|
||||||
|
title = "Example book"
|
||||||
|
authors = ["John Doe", "Jane Doe"]
|
||||||
|
description = "The example book covers examples."
|
||||||
|
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
||||||
|
language = "en"
|
||||||
|
text-direction = "ltr"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust options
|
||||||
|
|
||||||
|
Options for the Rust language, relevant to running tests and playground
|
||||||
|
integration.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[rust]
|
||||||
|
edition = "2015" # the default edition for code blocks
|
||||||
|
```
|
||||||
|
|
||||||
|
- **edition**: Rust edition to use by default for the code snippets. Default
|
||||||
|
is "2015". Individual code blocks can be controlled with the `edition2015`,
|
||||||
|
`edition2018` or `edition2021` annotations, such as:
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
```rust,edition2015
|
||||||
|
// This only works in 2015.
|
||||||
|
let try = true;
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
### Build options
|
||||||
|
|
||||||
|
This controls the build process of your book.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[build]
|
||||||
|
build-dir = "book" # the directory where the output is placed
|
||||||
|
create-missing = true # whether or not to create missing pages
|
||||||
|
use-default-preprocessors = true # use the default preprocessors
|
||||||
|
extra-watch-dirs = [] # directories to watch for triggering builds
|
||||||
|
```
|
||||||
|
|
||||||
|
- **build-dir:** The directory to put the rendered book in. By default this is
|
||||||
|
`book/` in the book's root directory.
|
||||||
|
This can overridden with the `--dest-dir` CLI option.
|
||||||
|
- **create-missing:** By default, any missing files specified in `SUMMARY.md`
|
||||||
|
will be created when the book is built (i.e. `create-missing = true`). If this
|
||||||
|
is `false` then the build process will instead exit with an error if any files
|
||||||
|
do not exist.
|
||||||
|
- **use-default-preprocessors:** Disable the default preprocessors (of `links` &
|
||||||
|
`index`) by setting this option to `false`.
|
||||||
|
|
||||||
|
If you have the same, and/or other preprocessors declared via their table
|
||||||
|
of configuration, they will run instead.
|
||||||
|
|
||||||
|
- For clarity, with no preprocessor configuration, the default `links` and
|
||||||
|
`index` will run.
|
||||||
|
- Setting `use-default-preprocessors = false` will disable these
|
||||||
|
default preprocessors from running.
|
||||||
|
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||||
|
`use-default-preprocessors` that `links` it will run.
|
||||||
|
- **extra-watch-dirs**: A list of paths to directories that will be watched in
|
||||||
|
the `watch` and `serve` commands. Changes to files under these directories will
|
||||||
|
trigger rebuilds. Useful if your book depends on files outside its `src` directory.
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Configuring Preprocessors
|
||||||
|
|
||||||
|
Preprocessors are extensions that can modify the raw Markdown source before it gets sent to the renderer.
|
||||||
|
|
||||||
|
The following preprocessors are built-in and included by default:
|
||||||
|
|
||||||
|
- `links`: Expands the `{{ #playground }}`, `{{ #include }}`, and `{{ #rustdoc_include }}` handlebars
|
||||||
|
helpers in a chapter to include the contents of a file.
|
||||||
|
See [Including files] for more.
|
||||||
|
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
|
||||||
|
to say, all `README.md` would be rendered to an index file `index.html` in the
|
||||||
|
rendered book.
|
||||||
|
|
||||||
|
The built-in preprocessors can be disabled with the [`build.use-default-preprocessors`] config option.
|
||||||
|
|
||||||
|
The community has developed several preprocessors.
|
||||||
|
See the [Third Party Plugins] wiki page for a list of available preprocessors.
|
||||||
|
|
||||||
|
For information on how to create a new preprocessor, see the [Preprocessors for Developers] chapter.
|
||||||
|
|
||||||
|
[Including files]: ../mdbook.md#including-files
|
||||||
|
[`build.use-default-preprocessors`]: general.md#build-options
|
||||||
|
[Third Party Plugins]: https://github.com/rust-lang/mdBook/wiki/Third-party-plugins
|
||||||
|
[Preprocessors for Developers]: ../../for_developers/preprocessors.md
|
||||||
|
|
||||||
|
## Custom Preprocessor Configuration
|
||||||
|
|
||||||
|
Preprocessors can be added by including a `preprocessor` table in `book.toml` with the name of the preprocessor.
|
||||||
|
For example, if you have a preprocessor called `mdbook-example`, then you can include it with:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[preprocessor.example]
|
||||||
|
```
|
||||||
|
|
||||||
|
With this table, mdBook will execute the `mdbook-example` preprocessor.
|
||||||
|
|
||||||
|
This table can include additional key-value pairs that are specific to the preprocessor.
|
||||||
|
For example, if our example preprocessor needed some extra configuration options:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[preprocessor.example]
|
||||||
|
some-extra-feature = true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Locking a Preprocessor dependency to a renderer
|
||||||
|
|
||||||
|
You can explicitly specify that a preprocessor should run for a renderer by
|
||||||
|
binding the two together.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[preprocessor.example]
|
||||||
|
renderers = ["html"] # example preprocessor only runs with the HTML renderer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Provide Your Own Command
|
||||||
|
|
||||||
|
By default when you add a `[preprocessor.foo]` table to your `book.toml` file,
|
||||||
|
`mdbook` will try to invoke the `mdbook-foo` executable. If you want to use a
|
||||||
|
different program name or pass in command-line arguments, this behaviour can
|
||||||
|
be overridden by adding a `command` field.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[preprocessor.random]
|
||||||
|
command = "python random.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Require A Certain Order
|
||||||
|
|
||||||
|
The order in which preprocessors are run can be controlled with the `before` and `after` fields.
|
||||||
|
For example, suppose you want your `linenos` preprocessor to process lines that may have been `{{#include}}`d; then you want it to run after the built-in `links` preprocessor, which you can require using either the `before` or `after` field:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[preprocessor.linenos]
|
||||||
|
after = [ "links" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[preprocessor.links]
|
||||||
|
before = [ "linenos" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
It would also be possible, though redundant, to specify both of the above in the same config file.
|
||||||
|
|
||||||
|
Preprocessors having the same priority specified through `before` and `after` are sorted by name.
|
||||||
|
Any infinite loops will be detected and produce an error.
|
|
@ -0,0 +1,319 @@
|
||||||
|
# Configuring Renderers
|
||||||
|
|
||||||
|
Renderers (also called "backends") are responsible for creating the output of the book.
|
||||||
|
|
||||||
|
The following backends are built-in:
|
||||||
|
|
||||||
|
* [`html`](#html-renderer-options) — This renders the book to HTML.
|
||||||
|
This is enabled by default if no other `[output]` tables are defined in `book.toml`.
|
||||||
|
* [`markdown`](#markdown-renderer) — This outputs the book as markdown after running the preprocessors.
|
||||||
|
This is useful for debugging preprocessors.
|
||||||
|
|
||||||
|
The community has developed several backends.
|
||||||
|
See the [Third Party Plugins] wiki page for a list of available backends.
|
||||||
|
|
||||||
|
For information on how to create a new backend, see the [Backends for Developers] chapter.
|
||||||
|
|
||||||
|
[Third Party Plugins]: https://github.com/rust-lang/mdBook/wiki/Third-party-plugins
|
||||||
|
[Backends for Developers]: ../../for_developers/backends.md
|
||||||
|
|
||||||
|
## Output tables
|
||||||
|
|
||||||
|
Backends can be added by including a `output` table in `book.toml` with the name of the backend.
|
||||||
|
For example, if you have a backend called `mdbook-wordcount`, then you can include it with:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.wordcount]
|
||||||
|
```
|
||||||
|
|
||||||
|
With this table, mdBook will execute the `mdbook-wordcount` backend.
|
||||||
|
|
||||||
|
This table can include additional key-value pairs that are specific to the backend.
|
||||||
|
For example, if our example backend needed some extra configuration options:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.wordcount]
|
||||||
|
ignores = ["Example Chapter"]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you define any `[output]` tables, then the `html` backend is not enabled by default.
|
||||||
|
If you want to keep the `html` backend running, then just include it in the `book.toml` file.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[book]
|
||||||
|
title = "My Awesome Book"
|
||||||
|
|
||||||
|
[output.wordcount]
|
||||||
|
|
||||||
|
[output.html]
|
||||||
|
```
|
||||||
|
|
||||||
|
If more than one `output` table is included, this changes the behavior for the layout of the output directory.
|
||||||
|
If there is only one backend, then it places its output directly in the `book` directory (see [`build.build-dir`] to override this location).
|
||||||
|
If there is more than one backend, then each backend is placed in a separate directory underneath `book`.
|
||||||
|
For example, the above would have directories `book/html` and `book/wordcount`.
|
||||||
|
|
||||||
|
[`build.build-dir`]: general.md#build-options
|
||||||
|
|
||||||
|
### Custom backend commands
|
||||||
|
|
||||||
|
By default when you add an `[output.foo]` table to your `book.toml` file,
|
||||||
|
`mdbook` will try to invoke the `mdbook-foo` executable.
|
||||||
|
If you want to use a different program name or pass in command-line arguments,
|
||||||
|
this behaviour can be overridden by adding a `command` field.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.random]
|
||||||
|
command = "python random.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional backends
|
||||||
|
|
||||||
|
If you enable a backend that isn't installed, the default behavior is to throw an error.
|
||||||
|
This behavior can be changed by marking the backend as optional:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.wordcount]
|
||||||
|
optional = true
|
||||||
|
```
|
||||||
|
|
||||||
|
This demotes the error to a warning.
|
||||||
|
|
||||||
|
|
||||||
|
## HTML renderer options
|
||||||
|
|
||||||
|
The HTML renderer has a variety of options detailed below.
|
||||||
|
They should be specified in the `[output.html]` table of the `book.toml` file.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Example book.toml file with all output options.
|
||||||
|
[book]
|
||||||
|
title = "Example book"
|
||||||
|
authors = ["John Doe", "Jane Doe"]
|
||||||
|
description = "The example book covers examples."
|
||||||
|
|
||||||
|
[output.html]
|
||||||
|
theme = "my-theme"
|
||||||
|
default-theme = "light"
|
||||||
|
preferred-dark-theme = "navy"
|
||||||
|
smart-punctuation = true
|
||||||
|
mathjax-support = false
|
||||||
|
copy-fonts = true
|
||||||
|
additional-css = ["custom.css", "custom2.css"]
|
||||||
|
additional-js = ["custom.js"]
|
||||||
|
no-section-label = false
|
||||||
|
git-repository-url = "https://github.com/rust-lang/mdBook"
|
||||||
|
git-repository-icon = "fa-github"
|
||||||
|
edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
|
||||||
|
site-url = "/example-book/"
|
||||||
|
cname = "myproject.rs"
|
||||||
|
input-404 = "not-found.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
The following configuration options are available:
|
||||||
|
|
||||||
|
- **theme:** mdBook comes with a default theme and all the resource files needed
|
||||||
|
for it. But if this option is set, mdBook will selectively overwrite the theme
|
||||||
|
files with the ones found in the specified folder.
|
||||||
|
- **default-theme:** The theme color scheme to select by default in the
|
||||||
|
'Change Theme' dropdown. Defaults to `light`.
|
||||||
|
- **preferred-dark-theme:** The default dark theme. This theme will be used if
|
||||||
|
the browser requests the dark version of the site via the
|
||||||
|
['prefers-color-scheme'](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
|
||||||
|
CSS media query. Defaults to `navy`.
|
||||||
|
- **smart-punctuation:** Converts quotes to curly quotes, `...` to `…`, `--` to en-dash, and `---` to em-dash.
|
||||||
|
See [Smart Punctuation].
|
||||||
|
Defaults to `false`.
|
||||||
|
- **curly-quotes:** Deprecated alias for `smart-punctuation`.
|
||||||
|
- **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to
|
||||||
|
`false`.
|
||||||
|
- **copy-fonts:** (**Deprecated**) If `true` (the default), mdBook uses its built-in fonts which are copied to the output directory.
|
||||||
|
If `false`, the built-in fonts will not be used.
|
||||||
|
This option is deprecated. If you want to define your own custom fonts,
|
||||||
|
create a `theme/fonts/fonts.css` file and store the fonts in the `theme/fonts/` directory.
|
||||||
|
- **google-analytics:** This field has been deprecated and will be removed in a future release.
|
||||||
|
Use the `theme/head.hbs` file to add the appropriate Google Analytics code instead.
|
||||||
|
- **additional-css:** If you need to slightly change the appearance of your book
|
||||||
|
without overwriting the whole style, you can specify a set of stylesheets that
|
||||||
|
will be loaded after the default ones where you can surgically change the
|
||||||
|
style.
|
||||||
|
- **additional-js:** If you need to add some behaviour to your book without
|
||||||
|
removing the current behaviour, you can specify a set of JavaScript files that
|
||||||
|
will be loaded alongside the default one.
|
||||||
|
- **no-section-label:** mdBook by defaults adds numeric section labels in the table of
|
||||||
|
contents column. For example, "1.", "2.1". Set this option to true to disable
|
||||||
|
those labels. Defaults to `false`.
|
||||||
|
- **git-repository-url:** A url to the git repository for the book. If provided
|
||||||
|
an icon link will be output in the menu bar of the book.
|
||||||
|
- **git-repository-icon:** The FontAwesome icon class to use for the git
|
||||||
|
repository link. Defaults to `fa-github` which looks like <i class="fa fa-github"></i>.
|
||||||
|
If you are not using GitHub, another option to consider is `fa-code-fork` which looks like <i class="fa fa-code-fork"></i>.
|
||||||
|
- **edit-url-template:** Edit url template, when provided shows a
|
||||||
|
"Suggest an edit" button (which looks like <i class="fa fa-edit"></i>) for directly jumping to editing the currently
|
||||||
|
viewed page. For e.g. GitHub projects set this to
|
||||||
|
`https://github.com/<owner>/<repo>/edit/<branch>/{path}` or for
|
||||||
|
Bitbucket projects set it to
|
||||||
|
`https://bitbucket.org/<owner>/<repo>/src/<branch>/{path}?mode=edit`
|
||||||
|
where {path} will be replaced with the full path of the file in the
|
||||||
|
repository.
|
||||||
|
- **input-404:** The name of the markdown file used for missing files.
|
||||||
|
The corresponding output file will be the same, with the extension replaced with `html`.
|
||||||
|
Defaults to `404.md`.
|
||||||
|
- **site-url:** The url where the book will be hosted. This is required to ensure
|
||||||
|
navigation links and script/css imports in the 404 file work correctly, even when accessing
|
||||||
|
urls in subdirectories. Defaults to `/`. If `site-url` is set,
|
||||||
|
make sure to use document relative links for your assets, meaning they should not start with `/`.
|
||||||
|
- **cname:** The DNS subdomain or apex domain at which your book will be hosted.
|
||||||
|
This string will be written to a file named CNAME in the root of your site, as
|
||||||
|
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages
|
||||||
|
site*][custom domain]).
|
||||||
|
|
||||||
|
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||||
|
|
||||||
|
### `[output.html.print]`
|
||||||
|
|
||||||
|
The `[output.html.print]` table provides options for controlling the printable output.
|
||||||
|
By default, mdBook will include an icon on the top right of the book (which looks like <i class="fa fa-print"></i>) that will print the book as a single page.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.html.print]
|
||||||
|
enable = true # include support for printable output
|
||||||
|
page-break = true # insert page-break after each chapter
|
||||||
|
```
|
||||||
|
|
||||||
|
- **enable:** Enable print support. When `false`, all print support will not be
|
||||||
|
rendered. Defaults to `true`.
|
||||||
|
- **page-break:** Insert page breaks between chapters. Defaults to `true`.
|
||||||
|
|
||||||
|
### `[output.html.fold]`
|
||||||
|
|
||||||
|
The `[output.html.fold]` table provides options for controlling folding of the chapter listing in the navigation sidebar.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.html.fold]
|
||||||
|
enable = false # whether or not to enable section folding
|
||||||
|
level = 0 # the depth to start folding
|
||||||
|
```
|
||||||
|
|
||||||
|
- **enable:** Enable section-folding. When off, all folds are open.
|
||||||
|
Defaults to `false`.
|
||||||
|
- **level:** The higher the more folded regions are open. When level is 0, all
|
||||||
|
folds are closed. Defaults to `0`.
|
||||||
|
|
||||||
|
### `[output.html.playground]`
|
||||||
|
|
||||||
|
The `[output.html.playground]` table provides options for controlling Rust sample code blocks, and their integration with the [Rust Playground].
|
||||||
|
|
||||||
|
[Rust Playground]: https://play.rust-lang.org/
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.html.playground]
|
||||||
|
editable = false # allows editing the source code
|
||||||
|
copyable = true # include the copy button for copying code snippets
|
||||||
|
copy-js = true # includes the JavaScript for the code editor
|
||||||
|
line-numbers = false # displays line numbers for editable code
|
||||||
|
runnable = true # displays a run button for rust code
|
||||||
|
```
|
||||||
|
|
||||||
|
- **editable:** Allow editing the source code. Defaults to `false`.
|
||||||
|
- **copyable:** Display the copy button on code snippets. Defaults to `true`.
|
||||||
|
- **copy-js:** Copy JavaScript files for the editor to the output directory.
|
||||||
|
Defaults to `true`.
|
||||||
|
- **line-numbers:** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`.
|
||||||
|
- **runnable:** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. Defaults to `true`.
|
||||||
|
|
||||||
|
[Ace]: https://ace.c9.io/
|
||||||
|
|
||||||
|
### `[output.html.code]`
|
||||||
|
|
||||||
|
The `[output.html.code]` table provides options for controlling code blocks.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.html.code]
|
||||||
|
# A prefix string per language (one or more chars).
|
||||||
|
# Any line starting with whitespace+prefix is hidden.
|
||||||
|
hidelines = { python = "~" }
|
||||||
|
```
|
||||||
|
|
||||||
|
- **hidelines:** A table that defines how [hidden code lines](../mdbook.md#hiding-code-lines) work for each language.
|
||||||
|
The key is the language and the value is a string that will cause code lines starting with that prefix to be hidden.
|
||||||
|
|
||||||
|
### `[output.html.search]`
|
||||||
|
|
||||||
|
The `[output.html.search]` table provides options for controlling the built-in text [search].
|
||||||
|
mdBook must be compiled with the `search` feature enabled (on by default).
|
||||||
|
|
||||||
|
[search]: ../../guide/reading.md#search
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.html.search]
|
||||||
|
enable = true # enables the search feature
|
||||||
|
limit-results = 30 # maximum number of search results
|
||||||
|
teaser-word-count = 30 # number of words used for a search result teaser
|
||||||
|
use-boolean-and = true # multiple search terms must all match
|
||||||
|
boost-title = 2 # ranking boost factor for matches in headers
|
||||||
|
boost-hierarchy = 1 # ranking boost factor for matches in page names
|
||||||
|
boost-paragraph = 1 # ranking boost factor for matches in text
|
||||||
|
expand = true # partial words will match longer terms
|
||||||
|
heading-split-level = 3 # link results to heading levels
|
||||||
|
copy-js = true # include Javascript code for search
|
||||||
|
```
|
||||||
|
|
||||||
|
- **enable:** Enables the search feature. Defaults to `true`.
|
||||||
|
- **limit-results:** The maximum number of search results. Defaults to `30`.
|
||||||
|
- **teaser-word-count:** The number of words used for a search result teaser.
|
||||||
|
Defaults to `30`.
|
||||||
|
- **use-boolean-and:** Define the logical link between multiple search words. If
|
||||||
|
true, all search words must appear in each result. Defaults to `false`.
|
||||||
|
- **boost-title:** Boost factor for the search result score if a search word
|
||||||
|
appears in the header. Defaults to `2`.
|
||||||
|
- **boost-hierarchy:** Boost factor for the search result score if a search word
|
||||||
|
appears in the hierarchy. The hierarchy contains all titles of the parent
|
||||||
|
documents and all parent headings. Defaults to `1`.
|
||||||
|
- **boost-paragraph:** Boost factor for the search result score if a search word
|
||||||
|
appears in the text. Defaults to `1`.
|
||||||
|
- **expand:** True if search should match longer results e.g. search `micro`
|
||||||
|
should match `microwave`. Defaults to `true`.
|
||||||
|
- **heading-split-level:** Search results will link to a section of the document
|
||||||
|
which contains the result. Documents are split into sections by headings this
|
||||||
|
level or less. Defaults to `3`. (`### This is a level 3 heading`)
|
||||||
|
- **copy-js:** Copy JavaScript files for the search implementation to the output
|
||||||
|
directory. Defaults to `true`.
|
||||||
|
|
||||||
|
### `[output.html.redirect]`
|
||||||
|
|
||||||
|
The `[output.html.redirect]` table provides a way to add redirects.
|
||||||
|
This is useful when you move, rename, or remove a page to ensure that links to the old URL will go to the new location.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.html.redirect]
|
||||||
|
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
|
||||||
|
"/other-installation-methods.html" = "../infra/other-installation-methods.html"
|
||||||
|
```
|
||||||
|
|
||||||
|
The table contains key-value pairs where the key is where the redirect file needs to be created, as an absolute path from the build directory, (e.g. `/appendices/bibliography.html`).
|
||||||
|
The value can be any valid URI the browser should navigate to (e.g. `https://rust-lang.org/`, `/overview.html`, or `../bibliography.html`).
|
||||||
|
|
||||||
|
This will generate an HTML page which will automatically redirect to the given location.
|
||||||
|
Note that the source location does not support `#` anchor redirects.
|
||||||
|
|
||||||
|
## Markdown Renderer
|
||||||
|
|
||||||
|
The Markdown renderer will run preprocessors and then output the resulting
|
||||||
|
Markdown. This is mostly useful for debugging preprocessors, especially in
|
||||||
|
conjunction with `mdbook test` to see the Markdown that `mdbook` is passing
|
||||||
|
to `rustdoc`.
|
||||||
|
|
||||||
|
The Markdown renderer is included with `mdbook` but disabled by default.
|
||||||
|
Enable it by adding an empty table to your `book.toml` as follows:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.markdown]
|
||||||
|
```
|
||||||
|
|
||||||
|
There are no configuration options for the Markdown renderer at this time;
|
||||||
|
only whether it is enabled or disabled.
|
||||||
|
|
||||||
|
See [the preprocessors documentation](preprocessors.md) for how to
|
||||||
|
specify which preprocessors should run before the Markdown renderer.
|
|
@ -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");
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<svg height="144" width="144" xmlns="http://www.w3.org/2000/svg"><path d="m71.05 23.68c-26.06 0-47.27 21.22-47.27 47.27s21.22 47.27 47.27 47.27 47.27-21.22 47.27-47.27-21.22-47.27-47.27-47.27zm-.07 4.2a3.1 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm7.12 5.12a38.27 38.27 0 0 1 26.2 18.66l-3.67 8.28c-.63 1.43.02 3.11 1.44 3.75l7.06 3.13a38.27 38.27 0 0 1 .08 6.64h-3.93c-.39 0-.55.26-.55.64v1.8c0 4.24-2.39 5.17-4.49 5.4-2 .23-4.21-.84-4.49-2.06-1.18-6.63-3.14-8.04-6.24-10.49 3.85-2.44 7.85-6.05 7.85-10.87 0-5.21-3.57-8.49-6-10.1-3.42-2.25-7.2-2.7-8.22-2.7h-40.6a38.27 38.27 0 0 1 21.41-12.08l4.79 5.02c1.08 1.13 2.87 1.18 4 .09zm-44.2 23.02a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm74.15.14a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm-68.29.5h5.42v24.44h-10.94a38.27 38.27 0 0 1 -1.24-14.61l6.7-2.98c1.43-.64 2.08-2.31 1.44-3.74zm22.62.26h12.91c.67 0 4.71.77 4.71 3.8 0 2.51-3.1 3.41-5.65 3.41h-11.98zm0 17.56h9.89c.9 0 4.83.26 6.08 5.28.39 1.54 1.26 6.56 1.85 8.17.59 1.8 2.98 5.4 5.53 5.4h16.14a38.27 38.27 0 0 1 -3.54 4.1l-6.57-1.41c-1.53-.33-3.04.65-3.37 2.18l-1.56 7.28a38.27 38.27 0 0 1 -31.91-.15l-1.56-7.28c-.33-1.53-1.83-2.51-3.36-2.18l-6.43 1.38a38.27 38.27 0 0 1 -3.32-3.92h31.27c.35 0 .59-.06.59-.39v-11.06c0-.32-.24-.39-.59-.39h-9.15zm-14.43 25.33a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11zm46.05.14a3.11 3.11 0 0 1 3.02 3.11 3.11 3.11 0 0 1 -6.22 0 3.11 3.11 0 0 1 3.2-3.11z"/><path d="m115.68 70.95a44.63 44.63 0 0 1 -44.63 44.63 44.63 44.63 0 0 1 -44.63-44.63 44.63 44.63 0 0 1 44.63-44.63 44.63 44.63 0 0 1 44.63 44.63zm-.84-4.31 6.96 4.31-6.96 4.31 5.98 5.59-7.66 2.87 4.78 6.65-8.09 1.32 3.4 7.46-8.19-.29 1.88 7.98-7.98-1.88.29 8.19-7.46-3.4-1.32 8.09-6.65-4.78-2.87 7.66-5.59-5.98-4.31 6.96-4.31-6.96-5.59 5.98-2.87-7.66-6.65 4.78-1.32-8.09-7.46 3.4.29-8.19-7.98 1.88 1.88-7.98-8.19.29 3.4-7.46-8.09-1.32 4.78-6.65-7.66-2.87 5.98-5.59-6.96-4.31 6.96-4.31-5.98-5.59 7.66-2.87-4.78-6.65 8.09-1.32-3.4-7.46 8.19.29-1.88-7.98 7.98 1.88-.29-8.19 7.46 3.4 1.32-8.09 6.65 4.78 2.87-7.66 5.59 5.98 4.31-6.96 4.31 6.96 5.59-5.98 2.87 7.66 6.65-4.78 1.32 8.09 7.46-3.4-.29 8.19 7.98-1.88-1.88 7.98 8.19-.29-3.4 7.46 8.09 1.32-4.78 6.65 7.66 2.87z" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/></svg>
|
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1,235 @@
|
||||||
|
# Markdown
|
||||||
|
|
||||||
|
mdBook's [parser](https://github.com/raphlinus/pulldown-cmark) adheres to the [CommonMark](https://commonmark.org/) specification with some extensions described below.
|
||||||
|
You can take a quick [tutorial](https://commonmark.org/help/tutorial/),
|
||||||
|
or [try out](https://spec.commonmark.org/dingus/) CommonMark in real time. A complete Markdown overview is out of scope for
|
||||||
|
this documentation, but below is a high level overview of some of the basics. For a more in-depth experience, check out the
|
||||||
|
[Markdown Guide](https://www.markdownguide.org).
|
||||||
|
|
||||||
|
## Text and Paragraphs
|
||||||
|
|
||||||
|
Text is rendered relatively predictably:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Here is a line of text.
|
||||||
|
|
||||||
|
This is a new line.
|
||||||
|
```
|
||||||
|
|
||||||
|
Will look like you might expect:
|
||||||
|
|
||||||
|
Here is a line of text.
|
||||||
|
|
||||||
|
This is a new line.
|
||||||
|
|
||||||
|
## Headings
|
||||||
|
|
||||||
|
Headings use the `#` marker and should be on a line by themselves. More `#` mean smaller headings:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### A heading
|
||||||
|
|
||||||
|
Some text.
|
||||||
|
|
||||||
|
#### A smaller heading
|
||||||
|
|
||||||
|
More text.
|
||||||
|
```
|
||||||
|
|
||||||
|
### A heading
|
||||||
|
|
||||||
|
Some text.
|
||||||
|
|
||||||
|
#### A smaller heading
|
||||||
|
|
||||||
|
More text.
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
Lists can be unordered or ordered. Ordered lists will order automatically:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
* milk
|
||||||
|
* eggs
|
||||||
|
* butter
|
||||||
|
|
||||||
|
1. carrots
|
||||||
|
1. celery
|
||||||
|
1. radishes
|
||||||
|
```
|
||||||
|
|
||||||
|
* milk
|
||||||
|
* eggs
|
||||||
|
* butter
|
||||||
|
|
||||||
|
1. carrots
|
||||||
|
1. celery
|
||||||
|
1. radishes
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
Linking to a URL or local file is easy:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||||
|
|
||||||
|
Read about [mdBook](mdbook.md).
|
||||||
|
|
||||||
|
A bare url: <https://www.rust-lang.org>.
|
||||||
|
```
|
||||||
|
|
||||||
|
Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||||
|
|
||||||
|
Read about [mdBook](mdbook.md).
|
||||||
|
|
||||||
|
A bare url: <https://www.rust-lang.org>.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Relative links that end with `.md` will be converted to the `.html` extension.
|
||||||
|
It is recommended to use `.md` links when possible.
|
||||||
|
This is useful when viewing the Markdown file outside of mdBook, for example on GitHub or GitLab which render Markdown automatically.
|
||||||
|
|
||||||
|
Links to `README.md` will be converted to `index.html`.
|
||||||
|
This is done since some services like GitHub render README files automatically, but web servers typically expect the root file to be called `index.html`.
|
||||||
|
|
||||||
|
You can link to individual headings with `#` fragments.
|
||||||
|
For example, `mdbook.md#text-and-paragraphs` would link to the [Text and Paragraphs](#text-and-paragraphs) section above.
|
||||||
|
The ID is created by transforming the heading such as converting to lowercase and replacing spaces with dashes.
|
||||||
|
You can click on any heading and look at the URL in your browser to see what the fragment looks like.
|
||||||
|
|
||||||
|
## Images
|
||||||
|
|
||||||
|
Including images is simply a matter of including a link to them, much like in the _Links_ section above. The following markdown
|
||||||
|
includes the Rust logo SVG image found in the `images` directory at the same level as this file:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
![The Rust Logo](images/rust-logo-blk.svg)
|
||||||
|
```
|
||||||
|
|
||||||
|
Produces the following HTML when built with mdBook:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p><img src="images/rust-logo-blk.svg" alt="The Rust Logo" /></p>
|
||||||
|
```
|
||||||
|
|
||||||
|
Which, of course displays the image like so:
|
||||||
|
|
||||||
|
![The Rust Logo](images/rust-logo-blk.svg)
|
||||||
|
|
||||||
|
## Extensions
|
||||||
|
|
||||||
|
mdBook has several extensions beyond the standard CommonMark specification.
|
||||||
|
|
||||||
|
### Strikethrough
|
||||||
|
|
||||||
|
Text may be rendered with a horizontal line through the center by wrapping the
|
||||||
|
text with one or two tilde characters on each side:
|
||||||
|
|
||||||
|
```text
|
||||||
|
An example of ~~strikethrough text~~.
|
||||||
|
```
|
||||||
|
|
||||||
|
This example will render as:
|
||||||
|
|
||||||
|
> An example of ~~strikethrough text~~.
|
||||||
|
|
||||||
|
This follows the [GitHub Strikethrough extension][strikethrough].
|
||||||
|
|
||||||
|
### Footnotes
|
||||||
|
|
||||||
|
A footnote generates a small numbered link in the text which when clicked
|
||||||
|
takes the reader to the footnote text at the bottom of the item. The footnote
|
||||||
|
label is written similarly to a link reference with a caret at the front. The
|
||||||
|
footnote text is written like a link reference definition, with the text
|
||||||
|
following the label. Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
This is an example of a footnote[^note].
|
||||||
|
|
||||||
|
[^note]: This text is the contents of the footnote, which will be rendered
|
||||||
|
towards the bottom.
|
||||||
|
```
|
||||||
|
|
||||||
|
This example will render as:
|
||||||
|
|
||||||
|
> This is an example of a footnote[^note].
|
||||||
|
>
|
||||||
|
> [^note]: This text is the contents of the footnote, which will be rendered
|
||||||
|
> towards the bottom.
|
||||||
|
|
||||||
|
The footnotes are automatically numbered based on the order the footnotes are
|
||||||
|
written.
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
|
||||||
|
Tables can be written using pipes and dashes to draw the rows and columns of
|
||||||
|
the table. These will be translated to HTML table matching the shape. Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
| Header1 | Header2 |
|
||||||
|
|---------|---------|
|
||||||
|
| abc | def |
|
||||||
|
```
|
||||||
|
|
||||||
|
This example will render similarly to this:
|
||||||
|
|
||||||
|
| Header1 | Header2 |
|
||||||
|
|---------|---------|
|
||||||
|
| abc | def |
|
||||||
|
|
||||||
|
See the specification for the [GitHub Tables extension][tables] for more
|
||||||
|
details on the exact syntax supported.
|
||||||
|
|
||||||
|
### Task lists
|
||||||
|
|
||||||
|
Task lists can be used as a checklist of items that have been completed.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```md
|
||||||
|
- [x] Complete task
|
||||||
|
- [ ] Incomplete task
|
||||||
|
```
|
||||||
|
|
||||||
|
This will render as:
|
||||||
|
|
||||||
|
> - [x] Complete task
|
||||||
|
> - [ ] Incomplete task
|
||||||
|
|
||||||
|
See the specification for the [task list extension] for more details.
|
||||||
|
|
||||||
|
### Smart punctuation
|
||||||
|
|
||||||
|
Some ASCII punctuation sequences will be automatically turned into fancy Unicode
|
||||||
|
characters:
|
||||||
|
|
||||||
|
| ASCII sequence | Unicode |
|
||||||
|
|----------------|---------|
|
||||||
|
| `--` | – |
|
||||||
|
| `---` | — |
|
||||||
|
| `...` | … |
|
||||||
|
| `"` | “ or ”, depending on context |
|
||||||
|
| `'` | ‘ or ’, depending on context |
|
||||||
|
|
||||||
|
So, no need to manually enter those Unicode characters!
|
||||||
|
|
||||||
|
This feature is disabled by default.
|
||||||
|
To enable it, see the [`output.html.smart-punctuation`] config option.
|
||||||
|
|
||||||
|
[strikethrough]: https://github.github.com/gfm/#strikethrough-extension-
|
||||||
|
[tables]: https://github.github.com/gfm/#tables-extension-
|
||||||
|
[task list extension]: https://github.github.com/gfm/#task-list-items-extension-
|
||||||
|
[`output.html.smart-punctuation`]: configuration/renderers.md#html-renderer-options
|
||||||
|
|
||||||
|
### Heading attributes
|
||||||
|
|
||||||
|
Headings can have a custom HTML ID and classes. This lets you maintain the same ID even if you change the heading's text, it also lets you add multiple classes in the heading.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```md
|
||||||
|
# Example heading { #first .class1 .class2 }
|
||||||
|
```
|
||||||
|
|
||||||
|
This makes the level 1 heading with the content `Example heading`, ID `first`, and classes `class1` and `class2`. Note that the attributes should be space-separated.
|
||||||
|
|
||||||
|
More information can be found in the [heading attrs spec page](https://github.com/raphlinus/pulldown-cmark/blob/master/pulldown-cmark/specs/heading_attrs.txt).
|
|
@ -0,0 +1,364 @@
|
||||||
|
# mdBook-specific features
|
||||||
|
|
||||||
|
## Hiding code lines
|
||||||
|
|
||||||
|
There is a feature in mdBook that lets you hide code lines by prepending them with a specific prefix.
|
||||||
|
|
||||||
|
For the Rust language, you can use the `#` character as a prefix which will hide lines [like you would with Rustdoc][rustdoc-hide].
|
||||||
|
|
||||||
|
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/write-documentation/documentation-tests.html#hiding-portions-of-the-example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# fn main() {
|
||||||
|
let x = 5;
|
||||||
|
let y = 6;
|
||||||
|
|
||||||
|
println!("{}", x + y);
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
Will render as
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# fn main() {
|
||||||
|
let x = 5;
|
||||||
|
let y = 6;
|
||||||
|
|
||||||
|
println!("{}", x + y);
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
When you tap or hover the mouse over the code block, there will be an eyeball icon (<i class="fa fa-eye"></i>) which will toggle the visibility of the hidden lines.
|
||||||
|
|
||||||
|
By default, this only works for code examples that are annotated with `rust`.
|
||||||
|
However, you can define custom prefixes for other languages by adding a new line-hiding prefix in your `book.toml` with the language name and prefix character(s):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.html.code.hidelines]
|
||||||
|
python = "~"
|
||||||
|
```
|
||||||
|
|
||||||
|
The prefix will hide any lines that begin with the given prefix. With the python prefix shown above, this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~hidden()
|
||||||
|
nothidden():
|
||||||
|
~ hidden()
|
||||||
|
~hidden()
|
||||||
|
nothidden()
|
||||||
|
```
|
||||||
|
|
||||||
|
will render as
|
||||||
|
|
||||||
|
```python
|
||||||
|
~hidden()
|
||||||
|
nothidden():
|
||||||
|
~ hidden()
|
||||||
|
~hidden()
|
||||||
|
nothidden()
|
||||||
|
```
|
||||||
|
|
||||||
|
This behavior can be overridden locally with a different prefix. This has the same effect as above:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
```python,hidelines=!!!
|
||||||
|
!!!hidden()
|
||||||
|
nothidden():
|
||||||
|
!!! hidden()
|
||||||
|
!!!hidden()
|
||||||
|
nothidden()
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## Rust Playground
|
||||||
|
|
||||||
|
Rust language code blocks will automatically get a play button (<i class="fa fa-play"></i>) which will execute the code and display the output just below the code block.
|
||||||
|
This works by sending the code to the [Rust Playground].
|
||||||
|
|
||||||
|
```rust
|
||||||
|
println!("Hello, World!");
|
||||||
|
```
|
||||||
|
|
||||||
|
If there is no `main` function, then the code is automatically wrapped inside one.
|
||||||
|
|
||||||
|
If you wish to disable the play button for a code block, you can include the `noplayground` option on the code block like this:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
```rust,noplayground
|
||||||
|
let mut name = String::new();
|
||||||
|
std::io::stdin().read_line(&mut name).expect("failed to read line");
|
||||||
|
println!("Hello {}!", name);
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Or, if you wish to disable the play button for all code blocks in your book, you can write the config to the `book.toml` like this.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[output.html.playground]
|
||||||
|
runnable = false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rust code block attributes
|
||||||
|
|
||||||
|
Additional attributes can be included in Rust code blocks with comma, space, or tab-separated terms just after the language term. For example:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
```rust,ignore
|
||||||
|
# This example won't be tested.
|
||||||
|
panic!("oops!");
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
These are particularly important when using [`mdbook test`] to test Rust examples.
|
||||||
|
These use the same attributes as [rustdoc attributes], with a few additions:
|
||||||
|
|
||||||
|
* `editable` — Enables the [editor].
|
||||||
|
* `noplayground` — Removes the play button, but will still be tested.
|
||||||
|
* `mdbook-runnable` — Forces the play button to be displayed.
|
||||||
|
This is intended to be combined with the `ignore` attribute for examples that should not be tested, but you want to allow the reader to run.
|
||||||
|
* `ignore` — Will not be tested and no play button is shown, but it is still highlighted as Rust syntax.
|
||||||
|
* `should_panic` — When executed, it should produce a panic.
|
||||||
|
* `no_run` — The code is compiled when tested, but it is not run.
|
||||||
|
The play button is also not shown.
|
||||||
|
* `compile_fail` — The code should fail to compile.
|
||||||
|
* `edition2015`, `edition2018`, `edition2021` — Forces the use of a specific Rust edition.
|
||||||
|
See [`rust.edition`] to set this globally.
|
||||||
|
|
||||||
|
[`mdbook test`]: ../cli/test.md
|
||||||
|
[rustdoc attributes]: https://doc.rust-lang.org/rustdoc/documentation-tests.html#attributes
|
||||||
|
[editor]: theme/editor.md
|
||||||
|
[`rust.edition`]: configuration/general.md#rust-options
|
||||||
|
|
||||||
|
## Including files
|
||||||
|
|
||||||
|
With the following syntax, you can include files into your book:
|
||||||
|
|
||||||
|
```hbs
|
||||||
|
\{{#include file.rs}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The path to the file has to be relative from the current source file.
|
||||||
|
|
||||||
|
mdBook will interpret included files as Markdown. Since the include command
|
||||||
|
is usually used for inserting code snippets and examples, you will often
|
||||||
|
wrap the command with ```` ``` ```` to display the file contents without
|
||||||
|
interpreting them.
|
||||||
|
|
||||||
|
````hbs
|
||||||
|
```
|
||||||
|
\{{#include file.rs}}
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
## Including portions of a file
|
||||||
|
Often you only need a specific part of the file, e.g. relevant lines for an
|
||||||
|
example. We support four different modes of partial includes:
|
||||||
|
|
||||||
|
```hbs
|
||||||
|
\{{#include file.rs:2}}
|
||||||
|
\{{#include file.rs::10}}
|
||||||
|
\{{#include file.rs:2:}}
|
||||||
|
\{{#include file.rs:2:10}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The first command only includes the second line from file `file.rs`. The second
|
||||||
|
command includes all lines up to line 10, i.e. the lines from 11 till the end of
|
||||||
|
the file are omitted. The third command includes all lines from line 2, i.e. the
|
||||||
|
first line is omitted. The last command includes the excerpt of `file.rs`
|
||||||
|
consisting of lines 2 to 10.
|
||||||
|
|
||||||
|
To avoid breaking your book when modifying included files, you can also
|
||||||
|
include a specific section using anchors instead of line numbers.
|
||||||
|
An anchor is a pair of matching lines. The line beginning an anchor must
|
||||||
|
match the regex `ANCHOR:\s*[\w_-]+` and similarly the ending line must match
|
||||||
|
the regex `ANCHOR_END:\s*[\w_-]+`. This allows you to put anchors in
|
||||||
|
any kind of commented line.
|
||||||
|
|
||||||
|
Consider the following file to include:
|
||||||
|
```rs
|
||||||
|
/* ANCHOR: all */
|
||||||
|
|
||||||
|
// ANCHOR: component
|
||||||
|
struct Paddle {
|
||||||
|
hello: f32,
|
||||||
|
}
|
||||||
|
// ANCHOR_END: component
|
||||||
|
|
||||||
|
////////// ANCHOR: system
|
||||||
|
impl System for MySystem { ... }
|
||||||
|
////////// ANCHOR_END: system
|
||||||
|
|
||||||
|
/* ANCHOR_END: all */
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in the book, all you have to do is:
|
||||||
|
````hbs
|
||||||
|
Here is a component:
|
||||||
|
```rust,no_run,noplayground
|
||||||
|
\{{#include file.rs:component}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is a system:
|
||||||
|
```rust,no_run,noplayground
|
||||||
|
\{{#include file.rs:system}}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the full file.
|
||||||
|
```rust,no_run,noplayground
|
||||||
|
\{{#include file.rs:all}}
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
Lines containing anchor patterns inside the included anchor are ignored.
|
||||||
|
|
||||||
|
## Including a file but initially hiding all except specified lines
|
||||||
|
|
||||||
|
The `rustdoc_include` helper is for including code from external Rust files that contain complete
|
||||||
|
examples, but only initially showing particular lines specified with line numbers or anchors in the
|
||||||
|
same way as with `include`.
|
||||||
|
|
||||||
|
The lines not in the line number range or between the anchors will still be included, but they will
|
||||||
|
be prefaced with `#`. This way, a reader can expand the snippet to see the complete example, and
|
||||||
|
Rustdoc will use the complete example when you run `mdbook test`.
|
||||||
|
|
||||||
|
For example, consider a file named `file.rs` that contains this Rust program:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
let x = add_one(2);
|
||||||
|
assert_eq!(x, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_one(num: i32) -> i32 {
|
||||||
|
num + 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can include a snippet that initially shows only line 2 by using this syntax:
|
||||||
|
|
||||||
|
````hbs
|
||||||
|
To call the `add_one` function, we pass it an `i32` and bind the returned value to `x`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
\{{#rustdoc_include file.rs:2}}
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
This would have the same effect as if we had manually inserted the code and hidden all but line 2
|
||||||
|
using `#`:
|
||||||
|
|
||||||
|
````hbs
|
||||||
|
To call the `add_one` function, we pass it an `i32` and bind the returned value to `x`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# fn main() {
|
||||||
|
let x = add_one(2);
|
||||||
|
# assert_eq!(x, 3);
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# fn add_one(num: i32) -> i32 {
|
||||||
|
# num + 1
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
That is, it looks like this (click the "expand" icon to see the rest of the file):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# fn main() {
|
||||||
|
let x = add_one(2);
|
||||||
|
# assert_eq!(x, 3);
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# fn add_one(num: i32) -> i32 {
|
||||||
|
# num + 1
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inserting runnable Rust files
|
||||||
|
|
||||||
|
With the following syntax, you can insert runnable Rust files into your book:
|
||||||
|
|
||||||
|
```hbs
|
||||||
|
\{{#playground file.rs}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The path to the Rust file has to be relative from the current source file.
|
||||||
|
|
||||||
|
When play is clicked, the code snippet will be sent to the [Rust Playground] to be
|
||||||
|
compiled and run. The result is sent back and displayed directly underneath the
|
||||||
|
code.
|
||||||
|
|
||||||
|
Here is what a rendered code snippet looks like:
|
||||||
|
|
||||||
|
{{#playground example.rs}}
|
||||||
|
|
||||||
|
Any additional values passed after the filename will be included as attributes of the code block.
|
||||||
|
For example `\{{#playground example.rs editable}}` will create the code block like the following:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
```rust,editable
|
||||||
|
# Contents of example.rs here.
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
And the `editable` attribute will enable the [editor] as described at [Rust code block attributes](#rust-code-block-attributes).
|
||||||
|
|
||||||
|
[Rust Playground]: https://play.rust-lang.org/
|
||||||
|
|
||||||
|
## Controlling page \<title\>
|
||||||
|
|
||||||
|
A chapter can set a \<title\> that is different from its entry in the table of
|
||||||
|
contents (sidebar) by including a `\{{#title ...}}` near the top of the page.
|
||||||
|
|
||||||
|
```hbs
|
||||||
|
\{{#title My Title}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTML classes provided by mdBook
|
||||||
|
|
||||||
|
<img class="right" src="images/rust-logo-blk.svg" alt="The Rust logo">
|
||||||
|
|
||||||
|
### `class="left"` and `"right"`
|
||||||
|
|
||||||
|
These classes are provided by default, for inline HTML to float images.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<img class="right" src="images/rust-logo-blk.svg" alt="The Rust logo">
|
||||||
|
```
|
||||||
|
|
||||||
|
### `class="hidden"`
|
||||||
|
|
||||||
|
HTML tags with class `hidden` will not be shown.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="hidden">This will not be seen.</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="hidden">This will not be seen.</div>
|
||||||
|
|
||||||
|
### `class="warning"`
|
||||||
|
|
||||||
|
To make a warning or similar note stand out, wrap it in a warning div.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="warning">
|
||||||
|
|
||||||
|
This is a bad thing that you should pay attention to.
|
||||||
|
|
||||||
|
Warning blocks should be used sparingly in documentation, to avoid "warning
|
||||||
|
fatigue," where people are trained to ignore them because they usually don't
|
||||||
|
matter for what they're doing.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
<div class="warning">
|
||||||
|
|
||||||
|
This is a bad thing that you should pay attention to.
|
||||||
|
|
||||||
|
Warning blocks should be used sparingly in documentation, to avoid "warning
|
||||||
|
fatigue," where people are trained to ignore them because they usually don't
|
||||||
|
matter for what they're doing.
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,100 @@
|
||||||
|
# SUMMARY.md
|
||||||
|
|
||||||
|
The summary file is used by mdBook to know what chapters to include, in what
|
||||||
|
order they should appear, what their hierarchy is and where the source files
|
||||||
|
are. Without this file, there is no book.
|
||||||
|
|
||||||
|
This markdown file must be named `SUMMARY.md`. Its formatting
|
||||||
|
is very strict and must follow the structure outlined below to allow for easy
|
||||||
|
parsing. Any element not specified below, be it formatting or textual, is likely
|
||||||
|
to be ignored at best, or may cause an error when attempting to build the book.
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
1. ***Title*** - While optional, it's common practice to begin with a title, generally <code
|
||||||
|
class="language-markdown"># Summary</code>. This is ignored by the parser however, and
|
||||||
|
can be omitted.
|
||||||
|
```markdown
|
||||||
|
# Summary
|
||||||
|
```
|
||||||
|
|
||||||
|
1. ***Prefix Chapter*** - Before the main numbered chapters, prefix chapters can be added
|
||||||
|
that will not be numbered. This is useful for forewords,
|
||||||
|
introductions, etc. There are, however, some constraints. Prefix chapters cannot be
|
||||||
|
nested; they should all be on the root level. And you cannot add
|
||||||
|
prefix chapters once you have added numbered chapters.
|
||||||
|
```markdown
|
||||||
|
[A Prefix Chapter](relative/path/to/markdown.md)
|
||||||
|
|
||||||
|
- [First Chapter](relative/path/to/markdown2.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. ***Part Title*** -
|
||||||
|
Level 1 headers can be used as a title for the following numbered chapters.
|
||||||
|
This can be used to logically separate different sections of the book.
|
||||||
|
The title is rendered as unclickable text.
|
||||||
|
Titles are optional, and the numbered chapters can be broken into as many parts as desired.
|
||||||
|
Part titles must be h1 headers (one `#`), other heading levels are ignored.
|
||||||
|
```markdown
|
||||||
|
# My Part Title
|
||||||
|
|
||||||
|
- [First Chapter](relative/path/to/markdown.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. ***Numbered Chapter*** - Numbered chapters outline the main content of the book
|
||||||
|
and can be nested, resulting in a nice hierarchy
|
||||||
|
(chapters, sub-chapters, etc.).
|
||||||
|
```markdown
|
||||||
|
# Title of Part
|
||||||
|
|
||||||
|
- [First Chapter](relative/path/to/markdown.md)
|
||||||
|
- [Second Chapter](relative/path/to/markdown2.md)
|
||||||
|
- [Sub Chapter](relative/path/to/markdown3.md)
|
||||||
|
|
||||||
|
# Title of Another Part
|
||||||
|
|
||||||
|
- [Another Chapter](relative/path/to/markdown4.md)
|
||||||
|
```
|
||||||
|
Numbered chapters can be denoted with either `-` or `*` (do not mix delimiters).
|
||||||
|
|
||||||
|
1. ***Suffix Chapter*** - Like prefix chapters, suffix chapters are unnumbered, but they come after
|
||||||
|
numbered chapters.
|
||||||
|
```markdown
|
||||||
|
- [Last Chapter](relative/path/to/markdown.md)
|
||||||
|
|
||||||
|
[Title of Suffix Chapter](relative/path/to/markdown2.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. ***Draft chapters*** - Draft chapters are chapters without a file and thus content.
|
||||||
|
The purpose of a draft chapter is to signal future chapters still to be written.
|
||||||
|
Or when still laying out the structure of the book to avoid creating the files
|
||||||
|
while you are still changing the structure of the book a lot.
|
||||||
|
Draft chapters will be rendered in the HTML renderer as disabled links in the table
|
||||||
|
of contents, as you can see for the next chapter in the table of contents on the left.
|
||||||
|
Draft chapters are written like normal chapters but without writing the path to the file.
|
||||||
|
```markdown
|
||||||
|
- [Draft Chapter]()
|
||||||
|
```
|
||||||
|
|
||||||
|
1. ***Separators*** - Separators can be added before, in between, and after any other element. They result
|
||||||
|
in an HTML rendered line in the built table of contents. A separator is
|
||||||
|
a line containing exclusively dashes and at least three of them: `---`.
|
||||||
|
```markdown
|
||||||
|
# My Part Title
|
||||||
|
|
||||||
|
[A Prefix Chapter](relative/path/to/markdown.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [First Chapter](relative/path/to/markdown2.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
Below is the markdown source for the `SUMMARY.md` for this guide, with the resulting table
|
||||||
|
of contents as rendered to the left.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
{{#include ../SUMMARY.md}}
|
||||||
|
```
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Theme
|
||||||
|
|
||||||
|
The default renderer uses a [handlebars](https://handlebarsjs.com) template to
|
||||||
|
render your markdown files and comes with a default theme included in the mdBook
|
||||||
|
binary.
|
||||||
|
|
||||||
|
The theme is totally customizable, you can selectively replace every file from
|
||||||
|
the theme by your own by adding a `theme` directory next to `src` folder in your
|
||||||
|
project root. Create a new file with the name of the file you want to override
|
||||||
|
and now that file will be used instead of the default file.
|
||||||
|
|
||||||
|
Here are the files you can override:
|
||||||
|
|
||||||
|
- **_index.hbs_** is the handlebars template.
|
||||||
|
- **_head.hbs_** is appended to the HTML `<head>` section.
|
||||||
|
- **_header.hbs_** content is appended on top of every book page.
|
||||||
|
- **_css/_** contains the CSS files for styling the book.
|
||||||
|
- **_css/chrome.css_** is for UI elements.
|
||||||
|
- **_css/general.css_** is the base styles.
|
||||||
|
- **_css/print.css_** is the style for printer output.
|
||||||
|
- **_css/variables.css_** contains variables used in other CSS files.
|
||||||
|
- **_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.svg_** and **_favicon.png_** the favicon that will be used. The SVG
|
||||||
|
version is used by [newer browsers].
|
||||||
|
- **fonts/fonts.css** contains the definition of which fonts to load.
|
||||||
|
Custom fonts can be included in the `fonts` directory.
|
||||||
|
|
||||||
|
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` and just
|
||||||
|
remove the files you don't want to override.
|
||||||
|
|
||||||
|
`mdbook init --theme` will not create every file listed above.
|
||||||
|
Some files, such as `head.hbs`, do not have built-in equivalents.
|
||||||
|
Just create the file if you need it.
|
||||||
|
|
||||||
|
If you completely replace all built-in themes, be sure to also set
|
||||||
|
[`output.html.preferred-dark-theme`] in the config, which defaults to the
|
||||||
|
built-in `navy` theme.
|
||||||
|
|
||||||
|
[`output.html.preferred-dark-theme`]: ../configuration/renderers.md#html-renderer-options
|
||||||
|
[newer browsers]: https://caniuse.com/#feat=link-icon-svg
|
|
@ -1,25 +1,27 @@
|
||||||
# Editor
|
# Editor
|
||||||
|
|
||||||
In addition to providing runnable code playpens, mdBook optionally allows them
|
In addition to providing runnable code playgrounds, mdBook optionally allows them
|
||||||
to be editable. In order to enable editable code blocks, the following needs to
|
to be editable. In order to enable editable code blocks, the following needs to
|
||||||
be added to the ***book.toml***:
|
be added to the ***book.toml***:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[output.html.playpen]
|
[output.html.playground]
|
||||||
editable = true
|
editable = true
|
||||||
```
|
```
|
||||||
|
|
||||||
To make a specific block available for editing, the attribute `editable` needs
|
To make a specific block available for editing, the attribute `editable` needs
|
||||||
to be added to it:
|
to be added to it:
|
||||||
|
|
||||||
<pre><code class="language-markdown">```rust,editable
|
~~~markdown
|
||||||
|
```rust,editable
|
||||||
fn main() {
|
fn main() {
|
||||||
let number = 5;
|
let number = 5;
|
||||||
print!("{}", number);
|
print!("{}", number);
|
||||||
}
|
}
|
||||||
```</code></pre>
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
The above will result in this editable playpen:
|
The above will result in this editable playground:
|
||||||
|
|
||||||
```rust,editable
|
```rust,editable
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -28,19 +30,19 @@ fn main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note the new `Undo Changes` button in the editable playpens.
|
Note the new `Undo Changes` button in the editable playgrounds.
|
||||||
|
|
||||||
## Customizing the Editor
|
## Customizing the Editor
|
||||||
|
|
||||||
By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired,
|
By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired,
|
||||||
the functionality may be overriden by providing a different folder:
|
the functionality may be overridden by providing a different folder:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[output.html.playpen]
|
[output.html.playground]
|
||||||
editable = true
|
editable = true
|
||||||
editor = "/path/to/editor"
|
editor = "/path/to/editor"
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that for the editor changes to function correctly, the `book.js` inside of
|
Note that for the editor changes to function correctly, the `book.js` inside of
|
||||||
the `theme` folder will need to be overriden as it has some couplings with the
|
the `theme` folder will need to be overridden as it has some couplings with the
|
||||||
default Ace editor.
|
default Ace editor.
|
|
@ -17,10 +17,10 @@ handlebars template you can access this information by using
|
||||||
|
|
||||||
Here is a list of the properties that are exposed:
|
Here is a list of the properties that are exposed:
|
||||||
|
|
||||||
- ***language*** Language of the book in the form `en`. To use in <code
|
- ***language*** Language of the book in the form `en`, as specified in `book.toml` (if not specified, defaults to `en`). To use in <code
|
||||||
class="language-html">\<html lang="{{ language }}"></code> for example. At the
|
class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||||
moment it is hardcoded.
|
- ***title*** Title used for the current page. This is identical to `{{ chapter_title }} - {{ book_title }}` unless `book_title` is not set in which case it just defaults to the `chapter_title`.
|
||||||
- ***title*** Title of the book, as specified in `book.toml`
|
- ***book_title*** Title of the book, as specified in `book.toml`
|
||||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||||
|
|
||||||
- ***path*** Relative path to the original markdown file from the source
|
- ***path*** Relative path to the original markdown file from the source
|
||||||
|
@ -51,7 +51,8 @@ at your disposal.
|
||||||
{{#toc}}{{/toc}}
|
{{#toc}}{{/toc}}
|
||||||
```
|
```
|
||||||
|
|
||||||
and outputs something that looks like this, depending on the structure of your book
|
and outputs something that looks like this, depending on the structure of your
|
||||||
|
book
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<ul class="chapter">
|
<ul class="chapter">
|
||||||
|
@ -64,8 +65,10 @@ at your disposal.
|
||||||
</ul>
|
</ul>
|
||||||
```
|
```
|
||||||
|
|
||||||
If you would like to make a toc with another structure, you have access to the chapters property containing all the data.
|
If you would like to make a toc with another structure, you have access to the
|
||||||
The only limitation at the moment is that you would have to do it with JavaScript instead of with a handlebars helper.
|
chapters property containing all the data. The only limitation at the moment
|
||||||
|
is that you would have to do it with JavaScript instead of with a handlebars
|
||||||
|
helper.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script>
|
<script>
|
||||||
|
@ -76,14 +79,15 @@ at your disposal.
|
||||||
|
|
||||||
### 2. previous / next
|
### 2. previous / next
|
||||||
|
|
||||||
The previous and next helpers expose a `link` and `name` property to the previous and next chapters.
|
The previous and next helpers expose a `link` and `title` property to the
|
||||||
|
previous and next chapters.
|
||||||
|
|
||||||
They are used like this
|
They are used like this
|
||||||
|
|
||||||
```handlebars
|
```handlebars
|
||||||
{{#previous}}
|
{{#previous}}
|
||||||
<a href="{{link}}" class="nav-chapters previous">
|
<a href="{{link}}" class="nav-chapters previous">
|
||||||
<i class="fa fa-angle-left"></i>
|
<i class="fa fa-angle-left"></i> {{title}}
|
||||||
</a>
|
</a>
|
||||||
{{/previous}}
|
{{/previous}}
|
||||||
```
|
```
|
||||||
|
@ -94,4 +98,4 @@ at your disposal.
|
||||||
------
|
------
|
||||||
|
|
||||||
*If you would like other properties or helpers exposed, please [create a new
|
*If you would like other properties or helpers exposed, please [create a new
|
||||||
issue](https://github.com/rust-lang-nursery/mdBook/issues)*
|
issue](https://github.com/rust-lang/mdBook/issues)*
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Syntax Highlighting
|
||||||
|
|
||||||
|
mdBook uses [Highlight.js](https://highlightjs.org) with a custom theme
|
||||||
|
for syntax highlighting.
|
||||||
|
|
||||||
|
Automatic language detection has been turned off, so you will probably want to
|
||||||
|
specify the programming language you use like this:
|
||||||
|
|
||||||
|
~~~markdown
|
||||||
|
```rust
|
||||||
|
fn main() {
|
||||||
|
// Some code
|
||||||
|
}
|
||||||
|
```
|
||||||
|
~~~
|
||||||
|
|
||||||
|
## Supported languages
|
||||||
|
|
||||||
|
These languages are supported by default, but you can add more by supplying
|
||||||
|
your own `highlight.js` file:
|
||||||
|
|
||||||
|
- apache
|
||||||
|
- armasm
|
||||||
|
- bash
|
||||||
|
- c
|
||||||
|
- coffeescript
|
||||||
|
- cpp
|
||||||
|
- csharp
|
||||||
|
- css
|
||||||
|
- d
|
||||||
|
- diff
|
||||||
|
- go
|
||||||
|
- handlebars
|
||||||
|
- haskell
|
||||||
|
- http
|
||||||
|
- ini
|
||||||
|
- java
|
||||||
|
- javascript
|
||||||
|
- json
|
||||||
|
- julia
|
||||||
|
- kotlin
|
||||||
|
- less
|
||||||
|
- lua
|
||||||
|
- makefile
|
||||||
|
- markdown
|
||||||
|
- nginx
|
||||||
|
- nim
|
||||||
|
- nix
|
||||||
|
- objectivec
|
||||||
|
- perl
|
||||||
|
- php
|
||||||
|
- plaintext
|
||||||
|
- properties
|
||||||
|
- python
|
||||||
|
- r
|
||||||
|
- ruby
|
||||||
|
- rust
|
||||||
|
- scala
|
||||||
|
- scss
|
||||||
|
- shell
|
||||||
|
- sql
|
||||||
|
- swift
|
||||||
|
- typescript
|
||||||
|
- x86asm
|
||||||
|
- xml
|
||||||
|
- yaml
|
||||||
|
|
||||||
|
## 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
|
||||||
|
the `theme` folder of your book.
|
||||||
|
|
||||||
|
Now your theme will be used instead of the default theme.
|
||||||
|
|
||||||
|
## Improve default theme
|
||||||
|
|
||||||
|
If you think the default theme doesn't look quite right for a specific language,
|
||||||
|
or could be improved, feel free to [submit a new
|
||||||
|
issue](https://github.com/rust-lang/mdBook/issues) explaining what you
|
||||||
|
have in mind and I will take a look at it.
|
||||||
|
|
||||||
|
You could also create a pull-request with the proposed improvements.
|
||||||
|
|
||||||
|
Overall the theme should be light and sober, without too many flashy colors.
|
|
@ -0,0 +1,7 @@
|
||||||
|
# User Guide
|
||||||
|
|
||||||
|
This user guide provides an introduction to basic concepts of using mdBook.
|
||||||
|
|
||||||
|
- [Installation](installation.md)
|
||||||
|
- [Reading Books](reading.md)
|
||||||
|
- [Creating a Book](creating.md)
|
|
@ -0,0 +1,109 @@
|
||||||
|
# Creating a Book
|
||||||
|
|
||||||
|
Once you have the `mdbook` CLI tool installed, you can use it to create and render a book.
|
||||||
|
|
||||||
|
## Initializing a book
|
||||||
|
|
||||||
|
The `mdbook init` command will create a new directory containing an empty book for you to get started.
|
||||||
|
Give it the name of the directory that you want to create:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mdbook init my-first-book
|
||||||
|
```
|
||||||
|
|
||||||
|
It will ask a few questions before generating the book.
|
||||||
|
After answering the questions, you can change the current directory into the new book:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd my-first-book
|
||||||
|
```
|
||||||
|
|
||||||
|
There are several ways to render a book, but one of the easiest methods is to use the `serve` command, which will build your book and start a local webserver:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mdbook serve --open
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--open` option will open your default web browser to view your new book.
|
||||||
|
You can leave the server running even while you edit the content of the book, and `mdbook` will automatically rebuild the output *and* automatically refresh your web browser.
|
||||||
|
|
||||||
|
Check out the [CLI Guide](../cli/index.html) for more information about other `mdbook` commands and CLI options.
|
||||||
|
|
||||||
|
## Anatomy of a book
|
||||||
|
|
||||||
|
A book is built from several files which define the settings and layout of the book.
|
||||||
|
|
||||||
|
### `book.toml`
|
||||||
|
|
||||||
|
In the root of your book, there is a `book.toml` file which contains settings for describing how to build your book.
|
||||||
|
This is written in the [TOML markup language](https://toml.io/).
|
||||||
|
The default settings are usually good enough to get you started.
|
||||||
|
When you are interested in exploring more features and options that mdBook provides, check out the [Configuration chapter](../format/configuration/index.html) for more details.
|
||||||
|
|
||||||
|
A very basic `book.toml` can be as simple as this:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[book]
|
||||||
|
title = "My First Book"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `SUMMARY.md`
|
||||||
|
|
||||||
|
The next major part of a book is the summary file located at `src/SUMMARY.md`.
|
||||||
|
This file contains a list of all the chapters in the book.
|
||||||
|
Before a chapter can be viewed, it must be added to this list.
|
||||||
|
|
||||||
|
Here's a basic summary file with a few chapters:
|
||||||
|
|
||||||
|
```md
|
||||||
|
# Summary
|
||||||
|
|
||||||
|
[Introduction](README.md)
|
||||||
|
|
||||||
|
- [My First Chapter](my-first-chapter.md)
|
||||||
|
- [Nested example](nested/README.md)
|
||||||
|
- [Sub-chapter](nested/sub-chapter.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
Try opening up `src/SUMMARY.md` in your editor and adding a few chapters.
|
||||||
|
If any of the chapter files do not exist, `mdbook` will automatically create them for you.
|
||||||
|
|
||||||
|
For more details on other formatting options for the summary file, check out the [Summary chapter](../format/summary.md).
|
||||||
|
|
||||||
|
### Source files
|
||||||
|
|
||||||
|
The content of your book is all contained in the `src` directory.
|
||||||
|
Each chapter is a separate Markdown file.
|
||||||
|
Typically, each chapter starts with a level 1 heading with the title of the chapter.
|
||||||
|
|
||||||
|
```md
|
||||||
|
# My First Chapter
|
||||||
|
|
||||||
|
Fill out your content here.
|
||||||
|
```
|
||||||
|
|
||||||
|
The precise layout of the files is up to you.
|
||||||
|
The organization of the files will correspond to the HTML files generated, so keep in mind that the file layout is part of the URL of each chapter.
|
||||||
|
|
||||||
|
While the `mdbook serve` command is running, you can open any of the chapter files and start editing them.
|
||||||
|
Each time you save the file, `mdbook` will rebuild the book and refresh your web browser.
|
||||||
|
|
||||||
|
Check out the [Markdown chapter](../format/markdown.md) for more information on formatting the content of your chapters.
|
||||||
|
|
||||||
|
All other files in the `src` directory will be included in the output.
|
||||||
|
So if you have images or other static files, just include them somewhere in the `src` directory.
|
||||||
|
|
||||||
|
## Publishing a book
|
||||||
|
|
||||||
|
Once you've written your book, you may want to host it somewhere for others to view.
|
||||||
|
The first step is to build the output of the book.
|
||||||
|
This can be done with the `mdbook build` command in the same directory where the `book.toml` file is located:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mdbook build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will generate a directory named `book` which contains the HTML content of your book.
|
||||||
|
You can then place this directory on any web server to host it.
|
||||||
|
|
||||||
|
For more information about publishing and deploying, check out the [Continuous Integration chapter](../continuous-integration.md) for more.
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
There are multiple ways to install the mdBook CLI tool.
|
||||||
|
Choose any one of the methods below that best suit your needs.
|
||||||
|
If you are installing mdBook for automatic deployment, check out the [continuous integration] chapter for more examples on how to install.
|
||||||
|
|
||||||
|
[continuous integration]: ../continuous-integration.md
|
||||||
|
|
||||||
|
## Pre-compiled binaries
|
||||||
|
|
||||||
|
Executable binaries are available for download on the [GitHub Releases page][releases].
|
||||||
|
Download the binary for your platform (Windows, macOS, or Linux) and extract the archive.
|
||||||
|
The archive contains an `mdbook` executable which you can run to build your books.
|
||||||
|
|
||||||
|
To make it easier to run, put the path to the binary into your `PATH`.
|
||||||
|
|
||||||
|
[releases]: https://github.com/rust-lang/mdBook/releases
|
||||||
|
|
||||||
|
## Build from source using Rust
|
||||||
|
|
||||||
|
To build the `mdbook` executable from source, you will first need to install Rust and Cargo.
|
||||||
|
Follow the instructions on the [Rust installation page].
|
||||||
|
mdBook currently requires at least Rust version 1.71.
|
||||||
|
|
||||||
|
Once you have installed Rust, the following command can be used to build and install mdBook:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install mdbook
|
||||||
|
```
|
||||||
|
|
||||||
|
This will automatically download mdBook from [crates.io], build it, and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||||
|
|
||||||
|
To uninstall, run the command `cargo uninstall mdbook`.
|
||||||
|
|
||||||
|
[Rust installation page]: https://www.rust-lang.org/tools/install
|
||||||
|
[crates.io]: https://crates.io/
|
||||||
|
|
||||||
|
### Installing the latest master version
|
||||||
|
|
||||||
|
The version published to crates.io will ever so slightly be behind the version hosted on GitHub.
|
||||||
|
If you need the latest version you can build the git version of mdBook yourself.
|
||||||
|
Cargo makes this ***super easy***!
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install --git https://github.com/rust-lang/mdBook.git mdbook
|
||||||
|
```
|
||||||
|
|
||||||
|
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||||
|
|
||||||
|
If you are interested in making modifications to mdBook itself, check out the [Contributing Guide] for more information.
|
||||||
|
|
||||||
|
[Contributing Guide]: https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Reading Books
|
||||||
|
|
||||||
|
This chapter gives an introduction on how to interact with a book produced by mdBook.
|
||||||
|
This assumes you are reading an HTML book.
|
||||||
|
The options and formatting will be different for other output formats such as PDF.
|
||||||
|
|
||||||
|
A book is organized into *chapters*.
|
||||||
|
Each chapter is a separate page.
|
||||||
|
Chapters can be nested into a hierarchy of sub-chapters.
|
||||||
|
Typically, each chapter will be organized into a series of *headings* to subdivide a chapter.
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
There are several methods for navigating through the chapters of a book.
|
||||||
|
|
||||||
|
The **sidebar** on the left provides a list of all chapters.
|
||||||
|
Clicking on any of the chapter titles will load that page.
|
||||||
|
|
||||||
|
The sidebar may not automatically appear if the window is too narrow, particularly on mobile displays.
|
||||||
|
In that situation, the menu icon (three horizontal bars) at the top-left of the page can be pressed to open and close the sidebar.
|
||||||
|
|
||||||
|
The **arrow buttons** at the bottom of the page can be used to navigate to the previous or the next chapter.
|
||||||
|
|
||||||
|
The **left and right arrow keys** on the keyboard can be used to navigate to the previous or the next chapter.
|
||||||
|
|
||||||
|
## Top menu bar
|
||||||
|
|
||||||
|
The menu bar at the top of the page provides some icons for interacting with the book.
|
||||||
|
The icons displayed will depend on the settings of how the book was generated.
|
||||||
|
|
||||||
|
| Icon | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| <i class="fa fa-bars"></i> | Opens and closes the chapter listing sidebar. |
|
||||||
|
| <i class="fa fa-paint-brush"></i> | Opens a picker to choose a different color theme. |
|
||||||
|
| <i class="fa fa-search"></i> | Opens a search bar for searching within the book. |
|
||||||
|
| <i class="fa fa-print"></i> | Instructs the web browser to print the entire book. |
|
||||||
|
| <i class="fa fa-github"></i> | Opens a link to the website that hosts the source code of the book. |
|
||||||
|
| <i class="fa fa-edit"></i> | Opens a page to directly edit the source of the page you are currently reading. |
|
||||||
|
|
||||||
|
Tapping the menu bar will scroll the page to the top.
|
||||||
|
|
||||||
|
## Search
|
||||||
|
|
||||||
|
Each book has a built-in search system.
|
||||||
|
Pressing the search icon (<i class="fa fa-search"></i>) in the menu bar, or pressing the `S` key on the keyboard will open an input box for entering search terms.
|
||||||
|
Typing some terms will show matching chapters and sections in real time.
|
||||||
|
|
||||||
|
Clicking any of the results will jump to that section.
|
||||||
|
The up and down arrow keys can be used to navigate the results, and enter will open the highlighted section.
|
||||||
|
|
||||||
|
After loading a search result, the matching search terms will be highlighted in the text.
|
||||||
|
Clicking a highlighted word or pressing the `Esc` key will remove the highlighting.
|
||||||
|
|
||||||
|
## Code blocks
|
||||||
|
|
||||||
|
mdBook books are often used for programming projects, and thus support highlighting code blocks and samples.
|
||||||
|
Code blocks may contain several different icons for interacting with them:
|
||||||
|
|
||||||
|
| Icon | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| <i class="fa fa-copy"></i> | Copies the code block into your local clipboard, to allow pasting into another application. |
|
||||||
|
| <i class="fa fa-play"></i> | For Rust code examples, this will execute the sample code and display the compiler output just below the example (see [playground]). |
|
||||||
|
| <i class="fa fa-eye"></i> | For Rust code examples, this will toggle visibility of "hidden" lines. Sometimes, larger examples will hide lines which are not particularly relevant to what is being illustrated (see [hiding code lines]). |
|
||||||
|
| <i class="fa fa-history"></i> | For [editable code examples][editor], this will undo any changes you have made. |
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
println!("Hello, World!");
|
||||||
|
```
|
||||||
|
|
||||||
|
[editor]: ../format/theme/editor.md
|
||||||
|
[playground]: ../format/mdbook.md#rust-playground
|
||||||
|
[hiding code lines]: ../format/mdbook.md#hiding-code-lines
|
|
@ -15,6 +15,10 @@ shout-out to them!
|
||||||
- [projektir](https://github.com/projektir)
|
- [projektir](https://github.com/projektir)
|
||||||
- [Phaiax](https://github.com/Phaiax)
|
- [Phaiax](https://github.com/Phaiax)
|
||||||
- Matt Ickstadt ([mattico](https://github.com/mattico))
|
- Matt Ickstadt ([mattico](https://github.com/mattico))
|
||||||
- Weihang Lo ([@weihanglo](https://github.com/weihanglo))
|
- Weihang Lo ([weihanglo](https://github.com/weihanglo))
|
||||||
|
- Avision Ho ([avisionh](https://github.com/avisionh))
|
||||||
|
- Vivek Akupatni ([apatniv](https://github.com/apatniv))
|
||||||
|
- Eric Huss ([ehuss](https://github.com/ehuss))
|
||||||
|
- Josh Rotenberg ([joshrotenberg](https://github.com/joshrotenberg))
|
||||||
|
|
||||||
If you feel you're missing from this list, feel free to add yourself in a PR.
|
If you feel you're missing from this list, feel free to add yourself in a PR.
|
188
src/book/book.rs
188
src/book/book.rs
|
@ -5,8 +5,11 @@ use std::io::{Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||||
use config::BuildConfig;
|
use crate::config::BuildConfig;
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
|
use crate::utils::bracket_escape;
|
||||||
|
use log::debug;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Load a book into memory from its `src/` directory.
|
/// Load a book into memory from its `src/` directory.
|
||||||
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
||||||
|
@ -14,14 +17,15 @@ pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>
|
||||||
let summary_md = src_dir.join("SUMMARY.md");
|
let summary_md = src_dir.join("SUMMARY.md");
|
||||||
|
|
||||||
let mut summary_content = String::new();
|
let mut summary_content = String::new();
|
||||||
File::open(summary_md)
|
File::open(&summary_md)
|
||||||
.chain_err(|| "Couldn't open SUMMARY.md")?
|
.with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))?
|
||||||
.read_to_string(&mut summary_content)?;
|
.read_to_string(&mut summary_content)?;
|
||||||
|
|
||||||
let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?;
|
let summary = parse_summary(&summary_content)
|
||||||
|
.with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
|
||||||
|
|
||||||
if cfg.create_missing {
|
if cfg.create_missing {
|
||||||
create_missing(&src_dir, &summary).chain_err(|| "Unable to create missing chapters")?;
|
create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
load_book_from_disk(&summary, src_dir)
|
load_book_from_disk(&summary, src_dir)
|
||||||
|
@ -35,11 +39,10 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||||
.chain(summary.suffix_chapters.iter())
|
.chain(summary.suffix_chapters.iter())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
while !items.is_empty() {
|
while let Some(next) = items.pop() {
|
||||||
let next = items.pop().expect("already checked");
|
|
||||||
|
|
||||||
if let SummaryItem::Link(ref link) = *next {
|
if let SummaryItem::Link(ref link) = *next {
|
||||||
let filename = src_dir.join(&link.location);
|
if let Some(ref location) = link.location {
|
||||||
|
let filename = src_dir.join(location);
|
||||||
if !filename.exists() {
|
if !filename.exists() {
|
||||||
if let Some(parent) = filename.parent() {
|
if let Some(parent) = filename.parent() {
|
||||||
if !parent.exists() {
|
if !parent.exists() {
|
||||||
|
@ -48,8 +51,11 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||||
}
|
}
|
||||||
debug!("Creating missing file {}", filename.display());
|
debug!("Creating missing file {}", filename.display());
|
||||||
|
|
||||||
let mut f = File::create(&filename)?;
|
let mut f = File::create(&filename).with_context(|| {
|
||||||
writeln!(f, "# {}", link.name)?;
|
format!("Unable to create missing file: {}", filename.display())
|
||||||
|
})?;
|
||||||
|
writeln!(f, "# {}", bracket_escape(&link.name))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items.extend(&link.nested_items);
|
items.extend(&link.nested_items);
|
||||||
|
@ -61,7 +67,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||||
|
|
||||||
/// A dumb tree structure representing a book.
|
/// A dumb tree structure representing a book.
|
||||||
///
|
///
|
||||||
/// For the moment a book is just a collection of `BookItems` which are
|
/// For the moment a book is just a collection of [`BookItems`] which are
|
||||||
/// accessible by either iterating (immutably) over the book with [`iter()`], or
|
/// accessible by either iterating (immutably) over the book with [`iter()`], or
|
||||||
/// recursively applying a closure to each section to mutate the chapters, using
|
/// recursively applying a closure to each section to mutate the chapters, using
|
||||||
/// [`for_each_mut()`].
|
/// [`for_each_mut()`].
|
||||||
|
@ -82,7 +88,7 @@ impl Book {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a depth-first iterator over the items in the book.
|
/// Get a depth-first iterator over the items in the book.
|
||||||
pub fn iter(&self) -> BookItems {
|
pub fn iter(&self) -> BookItems<'_> {
|
||||||
BookItems {
|
BookItems {
|
||||||
items: self.sections.iter().collect(),
|
items: self.sections.iter().collect(),
|
||||||
}
|
}
|
||||||
|
@ -116,7 +122,7 @@ where
|
||||||
I: IntoIterator<Item = &'a mut BookItem>,
|
I: IntoIterator<Item = &'a mut BookItem>,
|
||||||
{
|
{
|
||||||
for item in items {
|
for item in items {
|
||||||
if let &mut BookItem::Chapter(ref mut ch) = item {
|
if let BookItem::Chapter(ch) = item {
|
||||||
for_each_mut(func, &mut ch.sub_items);
|
for_each_mut(func, &mut ch.sub_items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +137,8 @@ pub enum BookItem {
|
||||||
Chapter(Chapter),
|
Chapter(Chapter),
|
||||||
/// A section separator.
|
/// A section separator.
|
||||||
Separator,
|
Separator,
|
||||||
|
/// A part title.
|
||||||
|
PartTitle(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Chapter> for BookItem {
|
impl From<Chapter> for BookItem {
|
||||||
|
@ -152,8 +160,22 @@ pub struct Chapter {
|
||||||
/// Nested items.
|
/// Nested items.
|
||||||
pub sub_items: Vec<BookItem>,
|
pub sub_items: Vec<BookItem>,
|
||||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||||
pub path: PathBuf,
|
///
|
||||||
/// An ordered list of the names of each chapter above this one, in the hierarchy.
|
/// **Note**: After the index preprocessor runs, any README files will be
|
||||||
|
/// modified to be `index.md`. If you need access to the actual filename
|
||||||
|
/// on disk, use [`Chapter::source_path`] instead.
|
||||||
|
///
|
||||||
|
/// This is `None` for a draft chapter.
|
||||||
|
pub path: Option<PathBuf>,
|
||||||
|
/// The chapter's source file, relative to the `SUMMARY.md` file.
|
||||||
|
///
|
||||||
|
/// **Note**: Beware that README files will internally be treated as
|
||||||
|
/// `index.md` via the [`Chapter::path`] field. The `source_path` field
|
||||||
|
/// exists if you need access to the true file path.
|
||||||
|
///
|
||||||
|
/// This is `None` for a draft chapter.
|
||||||
|
pub source_path: Option<PathBuf>,
|
||||||
|
/// An ordered list of the names of each chapter above this one in the hierarchy.
|
||||||
pub parent_names: Vec<String>,
|
pub parent_names: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,24 +184,44 @@ impl Chapter {
|
||||||
pub fn new<P: Into<PathBuf>>(
|
pub fn new<P: Into<PathBuf>>(
|
||||||
name: &str,
|
name: &str,
|
||||||
content: String,
|
content: String,
|
||||||
path: P,
|
p: P,
|
||||||
parent_names: Vec<String>,
|
parent_names: Vec<String>,
|
||||||
) -> Chapter {
|
) -> Chapter {
|
||||||
|
let path: PathBuf = p.into();
|
||||||
Chapter {
|
Chapter {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
content: content,
|
content,
|
||||||
path: path.into(),
|
path: Some(path.clone()),
|
||||||
parent_names: parent_names,
|
source_path: Some(path),
|
||||||
|
parent_names,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new draft chapter that is not attached to a source markdown file (and thus
|
||||||
|
/// has no content).
|
||||||
|
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
|
||||||
|
Chapter {
|
||||||
|
name: name.to_string(),
|
||||||
|
content: String::new(),
|
||||||
|
path: None,
|
||||||
|
source_path: None,
|
||||||
|
parent_names,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
|
||||||
|
pub fn is_draft_chapter(&self) -> bool {
|
||||||
|
self.path.is_none()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use the provided `Summary` to load a `Book` from disk.
|
/// Use the provided `Summary` to load a `Book` from disk.
|
||||||
///
|
///
|
||||||
/// You need to pass in the book's source directory because all the links in
|
/// You need to pass in the book's source directory because all the links in
|
||||||
/// `SUMMARY.md` give the chapter locations relative to it.
|
/// `SUMMARY.md` give the chapter locations relative to it.
|
||||||
fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||||
debug!("Loading the book from disk");
|
debug!("Loading the book from disk");
|
||||||
let src_dir = src_dir.as_ref();
|
let src_dir = src_dir.as_ref();
|
||||||
|
|
||||||
|
@ -202,16 +244,17 @@ fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_summary_item<P: AsRef<Path>>(
|
fn load_summary_item<P: AsRef<Path> + Clone>(
|
||||||
item: &SummaryItem,
|
item: &SummaryItem,
|
||||||
src_dir: P,
|
src_dir: P,
|
||||||
parent_names: Vec<String>,
|
parent_names: Vec<String>,
|
||||||
) -> Result<BookItem> {
|
) -> Result<BookItem> {
|
||||||
match *item {
|
match item {
|
||||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||||
SummaryItem::Link(ref link) => {
|
SummaryItem::Link(ref link) => {
|
||||||
load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c))
|
load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
|
||||||
}
|
}
|
||||||
|
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,28 +263,40 @@ fn load_chapter<P: AsRef<Path>>(
|
||||||
src_dir: P,
|
src_dir: P,
|
||||||
parent_names: Vec<String>,
|
parent_names: Vec<String>,
|
||||||
) -> Result<Chapter> {
|
) -> Result<Chapter> {
|
||||||
debug!("Loading {} ({})", link.name, link.location.display());
|
|
||||||
let src_dir = src_dir.as_ref();
|
let src_dir = src_dir.as_ref();
|
||||||
|
|
||||||
let location = if link.location.is_absolute() {
|
let mut ch = if let Some(ref link_location) = link.location {
|
||||||
link.location.clone()
|
debug!("Loading {} ({})", link.name, link_location.display());
|
||||||
|
|
||||||
|
let location = if link_location.is_absolute() {
|
||||||
|
link_location.clone()
|
||||||
} else {
|
} else {
|
||||||
src_dir.join(&link.location)
|
src_dir.join(link_location)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut f = File::open(&location)
|
let mut f = File::open(&location)
|
||||||
.chain_err(|| format!("Chapter file not found, {}", link.location.display()))?;
|
.with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
|
||||||
|
|
||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
f.read_to_string(&mut content)
|
f.read_to_string(&mut content).with_context(|| {
|
||||||
.chain_err(|| format!("Unable to read \"{}\" ({})", link.name, location.display()))?;
|
format!("Unable to read \"{}\" ({})", link.name, location.display())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
|
||||||
|
content.replace_range(..3, "");
|
||||||
|
}
|
||||||
|
|
||||||
let stripped = location
|
let stripped = location
|
||||||
.strip_prefix(&src_dir)
|
.strip_prefix(src_dir)
|
||||||
.expect("Chapters are always inside a book");
|
.expect("Chapters are always inside a book");
|
||||||
|
|
||||||
let mut sub_item_parents = parent_names.clone();
|
Chapter::new(&link.name, content, stripped, parent_names.clone())
|
||||||
let mut ch = Chapter::new(&link.name, content, stripped, parent_names);
|
} else {
|
||||||
|
Chapter::new_draft(&link.name, parent_names.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut sub_item_parents = parent_names;
|
||||||
|
|
||||||
ch.number = link.number.clone();
|
ch.number = link.number.clone();
|
||||||
|
|
||||||
sub_item_parents.push(link.name.clone());
|
sub_item_parents.push(link.name.clone());
|
||||||
|
@ -262,8 +317,6 @@ fn load_chapter<P: AsRef<Path>>(
|
||||||
///
|
///
|
||||||
/// This struct shouldn't be created directly, instead prefer the
|
/// This struct shouldn't be created directly, instead prefer the
|
||||||
/// [`Book::iter()`] method.
|
/// [`Book::iter()`] method.
|
||||||
///
|
|
||||||
/// [`Book::iter()`]: struct.Book.html#method.iter
|
|
||||||
pub struct BookItems<'a> {
|
pub struct BookItems<'a> {
|
||||||
items: VecDeque<&'a BookItem>,
|
items: VecDeque<&'a BookItem>,
|
||||||
}
|
}
|
||||||
|
@ -274,7 +327,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let item = self.items.pop_front();
|
let item = self.items.pop_front();
|
||||||
|
|
||||||
if let Some(&BookItem::Chapter(ref ch)) = item {
|
if let Some(BookItem::Chapter(ch)) = item {
|
||||||
// if we wanted a breadth-first iterator we'd `extend()` here
|
// if we wanted a breadth-first iterator we'd `extend()` here
|
||||||
for sub_item in ch.sub_items.iter().rev() {
|
for sub_item in ch.sub_items.iter().rev() {
|
||||||
self.items.push_front(sub_item);
|
self.items.push_front(sub_item);
|
||||||
|
@ -286,7 +339,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Chapter {
|
impl Display for Chapter {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
if let Some(ref section_number) = self.number {
|
if let Some(ref section_number) = self.number {
|
||||||
write!(f, "{} ", section_number)?;
|
write!(f, "{} ", section_number)?;
|
||||||
}
|
}
|
||||||
|
@ -298,10 +351,9 @@ impl Display for Chapter {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::io::Write;
|
|
||||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||||
|
|
||||||
const DUMMY_SRC: &'static str = "
|
const DUMMY_SRC: &str = "
|
||||||
# Dummy Chapter
|
# Dummy Chapter
|
||||||
|
|
||||||
this is some dummy text.
|
this is some dummy text.
|
||||||
|
@ -317,7 +369,7 @@ And here is some \
|
||||||
let chapter_path = temp.path().join("chapter_1.md");
|
let chapter_path = temp.path().join("chapter_1.md");
|
||||||
File::create(&chapter_path)
|
File::create(&chapter_path)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write(DUMMY_SRC.as_bytes())
|
.write_all(DUMMY_SRC.as_bytes())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let link = Link::new("Chapter 1", chapter_path);
|
let link = Link::new("Chapter 1", chapter_path);
|
||||||
|
@ -333,7 +385,7 @@ And here is some \
|
||||||
|
|
||||||
File::create(&second_path)
|
File::create(&second_path)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write_all("Hello World!".as_bytes())
|
.write_all(b"Hello World!")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut second = Link::new("Nested Chapter 1", &second_path);
|
let mut second = Link::new("Nested Chapter 1", &second_path);
|
||||||
|
@ -341,7 +393,7 @@ And here is some \
|
||||||
|
|
||||||
root.nested_items.push(second.clone().into());
|
root.nested_items.push(second.clone().into());
|
||||||
root.nested_items.push(SummaryItem::Separator);
|
root.nested_items.push(SummaryItem::Separator);
|
||||||
root.nested_items.push(second.clone().into());
|
root.nested_items.push(second.into());
|
||||||
|
|
||||||
(root, temp_dir)
|
(root, temp_dir)
|
||||||
}
|
}
|
||||||
|
@ -360,6 +412,29 @@ And here is some \
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_a_single_chapter_with_utf8_bom_from_disk() {
|
||||||
|
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
|
||||||
|
|
||||||
|
let chapter_path = temp_dir.path().join("chapter_1.md");
|
||||||
|
File::create(&chapter_path)
|
||||||
|
.unwrap()
|
||||||
|
.write_all(("\u{feff}".to_owned() + DUMMY_SRC).as_bytes())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let link = Link::new("Chapter 1", chapter_path);
|
||||||
|
|
||||||
|
let should_be = Chapter::new(
|
||||||
|
"Chapter 1",
|
||||||
|
DUMMY_SRC.to_string(),
|
||||||
|
"chapter_1.md",
|
||||||
|
Vec::new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cant_load_a_nonexistent_chapter() {
|
fn cant_load_a_nonexistent_chapter() {
|
||||||
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
|
let link = Link::new("Chapter 1", "/foo/bar/baz.md");
|
||||||
|
@ -376,7 +451,8 @@ And here is some \
|
||||||
name: String::from("Nested Chapter 1"),
|
name: String::from("Nested Chapter 1"),
|
||||||
content: String::from("Hello World!"),
|
content: String::from("Hello World!"),
|
||||||
number: Some(SectionNumber(vec![1, 2])),
|
number: Some(SectionNumber(vec![1, 2])),
|
||||||
path: PathBuf::from("second.md"),
|
path: Some(PathBuf::from("second.md")),
|
||||||
|
source_path: Some(PathBuf::from("second.md")),
|
||||||
parent_names: vec![String::from("Chapter 1")],
|
parent_names: vec![String::from("Chapter 1")],
|
||||||
sub_items: Vec::new(),
|
sub_items: Vec::new(),
|
||||||
};
|
};
|
||||||
|
@ -384,12 +460,13 @@ And here is some \
|
||||||
name: String::from("Chapter 1"),
|
name: String::from("Chapter 1"),
|
||||||
content: String::from(DUMMY_SRC),
|
content: String::from(DUMMY_SRC),
|
||||||
number: None,
|
number: None,
|
||||||
path: PathBuf::from("chapter_1.md"),
|
path: Some(PathBuf::from("chapter_1.md")),
|
||||||
|
source_path: Some(PathBuf::from("chapter_1.md")),
|
||||||
parent_names: Vec::new(),
|
parent_names: Vec::new(),
|
||||||
sub_items: vec![
|
sub_items: vec![
|
||||||
BookItem::Chapter(nested.clone()),
|
BookItem::Chapter(nested.clone()),
|
||||||
BookItem::Separator,
|
BookItem::Separator,
|
||||||
BookItem::Chapter(nested.clone()),
|
BookItem::Chapter(nested),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -408,7 +485,8 @@ And here is some \
|
||||||
sections: vec![BookItem::Chapter(Chapter {
|
sections: vec![BookItem::Chapter(Chapter {
|
||||||
name: String::from("Chapter 1"),
|
name: String::from("Chapter 1"),
|
||||||
content: String::from(DUMMY_SRC),
|
content: String::from(DUMMY_SRC),
|
||||||
path: PathBuf::from("chapter_1.md"),
|
path: Some(PathBuf::from("chapter_1.md")),
|
||||||
|
source_path: Some(PathBuf::from("chapter_1.md")),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})],
|
})],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -448,7 +526,8 @@ And here is some \
|
||||||
name: String::from("Chapter 1"),
|
name: String::from("Chapter 1"),
|
||||||
content: String::from(DUMMY_SRC),
|
content: String::from(DUMMY_SRC),
|
||||||
number: None,
|
number: None,
|
||||||
path: PathBuf::from("Chapter_1/index.md"),
|
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||||
|
source_path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||||
parent_names: Vec::new(),
|
parent_names: Vec::new(),
|
||||||
sub_items: vec![
|
sub_items: vec![
|
||||||
BookItem::Chapter(Chapter::new(
|
BookItem::Chapter(Chapter::new(
|
||||||
|
@ -481,7 +560,8 @@ And here is some \
|
||||||
.filter_map(|i| match *i {
|
.filter_map(|i| match *i {
|
||||||
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
|
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
let should_be: Vec<_> = vec![
|
let should_be: Vec<_> = vec![
|
||||||
String::from("Chapter 1"),
|
String::from("Chapter 1"),
|
||||||
String::from("Hello World"),
|
String::from("Hello World"),
|
||||||
|
@ -499,7 +579,8 @@ And here is some \
|
||||||
name: String::from("Chapter 1"),
|
name: String::from("Chapter 1"),
|
||||||
content: String::from(DUMMY_SRC),
|
content: String::from(DUMMY_SRC),
|
||||||
number: None,
|
number: None,
|
||||||
path: PathBuf::from("Chapter_1/index.md"),
|
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||||
|
source_path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||||
parent_names: Vec::new(),
|
parent_names: Vec::new(),
|
||||||
sub_items: vec![
|
sub_items: vec![
|
||||||
BookItem::Chapter(Chapter::new(
|
BookItem::Chapter(Chapter::new(
|
||||||
|
@ -536,9 +617,10 @@ And here is some \
|
||||||
let summary = Summary {
|
let summary = Summary {
|
||||||
numbered_chapters: vec![SummaryItem::Link(Link {
|
numbered_chapters: vec![SummaryItem::Link(Link {
|
||||||
name: String::from("Empty"),
|
name: String::from("Empty"),
|
||||||
location: PathBuf::from(""),
|
location: Some(PathBuf::from("")),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})],
|
})],
|
||||||
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -555,7 +637,7 @@ And here is some \
|
||||||
let summary = Summary {
|
let summary = Summary {
|
||||||
numbered_chapters: vec![SummaryItem::Link(Link {
|
numbered_chapters: vec![SummaryItem::Link(Link {
|
||||||
name: String::from("nested"),
|
name: String::from("nested"),
|
||||||
location: dir,
|
location: Some(dir),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})],
|
})],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use toml;
|
|
||||||
|
|
||||||
use super::MDBook;
|
use super::MDBook;
|
||||||
use config::Config;
|
use crate::config::Config;
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
use theme;
|
use crate::theme;
|
||||||
|
use crate::utils::fs::write_file;
|
||||||
|
use log::{debug, error, info, trace};
|
||||||
|
|
||||||
/// A helper for setting up a new book and its directory structure.
|
/// A helper for setting up a new book and its directory structure.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
@ -29,7 +30,7 @@ impl BookBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the `Config` to be used.
|
/// Set the [`Config`] to be used.
|
||||||
pub fn with_config(&mut self, cfg: Config) -> &mut BookBuilder {
|
pub fn with_config(&mut self, cfg: Config) -> &mut BookBuilder {
|
||||||
self.config = cfg;
|
self.config = cfg;
|
||||||
self
|
self
|
||||||
|
@ -65,19 +66,19 @@ impl BookBuilder {
|
||||||
info!("Creating a new book with stub content");
|
info!("Creating a new book with stub content");
|
||||||
|
|
||||||
self.create_directory_structure()
|
self.create_directory_structure()
|
||||||
.chain_err(|| "Unable to create directory structure")?;
|
.with_context(|| "Unable to create directory structure")?;
|
||||||
|
|
||||||
self.create_stub_files()
|
self.create_stub_files()
|
||||||
.chain_err(|| "Unable to create stub files")?;
|
.with_context(|| "Unable to create stub files")?;
|
||||||
|
|
||||||
if self.create_gitignore {
|
if self.create_gitignore {
|
||||||
self.build_gitignore()
|
self.build_gitignore()
|
||||||
.chain_err(|| "Unable to create .gitignore")?;
|
.with_context(|| "Unable to create .gitignore")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.copy_theme {
|
if self.copy_theme {
|
||||||
self.copy_across_theme()
|
self.copy_across_theme()
|
||||||
.chain_err(|| "Unable to copy across the theme")?;
|
.with_context(|| "Unable to copy across the theme")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.write_book_toml()?;
|
self.write_book_toml()?;
|
||||||
|
@ -98,24 +99,20 @@ impl BookBuilder {
|
||||||
fn write_book_toml(&self) -> Result<()> {
|
fn write_book_toml(&self) -> Result<()> {
|
||||||
debug!("Writing book.toml");
|
debug!("Writing book.toml");
|
||||||
let book_toml = self.root.join("book.toml");
|
let book_toml = self.root.join("book.toml");
|
||||||
let cfg = toml::to_vec(&self.config).chain_err(|| "Unable to serialize the config")?;
|
let cfg = toml::to_vec(&self.config).with_context(|| "Unable to serialize the config")?;
|
||||||
|
|
||||||
File::create(book_toml)
|
File::create(book_toml)
|
||||||
.chain_err(|| "Couldn't create book.toml")?
|
.with_context(|| "Couldn't create book.toml")?
|
||||||
.write_all(&cfg)
|
.write_all(&cfg)
|
||||||
.chain_err(|| "Unable to write config to book.toml")?;
|
.with_context(|| "Unable to write config to book.toml")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_across_theme(&self) -> Result<()> {
|
fn copy_across_theme(&self) -> Result<()> {
|
||||||
debug!("Copying theme");
|
debug!("Copying theme");
|
||||||
|
|
||||||
let themedir = self
|
let html_config = self.config.html_config().unwrap_or_default();
|
||||||
.config
|
let themedir = html_config.theme_dir(&self.root);
|
||||||
.html_config()
|
|
||||||
.and_then(|html| html.theme)
|
|
||||||
.unwrap_or_else(|| self.config.book.src.join("theme"));
|
|
||||||
let themedir = self.root.join(themedir);
|
|
||||||
|
|
||||||
if !themedir.exists() {
|
if !themedir.exists() {
|
||||||
debug!(
|
debug!(
|
||||||
|
@ -129,7 +126,9 @@ impl BookBuilder {
|
||||||
index.write_all(theme::INDEX)?;
|
index.write_all(theme::INDEX)?;
|
||||||
|
|
||||||
let cssdir = themedir.join("css");
|
let cssdir = themedir.join("css");
|
||||||
|
if !cssdir.exists() {
|
||||||
fs::create_dir(&cssdir)?;
|
fs::create_dir(&cssdir)?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut general_css = File::create(cssdir.join("general.css"))?;
|
let mut general_css = File::create(cssdir.join("general.css"))?;
|
||||||
general_css.write_all(theme::GENERAL_CSS)?;
|
general_css.write_all(theme::GENERAL_CSS)?;
|
||||||
|
@ -137,14 +136,19 @@ impl BookBuilder {
|
||||||
let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
|
let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
|
||||||
chrome_css.write_all(theme::CHROME_CSS)?;
|
chrome_css.write_all(theme::CHROME_CSS)?;
|
||||||
|
|
||||||
|
if html_config.print.enable {
|
||||||
let mut print_css = File::create(cssdir.join("print.css"))?;
|
let mut print_css = File::create(cssdir.join("print.css"))?;
|
||||||
print_css.write_all(theme::PRINT_CSS)?;
|
print_css.write_all(theme::PRINT_CSS)?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut variables_css = File::create(cssdir.join("variables.css"))?;
|
let mut variables_css = File::create(cssdir.join("variables.css"))?;
|
||||||
variables_css.write_all(theme::VARIABLES_CSS)?;
|
variables_css.write_all(theme::VARIABLES_CSS)?;
|
||||||
|
|
||||||
let mut favicon = File::create(themedir.join("favicon.png"))?;
|
let mut favicon = File::create(themedir.join("favicon.png"))?;
|
||||||
favicon.write_all(theme::FAVICON)?;
|
favicon.write_all(theme::FAVICON_PNG)?;
|
||||||
|
|
||||||
|
let mut favicon = File::create(themedir.join("favicon.svg"))?;
|
||||||
|
favicon.write_all(theme::FAVICON_SVG)?;
|
||||||
|
|
||||||
let mut js = File::create(themedir.join("book.js"))?;
|
let mut js = File::create(themedir.join("book.js"))?;
|
||||||
js.write_all(theme::JS)?;
|
js.write_all(theme::JS)?;
|
||||||
|
@ -155,6 +159,19 @@ impl BookBuilder {
|
||||||
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
|
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
|
||||||
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
|
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
|
||||||
|
|
||||||
|
write_file(&themedir.join("fonts"), "fonts.css", theme::fonts::CSS)?;
|
||||||
|
for (file_name, contents) in theme::fonts::LICENSES {
|
||||||
|
write_file(&themedir, file_name, contents)?;
|
||||||
|
}
|
||||||
|
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
|
||||||
|
write_file(&themedir, file_name, contents)?;
|
||||||
|
}
|
||||||
|
write_file(
|
||||||
|
&themedir,
|
||||||
|
theme::fonts::SOURCE_CODE_PRO.0,
|
||||||
|
theme::fonts::SOURCE_CODE_PRO.1,
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,15 +190,19 @@ impl BookBuilder {
|
||||||
let src_dir = self.root.join(&self.config.book.src);
|
let src_dir = self.root.join(&self.config.book.src);
|
||||||
|
|
||||||
let summary = src_dir.join("SUMMARY.md");
|
let summary = src_dir.join("SUMMARY.md");
|
||||||
let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?;
|
if !summary.exists() {
|
||||||
|
trace!("No summary found creating stub summary and chapter_1.md.");
|
||||||
|
let mut f = File::create(&summary).with_context(|| "Unable to create SUMMARY.md")?;
|
||||||
writeln!(f, "# Summary")?;
|
writeln!(f, "# Summary")?;
|
||||||
writeln!(f, "")?;
|
writeln!(f)?;
|
||||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||||
|
|
||||||
let chapter_1 = src_dir.join("chapter_1.md");
|
let chapter_1 = src_dir.join("chapter_1.md");
|
||||||
let mut f = File::create(&chapter_1).chain_err(|| "Unable to create chapter_1.md")?;
|
let mut f = File::create(chapter_1).with_context(|| "Unable to create chapter_1.md")?;
|
||||||
writeln!(f, "# Chapter 1")?;
|
writeln!(f, "# Chapter 1")?;
|
||||||
|
} else {
|
||||||
|
trace!("Existing summary found, no need to create stub files.");
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,10 +211,10 @@ impl BookBuilder {
|
||||||
fs::create_dir_all(&self.root)?;
|
fs::create_dir_all(&self.root)?;
|
||||||
|
|
||||||
let src = self.root.join(&self.config.book.src);
|
let src = self.root.join(&self.config.book.src);
|
||||||
fs::create_dir_all(&src)?;
|
fs::create_dir_all(src)?;
|
||||||
|
|
||||||
let build = self.root.join(&self.config.build.build_dir);
|
let build = self.root.join(&self.config.build.build_dir);
|
||||||
fs::create_dir_all(&build)?;
|
fs::create_dir_all(build)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
589
src/book/mod.rs
589
src/book/mod.rs
|
@ -5,6 +5,7 @@
|
||||||
//!
|
//!
|
||||||
//! [1]: ../index.html
|
//! [1]: ../index.html
|
||||||
|
|
||||||
|
#[allow(clippy::module_inception)]
|
||||||
mod book;
|
mod book;
|
||||||
mod init;
|
mod init;
|
||||||
mod summary;
|
mod summary;
|
||||||
|
@ -13,19 +14,23 @@ pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
|
||||||
pub use self::init::BookBuilder;
|
pub use self::init::BookBuilder;
|
||||||
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||||
|
|
||||||
use std::io::Write;
|
use log::{debug, error, info, log_enabled, trace, warn};
|
||||||
use std::path::PathBuf;
|
use std::ffi::OsString;
|
||||||
|
use std::io::{IsTerminal, Write};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use tempfile::Builder as TempFileBuilder;
|
use tempfile::Builder as TempFileBuilder;
|
||||||
use toml::Value;
|
use toml::Value;
|
||||||
|
use topological_sort::TopologicalSort;
|
||||||
|
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor,
|
use crate::preprocess::{
|
||||||
PreprocessorContext, CmdPreprocessor};
|
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
|
||||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
};
|
||||||
use utils;
|
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
|
||||||
|
use crate::utils;
|
||||||
|
|
||||||
use config::Config;
|
use crate::config::{Config, RustEdition};
|
||||||
|
|
||||||
/// The object used to manage and build a book.
|
/// The object used to manage and build a book.
|
||||||
pub struct MDBook {
|
pub struct MDBook {
|
||||||
|
@ -35,10 +40,10 @@ pub struct MDBook {
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
/// A representation of the book's contents in memory.
|
/// A representation of the book's contents in memory.
|
||||||
pub book: Book,
|
pub book: Book,
|
||||||
renderers: Vec<Box<Renderer>>,
|
renderers: Vec<Box<dyn Renderer>>,
|
||||||
|
|
||||||
/// List of pre-processors to be run on the book
|
/// List of pre-processors to be run on the book.
|
||||||
preprocessors: Vec<Box<Preprocessor>>,
|
preprocessors: Vec<Box<dyn Preprocessor>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MDBook {
|
impl MDBook {
|
||||||
|
@ -54,7 +59,7 @@ impl MDBook {
|
||||||
warn!("This format is no longer used, so you should migrate to the");
|
warn!("This format is no longer used, so you should migrate to the");
|
||||||
warn!("book.toml format.");
|
warn!("book.toml format.");
|
||||||
warn!("Check the user guide for migration information:");
|
warn!("Check the user guide for migration information:");
|
||||||
warn!("\thttps://rust-lang-nursery.github.io/mdBook/format/config.html");
|
warn!("\thttps://rust-lang.github.io/mdBook/format/config.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut config = if config_location.exists() {
|
let mut config = if config_location.exists() {
|
||||||
|
@ -66,7 +71,27 @@ impl MDBook {
|
||||||
|
|
||||||
config.update_from_env();
|
config.update_from_env();
|
||||||
|
|
||||||
if log_enabled!(::log::Level::Trace) {
|
if let Some(html_config) = config.html_config() {
|
||||||
|
if html_config.google_analytics.is_some() {
|
||||||
|
warn!(
|
||||||
|
"The output.html.google-analytics field has been deprecated; \
|
||||||
|
it will be removed in a future release.\n\
|
||||||
|
Consider placing the appropriate site tag code into the \
|
||||||
|
theme/head.hbs file instead.\n\
|
||||||
|
The tracking code may be found in the Google Analytics Admin page.\n\
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if html_config.curly_quotes {
|
||||||
|
warn!(
|
||||||
|
"The output.html.curly-quotes field has been renamed to \
|
||||||
|
output.html.smart-punctuation.\n\
|
||||||
|
Use the new name in book.toml to remove this warning."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if log_enabled!(log::Level::Trace) {
|
||||||
for line in format!("Config: {:#?}", config).lines() {
|
for line in format!("Config: {:#?}", config).lines() {
|
||||||
trace!("{}", line);
|
trace!("{}", line);
|
||||||
}
|
}
|
||||||
|
@ -75,12 +100,35 @@ impl MDBook {
|
||||||
MDBook::load_with_config(book_root, config)
|
MDBook::load_with_config(book_root, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a book from its root directory using a custom config.
|
/// Load a book from its root directory using a custom `Config`.
|
||||||
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
|
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
|
||||||
let root = book_root.into();
|
let root = book_root.into();
|
||||||
|
|
||||||
let src_dir = root.join(&config.book.src);
|
let src_dir = root.join(&config.book.src);
|
||||||
let book = book::load_book(&src_dir, &config.build)?;
|
let book = book::load_book(src_dir, &config.build)?;
|
||||||
|
|
||||||
|
let renderers = determine_renderers(&config);
|
||||||
|
let preprocessors = determine_preprocessors(&config)?;
|
||||||
|
|
||||||
|
Ok(MDBook {
|
||||||
|
root,
|
||||||
|
config,
|
||||||
|
book,
|
||||||
|
renderers,
|
||||||
|
preprocessors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a book from its root directory using a custom `Config` and a custom summary.
|
||||||
|
pub fn load_with_config_and_summary<P: Into<PathBuf>>(
|
||||||
|
book_root: P,
|
||||||
|
config: Config,
|
||||||
|
summary: Summary,
|
||||||
|
) -> Result<MDBook> {
|
||||||
|
let root = book_root.into();
|
||||||
|
|
||||||
|
let src_dir = root.join(&config.book.src);
|
||||||
|
let book = book::load_book_from_disk(&summary, src_dir)?;
|
||||||
|
|
||||||
let renderers = determine_renderers(&config);
|
let renderers = determine_renderers(&config);
|
||||||
let preprocessors = determine_preprocessors(&config)?;
|
let preprocessors = determine_preprocessors(&config)?;
|
||||||
|
@ -95,20 +143,18 @@ impl MDBook {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a flat depth-first iterator over the elements of the book,
|
/// Returns a flat depth-first iterator over the elements of the book,
|
||||||
/// it returns an [BookItem enum](bookitem.html):
|
/// it returns a [`BookItem`] enum:
|
||||||
/// `(section: String, bookitem: &BookItem)`
|
/// `(section: String, bookitem: &BookItem)`
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// # extern crate mdbook;
|
|
||||||
/// # use mdbook::MDBook;
|
/// # use mdbook::MDBook;
|
||||||
/// # use mdbook::book::BookItem;
|
/// # use mdbook::book::BookItem;
|
||||||
/// # #[allow(unused_variables)]
|
|
||||||
/// # fn main() {
|
|
||||||
/// # let book = MDBook::load("mybook").unwrap();
|
/// # let book = MDBook::load("mybook").unwrap();
|
||||||
/// for item in book.iter() {
|
/// for item in book.iter() {
|
||||||
/// match *item {
|
/// match *item {
|
||||||
/// BookItem::Chapter(ref chapter) => {},
|
/// BookItem::Chapter(ref chapter) => {},
|
||||||
/// BookItem::Separator => {},
|
/// BookItem::Separator => {},
|
||||||
|
/// BookItem::PartTitle(ref title) => {}
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
@ -119,9 +165,8 @@ impl MDBook {
|
||||||
/// // 2. Chapter 2
|
/// // 2. Chapter 2
|
||||||
/// //
|
/// //
|
||||||
/// // etc.
|
/// // etc.
|
||||||
/// # }
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn iter(&self) -> BookItems {
|
pub fn iter(&self) -> BookItems<'_> {
|
||||||
self.book.iter()
|
self.book.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,116 +202,177 @@ impl MDBook {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the entire build process for a particular `Renderer`.
|
/// Run preprocessors and return the final book.
|
||||||
fn execute_build_process(&self, renderer: &Renderer) -> Result<()> {
|
pub fn preprocess_book(&self, renderer: &dyn Renderer) -> Result<(Book, PreprocessorContext)> {
|
||||||
let mut preprocessed_book = self.book.clone();
|
let preprocess_ctx = PreprocessorContext::new(
|
||||||
let preprocess_ctx = PreprocessorContext::new(self.root.clone(),
|
self.root.clone(),
|
||||||
self.config.clone(),
|
self.config.clone(),
|
||||||
renderer.name().to_string());
|
renderer.name().to_string(),
|
||||||
|
);
|
||||||
|
let mut preprocessed_book = self.book.clone();
|
||||||
for preprocessor in &self.preprocessors {
|
for preprocessor in &self.preprocessors {
|
||||||
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
|
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
|
||||||
debug!("Running the {} preprocessor.", preprocessor.name());
|
debug!("Running the {} preprocessor.", preprocessor.name());
|
||||||
preprocessed_book =
|
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
||||||
preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok((preprocessed_book, preprocess_ctx))
|
||||||
info!("Running the {} backend", renderer.name());
|
|
||||||
self.render(&preprocessed_book, renderer)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(
|
/// Run the entire build process for a particular [`Renderer`].
|
||||||
&self,
|
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||||
preprocessed_book: &Book,
|
let (preprocessed_book, preprocess_ctx) = self.preprocess_book(renderer)?;
|
||||||
renderer: &Renderer,
|
|
||||||
) -> Result<()> {
|
|
||||||
let name = renderer.name();
|
let name = renderer.name();
|
||||||
let build_dir = self.build_dir_for(name);
|
let build_dir = self.build_dir_for(name);
|
||||||
if build_dir.exists() {
|
|
||||||
debug!(
|
|
||||||
"Cleaning build dir for the \"{}\" renderer ({})",
|
|
||||||
name,
|
|
||||||
build_dir.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
utils::fs::remove_dir_content(&build_dir)
|
let mut render_context = RenderContext::new(
|
||||||
.chain_err(|| "Unable to clear output directory")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let render_context = RenderContext::new(
|
|
||||||
self.root.clone(),
|
self.root.clone(),
|
||||||
preprocessed_book.clone(),
|
preprocessed_book,
|
||||||
self.config.clone(),
|
self.config.clone(),
|
||||||
build_dir,
|
build_dir,
|
||||||
);
|
);
|
||||||
|
render_context
|
||||||
|
.chapter_titles
|
||||||
|
.extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
|
||||||
|
|
||||||
|
info!("Running the {} backend", renderer.name());
|
||||||
renderer
|
renderer
|
||||||
.render(&render_context)
|
.render(&render_context)
|
||||||
.chain_err(|| "Rendering failed")
|
.with_context(|| "Rendering failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// You can change the default renderer to another one by using this method.
|
/// You can change the default renderer to another one by using this method.
|
||||||
/// The only requirement is for your renderer to implement the [`Renderer`
|
/// The only requirement is that your renderer implement the [`Renderer`]
|
||||||
/// trait](../renderer/trait.Renderer.html)
|
/// trait.
|
||||||
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
|
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
|
||||||
self.renderers.push(Box::new(renderer));
|
self.renderers.push(Box::new(renderer));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book.
|
/// Register a [`Preprocessor`] to be used when rendering the book.
|
||||||
pub fn with_preprecessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||||
self.preprocessors.push(Box::new(preprocessor));
|
self.preprocessors.push(Box::new(preprocessor));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run `rustdoc` tests on the book, linking against the provided libraries.
|
/// Run `rustdoc` tests on the book, linking against the provided libraries.
|
||||||
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
||||||
let library_args: Vec<&str> = (0..library_paths.len())
|
// test_chapter with chapter:None will run all tests.
|
||||||
.map(|_| "-L")
|
self.test_chapter(library_paths, None)
|
||||||
.zip(library_paths.into_iter())
|
}
|
||||||
.flat_map(|x| vec![x.0, x.1])
|
|
||||||
|
/// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
|
||||||
|
/// If `chapter` is `None`, all tests will be run.
|
||||||
|
pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
|
||||||
|
let cwd = std::env::current_dir()?;
|
||||||
|
let library_args: Vec<OsString> = library_paths
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|path| {
|
||||||
|
let path = Path::new(path);
|
||||||
|
let path = if path.is_relative() {
|
||||||
|
cwd.join(path).into_os_string()
|
||||||
|
} else {
|
||||||
|
path.to_path_buf().into_os_string()
|
||||||
|
};
|
||||||
|
[OsString::from("-L"), path]
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||||
|
|
||||||
|
let mut chapter_found = false;
|
||||||
|
|
||||||
|
struct TestRenderer;
|
||||||
|
impl Renderer for TestRenderer {
|
||||||
// FIXME: Is "test" the proper renderer name to use here?
|
// FIXME: Is "test" the proper renderer name to use here?
|
||||||
let preprocess_context = PreprocessorContext::new(self.root.clone(),
|
fn name(&self) -> &str {
|
||||||
self.config.clone(),
|
"test"
|
||||||
"test".to_string());
|
}
|
||||||
|
|
||||||
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
|
fn render(&self, _: &RenderContext) -> Result<()> {
|
||||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
Ok(())
|
||||||
// actual markdown files.
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index Preprocessor is disabled so that chapter paths
|
||||||
|
// continue to point to the actual markdown files.
|
||||||
|
self.preprocessors = determine_preprocessors(&self.config)?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|pre| pre.name() != IndexPreprocessor::NAME)
|
||||||
|
.collect();
|
||||||
|
let (book, _) = self.preprocess_book(&TestRenderer)?;
|
||||||
|
|
||||||
|
let color_output = std::io::stderr().is_terminal();
|
||||||
|
let mut failed = false;
|
||||||
for item in book.iter() {
|
for item in book.iter() {
|
||||||
if let BookItem::Chapter(ref ch) = *item {
|
if let BookItem::Chapter(ref ch) = *item {
|
||||||
if !ch.path.as_os_str().is_empty() {
|
let chapter_path = match ch.path {
|
||||||
let path = self.source_dir().join(&ch.path);
|
Some(ref path) if !path.as_os_str().is_empty() => path,
|
||||||
let content = utils::fs::file_to_string(&path)?;
|
_ => continue,
|
||||||
info!("Testing file: {:?}", path);
|
};
|
||||||
|
|
||||||
|
if let Some(chapter) = chapter {
|
||||||
|
if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
|
||||||
|
if chapter == "?" {
|
||||||
|
info!("Skipping chapter '{}'...", ch.name);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chapter_found = true;
|
||||||
|
info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
|
||||||
|
|
||||||
// write preprocessed file to tempdir
|
// write preprocessed file to tempdir
|
||||||
let path = temp_dir.path().join(&ch.path);
|
let path = temp_dir.path().join(chapter_path);
|
||||||
let mut tmpf = utils::fs::create_file(&path)?;
|
let mut tmpf = utils::fs::create_file(&path)?;
|
||||||
tmpf.write_all(content.as_bytes())?;
|
tmpf.write_all(ch.content.as_bytes())?;
|
||||||
|
|
||||||
let output = Command::new("rustdoc")
|
let mut cmd = Command::new("rustdoc");
|
||||||
.arg(&path)
|
cmd.current_dir(temp_dir.path())
|
||||||
|
.arg(&chapter_path)
|
||||||
.arg("--test")
|
.arg("--test")
|
||||||
.args(&library_args)
|
.args(&library_args);
|
||||||
.output()?;
|
|
||||||
|
if let Some(edition) = self.config.rust.edition {
|
||||||
|
match edition {
|
||||||
|
RustEdition::E2015 => {
|
||||||
|
cmd.args(["--edition", "2015"]);
|
||||||
|
}
|
||||||
|
RustEdition::E2018 => {
|
||||||
|
cmd.args(["--edition", "2018"]);
|
||||||
|
}
|
||||||
|
RustEdition::E2021 => {
|
||||||
|
cmd.args(["--edition", "2021"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if color_output {
|
||||||
|
cmd.args(&["--color", "always"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("running {:?}", cmd);
|
||||||
|
let output = cmd.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
bail!(ErrorKind::Subprocess(
|
failed = true;
|
||||||
"Rustdoc returned an error".to_string(),
|
error!(
|
||||||
output
|
"rustdoc returned an error:\n\
|
||||||
));
|
\n--- stdout\n{}\n--- stderr\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if failed {
|
||||||
|
bail!("One or more tests failed");
|
||||||
|
}
|
||||||
|
if let Some(chapter) = chapter {
|
||||||
|
if !chapter_found {
|
||||||
|
bail!("Chapter not found: {}", chapter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -275,7 +381,7 @@ impl MDBook {
|
||||||
/// artefacts.
|
/// artefacts.
|
||||||
///
|
///
|
||||||
/// If there is only 1 renderer, put it in the directory pointed to by the
|
/// If there is only 1 renderer, put it in the directory pointed to by the
|
||||||
/// `build.build_dir` key in `Config`. If there is more than one then the
|
/// `build.build_dir` key in [`Config`]. If there is more than one then the
|
||||||
/// renderer gets its own directory within the main build dir.
|
/// renderer gets its own directory within the main build dir.
|
||||||
///
|
///
|
||||||
/// i.e. If there were only one renderer (in this case, the HTML renderer):
|
/// i.e. If there were only one renderer (in this case, the HTML renderer):
|
||||||
|
@ -320,19 +426,19 @@ impl MDBook {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look at the `Config` and try to figure out what renderers to use.
|
/// Look at the `Config` and try to figure out what renderers to use.
|
||||||
fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
|
||||||
let mut renderers: Vec<Box<Renderer>> = Vec::new();
|
let mut renderers = Vec::new();
|
||||||
|
|
||||||
if let Some(output_table) = config.get("output").and_then(|o| o.as_table()) {
|
if let Some(output_table) = config.get("output").and_then(Value::as_table) {
|
||||||
for (key, table) in output_table.iter() {
|
renderers.extend(output_table.iter().map(|(key, table)| {
|
||||||
// the "html" backend has its own Renderer
|
|
||||||
if key == "html" {
|
if key == "html" {
|
||||||
renderers.push(Box::new(HtmlHandlebars::new()));
|
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
||||||
|
} else if key == "markdown" {
|
||||||
|
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
|
||||||
} else {
|
} else {
|
||||||
let renderer = interpret_custom_renderer(key, table);
|
interpret_custom_renderer(key, table)
|
||||||
renderers.push(renderer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we couldn't find anything, add the HTML renderer as a default
|
// if we couldn't find anything, add the HTML renderer as a default
|
||||||
|
@ -343,59 +449,136 @@ fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
||||||
renderers
|
renderers
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
|
const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
|
||||||
vec![
|
|
||||||
Box::new(LinkPreprocessor::new()),
|
|
||||||
Box::new(IndexPreprocessor::new()),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_default_preprocessor(pre: &Preprocessor) -> bool {
|
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||||
let name = pre.name();
|
let name = pre.name();
|
||||||
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
|
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
||||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
|
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
|
||||||
let mut preprocessors = Vec::new();
|
// Collect the names of all preprocessors intended to be run, and the order
|
||||||
|
// in which they should be run.
|
||||||
|
let mut preprocessor_names = TopologicalSort::<String>::new();
|
||||||
|
|
||||||
if config.build.use_default_preprocessors {
|
if config.build.use_default_preprocessors {
|
||||||
preprocessors.extend(default_preprocessors());
|
for name in DEFAULT_PREPROCESSORS {
|
||||||
|
preprocessor_names.insert(name.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(preprocessor_table) =
|
if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
|
||||||
config.get("preprocessor").and_then(|v| v.as_table())
|
for (name, table) in preprocessor_table.iter() {
|
||||||
|
preprocessor_names.insert(name.to_string());
|
||||||
|
|
||||||
|
let exists = |name| {
|
||||||
|
(config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
|
||||||
|
|| preprocessor_table.contains_key(name)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(before) = table.get("before") {
|
||||||
|
let before = before.as_array().ok_or_else(|| {
|
||||||
|
Error::msg(format!(
|
||||||
|
"Expected preprocessor.{}.before to be an array",
|
||||||
|
name
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
for after in before {
|
||||||
|
let after = after.as_str().ok_or_else(|| {
|
||||||
|
Error::msg(format!(
|
||||||
|
"Expected preprocessor.{}.before to contain strings",
|
||||||
|
name
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !exists(after) {
|
||||||
|
// Only warn so that preprocessors can be toggled on and off (e.g. for
|
||||||
|
// troubleshooting) without having to worry about order too much.
|
||||||
|
warn!(
|
||||||
|
"preprocessor.{}.after contains \"{}\", which was not found",
|
||||||
|
name, after
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
preprocessor_names.add_dependency(name, after);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(after) = table.get("after") {
|
||||||
|
let after = after.as_array().ok_or_else(|| {
|
||||||
|
Error::msg(format!(
|
||||||
|
"Expected preprocessor.{}.after to be an array",
|
||||||
|
name
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
for before in after {
|
||||||
|
let before = before.as_str().ok_or_else(|| {
|
||||||
|
Error::msg(format!(
|
||||||
|
"Expected preprocessor.{}.after to contain strings",
|
||||||
|
name
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !exists(before) {
|
||||||
|
// See equivalent warning above for rationale
|
||||||
|
warn!(
|
||||||
|
"preprocessor.{}.before contains \"{}\", which was not found",
|
||||||
|
name, before
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
preprocessor_names.add_dependency(before, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that all links have been established, queue preprocessors in a suitable order
|
||||||
|
let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
|
||||||
|
// `pop_all()` returns an empty vector when no more items are not being depended upon
|
||||||
|
for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
|
||||||
|
.take_while(|names| !names.is_empty())
|
||||||
{
|
{
|
||||||
for key in preprocessor_table.keys() {
|
// The `topological_sort` crate does not guarantee a stable order for ties, even across
|
||||||
match key.as_ref() {
|
// runs of the same program. Thus, we break ties manually by sorting.
|
||||||
"links" => {
|
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
|
||||||
preprocessors.push(Box::new(LinkPreprocessor::new()))
|
// values ([1]), which may not be an alphabetical sort.
|
||||||
}
|
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
|
||||||
"index" => {
|
// preprocessor execution order.
|
||||||
preprocessors.push(Box::new(IndexPreprocessor::new()))
|
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
|
||||||
}
|
names.sort();
|
||||||
name => preprocessors.push(interpret_custom_preprocessor(
|
for name in names {
|
||||||
name,
|
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
|
||||||
&preprocessor_table[name],
|
"links" => Box::new(LinkPreprocessor::new()),
|
||||||
)),
|
"index" => Box::new(IndexPreprocessor::new()),
|
||||||
|
_ => {
|
||||||
|
// The only way to request a custom preprocessor is through the `preprocessor`
|
||||||
|
// table, so it must exist, be a table, and contain the key.
|
||||||
|
let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
|
||||||
|
let command = get_custom_preprocessor_cmd(&name, table);
|
||||||
|
Box::new(CmdPreprocessor::new(name, command))
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
preprocessors.push(preprocessor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
|
||||||
|
// Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
|
||||||
|
if preprocessor_names.is_empty() {
|
||||||
Ok(preprocessors)
|
Ok(preprocessors)
|
||||||
|
} else {
|
||||||
|
Err(Error::msg("Cyclic dependency detected in preprocessors"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn interpret_custom_preprocessor(
|
fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
|
||||||
key: &str,
|
table
|
||||||
table: &Value,
|
|
||||||
) -> Box<CmdPreprocessor> {
|
|
||||||
let command = table
|
|
||||||
.get("command")
|
.get("command")
|
||||||
.and_then(|c| c.as_str())
|
.and_then(Value::as_str)
|
||||||
.map(|s| s.to_string())
|
.map(ToString::to_string)
|
||||||
.unwrap_or_else(|| format!("mdbook-{}", key));
|
.unwrap_or_else(|| format!("mdbook-{}", key))
|
||||||
|
|
||||||
Box::new(CmdPreprocessor::new(key.to_string(), command.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||||
|
@ -403,13 +586,12 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||||
// prepended by "mdbook-"
|
// prepended by "mdbook-"
|
||||||
let table_dot_command = table
|
let table_dot_command = table
|
||||||
.get("command")
|
.get("command")
|
||||||
.and_then(|c| c.as_str())
|
.and_then(Value::as_str)
|
||||||
.map(|s| s.to_string());
|
.map(ToString::to_string);
|
||||||
|
|
||||||
let command =
|
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||||
table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
|
||||||
|
|
||||||
Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
|
Box::new(CmdRenderer::new(key.to_string(), command))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether we should run a particular `Preprocessor` in combination
|
/// Check whether we should run a particular `Preprocessor` in combination
|
||||||
|
@ -418,7 +600,11 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||||
///
|
///
|
||||||
/// The `build.use-default-preprocessors` config option can be used to ensure
|
/// The `build.use-default-preprocessors` config option can be used to ensure
|
||||||
/// default preprocessors always run if they support the renderer.
|
/// default preprocessors always run if they support the renderer.
|
||||||
fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg: &Config) -> bool {
|
fn preprocessor_should_run(
|
||||||
|
preprocessor: &dyn Preprocessor,
|
||||||
|
renderer: &dyn Renderer,
|
||||||
|
cfg: &Config,
|
||||||
|
) -> bool {
|
||||||
// default preprocessors should be run by default (if supported)
|
// default preprocessors should be run by default (if supported)
|
||||||
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
|
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
|
||||||
return preprocessor.supports_renderer(renderer.name());
|
return preprocessor.supports_renderer(renderer.name());
|
||||||
|
@ -428,19 +614,20 @@ fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg
|
||||||
let renderer_name = renderer.name();
|
let renderer_name = renderer.name();
|
||||||
|
|
||||||
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
|
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
|
||||||
return explicit_renderers.into_iter()
|
return explicit_renderers
|
||||||
.filter_map(|val| val.as_str())
|
.iter()
|
||||||
|
.filter_map(Value::as_str)
|
||||||
.any(|name| name == renderer_name);
|
.any(|name| name == renderer_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
preprocessor.supports_renderer(renderer_name)
|
preprocessor.supports_renderer(renderer_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use toml::value::{Table, Value};
|
use std::str::FromStr;
|
||||||
|
use toml::value::Table;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_defaults_to_html_renderer_if_empty() {
|
fn config_defaults_to_html_renderer_if_empty() {
|
||||||
|
@ -491,8 +678,8 @@ mod tests {
|
||||||
|
|
||||||
assert!(got.is_ok());
|
assert!(got.is_ok());
|
||||||
assert_eq!(got.as_ref().unwrap().len(), 2);
|
assert_eq!(got.as_ref().unwrap().len(), 2);
|
||||||
assert_eq!(got.as_ref().unwrap()[0].name(), "links");
|
assert_eq!(got.as_ref().unwrap()[0].name(), "index");
|
||||||
assert_eq!(got.as_ref().unwrap()[1].name(), "index");
|
assert_eq!(got.as_ref().unwrap()[1].name(), "links");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -507,7 +694,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_determine_third_party_preprocessors() {
|
fn can_determine_third_party_preprocessors() {
|
||||||
let cfg_str: &'static str = r#"
|
let cfg_str = r#"
|
||||||
[book]
|
[book]
|
||||||
title = "Some Book"
|
title = "Some Book"
|
||||||
|
|
||||||
|
@ -539,17 +726,126 @@ mod tests {
|
||||||
|
|
||||||
// make sure the `preprocessor.random` table exists
|
// make sure the `preprocessor.random` table exists
|
||||||
let random = cfg.get_preprocessor("random").unwrap();
|
let random = cfg.get_preprocessor("random").unwrap();
|
||||||
let random = interpret_custom_preprocessor(
|
let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
|
||||||
"random",
|
|
||||||
&Value::Table(random.clone()),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(random.cmd(), "python random.py");
|
assert_eq!(random, "python random.py");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preprocessor_before_must_be_array() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
[preprocessor.random]
|
||||||
|
before = 0
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let cfg = Config::from_str(cfg_str).unwrap();
|
||||||
|
|
||||||
|
assert!(determine_preprocessors(&cfg).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preprocessor_after_must_be_array() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
[preprocessor.random]
|
||||||
|
after = 0
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let cfg = Config::from_str(cfg_str).unwrap();
|
||||||
|
|
||||||
|
assert!(determine_preprocessors(&cfg).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preprocessor_order_is_honored() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
[preprocessor.random]
|
||||||
|
before = [ "last" ]
|
||||||
|
after = [ "index" ]
|
||||||
|
|
||||||
|
[preprocessor.last]
|
||||||
|
after = [ "links", "index" ]
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let cfg = Config::from_str(cfg_str).unwrap();
|
||||||
|
|
||||||
|
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||||
|
let index = |name| {
|
||||||
|
preprocessors
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, preprocessor)| preprocessor.name() == name)
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
};
|
||||||
|
let assert_before = |before, after| {
|
||||||
|
if index(before) >= index(after) {
|
||||||
|
eprintln!("Preprocessor order:");
|
||||||
|
for preprocessor in &preprocessors {
|
||||||
|
eprintln!(" {}", preprocessor.name());
|
||||||
|
}
|
||||||
|
panic!("{} should come before {}", before, after);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_before("index", "random");
|
||||||
|
assert_before("index", "last");
|
||||||
|
assert_before("random", "last");
|
||||||
|
assert_before("links", "last");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cyclic_dependencies_are_detected() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
[preprocessor.links]
|
||||||
|
before = [ "index" ]
|
||||||
|
|
||||||
|
[preprocessor.index]
|
||||||
|
before = [ "links" ]
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let cfg = Config::from_str(cfg_str).unwrap();
|
||||||
|
|
||||||
|
assert!(determine_preprocessors(&cfg).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dependencies_dont_register_undefined_preprocessors() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
[preprocessor.links]
|
||||||
|
before = [ "random" ]
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let cfg = Config::from_str(cfg_str).unwrap();
|
||||||
|
|
||||||
|
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||||
|
|
||||||
|
assert!(!preprocessors
|
||||||
|
.iter()
|
||||||
|
.any(|preprocessor| preprocessor.name() == "random"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
[preprocessor.random]
|
||||||
|
before = [ "links" ]
|
||||||
|
|
||||||
|
[build]
|
||||||
|
use-default-preprocessors = false
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let cfg = Config::from_str(cfg_str).unwrap();
|
||||||
|
|
||||||
|
let preprocessors = determine_preprocessors(&cfg).unwrap();
|
||||||
|
|
||||||
|
assert!(!preprocessors
|
||||||
|
.iter()
|
||||||
|
.any(|preprocessor| preprocessor.name() == "links"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_respects_preprocessor_selection() {
|
fn config_respects_preprocessor_selection() {
|
||||||
let cfg_str: &'static str = r#"
|
let cfg_str = r#"
|
||||||
[preprocessor.links]
|
[preprocessor.links]
|
||||||
renderers = ["html"]
|
renderers = ["html"]
|
||||||
"#;
|
"#;
|
||||||
|
@ -557,11 +853,12 @@ mod tests {
|
||||||
let cfg = Config::from_str(cfg_str).unwrap();
|
let cfg = Config::from_str(cfg_str).unwrap();
|
||||||
|
|
||||||
// double-check that we can access preprocessor.links.renderers[0]
|
// double-check that we can access preprocessor.links.renderers[0]
|
||||||
let html = cfg.get_preprocessor("links")
|
let html = cfg
|
||||||
|
.get_preprocessor("links")
|
||||||
.and_then(|links| links.get("renderers"))
|
.and_then(|links| links.get("renderers"))
|
||||||
.and_then(|renderers| renderers.as_array())
|
.and_then(Value::as_array)
|
||||||
.and_then(|renderers| renderers.get(0))
|
.and_then(|renderers| renderers.get(0))
|
||||||
.and_then(|renderer| renderer.as_str())
|
.and_then(Value::as_str)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(html, "html");
|
assert_eq!(html, "html");
|
||||||
let html_renderer = HtmlHandlebars::default();
|
let html_renderer = HtmlHandlebars::default();
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
use memchr::{self, Memchr};
|
use log::{debug, trace, warn};
|
||||||
use pulldown_cmark::{self, Event, Tag};
|
use memchr::Memchr;
|
||||||
|
use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::{self, Display, Formatter};
|
use std::fmt::{self, Display, Formatter};
|
||||||
use std::iter::FromIterator;
|
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
@ -25,12 +26,17 @@ use std::path::{Path, PathBuf};
|
||||||
/// [Title of prefix element](relative/path/to/markdown.md)
|
/// [Title of prefix element](relative/path/to/markdown.md)
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
/// **Part Title:** An optional title for the next collect of numbered chapters. The numbered
|
||||||
|
/// chapters can be broken into as many parts as desired.
|
||||||
|
///
|
||||||
/// **Numbered Chapter:** Numbered chapters are the main content of the book,
|
/// **Numbered Chapter:** Numbered chapters are the main content of the book,
|
||||||
/// they
|
/// they
|
||||||
/// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
|
/// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
|
||||||
/// sub-chapters, etc.)
|
/// sub-chapters, etc.)
|
||||||
///
|
///
|
||||||
/// ```markdown
|
/// ```markdown
|
||||||
|
/// # Title of Part
|
||||||
|
///
|
||||||
/// - [Title of the Chapter](relative/path/to/markdown.md)
|
/// - [Title of the Chapter](relative/path/to/markdown.md)
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
@ -55,7 +61,7 @@ pub struct Summary {
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
/// Chapters before the main text (e.g. an introduction).
|
/// Chapters before the main text (e.g. an introduction).
|
||||||
pub prefix_chapters: Vec<SummaryItem>,
|
pub prefix_chapters: Vec<SummaryItem>,
|
||||||
/// The main chapters in the document.
|
/// The main numbered chapters of the book, broken into one or more possibly named parts.
|
||||||
pub numbered_chapters: Vec<SummaryItem>,
|
pub numbered_chapters: Vec<SummaryItem>,
|
||||||
/// Items which come after the main document (e.g. a conclusion).
|
/// Items which come after the main document (e.g. a conclusion).
|
||||||
pub suffix_chapters: Vec<SummaryItem>,
|
pub suffix_chapters: Vec<SummaryItem>,
|
||||||
|
@ -71,7 +77,7 @@ pub struct Link {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// The location of the chapter's source file, taking the book's `src`
|
/// The location of the chapter's source file, taking the book's `src`
|
||||||
/// directory as the root.
|
/// directory as the root.
|
||||||
pub location: PathBuf,
|
pub location: Option<PathBuf>,
|
||||||
/// The section number, if this chapter is in the numbered section.
|
/// The section number, if this chapter is in the numbered section.
|
||||||
pub number: Option<SectionNumber>,
|
pub number: Option<SectionNumber>,
|
||||||
/// Any nested items this chapter may contain.
|
/// Any nested items this chapter may contain.
|
||||||
|
@ -83,7 +89,7 @@ impl Link {
|
||||||
pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
|
pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
|
||||||
Link {
|
Link {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
location: location.as_ref().to_path_buf(),
|
location: Some(location.as_ref().to_path_buf()),
|
||||||
number: None,
|
number: None,
|
||||||
nested_items: Vec::new(),
|
nested_items: Vec::new(),
|
||||||
}
|
}
|
||||||
|
@ -94,7 +100,7 @@ impl Default for Link {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Link {
|
Link {
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
location: PathBuf::new(),
|
location: Some(PathBuf::new()),
|
||||||
number: None,
|
number: None,
|
||||||
nested_items: Vec::new(),
|
nested_items: Vec::new(),
|
||||||
}
|
}
|
||||||
|
@ -108,6 +114,8 @@ pub enum SummaryItem {
|
||||||
Link(Link),
|
Link(Link),
|
||||||
/// A separator (`---`).
|
/// A separator (`---`).
|
||||||
Separator,
|
Separator,
|
||||||
|
/// A part title.
|
||||||
|
PartTitle(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SummaryItem {
|
impl SummaryItem {
|
||||||
|
@ -139,7 +147,8 @@ impl From<Link> for SummaryItem {
|
||||||
/// | EPSILON
|
/// | EPSILON
|
||||||
/// prefix_chapters ::= item*
|
/// prefix_chapters ::= item*
|
||||||
/// suffix_chapters ::= item*
|
/// suffix_chapters ::= item*
|
||||||
/// numbered_chapters ::= dotted_item+
|
/// numbered_chapters ::= part+
|
||||||
|
/// part ::= title dotted_item+
|
||||||
/// dotted_item ::= INDENT* DOT_POINT item
|
/// dotted_item ::= INDENT* DOT_POINT item
|
||||||
/// item ::= link
|
/// item ::= link
|
||||||
/// | separator
|
/// | separator
|
||||||
|
@ -153,14 +162,19 @@ impl From<Link> for SummaryItem {
|
||||||
/// > match the following regex: "[^<>\n[]]+".
|
/// > match the following regex: "[^<>\n[]]+".
|
||||||
struct SummaryParser<'a> {
|
struct SummaryParser<'a> {
|
||||||
src: &'a str,
|
src: &'a str,
|
||||||
stream: pulldown_cmark::Parser<'a>,
|
stream: pulldown_cmark::OffsetIter<'a, DefaultBrokenLinkCallback>,
|
||||||
|
offset: usize,
|
||||||
|
|
||||||
|
/// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
|
||||||
|
/// here until somebody calls `next_event` again.
|
||||||
|
back: Option<Event<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads `Events` from the provided stream until the corresponding
|
/// Reads `Events` from the provided stream until the corresponding
|
||||||
/// `Event::End` is encountered which matches the `$delimiter` pattern.
|
/// `Event::End` is encountered which matches the `$delimiter` pattern.
|
||||||
///
|
///
|
||||||
/// This is the equivalent of doing
|
/// This is the equivalent of doing
|
||||||
/// `$stream.take_while(|e| e != $delimeter).collect()` but it allows you to
|
/// `$stream.take_while(|e| e != $delimiter).collect()` but it allows you to
|
||||||
/// use pattern matching and you won't get errors because `take_while()`
|
/// use pattern matching and you won't get errors because `take_while()`
|
||||||
/// moves `$stream` out of self.
|
/// moves `$stream` out of self.
|
||||||
macro_rules! collect_events {
|
macro_rules! collect_events {
|
||||||
|
@ -174,7 +188,7 @@ macro_rules! collect_events {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let event = $stream.next();
|
let event = $stream.next().map(|(ev, _range)| ev);
|
||||||
trace!("Next event: {:?}", event);
|
trace!("Next event: {:?}", event);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
|
@ -195,24 +209,24 @@ macro_rules! collect_events {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SummaryParser<'a> {
|
impl<'a> SummaryParser<'a> {
|
||||||
fn new(text: &str) -> SummaryParser {
|
fn new(text: &'a str) -> SummaryParser<'a> {
|
||||||
let pulldown_parser = pulldown_cmark::Parser::new(text);
|
let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
|
||||||
|
|
||||||
SummaryParser {
|
SummaryParser {
|
||||||
src: text,
|
src: text,
|
||||||
stream: pulldown_parser,
|
stream: pulldown_parser,
|
||||||
|
offset: 0,
|
||||||
|
back: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current line and column to give the user more useful error
|
/// Get the current line and column to give the user more useful error
|
||||||
/// messages.
|
/// messages.
|
||||||
fn current_location(&self) -> (usize, usize) {
|
fn current_location(&self) -> (usize, usize) {
|
||||||
let byte_offset = self.stream.get_offset();
|
let previous_text = self.src[..self.offset].as_bytes();
|
||||||
|
|
||||||
let previous_text = self.src[..byte_offset].as_bytes();
|
|
||||||
let line = Memchr::new(b'\n', previous_text).count() + 1;
|
let line = Memchr::new(b'\n', previous_text).count() + 1;
|
||||||
let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
|
let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
|
||||||
let col = self.src[start_of_line..byte_offset].chars().count();
|
let col = self.src[start_of_line..self.offset].chars().count();
|
||||||
|
|
||||||
(line, col)
|
(line, col)
|
||||||
}
|
}
|
||||||
|
@ -223,13 +237,13 @@ impl<'a> SummaryParser<'a> {
|
||||||
|
|
||||||
let prefix_chapters = self
|
let prefix_chapters = self
|
||||||
.parse_affix(true)
|
.parse_affix(true)
|
||||||
.chain_err(|| "There was an error parsing the prefix chapters")?;
|
.with_context(|| "There was an error parsing the prefix chapters")?;
|
||||||
let numbered_chapters = self
|
let numbered_chapters = self
|
||||||
.parse_numbered()
|
.parse_parts()
|
||||||
.chain_err(|| "There was an error parsing the numbered chapters")?;
|
.with_context(|| "There was an error parsing the numbered chapters")?;
|
||||||
let suffix_chapters = self
|
let suffix_chapters = self
|
||||||
.parse_affix(false)
|
.parse_affix(false)
|
||||||
.chain_err(|| "There was an error parsing the suffix chapters")?;
|
.with_context(|| "There was an error parsing the suffix chapters")?;
|
||||||
|
|
||||||
Ok(Summary {
|
Ok(Summary {
|
||||||
title,
|
title,
|
||||||
|
@ -239,8 +253,7 @@ impl<'a> SummaryParser<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the affix chapters. This expects the first event (start of
|
/// Parse the affix chapters.
|
||||||
/// paragraph) to have already been consumed by the previous parser.
|
|
||||||
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
|
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
debug!(
|
debug!(
|
||||||
|
@ -250,20 +263,27 @@ impl<'a> SummaryParser<'a> {
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match self.next_event() {
|
match self.next_event() {
|
||||||
Some(Event::Start(Tag::List(..))) => {
|
Some(ev @ Event::Start(Tag::List(..)))
|
||||||
|
| Some(
|
||||||
|
ev @ Event::Start(Tag::Heading {
|
||||||
|
level: HeadingLevel::H1,
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
) => {
|
||||||
if is_prefix {
|
if is_prefix {
|
||||||
// we've finished prefix chapters and are at the start
|
// we've finished prefix chapters and are at the start
|
||||||
// of the numbered section.
|
// of the numbered section.
|
||||||
|
self.back(ev);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
|
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Event::Start(Tag::Link(href, _))) => {
|
Some(Event::Start(Tag::Link { dest_url, .. })) => {
|
||||||
let link = self.parse_link(href.to_string())?;
|
let link = self.parse_link(dest_url.to_string());
|
||||||
items.push(SummaryItem::Link(link));
|
items.push(SummaryItem::Link(link));
|
||||||
}
|
}
|
||||||
Some(Event::Start(Tag::Rule)) => items.push(SummaryItem::Separator),
|
Some(Event::Rule) => items.push(SummaryItem::Separator),
|
||||||
Some(_) => {}
|
Some(_) => {}
|
||||||
None => break,
|
None => break,
|
||||||
}
|
}
|
||||||
|
@ -272,88 +292,164 @@ impl<'a> SummaryParser<'a> {
|
||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_link(&mut self, href: String) -> Result<Link> {
|
fn parse_parts(&mut self) -> Result<Vec<SummaryItem>> {
|
||||||
let link_content = collect_events!(self.stream, end Tag::Link(..));
|
let mut parts = vec![];
|
||||||
let name = stringify_events(link_content);
|
|
||||||
|
|
||||||
if href.is_empty() {
|
// We want the section numbers to be continues through all parts.
|
||||||
Err(self.parse_error("You can't have an empty link."))
|
let mut root_number = SectionNumber::default();
|
||||||
} else {
|
|
||||||
Ok(Link {
|
|
||||||
name: name,
|
|
||||||
location: PathBuf::from(href.to_string()),
|
|
||||||
number: None,
|
|
||||||
nested_items: Vec::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse the numbered chapters. This assumes the opening list tag has
|
|
||||||
/// already been consumed by a previous parser.
|
|
||||||
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
|
|
||||||
let mut items = Vec::new();
|
|
||||||
let mut root_items = 0;
|
let mut root_items = 0;
|
||||||
let root_number = SectionNumber::default();
|
|
||||||
|
|
||||||
// we need to do this funny loop-match-if-let dance because a rule will
|
|
||||||
// close off any currently running list. Therefore we try to read the
|
|
||||||
// list items before the rule, then if we encounter a rule we'll add a
|
|
||||||
// separator and try to resume parsing numbered chapters if we start a
|
|
||||||
// list immediately afterwards.
|
|
||||||
//
|
|
||||||
// If you can think of a better way to do this then please make a PR :)
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
|
// Possibly match a title or the end of the "numbered chapters part".
|
||||||
|
let title = match self.next_event() {
|
||||||
|
Some(ev @ Event::Start(Tag::Paragraph)) => {
|
||||||
|
// we're starting the suffix chapters
|
||||||
|
self.back(ev);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Event::Start(Tag::Heading {
|
||||||
|
level: HeadingLevel::H1,
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
|
debug!("Found a h1 in the SUMMARY");
|
||||||
|
|
||||||
|
let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
|
||||||
|
Some(stringify_events(tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ev) => {
|
||||||
|
self.back(ev);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
None => break, // EOF, bail...
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the rest of the part.
|
||||||
|
let numbered_chapters = self
|
||||||
|
.parse_numbered(&mut root_items, &mut root_number)
|
||||||
|
.with_context(|| "There was an error parsing the numbered chapters")?;
|
||||||
|
|
||||||
|
if let Some(title) = title {
|
||||||
|
parts.push(SummaryItem::PartTitle(title));
|
||||||
|
}
|
||||||
|
parts.extend(numbered_chapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened.
|
||||||
|
fn parse_link(&mut self, href: String) -> Link {
|
||||||
|
let href = href.replace("%20", " ");
|
||||||
|
let link_content = collect_events!(self.stream, end TagEnd::Link);
|
||||||
|
let name = stringify_events(link_content);
|
||||||
|
|
||||||
|
let path = if href.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(PathBuf::from(href))
|
||||||
|
};
|
||||||
|
|
||||||
|
Link {
|
||||||
|
name,
|
||||||
|
location: path,
|
||||||
|
number: None,
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the numbered chapters.
|
||||||
|
fn parse_numbered(
|
||||||
|
&mut self,
|
||||||
|
root_items: &mut u32,
|
||||||
|
root_number: &mut SectionNumber,
|
||||||
|
) -> Result<Vec<SummaryItem>> {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
// For the first iteration, we want to just skip any opening paragraph tags, as that just
|
||||||
|
// marks the start of the list. But after that, another opening paragraph indicates that we
|
||||||
|
// have started a new part or the suffix chapters.
|
||||||
|
let mut first = true;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match self.next_event() {
|
||||||
|
Some(ev @ Event::Start(Tag::Paragraph)) => {
|
||||||
|
if !first {
|
||||||
|
// we're starting the suffix chapters
|
||||||
|
self.back(ev);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The expectation is that pulldown cmark will terminate a paragraph before a new
|
||||||
|
// heading, so we can always count on this to return without skipping headings.
|
||||||
|
Some(
|
||||||
|
ev @ Event::Start(Tag::Heading {
|
||||||
|
level: HeadingLevel::H1,
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
) => {
|
||||||
|
// we're starting a new part
|
||||||
|
self.back(ev);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(ev @ Event::Start(Tag::List(..))) => {
|
||||||
|
self.back(ev);
|
||||||
|
let mut bunch_of_items = self.parse_nested_numbered(root_number)?;
|
||||||
|
|
||||||
// if we've resumed after something like a rule the root sections
|
// if we've resumed after something like a rule the root sections
|
||||||
// will be numbered from 1. We need to manually go back and update
|
// will be numbered from 1. We need to manually go back and update
|
||||||
// them
|
// them
|
||||||
update_section_numbers(&mut bunch_of_items, 0, root_items);
|
update_section_numbers(&mut bunch_of_items, 0, *root_items);
|
||||||
root_items += bunch_of_items.len() as u32;
|
*root_items += bunch_of_items.len() as u32;
|
||||||
items.extend(bunch_of_items);
|
items.extend(bunch_of_items);
|
||||||
|
|
||||||
|
|
||||||
match self.next_event() {
|
|
||||||
Some(Event::Start(Tag::Paragraph)) => {
|
|
||||||
// we're starting the suffix chapters
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
Some(Event::Start(other_tag)) => {
|
Some(Event::Start(other_tag)) => {
|
||||||
if other_tag == Tag::Rule {
|
|
||||||
items.push(SummaryItem::Separator);
|
|
||||||
}
|
|
||||||
trace!("Skipping contents of {:?}", other_tag);
|
trace!("Skipping contents of {:?}", other_tag);
|
||||||
|
|
||||||
// Skip over the contents of this tag
|
// Skip over the contents of this tag
|
||||||
while let Some(event) = self.next_event() {
|
while let Some(event) = self.next_event() {
|
||||||
if event == Event::End(other_tag.clone()) {
|
if event == Event::End(other_tag.clone().into()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Event::Rule) => {
|
||||||
|
items.push(SummaryItem::Separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// something else... ignore
|
||||||
|
Some(_) => {}
|
||||||
|
|
||||||
|
// EOF, bail...
|
||||||
|
None => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
|
// From now on, we cannot accept any new paragraph opening tags.
|
||||||
continue;
|
first = false;
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(_) => {
|
|
||||||
// something else... ignore
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// EOF, bail...
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push an event back to the tail of the stream.
|
||||||
|
fn back(&mut self, ev: Event<'a>) {
|
||||||
|
assert!(self.back.is_none());
|
||||||
|
trace!("Back: {:?}", ev);
|
||||||
|
self.back = Some(ev);
|
||||||
|
}
|
||||||
|
|
||||||
fn next_event(&mut self) -> Option<Event<'a>> {
|
fn next_event(&mut self) -> Option<Event<'a>> {
|
||||||
let next = self.stream.next();
|
let next = self.back.take().or_else(|| {
|
||||||
|
self.stream.next().map(|(ev, range)| {
|
||||||
|
self.offset = range.start;
|
||||||
|
ev
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
trace!("Next event: {:?}", next);
|
trace!("Next event: {:?}", next);
|
||||||
|
|
||||||
next
|
next
|
||||||
|
@ -370,6 +466,10 @@ impl<'a> SummaryParser<'a> {
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
Some(Event::Start(Tag::List(..))) => {
|
Some(Event::Start(Tag::List(..))) => {
|
||||||
|
// Skip this tag after comment because it is not nested.
|
||||||
|
if items.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// recurse to parse the nested list
|
// recurse to parse the nested list
|
||||||
let (_, last_item) = get_last_link(&mut items)?;
|
let (_, last_item) = get_last_link(&mut items)?;
|
||||||
let last_item_number = last_item
|
let last_item_number = last_item
|
||||||
|
@ -381,7 +481,7 @@ impl<'a> SummaryParser<'a> {
|
||||||
|
|
||||||
last_item.nested_items = sub_items;
|
last_item.nested_items = sub_items;
|
||||||
}
|
}
|
||||||
Some(Event::End(Tag::List(..))) => break,
|
Some(Event::End(TagEnd::List(..))) => break,
|
||||||
Some(_) => {}
|
Some(_) => {}
|
||||||
None => break,
|
None => break,
|
||||||
}
|
}
|
||||||
|
@ -398,8 +498,8 @@ impl<'a> SummaryParser<'a> {
|
||||||
loop {
|
loop {
|
||||||
match self.next_event() {
|
match self.next_event() {
|
||||||
Some(Event::Start(Tag::Paragraph)) => continue,
|
Some(Event::Start(Tag::Paragraph)) => continue,
|
||||||
Some(Event::Start(Tag::Link(href, _))) => {
|
Some(Event::Start(Tag::Link { dest_url, .. })) => {
|
||||||
let mut link = self.parse_link(href.to_string())?;
|
let mut link = self.parse_link(dest_url.to_string());
|
||||||
|
|
||||||
let mut number = parent.clone();
|
let mut number = parent.clone();
|
||||||
number.0.push(num_existing_items as u32 + 1);
|
number.0.push(num_existing_items as u32 + 1);
|
||||||
|
@ -407,7 +507,10 @@ impl<'a> SummaryParser<'a> {
|
||||||
"Found chapter: {} {} ({})",
|
"Found chapter: {} {} ({})",
|
||||||
number,
|
number,
|
||||||
link.name,
|
link.name,
|
||||||
link.location.display()
|
link.location
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.to_str().unwrap_or(""))
|
||||||
|
.unwrap_or("[draft]")
|
||||||
);
|
);
|
||||||
|
|
||||||
link.number = Some(number);
|
link.number = Some(number);
|
||||||
|
@ -426,19 +529,37 @@ impl<'a> SummaryParser<'a> {
|
||||||
|
|
||||||
fn parse_error<D: Display>(&self, msg: D) -> Error {
|
fn parse_error<D: Display>(&self, msg: D) -> Error {
|
||||||
let (line, col) = self.current_location();
|
let (line, col) = self.current_location();
|
||||||
|
anyhow::anyhow!(
|
||||||
ErrorKind::ParseError(line, col, msg.to_string()).into()
|
"failed to parse SUMMARY.md line {}, column {}: {}",
|
||||||
|
line,
|
||||||
|
col,
|
||||||
|
msg
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to parse the title line.
|
/// Try to parse the title line.
|
||||||
fn parse_title(&mut self) -> Option<String> {
|
fn parse_title(&mut self) -> Option<String> {
|
||||||
if let Some(Event::Start(Tag::Header(1))) = self.next_event() {
|
loop {
|
||||||
|
match self.next_event() {
|
||||||
|
Some(Event::Start(Tag::Heading {
|
||||||
|
level: HeadingLevel::H1,
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
debug!("Found a h1 in the SUMMARY");
|
debug!("Found a h1 in the SUMMARY");
|
||||||
|
|
||||||
let tags = collect_events!(self.stream, end Tag::Header(1));
|
let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
|
||||||
Some(stringify_events(tags))
|
return Some(stringify_events(tags));
|
||||||
} else {
|
}
|
||||||
None
|
// Skip a HTML element such as a comment line.
|
||||||
|
Some(Event::Html(_) | Event::InlineHtml(_))
|
||||||
|
| Some(Event::Start(Tag::HtmlBlock) | Event::End(TagEnd::HtmlBlock)) => {}
|
||||||
|
// Otherwise, no title.
|
||||||
|
Some(ev) => {
|
||||||
|
self.back(ev);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -464,21 +585,22 @@ fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
|
||||||
.filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
|
.filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
|
||||||
.rev()
|
.rev()
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| {
|
.ok_or_else(||
|
||||||
"Unable to get last link because the list of SummaryItems doesn't contain any Links"
|
anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links")
|
||||||
.into()
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the styling from a list of Markdown events and returns just the
|
/// Removes the styling from a list of Markdown events and returns just the
|
||||||
/// plain text.
|
/// plain text.
|
||||||
fn stringify_events(events: Vec<Event>) -> String {
|
fn stringify_events(events: Vec<Event<'_>>) -> String {
|
||||||
events
|
events
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|t| match t {
|
.filter_map(|t| match t {
|
||||||
Event::Text(text) => Some(text.into_owned()),
|
Event::Text(text) | Event::Code(text) => Some(text.into_string()),
|
||||||
|
Event::SoftBreak => Some(String::from(" ")),
|
||||||
_ => None,
|
_ => None,
|
||||||
}).collect()
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
||||||
|
@ -487,7 +609,7 @@ fn stringify_events(events: Vec<Event>) -> String {
|
||||||
pub struct SectionNumber(pub Vec<u32>);
|
pub struct SectionNumber(pub Vec<u32>);
|
||||||
|
|
||||||
impl Display for SectionNumber {
|
impl Display for SectionNumber {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
if self.0.is_empty() {
|
if self.0.is_empty() {
|
||||||
write!(f, "0")
|
write!(f, "0")
|
||||||
} else {
|
} else {
|
||||||
|
@ -547,6 +669,18 @@ mod tests {
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_initial_title() {
|
||||||
|
let src = "[Link]()";
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
|
||||||
|
assert!(parser.parse_title().is_none());
|
||||||
|
assert!(matches!(
|
||||||
|
parser.next_event(),
|
||||||
|
Some(Event::Start(Tag::Paragraph))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_title_with_styling() {
|
fn parse_title_with_styling() {
|
||||||
let src = "# My **Awesome** Summary";
|
let src = "# My **Awesome** Summary";
|
||||||
|
@ -577,17 +711,16 @@ mod tests {
|
||||||
let should_be = vec![
|
let should_be = vec![
|
||||||
SummaryItem::Link(Link {
|
SummaryItem::Link(Link {
|
||||||
name: String::from("First"),
|
name: String::from("First"),
|
||||||
location: PathBuf::from("./first.md"),
|
location: Some(PathBuf::from("./first.md")),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
SummaryItem::Link(Link {
|
SummaryItem::Link(Link {
|
||||||
name: String::from("Second"),
|
name: String::from("Second"),
|
||||||
location: PathBuf::from("./second.md"),
|
location: Some(PathBuf::from("./second.md")),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let _ = parser.stream.next(); // step past first event
|
|
||||||
let got = parser.parse_affix(true).unwrap();
|
let got = parser.parse_affix(true).unwrap();
|
||||||
|
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
|
@ -598,7 +731,6 @@ mod tests {
|
||||||
let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
|
let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
|
||||||
let mut parser = SummaryParser::new(src);
|
let mut parser = SummaryParser::new(src);
|
||||||
|
|
||||||
let _ = parser.stream.next(); // step past first event
|
|
||||||
let got = parser.parse_affix(true).unwrap();
|
let got = parser.parse_affix(true).unwrap();
|
||||||
|
|
||||||
assert_eq!(got.len(), 3);
|
assert_eq!(got.len(), 3);
|
||||||
|
@ -610,7 +742,6 @@ mod tests {
|
||||||
let src = "[First](./first.md)\n- [Second](./second.md)\n";
|
let src = "[First](./first.md)\n- [Second](./second.md)\n";
|
||||||
let mut parser = SummaryParser::new(src);
|
let mut parser = SummaryParser::new(src);
|
||||||
|
|
||||||
let _ = parser.stream.next(); // step past first event
|
|
||||||
let got = parser.parse_affix(false);
|
let got = parser.parse_affix(false);
|
||||||
|
|
||||||
assert!(got.is_err());
|
assert!(got.is_err());
|
||||||
|
@ -621,19 +752,19 @@ mod tests {
|
||||||
let src = "[First](./first.md)";
|
let src = "[First](./first.md)";
|
||||||
let should_be = Link {
|
let should_be = Link {
|
||||||
name: String::from("First"),
|
name: String::from("First"),
|
||||||
location: PathBuf::from("./first.md"),
|
location: Some(PathBuf::from("./first.md")),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut parser = SummaryParser::new(src);
|
let mut parser = SummaryParser::new(src);
|
||||||
let _ = parser.stream.next(); // skip past start of paragraph
|
let _ = parser.stream.next(); // Discard opening paragraph
|
||||||
|
|
||||||
let href = match parser.stream.next() {
|
let href = match parser.stream.next() {
|
||||||
Some(Event::Start(Tag::Link(href, _))) => href.to_string(),
|
Some((Event::Start(Tag::Link { dest_url, .. }), _range)) => dest_url.to_string(),
|
||||||
other => panic!("Unreachable, {:?}", other),
|
other => panic!("Unreachable, {:?}", other),
|
||||||
};
|
};
|
||||||
|
|
||||||
let got = parser.parse_link(href).unwrap();
|
let got = parser.parse_link(href);
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -642,16 +773,16 @@ mod tests {
|
||||||
let src = "- [First](./first.md)\n";
|
let src = "- [First](./first.md)\n";
|
||||||
let link = Link {
|
let link = Link {
|
||||||
name: String::from("First"),
|
name: String::from("First"),
|
||||||
location: PathBuf::from("./first.md"),
|
location: Some(PathBuf::from("./first.md")),
|
||||||
number: Some(SectionNumber(vec![1])),
|
number: Some(SectionNumber(vec![1])),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let should_be = vec![SummaryItem::Link(link)];
|
let should_be = vec![SummaryItem::Link(link)];
|
||||||
|
|
||||||
let mut parser = SummaryParser::new(src);
|
let mut parser = SummaryParser::new(src);
|
||||||
let _ = parser.stream.next();
|
let got = parser
|
||||||
|
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||||
let got = parser.parse_numbered().unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
|
@ -663,27 +794,92 @@ mod tests {
|
||||||
let should_be = vec![
|
let should_be = vec![
|
||||||
SummaryItem::Link(Link {
|
SummaryItem::Link(Link {
|
||||||
name: String::from("First"),
|
name: String::from("First"),
|
||||||
location: PathBuf::from("./first.md"),
|
location: Some(PathBuf::from("./first.md")),
|
||||||
number: Some(SectionNumber(vec![1])),
|
number: Some(SectionNumber(vec![1])),
|
||||||
nested_items: vec![SummaryItem::Link(Link {
|
nested_items: vec![SummaryItem::Link(Link {
|
||||||
name: String::from("Nested"),
|
name: String::from("Nested"),
|
||||||
location: PathBuf::from("./nested.md"),
|
location: Some(PathBuf::from("./nested.md")),
|
||||||
number: Some(SectionNumber(vec![1, 1])),
|
number: Some(SectionNumber(vec![1, 1])),
|
||||||
nested_items: Vec::new(),
|
nested_items: Vec::new(),
|
||||||
})],
|
})],
|
||||||
}),
|
}),
|
||||||
SummaryItem::Link(Link {
|
SummaryItem::Link(Link {
|
||||||
name: String::from("Second"),
|
name: String::from("Second"),
|
||||||
location: PathBuf::from("./second.md"),
|
location: Some(PathBuf::from("./second.md")),
|
||||||
number: Some(SectionNumber(vec![2])),
|
number: Some(SectionNumber(vec![2])),
|
||||||
nested_items: Vec::new(),
|
nested_items: Vec::new(),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut parser = SummaryParser::new(src);
|
let mut parser = SummaryParser::new(src);
|
||||||
let _ = parser.stream.next();
|
let got = parser
|
||||||
|
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let got = parser.parse_numbered().unwrap();
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_numbered_chapters_separated_by_comment() {
|
||||||
|
let src = "- [First](./first.md)\n<!-- this is a comment -->\n- [Second](./second.md)";
|
||||||
|
|
||||||
|
let should_be = vec![
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from("First"),
|
||||||
|
location: Some(PathBuf::from("./first.md")),
|
||||||
|
number: Some(SectionNumber(vec![1])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
}),
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from("Second"),
|
||||||
|
location: Some(PathBuf::from("./second.md")),
|
||||||
|
number: Some(SectionNumber(vec![2])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
let got = parser
|
||||||
|
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_titled_parts() {
|
||||||
|
let src = "- [First](./first.md)\n- [Second](./second.md)\n\
|
||||||
|
# Title 2\n- [Third](./third.md)\n\t- [Fourth](./fourth.md)";
|
||||||
|
|
||||||
|
let should_be = vec![
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from("First"),
|
||||||
|
location: Some(PathBuf::from("./first.md")),
|
||||||
|
number: Some(SectionNumber(vec![1])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
}),
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from("Second"),
|
||||||
|
location: Some(PathBuf::from("./second.md")),
|
||||||
|
number: Some(SectionNumber(vec![2])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
}),
|
||||||
|
SummaryItem::PartTitle(String::from("Title 2")),
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from("Third"),
|
||||||
|
location: Some(PathBuf::from("./third.md")),
|
||||||
|
number: Some(SectionNumber(vec![3])),
|
||||||
|
nested_items: vec![SummaryItem::Link(Link {
|
||||||
|
name: String::from("Fourth"),
|
||||||
|
location: Some(PathBuf::from("./fourth.md")),
|
||||||
|
number: Some(SectionNumber(vec![3, 1])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
let got = parser.parse_parts().unwrap();
|
||||||
|
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
|
@ -698,69 +894,221 @@ mod tests {
|
||||||
let should_be = vec![
|
let should_be = vec![
|
||||||
SummaryItem::Link(Link {
|
SummaryItem::Link(Link {
|
||||||
name: String::from("First"),
|
name: String::from("First"),
|
||||||
location: PathBuf::from("./first.md"),
|
location: Some(PathBuf::from("./first.md")),
|
||||||
number: Some(SectionNumber(vec![1])),
|
number: Some(SectionNumber(vec![1])),
|
||||||
nested_items: Vec::new(),
|
nested_items: Vec::new(),
|
||||||
}),
|
}),
|
||||||
SummaryItem::Link(Link {
|
SummaryItem::Link(Link {
|
||||||
name: String::from("Second"),
|
name: String::from("Second"),
|
||||||
location: PathBuf::from("./second.md"),
|
location: Some(PathBuf::from("./second.md")),
|
||||||
number: Some(SectionNumber(vec![2])),
|
number: Some(SectionNumber(vec![2])),
|
||||||
nested_items: Vec::new(),
|
nested_items: Vec::new(),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut parser = SummaryParser::new(src);
|
let mut parser = SummaryParser::new(src);
|
||||||
let _ = parser.stream.next();
|
let got = parser
|
||||||
|
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||||
let got = parser.parse_numbered().unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_empty_link_location_is_an_error() {
|
fn an_empty_link_location_is_a_draft_chapter() {
|
||||||
let src = "- [Empty]()\n";
|
let src = "- [Empty]()\n";
|
||||||
let mut parser = SummaryParser::new(src);
|
let mut parser = SummaryParser::new(src);
|
||||||
parser.stream.next();
|
|
||||||
|
|
||||||
let got = parser.parse_numbered();
|
let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default());
|
||||||
assert!(got.is_err());
|
let should_be = vec![SummaryItem::Link(Link {
|
||||||
|
name: String::from("Empty"),
|
||||||
|
location: None,
|
||||||
|
number: Some(SectionNumber(vec![1])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
})];
|
||||||
|
|
||||||
|
assert!(got.is_ok());
|
||||||
|
assert_eq!(got.unwrap(), should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Regression test for https://github.com/rust-lang-nursery/mdBook/issues/779
|
/// Regression test for https://github.com/rust-lang/mdBook/issues/779
|
||||||
/// Ensure section numbers are correctly incremented after a horizontal separator.
|
/// Ensure section numbers are correctly incremented after a horizontal separator.
|
||||||
#[test]
|
#[test]
|
||||||
fn keep_numbering_after_separator() {
|
fn keep_numbering_after_separator() {
|
||||||
let src = "- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
|
let src =
|
||||||
|
"- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
|
||||||
let should_be = vec![
|
let should_be = vec![
|
||||||
SummaryItem::Link(Link {
|
SummaryItem::Link(Link {
|
||||||
name: String::from("First"),
|
name: String::from("First"),
|
||||||
location: PathBuf::from("./first.md"),
|
location: Some(PathBuf::from("./first.md")),
|
||||||
number: Some(SectionNumber(vec![1])),
|
number: Some(SectionNumber(vec![1])),
|
||||||
nested_items: Vec::new(),
|
nested_items: Vec::new(),
|
||||||
}),
|
}),
|
||||||
SummaryItem::Separator,
|
SummaryItem::Separator,
|
||||||
SummaryItem::Link(Link {
|
SummaryItem::Link(Link {
|
||||||
name: String::from("Second"),
|
name: String::from("Second"),
|
||||||
location: PathBuf::from("./second.md"),
|
location: Some(PathBuf::from("./second.md")),
|
||||||
number: Some(SectionNumber(vec![2])),
|
number: Some(SectionNumber(vec![2])),
|
||||||
nested_items: Vec::new(),
|
nested_items: Vec::new(),
|
||||||
}),
|
}),
|
||||||
SummaryItem::Separator,
|
SummaryItem::Separator,
|
||||||
SummaryItem::Link(Link {
|
SummaryItem::Link(Link {
|
||||||
name: String::from("Third"),
|
name: String::from("Third"),
|
||||||
location: PathBuf::from("./third.md"),
|
location: Some(PathBuf::from("./third.md")),
|
||||||
number: Some(SectionNumber(vec![3])),
|
number: Some(SectionNumber(vec![3])),
|
||||||
nested_items: Vec::new(),
|
nested_items: Vec::new(),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut parser = SummaryParser::new(src);
|
let mut parser = SummaryParser::new(src);
|
||||||
let _ = parser.stream.next();
|
let got = parser
|
||||||
|
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||||
let got = parser.parse_numbered().unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(got, should_be);
|
assert_eq!(got, should_be);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Regression test for https://github.com/rust-lang/mdBook/issues/1218
|
||||||
|
/// Ensure chapter names spread across multiple lines have spaces between all the words.
|
||||||
|
#[test]
|
||||||
|
fn add_space_for_multi_line_chapter_names() {
|
||||||
|
let src = "- [Chapter\ntitle](./chapter.md)";
|
||||||
|
let should_be = vec![SummaryItem::Link(Link {
|
||||||
|
name: String::from("Chapter title"),
|
||||||
|
location: Some(PathBuf::from("./chapter.md")),
|
||||||
|
number: Some(SectionNumber(vec![1])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
})];
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
let got = parser
|
||||||
|
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allow_space_in_link_destination() {
|
||||||
|
let src = "- [test1](./test%20link1.md)\n- [test2](<./test link2.md>)";
|
||||||
|
let should_be = vec![
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from("test1"),
|
||||||
|
location: Some(PathBuf::from("./test link1.md")),
|
||||||
|
number: Some(SectionNumber(vec![1])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
}),
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from("test2"),
|
||||||
|
location: Some(PathBuf::from("./test link2.md")),
|
||||||
|
number: Some(SectionNumber(vec![2])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
let got = parser
|
||||||
|
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skip_html_comments() {
|
||||||
|
let src = r#"<!--
|
||||||
|
# Title - En
|
||||||
|
-->
|
||||||
|
# Title - Local
|
||||||
|
|
||||||
|
<!--
|
||||||
|
[Prefix 00-01 - En](ch00-01.md)
|
||||||
|
[Prefix 00-02 - En](ch00-02.md)
|
||||||
|
-->
|
||||||
|
[Prefix 00-01 - Local](ch00-01.md)
|
||||||
|
[Prefix 00-02 - Local](ch00-02.md)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## Section Title - En
|
||||||
|
-->
|
||||||
|
## Section Title - Localized
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- [Ch 01-00 - En](ch01-00.md)
|
||||||
|
- [Ch 01-01 - En](ch01-01.md)
|
||||||
|
- [Ch 01-02 - En](ch01-02.md)
|
||||||
|
-->
|
||||||
|
- [Ch 01-00 - Local](ch01-00.md)
|
||||||
|
- [Ch 01-01 - Local](ch01-01.md)
|
||||||
|
- [Ch 01-02 - Local](ch01-02.md)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- [Ch 02-00 - En](ch02-00.md)
|
||||||
|
-->
|
||||||
|
- [Ch 02-00 - Local](ch02-00.md)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
[Appendix A - En](appendix-01.md)
|
||||||
|
[Appendix B - En](appendix-02.md)
|
||||||
|
-->`
|
||||||
|
[Appendix A - Local](appendix-01.md)
|
||||||
|
[Appendix B - Local](appendix-02.md)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let mut parser = SummaryParser::new(src);
|
||||||
|
|
||||||
|
// ---- Title ----
|
||||||
|
let title = parser.parse_title();
|
||||||
|
assert_eq!(title, Some(String::from("Title - Local")));
|
||||||
|
|
||||||
|
// ---- Prefix Chapters ----
|
||||||
|
|
||||||
|
let new_affix_item = |name, location| {
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from(name),
|
||||||
|
location: Some(PathBuf::from(location)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let should_be = vec![
|
||||||
|
new_affix_item("Prefix 00-01 - Local", "ch00-01.md"),
|
||||||
|
new_affix_item("Prefix 00-02 - Local", "ch00-02.md"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let got = parser.parse_affix(true).unwrap();
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
|
||||||
|
// ---- Numbered Chapters ----
|
||||||
|
|
||||||
|
let new_numbered_item = |name, location, numbers: &[u32], nested_items| {
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from(name),
|
||||||
|
location: Some(PathBuf::from(location)),
|
||||||
|
number: Some(SectionNumber(numbers.to_vec())),
|
||||||
|
nested_items,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let ch01_nested = vec![
|
||||||
|
new_numbered_item("Ch 01-01 - Local", "ch01-01.md", &[1, 1], vec![]),
|
||||||
|
new_numbered_item("Ch 01-02 - Local", "ch01-02.md", &[1, 2], vec![]),
|
||||||
|
];
|
||||||
|
|
||||||
|
let should_be = vec![
|
||||||
|
new_numbered_item("Ch 01-00 - Local", "ch01-00.md", &[1], ch01_nested),
|
||||||
|
new_numbered_item("Ch 02-00 - Local", "ch02-00.md", &[2], vec![]),
|
||||||
|
];
|
||||||
|
let got = parser.parse_parts().unwrap();
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
|
||||||
|
// ---- Suffix Chapters ----
|
||||||
|
|
||||||
|
let should_be = vec![
|
||||||
|
new_affix_item("Appendix A - Local", "appendix-01.md"),
|
||||||
|
new_affix_item("Appendix B - Local", "appendix-02.md"),
|
||||||
|
];
|
||||||
|
|
||||||
|
let got = parser.parse_affix(false).unwrap();
|
||||||
|
assert_eq!(got, should_be);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,37 @@
|
||||||
use clap::{App, ArgMatches, SubCommand};
|
use super::command_prelude::*;
|
||||||
|
use crate::{get_book_dir, open};
|
||||||
use mdbook::errors::Result;
|
use mdbook::errors::Result;
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
use {get_book_dir, open};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
// Create clap subcommand arguments
|
// Create clap subcommand arguments
|
||||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
pub fn make_subcommand() -> Command {
|
||||||
SubCommand::with_name("build")
|
Command::new("build")
|
||||||
.about("Builds a book from its markdown files")
|
.about("Builds a book from its markdown files")
|
||||||
.arg_from_usage(
|
.arg_dest_dir()
|
||||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
.arg_root_dir()
|
||||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
.arg_open()
|
||||||
).arg_from_usage(
|
|
||||||
"[dir] 'Root directory for the book{n}\
|
|
||||||
(Defaults to the Current Directory when omitted)'",
|
|
||||||
).arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build command implementation
|
// Build command implementation
|
||||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let mut book = MDBook::load(&book_dir)?;
|
let mut book = MDBook::load(book_dir)?;
|
||||||
|
|
||||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||||
book.config.build.build_dir = dest_dir.into();
|
book.config.build.build_dir = dest_dir.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
book.build()?;
|
book.build()?;
|
||||||
|
|
||||||
if args.is_present("open") {
|
if args.get_flag("open") {
|
||||||
// FIXME: What's the right behaviour if we don't use the HTML renderer?
|
// FIXME: What's the right behaviour if we don't use the HTML renderer?
|
||||||
open(book.build_dir_for("html").join("index.html"));
|
let path = book.build_dir_for("html").join("index.html");
|
||||||
|
if !path.exists() {
|
||||||
|
error!("No chapter available to open");
|
||||||
|
std::process::exit(1)
|
||||||
|
}
|
||||||
|
open(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
use clap::{App, ArgMatches, SubCommand};
|
use super::command_prelude::*;
|
||||||
use get_book_dir;
|
use crate::get_book_dir;
|
||||||
use mdbook::errors::*;
|
use anyhow::Context;
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
// Create clap subcommand arguments
|
// Create clap subcommand arguments
|
||||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
pub fn make_subcommand() -> Command {
|
||||||
SubCommand::with_name("clean")
|
Command::new("clean")
|
||||||
.about("Deletes a built book")
|
.about("Deletes a built book")
|
||||||
.arg_from_usage(
|
.arg_dest_dir()
|
||||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
.arg_root_dir()
|
||||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
|
||||||
).arg_from_usage(
|
|
||||||
"[dir] 'Root directory for the book{n}\
|
|
||||||
(Defaults to the Current Directory when omitted)'",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean command implementation
|
// Clean command implementation
|
||||||
pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
|
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let book = MDBook::load(&book_dir)?;
|
let book = MDBook::load(book_dir)?;
|
||||||
|
|
||||||
let dir_to_remove = match args.value_of("dest-dir") {
|
let dir_to_remove = match args.get_one::<PathBuf>("dest-dir") {
|
||||||
Some(dest_dir) => dest_dir.into(),
|
Some(dest_dir) => dest_dir.into(),
|
||||||
None => book.root.join(&book.config.build.build_dir),
|
None => book.root.join(&book.config.build.build_dir),
|
||||||
};
|
};
|
||||||
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
|
|
||||||
|
if dir_to_remove.exists() {
|
||||||
|
fs::remove_dir_all(&dir_to_remove)
|
||||||
|
.with_context(|| "Unable to remove the build directory")?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
//! Helpers for building the command-line arguments for commands.
|
||||||
|
|
||||||
|
pub use clap::{arg, Arg, ArgMatches, Command};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub trait CommandExt: Sized {
|
||||||
|
fn _arg(self, arg: Arg) -> Self;
|
||||||
|
|
||||||
|
fn arg_dest_dir(self) -> Self {
|
||||||
|
self._arg(
|
||||||
|
Arg::new("dest-dir")
|
||||||
|
.short('d')
|
||||||
|
.long("dest-dir")
|
||||||
|
.value_name("dest-dir")
|
||||||
|
.value_parser(clap::value_parser!(PathBuf))
|
||||||
|
.help(
|
||||||
|
"Output directory for the book\n\
|
||||||
|
Relative paths are interpreted relative to the book's root directory.\n\
|
||||||
|
If omitted, mdBook uses build.build-dir from book.toml \
|
||||||
|
or defaults to `./book`.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arg_root_dir(self) -> Self {
|
||||||
|
self._arg(
|
||||||
|
Arg::new("dir")
|
||||||
|
.help(
|
||||||
|
"Root directory for the book\n\
|
||||||
|
(Defaults to the current directory when omitted)",
|
||||||
|
)
|
||||||
|
.value_parser(clap::value_parser!(PathBuf)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arg_open(self) -> Self {
|
||||||
|
self._arg(arg!(-o --open "Opens the compiled book in a web browser"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandExt for Command {
|
||||||
|
fn _arg(self, arg: Arg) -> Self {
|
||||||
|
self.arg(arg)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
use clap::{App, ArgMatches, SubCommand};
|
use crate::get_book_dir;
|
||||||
use get_book_dir;
|
use clap::{arg, ArgMatches, Command as ClapCommand};
|
||||||
use mdbook::config;
|
use mdbook::config;
|
||||||
use mdbook::errors::Result;
|
use mdbook::errors::Result;
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
|
@ -8,14 +8,23 @@ use std::io::Write;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
// Create clap subcommand arguments
|
// Create clap subcommand arguments
|
||||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
pub fn make_subcommand() -> ClapCommand {
|
||||||
SubCommand::with_name("init")
|
ClapCommand::new("init")
|
||||||
.about("Creates the boilerplate structure and files for a new book")
|
.about("Creates the boilerplate structure and files for a new book")
|
||||||
// the {n} denotes a newline which will properly aligned in all help messages
|
.arg(
|
||||||
.arg_from_usage("[dir] 'Directory to create the book in{n}\
|
arg!([dir]
|
||||||
(Defaults to the Current Directory when omitted)'")
|
"Directory to create the book in\n\
|
||||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
(Defaults to the current directory when omitted)"
|
||||||
.arg_from_usage("--force 'Skips confirmation prompts'")
|
)
|
||||||
|
.value_parser(clap::value_parser!(std::path::PathBuf)),
|
||||||
|
)
|
||||||
|
.arg(arg!(--theme "Copies the default theme into your source folder"))
|
||||||
|
.arg(arg!(--force "Skips confirmation prompts"))
|
||||||
|
.arg(arg!(--title <title> "Sets the book title"))
|
||||||
|
.arg(
|
||||||
|
arg!(--ignore <ignore> "Creates a VCS ignore file (i.e. .gitignore)")
|
||||||
|
.value_parser(["none", "git"]),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init command implementation
|
// Init command implementation
|
||||||
|
@ -23,18 +32,13 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let mut builder = MDBook::init(&book_dir);
|
let mut builder = MDBook::init(&book_dir);
|
||||||
let mut config = config::Config::default();
|
let mut config = config::Config::default();
|
||||||
|
|
||||||
// If flag `--theme` is present, copy theme to src
|
// If flag `--theme` is present, copy theme to src
|
||||||
if args.is_present("theme") {
|
if args.get_flag("theme") {
|
||||||
config.set("output.html.theme", "src/theme")?;
|
let theme_dir = book_dir.join("theme");
|
||||||
// Skip this if `--force` is present
|
|
||||||
if !args.is_present("force") {
|
|
||||||
// Print warning
|
|
||||||
println!();
|
println!();
|
||||||
println!(
|
println!("Copying the default theme to {}", theme_dir.display());
|
||||||
"Copying the default theme to {}",
|
// Skip this if `--force` is present
|
||||||
builder.config().book.src.display()
|
if !args.get_flag("force") && theme_dir.exists() {
|
||||||
);
|
|
||||||
println!("This could potentially overwrite files already present in that directory.");
|
println!("This could potentially overwrite files already present in that directory.");
|
||||||
print!("\nAre you sure you want to continue? (y/n) ");
|
print!("\nAre you sure you want to continue? (y/n) ");
|
||||||
|
|
||||||
|
@ -47,13 +51,25 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ignore) = args.get_one::<String>("ignore").map(|s| s.as_str()) {
|
||||||
|
match ignore {
|
||||||
|
"git" => builder.create_gitignore(true),
|
||||||
|
_ => builder.create_gitignore(false),
|
||||||
|
};
|
||||||
|
} else if !args.get_flag("force") {
|
||||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||||
|
|
||||||
if confirm() {
|
if confirm() {
|
||||||
builder.create_gitignore(true);
|
builder.create_gitignore(true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config.book.title = request_book_title();
|
config.book.title = if args.contains_id("title") {
|
||||||
|
args.get_one::<String>("title").map(String::from)
|
||||||
|
} else if args.get_flag("force") {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
request_book_title()
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(author) = get_author_name() {
|
if let Some(author) = get_author_name() {
|
||||||
debug!("Obtained user name from gitconfig: {:?}", author);
|
debug!("Obtained user name from gitconfig: {:?}", author);
|
||||||
|
@ -70,7 +86,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
/// Obtains author name from git config file by running the `git config` command.
|
/// Obtains author name from git config file by running the `git config` command.
|
||||||
fn get_author_name() -> Option<String> {
|
fn get_author_name() -> Option<String> {
|
||||||
let output = Command::new("git")
|
let output = Command::new("git")
|
||||||
.args(&["config", "--get", "user.name"])
|
.args(["config", "--get", "user.name"])
|
||||||
.output()
|
.output()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
|
@ -100,8 +116,5 @@ fn confirm() -> bool {
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
io::stdin().read_line(&mut s).ok();
|
io::stdin().read_line(&mut s).ok();
|
||||||
match &*s.trim() {
|
matches!(s.trim(), "Y" | "y" | "yes" | "Yes")
|
||||||
"Y" | "y" | "yes" | "Yes" => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
pub mod build;
|
pub mod build;
|
||||||
pub mod clean;
|
pub mod clean;
|
||||||
|
pub mod command_prelude;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
#[cfg(feature = "serve")]
|
#[cfg(feature = "serve")]
|
||||||
pub mod serve;
|
pub mod serve;
|
||||||
|
|
193
src/cmd/serve.rs
193
src/cmd/serve.rs
|
@ -1,109 +1,92 @@
|
||||||
extern crate iron;
|
use super::command_prelude::*;
|
||||||
extern crate staticfile;
|
|
||||||
extern crate ws;
|
|
||||||
|
|
||||||
use self::iron::{
|
|
||||||
status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set,
|
|
||||||
};
|
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
use super::watch;
|
use super::watch;
|
||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
use crate::{get_book_dir, open};
|
||||||
|
use clap::builder::NonEmptyStringValueParser;
|
||||||
|
use futures_util::sink::SinkExt;
|
||||||
|
use futures_util::StreamExt;
|
||||||
use mdbook::errors::*;
|
use mdbook::errors::*;
|
||||||
use mdbook::utils;
|
use mdbook::utils;
|
||||||
|
use mdbook::utils::fs::get_404_output_file;
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
use std;
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
use {get_book_dir, open};
|
use std::path::PathBuf;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
use warp::ws::Message;
|
||||||
|
use warp::Filter;
|
||||||
|
|
||||||
struct ErrorRecover;
|
/// The HTTP endpoint for the websocket used to trigger reloads when a file changes.
|
||||||
|
const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
|
||||||
|
|
||||||
// Create clap subcommand arguments
|
// Create clap subcommand arguments
|
||||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
pub fn make_subcommand() -> Command {
|
||||||
SubCommand::with_name("serve")
|
Command::new("serve")
|
||||||
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
|
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
|
||||||
.arg_from_usage(
|
.arg_dest_dir()
|
||||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
.arg_root_dir()
|
||||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
|
||||||
)
|
|
||||||
.arg_from_usage(
|
|
||||||
"[dir] 'Root directory for the book{n}\
|
|
||||||
(Defaults to the Current Directory when omitted)'",
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("hostname")
|
Arg::new("hostname")
|
||||||
.short("n")
|
.short('n')
|
||||||
.long("hostname")
|
.long("hostname")
|
||||||
.takes_value(true)
|
.num_args(1)
|
||||||
.default_value("localhost")
|
.default_value("localhost")
|
||||||
.empty_values(false)
|
.value_parser(NonEmptyStringValueParser::new())
|
||||||
.help("Hostname to listen on for HTTP connections"),
|
.help("Hostname to listen on for HTTP connections"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("port")
|
Arg::new("port")
|
||||||
.short("p")
|
.short('p')
|
||||||
.long("port")
|
.long("port")
|
||||||
.takes_value(true)
|
.num_args(1)
|
||||||
.default_value("3000")
|
.default_value("3000")
|
||||||
.empty_values(false)
|
.value_parser(NonEmptyStringValueParser::new())
|
||||||
.help("Port to use for HTTP connections"),
|
.help("Port to use for HTTP connections"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg_open()
|
||||||
Arg::with_name("websocket-hostname")
|
|
||||||
.long("websocket-hostname")
|
|
||||||
.takes_value(true)
|
|
||||||
.empty_values(false)
|
|
||||||
.help(
|
|
||||||
"Hostname to connect to for WebSockets connections (Defaults to the HTTP hostname)",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::with_name("websocket-port")
|
|
||||||
.short("w")
|
|
||||||
.long("websocket-port")
|
|
||||||
.takes_value(true)
|
|
||||||
.default_value("3001")
|
|
||||||
.empty_values(false)
|
|
||||||
.help("Port to use for WebSockets livereload connections"),
|
|
||||||
)
|
|
||||||
.arg_from_usage("-o, --open 'Opens the book server in a web browser'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch command implementation
|
// Serve command implementation
|
||||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let mut book = MDBook::load(&book_dir)?;
|
let mut book = MDBook::load(book_dir)?;
|
||||||
|
|
||||||
let port = args.value_of("port").unwrap();
|
let port = args.get_one::<String>("port").unwrap();
|
||||||
let ws_port = args.value_of("websocket-port").unwrap();
|
let hostname = args.get_one::<String>("hostname").unwrap();
|
||||||
let hostname = args.value_of("hostname").unwrap();
|
let open_browser = args.get_flag("open");
|
||||||
let public_address = args.value_of("websocket-address").unwrap_or(hostname);
|
|
||||||
let open_browser = args.is_present("open");
|
|
||||||
|
|
||||||
let address = format!("{}:{}", hostname, port);
|
let address = format!("{}:{}", hostname, port);
|
||||||
let ws_address = format!("{}:{}", hostname, ws_port);
|
|
||||||
|
|
||||||
let livereload_url = format!("ws://{}:{}", public_address, ws_port);
|
let update_config = |book: &mut MDBook| {
|
||||||
book.config
|
book.config
|
||||||
.set("output.html.livereload-url", &livereload_url)?;
|
.set("output.html.live-reload-endpoint", LIVE_RELOAD_ENDPOINT)
|
||||||
|
.expect("live-reload-endpoint update failed");
|
||||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||||
book.config.build.build_dir = dest_dir.into();
|
book.config.build.build_dir = dest_dir.into();
|
||||||
}
|
}
|
||||||
|
// Override site-url for local serving of the 404 file
|
||||||
|
book.config.set("output.html.site-url", "/").unwrap();
|
||||||
|
};
|
||||||
|
update_config(&mut book);
|
||||||
book.build()?;
|
book.build()?;
|
||||||
|
|
||||||
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
|
let sockaddr: SocketAddr = address
|
||||||
chain.link_after(ErrorRecover);
|
.to_socket_addrs()?
|
||||||
let _iron = Iron::new(chain)
|
.next()
|
||||||
.http(&*address)
|
.ok_or_else(|| anyhow::anyhow!("no address found for {}", address))?;
|
||||||
.chain_err(|| "Unable to launch the server")?;
|
let build_dir = book.build_dir_for("html");
|
||||||
|
let input_404 = book
|
||||||
|
.config
|
||||||
|
.get("output.html.input-404")
|
||||||
|
.and_then(toml::Value::as_str)
|
||||||
|
.map(ToString::to_string);
|
||||||
|
let file_404 = get_404_output_file(&input_404);
|
||||||
|
|
||||||
let ws_server =
|
// A channel used to broadcast to any websockets to reload when a file changes.
|
||||||
ws::WebSocket::new(|_| |_| Ok(())).chain_err(|| "Unable to start the websocket")?;
|
let (tx, _rx) = tokio::sync::broadcast::channel::<Message>(100);
|
||||||
|
|
||||||
let broadcaster = ws_server.broadcaster();
|
let reload_tx = tx.clone();
|
||||||
|
let thread_handle = std::thread::spawn(move || {
|
||||||
std::thread::spawn(move || {
|
serve(build_dir, sockaddr, reload_tx, &file_404);
|
||||||
ws_server.listen(&*ws_address).unwrap();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let serving_url = format!("http://{}", address);
|
let serving_url = format!("http://{}", address);
|
||||||
|
@ -114,36 +97,68 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
watch::trigger_on_change(&mut book, move |path, book_dir| {
|
watch::trigger_on_change(&book, move |paths, book_dir| {
|
||||||
info!("File changed: {:?}", path);
|
info!("Files changed: {:?}", paths);
|
||||||
info!("Building book...");
|
info!("Building book...");
|
||||||
|
|
||||||
// FIXME: This area is really ugly because we need to re-set livereload :(
|
// FIXME: This area is really ugly because we need to re-set livereload :(
|
||||||
|
let result = MDBook::load(book_dir).and_then(|mut b| {
|
||||||
let result = MDBook::load(&book_dir)
|
update_config(&mut b);
|
||||||
.and_then(|mut b| {
|
b.build()
|
||||||
b.config
|
});
|
||||||
.set("output.html.livereload-url", &livereload_url)?;
|
|
||||||
Ok(b)
|
|
||||||
}).and_then(|b| b.build());
|
|
||||||
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
error!("Unable to load the book");
|
error!("Unable to load the book");
|
||||||
utils::log_backtrace(&e);
|
utils::log_backtrace(&e);
|
||||||
} else {
|
} else {
|
||||||
let _ = broadcaster.send("reload");
|
let _ = tx.send(Message::text("reload"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let _ = thread_handle.join();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AfterMiddleware for ErrorRecover {
|
#[tokio::main]
|
||||||
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> {
|
async fn serve(
|
||||||
match err.response.status {
|
build_dir: PathBuf,
|
||||||
// each error will result in 404 response
|
address: SocketAddr,
|
||||||
Some(_) => Ok(err.response.set(status::NotFound)),
|
reload_tx: broadcast::Sender<Message>,
|
||||||
_ => Err(err),
|
file_404: &str,
|
||||||
}
|
) {
|
||||||
|
// A warp Filter which captures `reload_tx` and provides an `rx` copy to
|
||||||
|
// receive reload messages.
|
||||||
|
let sender = warp::any().map(move || reload_tx.subscribe());
|
||||||
|
|
||||||
|
// A warp Filter to handle the livereload endpoint. This upgrades to a
|
||||||
|
// websocket, and then waits for any filesystem change notifications, and
|
||||||
|
// relays them over the websocket.
|
||||||
|
let livereload = warp::path(LIVE_RELOAD_ENDPOINT)
|
||||||
|
.and(warp::ws())
|
||||||
|
.and(sender)
|
||||||
|
.map(|ws: warp::ws::Ws, mut rx: broadcast::Receiver<Message>| {
|
||||||
|
ws.on_upgrade(move |ws| async move {
|
||||||
|
let (mut user_ws_tx, _user_ws_rx) = ws.split();
|
||||||
|
trace!("websocket got connection");
|
||||||
|
if let Ok(m) = rx.recv().await {
|
||||||
|
trace!("notify of reload");
|
||||||
|
let _ = user_ws_tx.send(m).await;
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
// A warp Filter that serves from the filesystem.
|
||||||
|
let book_route = warp::fs::dir(build_dir.clone());
|
||||||
|
// The fallback route for 404 errors
|
||||||
|
let fallback_route = warp::fs::file(build_dir.join(file_404))
|
||||||
|
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
|
||||||
|
let routes = livereload.or(book_route).or(fallback_route);
|
||||||
|
|
||||||
|
std::panic::set_hook(Box::new(move |panic_info| {
|
||||||
|
// exit if serve panics
|
||||||
|
error!("Unable to serve: {}", panic_info);
|
||||||
|
std::process::exit(1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
warp::serve(routes).run(address).await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,58 @@
|
||||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
use super::command_prelude::*;
|
||||||
use get_book_dir;
|
use crate::get_book_dir;
|
||||||
|
use clap::builder::NonEmptyStringValueParser;
|
||||||
|
use clap::ArgAction;
|
||||||
use mdbook::errors::Result;
|
use mdbook::errors::Result;
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
// Create clap subcommand arguments
|
// Create clap subcommand arguments
|
||||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
pub fn make_subcommand() -> Command {
|
||||||
SubCommand::with_name("test")
|
Command::new("test")
|
||||||
.about("Tests that a book's Rust code samples compile")
|
.about("Tests that a book's Rust code samples compile")
|
||||||
.arg_from_usage(
|
// FIXME: --dest-dir is unused by the test command, it should be removed
|
||||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
.arg_dest_dir()
|
||||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
.arg_root_dir()
|
||||||
|
.arg(
|
||||||
|
Arg::new("chapter")
|
||||||
|
.short('c')
|
||||||
|
.long("chapter")
|
||||||
|
.value_name("chapter"),
|
||||||
)
|
)
|
||||||
.arg_from_usage(
|
.arg(
|
||||||
"[dir] 'Root directory for the book{n}\
|
Arg::new("library-path")
|
||||||
(Defaults to the Current Directory when omitted)'",
|
.short('L')
|
||||||
)
|
|
||||||
.arg(Arg::with_name("library-path")
|
|
||||||
.short("L")
|
|
||||||
.long("library-path")
|
.long("library-path")
|
||||||
.value_name("dir")
|
.value_name("dir")
|
||||||
.takes_value(true)
|
.value_delimiter(',')
|
||||||
.require_delimiter(true)
|
.value_parser(NonEmptyStringValueParser::new())
|
||||||
.multiple(true)
|
.action(ArgAction::Append)
|
||||||
.empty_values(false)
|
.help(
|
||||||
.help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
|
"A comma-separated list of directories to add to the crate \
|
||||||
|
search path when building tests",
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// test command implementation
|
// test command implementation
|
||||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
let library_paths: Vec<&str> = args
|
let library_paths: Vec<&str> = args
|
||||||
.values_of("library-path")
|
.get_many("library-path")
|
||||||
.map(|v| v.collect())
|
.map(|it| it.map(String::as_str).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let chapter: Option<&str> = args.get_one::<String>("chapter").map(|s| s.as_str());
|
||||||
|
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let mut book = MDBook::load(&book_dir)?;
|
let mut book = MDBook::load(book_dir)?;
|
||||||
|
|
||||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||||
book.config.build.build_dir = dest_dir.into();
|
book.config.build.build_dir = dest_dir.to_path_buf();
|
||||||
}
|
}
|
||||||
|
match chapter {
|
||||||
book.test(library_paths)?;
|
Some(_) => book.test_chapter(library_paths, chapter),
|
||||||
|
None => book.test(library_paths),
|
||||||
|
}?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
213
src/cmd/watch.rs
213
src/cmd/watch.rs
|
@ -1,41 +1,52 @@
|
||||||
extern crate notify;
|
use super::command_prelude::*;
|
||||||
|
use crate::{get_book_dir, open};
|
||||||
use self::notify::Watcher;
|
use ignore::gitignore::Gitignore;
|
||||||
use clap::{App, ArgMatches, SubCommand};
|
|
||||||
use mdbook::errors::Result;
|
use mdbook::errors::Result;
|
||||||
use mdbook::utils;
|
use mdbook::utils;
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
use std::path::Path;
|
use pathdiff::diff_paths;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::channel;
|
||||||
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use {get_book_dir, open};
|
|
||||||
|
|
||||||
// Create clap subcommand arguments
|
// Create clap subcommand arguments
|
||||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
pub fn make_subcommand() -> Command {
|
||||||
SubCommand::with_name("watch")
|
Command::new("watch")
|
||||||
.about("Watches a book's files and rebuilds it on changes")
|
.about("Watches a book's files and rebuilds it on changes")
|
||||||
.arg_from_usage(
|
.arg_dest_dir()
|
||||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
.arg_root_dir()
|
||||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
.arg_open()
|
||||||
).arg_from_usage(
|
|
||||||
"[dir] 'Root directory for the book{n}\
|
|
||||||
(Defaults to the Current Directory when omitted)'",
|
|
||||||
).arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch command implementation
|
// Watch command implementation
|
||||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let book = MDBook::load(&book_dir)?;
|
let mut book = MDBook::load(book_dir)?;
|
||||||
|
|
||||||
if args.is_present("open") {
|
let update_config = |book: &mut MDBook| {
|
||||||
|
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||||
|
book.config.build.build_dir = dest_dir.into();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
update_config(&mut book);
|
||||||
|
|
||||||
|
if args.get_flag("open") {
|
||||||
book.build()?;
|
book.build()?;
|
||||||
open(book.build_dir_for("html").join("index.html"));
|
let path = book.build_dir_for("html").join("index.html");
|
||||||
|
if !path.exists() {
|
||||||
|
error!("No chapter available to open");
|
||||||
|
std::process::exit(1)
|
||||||
|
}
|
||||||
|
open(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger_on_change(&book, |path, book_dir| {
|
trigger_on_change(&book, |paths, book_dir| {
|
||||||
info!("File changed: {:?}\nBuilding book...\n", path);
|
info!("Files changed: {:?}\nBuilding book...\n", paths);
|
||||||
let result = MDBook::load(&book_dir).and_then(|b| b.build());
|
let result = MDBook::load(book_dir).and_then(|mut b| {
|
||||||
|
update_config(&mut b);
|
||||||
|
b.build()
|
||||||
|
});
|
||||||
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
error!("Unable to build the book");
|
error!("Unable to build the book");
|
||||||
|
@ -46,45 +57,173 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||||
|
if paths.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
match find_gitignore(book_root) {
|
||||||
|
Some(gitignore_path) => {
|
||||||
|
let (ignore, err) = Gitignore::new(&gitignore_path);
|
||||||
|
if let Some(err) = err {
|
||||||
|
warn!(
|
||||||
|
"error reading gitignore `{}`: {err}",
|
||||||
|
gitignore_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
filter_ignored_files(ignore, paths)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// There is no .gitignore file.
|
||||||
|
paths.iter().map(|path| path.to_path_buf()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
|
||||||
|
book_root
|
||||||
|
.ancestors()
|
||||||
|
.map(|p| p.join(".gitignore"))
|
||||||
|
.find(|p| p.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The usage of `canonicalize` may encounter occasional failures on the Windows platform, presenting a potential risk.
|
||||||
|
// For more details, refer to [Pull Request #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981).
|
||||||
|
fn filter_ignored_files(ignore: Gitignore, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||||
|
let ignore_root = ignore
|
||||||
|
.path()
|
||||||
|
.canonicalize()
|
||||||
|
.expect("ignore root canonicalize error");
|
||||||
|
|
||||||
|
paths
|
||||||
|
.iter()
|
||||||
|
.filter(|path| {
|
||||||
|
let relative_path =
|
||||||
|
diff_paths(&path, &ignore_root).expect("One of the paths should be an absolute");
|
||||||
|
!ignore
|
||||||
|
.matched_path_or_any_parents(&relative_path, relative_path.is_dir())
|
||||||
|
.is_ignore()
|
||||||
|
})
|
||||||
|
.map(|path| path.to_path_buf())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Calls the closure when a book source file is changed, blocking indefinitely.
|
/// Calls the closure when a book source file is changed, blocking indefinitely.
|
||||||
pub fn trigger_on_change<F>(book: &MDBook, closure: F)
|
pub fn trigger_on_change<F>(book: &MDBook, closure: F)
|
||||||
where
|
where
|
||||||
F: Fn(&Path, &Path),
|
F: Fn(Vec<PathBuf>, &Path),
|
||||||
{
|
{
|
||||||
use self::notify::DebouncedEvent::*;
|
use notify::RecursiveMode::*;
|
||||||
use self::notify::RecursiveMode::*;
|
|
||||||
|
|
||||||
// Create a channel to receive the events.
|
// Create a channel to receive the events.
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
|
|
||||||
let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) {
|
let mut debouncer = match notify_debouncer_mini::new_debouncer(Duration::from_secs(1), tx) {
|
||||||
Ok(w) => w,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||||
::std::process::exit(1)
|
std::process::exit(1)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let watcher = debouncer.watcher();
|
||||||
|
|
||||||
// Add the source directory to the watcher
|
// Add the source directory to the watcher
|
||||||
if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
|
if let Err(e) = watcher.watch(&book.source_dir(), Recursive) {
|
||||||
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||||
::std::process::exit(1);
|
std::process::exit(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = watcher.watch(book.theme_dir(), Recursive);
|
let _ = watcher.watch(&book.theme_dir(), Recursive);
|
||||||
|
|
||||||
// Add the book.toml file to the watcher if it exists
|
// Add the book.toml file to the watcher if it exists
|
||||||
let _ = watcher.watch(book.root.join("book.toml"), NonRecursive);
|
let _ = watcher.watch(&book.root.join("book.toml"), NonRecursive);
|
||||||
|
|
||||||
|
for dir in &book.config.build.extra_watch_dirs {
|
||||||
|
let path = book.root.join(dir);
|
||||||
|
let canonical_path = path.canonicalize().unwrap_or_else(|e| {
|
||||||
|
error!("Error while watching extra directory {path:?}:\n {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Err(e) = watcher.watch(&canonical_path, Recursive) {
|
||||||
|
error!(
|
||||||
|
"Error while watching extra directory {:?}:\n {:?}",
|
||||||
|
canonical_path, e
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
info!("Listening for changes...");
|
info!("Listening for changes...");
|
||||||
|
|
||||||
for event in rx.iter() {
|
loop {
|
||||||
debug!("Received filesystem event: {:?}", event);
|
let first_event = rx.recv().unwrap();
|
||||||
match event {
|
sleep(Duration::from_millis(50));
|
||||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
|
let other_events = rx.try_iter();
|
||||||
closure(&path, &book.root);
|
|
||||||
|
let all_events = std::iter::once(first_event).chain(other_events);
|
||||||
|
|
||||||
|
let paths: Vec<_> = all_events
|
||||||
|
.filter_map(|event| match event {
|
||||||
|
Ok(events) => Some(events),
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("error while watching for changes: {error}");
|
||||||
|
None
|
||||||
}
|
}
|
||||||
_ => {}
|
})
|
||||||
|
.flatten()
|
||||||
|
.map(|event| event.path)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// If we are watching files outside the current repository (via extra-watch-dirs), then they are definitionally
|
||||||
|
// ignored by gitignore. So we handle this case by including such files into the watched paths list.
|
||||||
|
let any_external_paths = paths.iter().filter(|p| !p.starts_with(&book.root)).cloned();
|
||||||
|
let mut paths = remove_ignored_files(&book.root, &paths[..]);
|
||||||
|
paths.extend(any_external_paths);
|
||||||
|
|
||||||
|
if !paths.is_empty() {
|
||||||
|
closure(paths, &book.root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use ignore::gitignore::GitignoreBuilder;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_filter_ignored_files() {
|
||||||
|
let current_dir = env::current_dir().unwrap();
|
||||||
|
|
||||||
|
let ignore = GitignoreBuilder::new(¤t_dir)
|
||||||
|
.add_line(None, "*.html")
|
||||||
|
.unwrap()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let should_remain = current_dir.join("record.text");
|
||||||
|
let should_filter = current_dir.join("index.html");
|
||||||
|
|
||||||
|
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
|
||||||
|
assert_eq!(remain, vec![should_remain])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_ignored_files_should_handle_parent_dir() {
|
||||||
|
let current_dir = env::current_dir().unwrap();
|
||||||
|
|
||||||
|
let ignore = GitignoreBuilder::new(¤t_dir)
|
||||||
|
.add_line(None, "*.html")
|
||||||
|
.unwrap()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let parent_dir = current_dir.join("..");
|
||||||
|
let should_remain = parent_dir.join("record.text");
|
||||||
|
let should_filter = parent_dir.join("index.html");
|
||||||
|
|
||||||
|
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
|
||||||
|
assert_eq!(remain, vec![should_remain])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
812
src/config.rs
812
src/config.rs
File diff suppressed because it is too large
Load Diff
86
src/lib.rs
86
src/lib.rs
|
@ -75,37 +75,13 @@
|
||||||
//! directly, making deserializing the `RenderContext` easy and giving you
|
//! directly, making deserializing the `RenderContext` easy and giving you
|
||||||
//! access to the various methods for working with the [`Config`].
|
//! access to the various methods for working with the [`Config`].
|
||||||
//!
|
//!
|
||||||
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/
|
//! [user guide]: https://rust-lang.github.io/mdBook/
|
||||||
//! [`RenderContext`]: renderer/struct.RenderContext.html
|
//! [`RenderContext`]: renderer::RenderContext
|
||||||
//! [relevant chapter]: https://rust-lang-nursery.github.io/mdBook/for_developers/backends.html
|
//! [relevant chapter]: https://rust-lang.github.io/mdBook/for_developers/backends.html
|
||||||
//! [`Config`]: config/struct.Config.html
|
//! [`Config`]: config::Config
|
||||||
|
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
#![deny(rust_2018_idioms)]
|
||||||
#[macro_use]
|
|
||||||
extern crate error_chain;
|
|
||||||
extern crate handlebars;
|
|
||||||
extern crate itertools;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate lazy_static;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate log;
|
|
||||||
extern crate memchr;
|
|
||||||
extern crate pulldown_cmark;
|
|
||||||
extern crate regex;
|
|
||||||
extern crate serde;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_derive;
|
|
||||||
#[macro_use]
|
|
||||||
extern crate serde_json;
|
|
||||||
extern crate shlex;
|
|
||||||
extern crate tempfile;
|
|
||||||
extern crate toml;
|
|
||||||
extern crate toml_query;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[macro_use]
|
|
||||||
extern crate pretty_assertions;
|
|
||||||
|
|
||||||
pub mod book;
|
pub mod book;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
@ -120,53 +96,13 @@ pub mod utils;
|
||||||
/// compatibility checks.
|
/// compatibility checks.
|
||||||
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
pub use book::BookItem;
|
pub use crate::book::BookItem;
|
||||||
pub use book::MDBook;
|
pub use crate::book::MDBook;
|
||||||
pub use config::Config;
|
pub use crate::config::Config;
|
||||||
pub use renderer::Renderer;
|
pub use crate::renderer::Renderer;
|
||||||
|
|
||||||
/// The error types used through out this crate.
|
/// The error types used through out this crate.
|
||||||
pub mod errors {
|
pub mod errors {
|
||||||
use std::path::PathBuf;
|
pub(crate) use anyhow::{bail, ensure, Context};
|
||||||
|
pub use anyhow::{Error, Result};
|
||||||
error_chain!{
|
|
||||||
foreign_links {
|
|
||||||
Io(::std::io::Error) #[doc = "A wrapper around `std::io::Error`"];
|
|
||||||
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
|
|
||||||
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
|
|
||||||
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
|
|
||||||
SerdeJson(::serde_json::Error) #[doc = "JSON conversion failed"];
|
|
||||||
}
|
|
||||||
|
|
||||||
links {
|
|
||||||
TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind) #[doc = "A TomlQuery error"];
|
|
||||||
}
|
|
||||||
|
|
||||||
errors {
|
|
||||||
/// A subprocess exited with an unsuccessful return code.
|
|
||||||
Subprocess(message: String, output: ::std::process::Output) {
|
|
||||||
description("A subprocess failed")
|
|
||||||
display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An error was encountered while parsing the `SUMMARY.md` file.
|
|
||||||
ParseError(line: usize, col: usize, message: String) {
|
|
||||||
description("A SUMMARY.md parsing error")
|
|
||||||
display("Error at line {}, column {}: {}", line, col, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The user tried to use a reserved filename.
|
|
||||||
ReservedFilenameError(filename: PathBuf) {
|
|
||||||
description("Reserved Filename")
|
|
||||||
display("{} is reserved for internal use", filename.display())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Box to halve the size of Error
|
|
||||||
impl From<::handlebars::TemplateError> for Error {
|
|
||||||
fn from(e: ::handlebars::TemplateError) -> Error {
|
|
||||||
From::from(Box::new(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
110
src/main.rs
110
src/main.rs
|
@ -1,70 +1,97 @@
|
||||||
extern crate chrono;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate clap;
|
extern crate clap;
|
||||||
extern crate env_logger;
|
|
||||||
extern crate error_chain;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
extern crate mdbook;
|
|
||||||
extern crate open;
|
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use clap::{App, AppSettings, ArgMatches};
|
use clap::{Arg, ArgMatches, Command};
|
||||||
|
use clap_complete::Shell;
|
||||||
use env_logger::Builder;
|
use env_logger::Builder;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use mdbook::utils;
|
use mdbook::utils;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
mod cmd;
|
mod cmd;
|
||||||
|
|
||||||
const NAME: &'static str = "mdBook";
|
const VERSION: &str = concat!("v", crate_version!());
|
||||||
const VERSION: &'static str = concat!("v", crate_version!());
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
init_logger();
|
init_logger();
|
||||||
|
|
||||||
// Create a list of valid arguments and sub-commands
|
let command = create_clap_command();
|
||||||
let app = App::new(NAME)
|
|
||||||
.about("Creates a book from markdown files")
|
// Check which subcommand the user ran...
|
||||||
|
let res = match command.get_matches().subcommand() {
|
||||||
|
Some(("init", sub_matches)) => cmd::init::execute(sub_matches),
|
||||||
|
Some(("build", sub_matches)) => cmd::build::execute(sub_matches),
|
||||||
|
Some(("clean", sub_matches)) => cmd::clean::execute(sub_matches),
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
|
Some(("watch", sub_matches)) => cmd::watch::execute(sub_matches),
|
||||||
|
#[cfg(feature = "serve")]
|
||||||
|
Some(("serve", sub_matches)) => cmd::serve::execute(sub_matches),
|
||||||
|
Some(("test", sub_matches)) => cmd::test::execute(sub_matches),
|
||||||
|
Some(("completions", sub_matches)) => (|| {
|
||||||
|
let shell = sub_matches
|
||||||
|
.get_one::<Shell>("shell")
|
||||||
|
.ok_or_else(|| anyhow!("Shell name missing."))?;
|
||||||
|
|
||||||
|
let mut complete_app = create_clap_command();
|
||||||
|
clap_complete::generate(
|
||||||
|
*shell,
|
||||||
|
&mut complete_app,
|
||||||
|
"mdbook",
|
||||||
|
&mut std::io::stdout().lock(),
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
})(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
utils::log_backtrace(&e);
|
||||||
|
|
||||||
|
std::process::exit(101);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a list of valid arguments and sub-commands
|
||||||
|
fn create_clap_command() -> Command {
|
||||||
|
let app = Command::new(crate_name!())
|
||||||
|
.about(crate_description!())
|
||||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||||
.version(VERSION)
|
.version(VERSION)
|
||||||
.setting(AppSettings::GlobalVersion)
|
.propagate_version(true)
|
||||||
.setting(AppSettings::ArgRequiredElseHelp)
|
.arg_required_else_help(true)
|
||||||
.after_help(
|
.after_help(
|
||||||
"For more information about a specific command, try `mdbook <command> --help`\n\
|
"For more information about a specific command, try `mdbook <command> --help`\n\
|
||||||
The source code for mdBook is available at: https://github.com/rust-lang-nursery/mdBook",
|
The source code for mdBook is available at: https://github.com/rust-lang/mdBook",
|
||||||
)
|
)
|
||||||
.subcommand(cmd::init::make_subcommand())
|
.subcommand(cmd::init::make_subcommand())
|
||||||
.subcommand(cmd::build::make_subcommand())
|
.subcommand(cmd::build::make_subcommand())
|
||||||
.subcommand(cmd::test::make_subcommand())
|
.subcommand(cmd::test::make_subcommand())
|
||||||
.subcommand(cmd::clean::make_subcommand());
|
.subcommand(cmd::clean::make_subcommand())
|
||||||
|
.subcommand(
|
||||||
|
Command::new("completions")
|
||||||
|
.about("Generate shell completions for your shell to stdout")
|
||||||
|
.arg(
|
||||||
|
Arg::new("shell")
|
||||||
|
.value_parser(clap::value_parser!(Shell))
|
||||||
|
.help("the shell to generate completions for")
|
||||||
|
.value_name("SHELL")
|
||||||
|
.required(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
let app = app.subcommand(cmd::watch::make_subcommand());
|
let app = app.subcommand(cmd::watch::make_subcommand());
|
||||||
#[cfg(feature = "serve")]
|
#[cfg(feature = "serve")]
|
||||||
let app = app.subcommand(cmd::serve::make_subcommand());
|
let app = app.subcommand(cmd::serve::make_subcommand());
|
||||||
|
|
||||||
// Check which subcomamnd the user ran...
|
app
|
||||||
let res = match app.get_matches().subcommand() {
|
|
||||||
("init", Some(sub_matches)) => cmd::init::execute(sub_matches),
|
|
||||||
("build", Some(sub_matches)) => cmd::build::execute(sub_matches),
|
|
||||||
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches),
|
|
||||||
#[cfg(feature = "watch")]
|
|
||||||
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
|
|
||||||
#[cfg(feature = "serve")]
|
|
||||||
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches),
|
|
||||||
("test", Some(sub_matches)) => cmd::test::execute(sub_matches),
|
|
||||||
(_, _) => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = res {
|
|
||||||
utils::log_backtrace(&e);
|
|
||||||
|
|
||||||
::std::process::exit(101);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_logger() {
|
fn init_logger() {
|
||||||
|
@ -82,7 +109,7 @@ fn init_logger() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(var) = env::var("RUST_LOG") {
|
if let Ok(var) = env::var("RUST_LOG") {
|
||||||
builder.parse(&var);
|
builder.parse_filters(&var);
|
||||||
} else {
|
} else {
|
||||||
// if no RUST_LOG provided, default to logging at the Info level
|
// if no RUST_LOG provided, default to logging at the Info level
|
||||||
builder.filter(None, LevelFilter::Info);
|
builder.filter(None, LevelFilter::Info);
|
||||||
|
@ -94,11 +121,10 @@ fn init_logger() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||||
if let Some(dir) = args.value_of("dir") {
|
if let Some(p) = args.get_one::<PathBuf>("dir") {
|
||||||
// Check if path is relative from current dir, or absolute...
|
// Check if path is relative from current dir, or absolute...
|
||||||
let p = Path::new(dir);
|
|
||||||
if p.is_relative() {
|
if p.is_relative() {
|
||||||
env::current_dir().unwrap().join(dir)
|
env::current_dir().unwrap().join(p)
|
||||||
} else {
|
} else {
|
||||||
p.to_path_buf()
|
p.to_path_buf()
|
||||||
}
|
}
|
||||||
|
@ -108,7 +134,13 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open<P: AsRef<OsStr>>(path: P) {
|
fn open<P: AsRef<OsStr>>(path: P) {
|
||||||
if let Err(e) = open::that(path) {
|
info!("Opening web browser");
|
||||||
|
if let Err(e) = opener::open(path) {
|
||||||
error!("Error opening web browser: {}", e);
|
error!("Error opening web browser: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_app() {
|
||||||
|
create_clap_command().debug_assert();
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use super::{Preprocessor, PreprocessorContext};
|
use super::{Preprocessor, PreprocessorContext};
|
||||||
use book::Book;
|
use crate::book::Book;
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
use serde_json;
|
use log::{debug, trace, warn};
|
||||||
use shlex::Shlex;
|
use shlex::Shlex;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
|
@ -43,31 +43,27 @@ impl CmdPreprocessor {
|
||||||
|
|
||||||
/// A convenience function custom preprocessors can use to parse the input
|
/// A convenience function custom preprocessors can use to parse the input
|
||||||
/// written to `stdin` by a `CmdRenderer`.
|
/// written to `stdin` by a `CmdRenderer`.
|
||||||
pub fn parse_input<R: Read>(
|
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
|
||||||
reader: R,
|
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
|
||||||
) -> Result<(PreprocessorContext, Book)> {
|
|
||||||
serde_json::from_reader(reader)
|
|
||||||
.chain_err(|| "Unable to parse the input")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_input_to_child(
|
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
|
||||||
&self,
|
|
||||||
child: &mut Child,
|
|
||||||
book: &Book,
|
|
||||||
ctx: &PreprocessorContext,
|
|
||||||
) {
|
|
||||||
let stdin = child.stdin.take().expect("Child has stdin");
|
let stdin = child.stdin.take().expect("Child has stdin");
|
||||||
|
|
||||||
if let Err(e) = self.write_input(stdin, &book, &ctx) {
|
if let Err(e) = self.write_input(stdin, book, ctx) {
|
||||||
// Looks like the backend hung up before we could finish
|
// Looks like the backend hung up before we could finish
|
||||||
// sending it the render context. Log the error and keep going
|
// sending it the render context. Log the error and keep going
|
||||||
warn!("Error writing the RenderContext to the backend, {}", e);
|
warn!("Error writing the RenderContext to the backend, {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_input<W: Write>(&self, writer: W, book: &Book, ctx: &PreprocessorContext) -> Result<()> {
|
fn write_input<W: Write>(
|
||||||
serde_json::to_writer(writer, &(ctx, book))
|
&self,
|
||||||
.map_err(Into::into)
|
writer: W,
|
||||||
|
book: &Book,
|
||||||
|
ctx: &PreprocessorContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The command this `Preprocessor` will invoke.
|
/// The command this `Preprocessor` will invoke.
|
||||||
|
@ -105,27 +101,54 @@ impl Preprocessor for CmdPreprocessor {
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::inherit())
|
||||||
.spawn()
|
.spawn()
|
||||||
.chain_err(|| format!("Unable to start the \"{}\" preprocessor. Is it installed?", self.name()))?;
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Unable to start the \"{}\" preprocessor. Is it installed?",
|
||||||
|
self.name()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
self.write_input_to_child(&mut child, &book, ctx);
|
self.write_input_to_child(&mut child, &book, ctx);
|
||||||
|
|
||||||
let output = child
|
let output = child.wait_with_output().with_context(|| {
|
||||||
.wait_with_output()
|
format!(
|
||||||
.chain_err(|| "Error waiting for the preprocessor to complete")?;
|
"Error waiting for the \"{}\" preprocessor to complete",
|
||||||
|
self.name
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
trace!("{} exited with output: {:?}", self.cmd, output);
|
trace!("{} exited with output: {:?}", self.cmd, output);
|
||||||
ensure!(output.status.success(), "The preprocessor exited unsuccessfully");
|
ensure!(
|
||||||
|
output.status.success(),
|
||||||
|
format!(
|
||||||
|
"The \"{}\" preprocessor exited unsuccessfully with {} status",
|
||||||
|
self.name, output.status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
serde_json::from_slice(&output.stdout).chain_err(|| "Unable to parse the preprocessed book")
|
serde_json::from_slice(&output.stdout).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Unable to parse the preprocessed book from \"{}\" processor",
|
||||||
|
self.name
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||||
debug!("Checking if the \"{}\" preprocessor supports \"{}\"", self.name(), renderer);
|
debug!(
|
||||||
|
"Checking if the \"{}\" preprocessor supports \"{}\"",
|
||||||
|
self.name(),
|
||||||
|
renderer
|
||||||
|
);
|
||||||
|
|
||||||
let mut cmd = match self.command() {
|
let mut cmd = match self.command() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Unable to create the command for the \"{}\" preprocessor, {}", self.name(), e);
|
warn!(
|
||||||
|
"Unable to create the command for the \"{}\" preprocessor, {}",
|
||||||
|
self.name(),
|
||||||
|
e
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -152,3 +175,34 @@ impl Preprocessor for CmdPreprocessor {
|
||||||
outcome.unwrap_or(false)
|
outcome.unwrap_or(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::MDBook;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn guide() -> MDBook {
|
||||||
|
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
|
||||||
|
MDBook::load(example).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trip_write_and_parse_input() {
|
||||||
|
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
|
||||||
|
let md = guide();
|
||||||
|
let ctx = PreprocessorContext::new(
|
||||||
|
md.root.clone(),
|
||||||
|
md.config.clone(),
|
||||||
|
"some-renderer".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
|
||||||
|
|
||||||
|
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(got_book, md.book);
|
||||||
|
assert_eq!(got_ctx, ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use errors::*;
|
|
||||||
|
|
||||||
use super::{Preprocessor, PreprocessorContext};
|
use super::{Preprocessor, PreprocessorContext};
|
||||||
use book::{Book, BookItem};
|
use crate::book::{Book, BookItem};
|
||||||
|
use crate::errors::*;
|
||||||
|
use log::warn;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||||
/// `README.md` is the de facto index file in a markdown-based documentation.
|
/// `README.md` is the de facto index file in markdown-based documentation.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct IndexPreprocessor;
|
pub struct IndexPreprocessor;
|
||||||
|
|
||||||
impl IndexPreprocessor {
|
impl IndexPreprocessor {
|
||||||
|
@ -28,13 +30,15 @@ impl Preprocessor for IndexPreprocessor {
|
||||||
let source_dir = ctx.root.join(&ctx.config.book.src);
|
let source_dir = ctx.root.join(&ctx.config.book.src);
|
||||||
book.for_each_mut(|section: &mut BookItem| {
|
book.for_each_mut(|section: &mut BookItem| {
|
||||||
if let BookItem::Chapter(ref mut ch) = *section {
|
if let BookItem::Chapter(ref mut ch) = *section {
|
||||||
if is_readme_file(&ch.path) {
|
if let Some(ref mut path) = ch.path {
|
||||||
let index_md = source_dir.join(ch.path.with_file_name("index.md"));
|
if is_readme_file(&path) {
|
||||||
|
let mut index_md = source_dir.join(path.with_file_name("index.md"));
|
||||||
if index_md.exists() {
|
if index_md.exists() {
|
||||||
warn_readme_name_conflict(&ch.path, &index_md);
|
warn_readme_name_conflict(&path, &&mut index_md);
|
||||||
}
|
}
|
||||||
|
|
||||||
ch.path.set_file_name("index.md");
|
path.set_file_name("index.md");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -45,7 +49,10 @@ impl Preprocessor for IndexPreprocessor {
|
||||||
|
|
||||||
fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
||||||
let file_name = readme_path.as_ref().file_name().unwrap_or_default();
|
let file_name = readme_path.as_ref().file_name().unwrap_or_default();
|
||||||
let parent_dir = index_path.as_ref().parent().unwrap_or(index_path.as_ref());
|
let parent_dir = index_path
|
||||||
|
.as_ref()
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| index_path.as_ref());
|
||||||
warn!(
|
warn!(
|
||||||
"It seems that there are both {:?} and index.md under \"{}\".",
|
"It seems that there are both {:?} and index.md under \"{}\".",
|
||||||
file_name,
|
file_name,
|
||||||
|
@ -61,13 +68,12 @@ fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
|
fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
|
||||||
lazy_static! {
|
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)^readme$").unwrap());
|
||||||
static ref RE: Regex = Regex::new(r"(?i)^readme$").unwrap();
|
|
||||||
}
|
|
||||||
RE.is_match(
|
RE.is_match(
|
||||||
path.as_ref()
|
path.as_ref()
|
||||||
.file_stem()
|
.file_stem()
|
||||||
.and_then(|s| s.to_str())
|
.and_then(std::ffi::OsStr::to_str)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,32 @@
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
|
use crate::utils::{
|
||||||
|
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
||||||
|
take_rustdoc_include_lines,
|
||||||
|
};
|
||||||
use regex::{CaptureMatches, Captures, Regex};
|
use regex::{CaptureMatches, Captures, Regex};
|
||||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
use std::fs;
|
||||||
|
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use utils::fs::file_to_string;
|
|
||||||
use utils::take_lines;
|
|
||||||
|
|
||||||
use super::{Preprocessor, PreprocessorContext};
|
use super::{Preprocessor, PreprocessorContext};
|
||||||
use book::{Book, BookItem};
|
use crate::book::{Book, BookItem};
|
||||||
|
use log::{error, warn};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
const ESCAPE_CHAR: char = '\\';
|
const ESCAPE_CHAR: char = '\\';
|
||||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||||
|
|
||||||
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
|
/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
|
||||||
/// helpers in a chapter.
|
///
|
||||||
|
/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
|
||||||
|
///. lines, or only between the specified anchors.
|
||||||
|
/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
|
||||||
|
///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
|
||||||
|
/// This hides the lines from initial display but shows them when the reader expands the code
|
||||||
|
/// block and provides them to Rustdoc for testing.
|
||||||
|
/// - `{{# playground}}` - Insert runnable Rust files
|
||||||
|
/// - `{{# title}}` - Override \<title\> of a webpage.
|
||||||
|
#[derive(Default)]
|
||||||
pub struct LinkPreprocessor;
|
pub struct LinkPreprocessor;
|
||||||
|
|
||||||
impl LinkPreprocessor {
|
impl LinkPreprocessor {
|
||||||
|
@ -34,14 +48,22 @@ impl Preprocessor for LinkPreprocessor {
|
||||||
|
|
||||||
book.for_each_mut(|section: &mut BookItem| {
|
book.for_each_mut(|section: &mut BookItem| {
|
||||||
if let BookItem::Chapter(ref mut ch) = *section {
|
if let BookItem::Chapter(ref mut ch) = *section {
|
||||||
let base = ch
|
if let Some(ref chapter_path) = ch.path {
|
||||||
.path
|
let base = chapter_path
|
||||||
.parent()
|
.parent()
|
||||||
.map(|dir| src_dir.join(dir))
|
.map(|dir| src_dir.join(dir))
|
||||||
.expect("All book items have a parent");
|
.expect("All book items have a parent");
|
||||||
|
|
||||||
let content = replace_all(&ch.content, base, &ch.path, 0);
|
let mut chapter_title = ch.name.clone();
|
||||||
|
let content =
|
||||||
|
replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title);
|
||||||
ch.content = content;
|
ch.content = content;
|
||||||
|
if chapter_title != ch.name {
|
||||||
|
ctx.chapter_titles
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(chapter_path.clone(), chapter_title);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,7 +71,13 @@ impl Preprocessor for LinkPreprocessor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_all<P1, P2>(s: &str, path: P1, source: P2, depth: usize) -> String
|
fn replace_all<P1, P2>(
|
||||||
|
s: &str,
|
||||||
|
path: P1,
|
||||||
|
source: P2,
|
||||||
|
depth: usize,
|
||||||
|
chapter_title: &mut String,
|
||||||
|
) -> String
|
||||||
where
|
where
|
||||||
P1: AsRef<Path>,
|
P1: AsRef<Path>,
|
||||||
P2: AsRef<Path>,
|
P2: AsRef<Path>,
|
||||||
|
@ -62,18 +90,19 @@ where
|
||||||
let mut previous_end_index = 0;
|
let mut previous_end_index = 0;
|
||||||
let mut replaced = String::new();
|
let mut replaced = String::new();
|
||||||
|
|
||||||
for playpen in find_links(s) {
|
for link in find_links(s) {
|
||||||
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
replaced.push_str(&s[previous_end_index..link.start_index]);
|
||||||
|
|
||||||
match playpen.render_with_path(&path) {
|
match link.render_with_path(path, chapter_title) {
|
||||||
Ok(new_content) => {
|
Ok(new_content) => {
|
||||||
if depth < MAX_LINK_NESTED_DEPTH {
|
if depth < MAX_LINK_NESTED_DEPTH {
|
||||||
if let Some(rel_path) = playpen.link.relative_path(path) {
|
if let Some(rel_path) = link.link_type.relative_path(path) {
|
||||||
replaced.push_str(&replace_all(
|
replaced.push_str(&replace_all(
|
||||||
&new_content,
|
&new_content,
|
||||||
rel_path,
|
rel_path,
|
||||||
source,
|
source,
|
||||||
depth + 1,
|
depth + 1,
|
||||||
|
chapter_title,
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
replaced.push_str(&new_content);
|
replaced.push_str(&new_content);
|
||||||
|
@ -84,17 +113,17 @@ where
|
||||||
source.display()
|
source.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
previous_end_index = playpen.end_index;
|
previous_end_index = link.end_index;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error updating \"{}\", {}", playpen.link_text, e);
|
error!("Error updating \"{}\", {}", link.link_text, e);
|
||||||
for cause in e.iter().skip(1) {
|
for cause in e.chain().skip(1) {
|
||||||
warn!("Caused By: {}", cause);
|
warn!("Caused By: {}", cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should make sure we include the raw `{{# ... }}` snippet
|
// This should make sure we include the raw `{{# ... }}` snippet
|
||||||
// in the page content if there are any errors.
|
// in the page content if there are any errors.
|
||||||
previous_end_index = playpen.start_index;
|
previous_end_index = link.start_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,11 +135,70 @@ where
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
enum LinkType<'a> {
|
enum LinkType<'a> {
|
||||||
Escaped,
|
Escaped,
|
||||||
IncludeRange(PathBuf, Range<usize>),
|
Include(PathBuf, RangeOrAnchor),
|
||||||
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
|
Playground(PathBuf, Vec<&'a str>),
|
||||||
IncludeRangeTo(PathBuf, RangeTo<usize>),
|
RustdocInclude(PathBuf, RangeOrAnchor),
|
||||||
IncludeRangeFull(PathBuf, RangeFull),
|
Title(&'a str),
|
||||||
Playpen(PathBuf, Vec<&'a str>),
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
enum RangeOrAnchor {
|
||||||
|
Range(LineRange),
|
||||||
|
Anchor(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// A range of lines specified with some include directive.
|
||||||
|
#[allow(clippy::enum_variant_names)] // The prefix can't be removed, and is meant to mirror the contained type
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
enum LineRange {
|
||||||
|
Range(Range<usize>),
|
||||||
|
RangeFrom(RangeFrom<usize>),
|
||||||
|
RangeTo(RangeTo<usize>),
|
||||||
|
RangeFull(RangeFull),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RangeBounds<usize> for LineRange {
|
||||||
|
fn start_bound(&self) -> Bound<&usize> {
|
||||||
|
match self {
|
||||||
|
LineRange::Range(r) => r.start_bound(),
|
||||||
|
LineRange::RangeFrom(r) => r.start_bound(),
|
||||||
|
LineRange::RangeTo(r) => r.start_bound(),
|
||||||
|
LineRange::RangeFull(r) => r.start_bound(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_bound(&self) -> Bound<&usize> {
|
||||||
|
match self {
|
||||||
|
LineRange::Range(r) => r.end_bound(),
|
||||||
|
LineRange::RangeFrom(r) => r.end_bound(),
|
||||||
|
LineRange::RangeTo(r) => r.end_bound(),
|
||||||
|
LineRange::RangeFull(r) => r.end_bound(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Range<usize>> for LineRange {
|
||||||
|
fn from(r: Range<usize>) -> LineRange {
|
||||||
|
LineRange::Range(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RangeFrom<usize>> for LineRange {
|
||||||
|
fn from(r: RangeFrom<usize>) -> LineRange {
|
||||||
|
LineRange::RangeFrom(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RangeTo<usize>> for LineRange {
|
||||||
|
fn from(r: RangeTo<usize>) -> LineRange {
|
||||||
|
LineRange::RangeTo(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RangeFull> for LineRange {
|
||||||
|
fn from(r: RangeFull) -> LineRange {
|
||||||
|
LineRange::RangeFull(r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> LinkType<'a> {
|
impl<'a> LinkType<'a> {
|
||||||
|
@ -118,19 +206,10 @@ impl<'a> LinkType<'a> {
|
||||||
let base = base.as_ref();
|
let base = base.as_ref();
|
||||||
match self {
|
match self {
|
||||||
LinkType::Escaped => None,
|
LinkType::Escaped => None,
|
||||||
LinkType::IncludeRange(p, _) => {
|
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
|
||||||
Some(return_relative_path(base, &p))
|
LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
|
||||||
}
|
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
|
||||||
LinkType::IncludeRangeFrom(p, _) => {
|
LinkType::Title(_) => None,
|
||||||
Some(return_relative_path(base, &p))
|
|
||||||
}
|
|
||||||
LinkType::IncludeRangeTo(p, _) => {
|
|
||||||
Some(return_relative_path(base, &p))
|
|
||||||
}
|
|
||||||
LinkType::IncludeRangeFull(p, _) => {
|
|
||||||
Some(return_relative_path(base, &p))
|
|
||||||
}
|
|
||||||
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,56 +221,68 @@ fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
|
||||||
.to_path_buf()
|
.to_path_buf()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_include_path(path: &str) -> LinkType<'static> {
|
fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor {
|
||||||
let mut parts = path.split(':');
|
let mut parts = parts.unwrap_or("").splitn(3, ':').fuse();
|
||||||
let path = parts.next().unwrap().into();
|
|
||||||
|
let next_element = parts.next();
|
||||||
|
let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
|
||||||
// subtract 1 since line numbers usually begin with 1
|
// subtract 1 since line numbers usually begin with 1
|
||||||
let start = parts
|
Some(value.saturating_sub(1))
|
||||||
.next()
|
} else if let Some("") = next_element {
|
||||||
.and_then(|s| s.parse::<usize>().ok())
|
None
|
||||||
.map(|val| val.saturating_sub(1));
|
} else if let Some(anchor) = next_element {
|
||||||
let end = parts.next();
|
return RangeOrAnchor::Anchor(String::from(anchor));
|
||||||
let has_end = end.is_some();
|
|
||||||
let end = end.and_then(|s| s.parse::<usize>().ok());
|
|
||||||
match start {
|
|
||||||
Some(start) => match end {
|
|
||||||
Some(end) => LinkType::IncludeRange(
|
|
||||||
path,
|
|
||||||
Range {
|
|
||||||
start: start,
|
|
||||||
end: end,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
None => if has_end {
|
|
||||||
LinkType::IncludeRangeFrom(path, RangeFrom { start: start })
|
|
||||||
} else {
|
} else {
|
||||||
LinkType::IncludeRange(
|
None
|
||||||
path,
|
};
|
||||||
Range {
|
|
||||||
start: start,
|
let end = parts.next();
|
||||||
end: start + 1,
|
// If `end` is empty string or any other value that can't be parsed as a usize, treat this
|
||||||
},
|
// include as a range with only a start bound. However, if end isn't specified, include only
|
||||||
)
|
// the single line specified by `start`.
|
||||||
},
|
let end = end.map(|s| s.parse::<usize>());
|
||||||
},
|
|
||||||
None => match end {
|
match (start, end) {
|
||||||
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end: end }),
|
(Some(start), Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(start..end)),
|
||||||
None => LinkType::IncludeRangeFull(path, RangeFull),
|
(Some(start), Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(start..)),
|
||||||
},
|
(Some(start), None) => RangeOrAnchor::Range(LineRange::from(start..start + 1)),
|
||||||
|
(None, Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(..end)),
|
||||||
|
(None, None) | (None, Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(RangeFull)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||||
|
let mut parts = path.splitn(2, ':');
|
||||||
|
|
||||||
|
let path = parts.next().unwrap().into();
|
||||||
|
let range_or_anchor = parse_range_or_anchor(parts.next());
|
||||||
|
|
||||||
|
LinkType::Include(path, range_or_anchor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_rustdoc_include_path(path: &str) -> LinkType<'static> {
|
||||||
|
let mut parts = path.splitn(2, ':');
|
||||||
|
|
||||||
|
let path = parts.next().unwrap().into();
|
||||||
|
let range_or_anchor = parse_range_or_anchor(parts.next());
|
||||||
|
|
||||||
|
LinkType::RustdocInclude(path, range_or_anchor)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
struct Link<'a> {
|
struct Link<'a> {
|
||||||
start_index: usize,
|
start_index: usize,
|
||||||
end_index: usize,
|
end_index: usize,
|
||||||
link: LinkType<'a>,
|
link_type: LinkType<'a>,
|
||||||
link_text: &'a str,
|
link_text: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Link<'a> {
|
impl<'a> Link<'a> {
|
||||||
fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
|
fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
|
||||||
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
|
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
|
||||||
|
(_, Some(typ), Some(title)) if typ.as_str() == "title" => {
|
||||||
|
Some(LinkType::Title(title.as_str()))
|
||||||
|
}
|
||||||
(_, Some(typ), Some(rest)) => {
|
(_, Some(typ), Some(rest)) => {
|
||||||
let mut path_props = rest.as_str().split_whitespace();
|
let mut path_props = rest.as_str().split_whitespace();
|
||||||
let file_arg = path_props.next();
|
let file_arg = path_props.next();
|
||||||
|
@ -199,41 +290,53 @@ impl<'a> Link<'a> {
|
||||||
|
|
||||||
match (typ.as_str(), file_arg) {
|
match (typ.as_str(), file_arg) {
|
||||||
("include", Some(pth)) => Some(parse_include_path(pth)),
|
("include", Some(pth)) => Some(parse_include_path(pth)),
|
||||||
|
("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)),
|
||||||
("playpen", Some(pth)) => {
|
("playpen", Some(pth)) => {
|
||||||
Some(LinkType::Playpen(pth.into(), props))
|
warn!(
|
||||||
|
"the {{{{#playpen}}}} expression has been \
|
||||||
|
renamed to {{{{#playground}}}}, \
|
||||||
|
please update your book to use the new name"
|
||||||
|
);
|
||||||
|
Some(LinkType::Playground(pth.into(), props))
|
||||||
}
|
}
|
||||||
|
("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Some(mat), None, None)
|
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
|
||||||
if mat.as_str().starts_with(ESCAPE_CHAR) =>
|
|
||||||
{
|
|
||||||
Some(LinkType::Escaped)
|
Some(LinkType::Escaped)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
link_type.and_then(|lnk| {
|
link_type.and_then(|lnk_type| {
|
||||||
cap.get(0).map(|mat| Link {
|
cap.get(0).map(|mat| Link {
|
||||||
start_index: mat.start(),
|
start_index: mat.start(),
|
||||||
end_index: mat.end(),
|
end_index: mat.end(),
|
||||||
link: lnk,
|
link_type: lnk_type,
|
||||||
link_text: mat.as_str(),
|
link_text: mat.as_str(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
|
fn render_with_path<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
base: P,
|
||||||
|
chapter_title: &mut String,
|
||||||
|
) -> Result<String> {
|
||||||
let base = base.as_ref();
|
let base = base.as_ref();
|
||||||
match self.link {
|
match self.link_type {
|
||||||
// omit the escape char
|
// omit the escape char
|
||||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
LinkType::Escaped => Ok(self.link_text[1..].to_owned()),
|
||||||
LinkType::IncludeRange(ref pat, ref range) => {
|
LinkType::Include(ref pat, ref range_or_anchor) => {
|
||||||
let target = base.join(pat);
|
let target = base.join(pat);
|
||||||
|
|
||||||
file_to_string(&target)
|
fs::read_to_string(&target)
|
||||||
.map(|s| take_lines(&s, range.clone()))
|
.map(|s| match range_or_anchor {
|
||||||
.chain_err(|| {
|
RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
|
||||||
|
RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
|
||||||
|
})
|
||||||
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Could not read file for link {} ({})",
|
"Could not read file for link {} ({})",
|
||||||
self.link_text,
|
self.link_text,
|
||||||
|
@ -241,12 +344,19 @@ impl<'a> Link<'a> {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
LinkType::IncludeRangeFrom(ref pat, ref range) => {
|
LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
|
||||||
let target = base.join(pat);
|
let target = base.join(pat);
|
||||||
|
|
||||||
file_to_string(&target)
|
fs::read_to_string(&target)
|
||||||
.map(|s| take_lines(&s, range.clone()))
|
.map(|s| match range_or_anchor {
|
||||||
.chain_err(|| {
|
RangeOrAnchor::Range(range) => {
|
||||||
|
take_rustdoc_include_lines(&s, range.clone())
|
||||||
|
}
|
||||||
|
RangeOrAnchor::Anchor(anchor) => {
|
||||||
|
take_rustdoc_include_anchored_lines(&s, anchor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Could not read file for link {} ({})",
|
"Could not read file for link {} ({})",
|
||||||
self.link_text,
|
self.link_text,
|
||||||
|
@ -254,33 +364,10 @@ impl<'a> Link<'a> {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
LinkType::IncludeRangeTo(ref pat, ref range) => {
|
LinkType::Playground(ref pat, ref attrs) => {
|
||||||
let target = base.join(pat);
|
let target = base.join(pat);
|
||||||
|
|
||||||
file_to_string(&target)
|
let mut contents = fs::read_to_string(&target).with_context(|| {
|
||||||
.map(|s| take_lines(&s, range.clone()))
|
|
||||||
.chain_err(|| {
|
|
||||||
format!(
|
|
||||||
"Could not read file for link {} ({})",
|
|
||||||
self.link_text,
|
|
||||||
target.display(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
LinkType::IncludeRangeFull(ref pat, _) => {
|
|
||||||
let target = base.join(pat);
|
|
||||||
|
|
||||||
file_to_string(&target).chain_err(|| {
|
|
||||||
format!("Could not read file for link {} ({})",
|
|
||||||
self.link_text,
|
|
||||||
target.display())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
LinkType::Playpen(ref pat, ref attrs) => {
|
|
||||||
let target = base.join(pat);
|
|
||||||
|
|
||||||
let contents =
|
|
||||||
file_to_string(&target).chain_err(|| {
|
|
||||||
format!(
|
format!(
|
||||||
"Could not read file for link {} ({})",
|
"Could not read file for link {} ({})",
|
||||||
self.link_text,
|
self.link_text,
|
||||||
|
@ -288,13 +375,20 @@ impl<'a> Link<'a> {
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
||||||
|
if !contents.ends_with('\n') {
|
||||||
|
contents.push('\n');
|
||||||
|
}
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"```{}{}\n{}\n```\n",
|
"```{}{}\n{}```\n",
|
||||||
ftype,
|
ftype,
|
||||||
attrs.join(","),
|
attrs.join(","),
|
||||||
contents
|
contents
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
LinkType::Title(title) => {
|
||||||
|
*chapter_title = title.to_owned();
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,21 +407,23 @@ impl<'a> Iterator for LinkIter<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_links(contents: &str) -> LinkIter {
|
fn find_links(contents: &str) -> LinkIter<'_> {
|
||||||
// lazily compute following regex
|
// lazily compute following regex
|
||||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
|
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([^}]+)\}\}")?;
|
||||||
lazy_static! {
|
static RE: Lazy<Regex> = Lazy::new(|| {
|
||||||
static ref RE: Regex = Regex::new(
|
Regex::new(
|
||||||
r"(?x) # insignificant whitespace mode
|
r"(?x) # insignificant whitespace mode
|
||||||
\\\{\{\#.*\}\} # match escaped link
|
\\\{\{\#.*\}\} # match escaped link
|
||||||
| # or
|
| # or
|
||||||
\{\{\s* # link opening parens and whitespace
|
\{\{\s* # link opening parens and whitespace
|
||||||
\#([a-zA-Z0-9]+) # link type
|
\#([a-zA-Z0-9_]+) # link type
|
||||||
\s+ # separating whitespace
|
\s+ # separating whitespace
|
||||||
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
|
([^}]+) # link target path and space separated properties
|
||||||
\s*\}\} # whitespace and link closing parens"
|
\}\} # link closing parens",
|
||||||
).unwrap();
|
)
|
||||||
}
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
LinkIter(RE.captures_iter(contents))
|
LinkIter(RE.captures_iter(contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,7 +443,21 @@ mod tests {
|
||||||
```hbs
|
```hbs
|
||||||
{{#include file.rs}} << an escaped link!
|
{{#include file.rs}} << an escaped link!
|
||||||
```";
|
```";
|
||||||
assert_eq!(replace_all(start, "", "", 0), end);
|
let mut chapter_title = "test_replace_all_escaped".to_owned();
|
||||||
|
assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_chapter_title() {
|
||||||
|
let start = r"{{#title My Title}}
|
||||||
|
# My Chapter
|
||||||
|
";
|
||||||
|
let end = r"
|
||||||
|
# My Chapter
|
||||||
|
";
|
||||||
|
let mut chapter_title = "test_set_chapter_title".to_owned();
|
||||||
|
assert_eq!(replace_all(start, "", "", 0, &mut chapter_title), end);
|
||||||
|
assert_eq!(chapter_title, "My Title");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -358,7 +468,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_links_partial_link() {
|
fn test_find_links_partial_link() {
|
||||||
let s = "Some random text with {{#playpen...";
|
let s = "Some random text with {{#playground...";
|
||||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||||
let s = "Some random text with {{#include...";
|
let s = "Some random text with {{#include...";
|
||||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||||
|
@ -368,19 +478,19 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_links_empty_link() {
|
fn test_find_links_empty_link() {
|
||||||
let s = "Some random text with {{#playpen}} and {{#playpen }} {{}} {{#}}...";
|
let s = "Some random text with {{#playground}} and {{#playground }} {{}} {{#}}...";
|
||||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_links_unknown_link_type() {
|
fn test_find_links_unknown_link_type() {
|
||||||
let s = "Some random text with {{#playpenz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
|
let s = "Some random text with {{#playgroundz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
|
||||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_links_simple_link() {
|
fn test_find_links_simple_link() {
|
||||||
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
|
let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
|
||||||
|
|
||||||
let res = find_links(s).collect::<Vec<_>>();
|
let res = find_links(s).collect::<Vec<_>>();
|
||||||
println!("\nOUTPUT: {:?}\n", res);
|
println!("\nOUTPUT: {:?}\n", res);
|
||||||
|
@ -390,20 +500,38 @@ mod tests {
|
||||||
vec![
|
vec![
|
||||||
Link {
|
Link {
|
||||||
start_index: 22,
|
start_index: 22,
|
||||||
end_index: 42,
|
end_index: 45,
|
||||||
link: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
|
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec![]),
|
||||||
link_text: "{{#playpen file.rs}}",
|
link_text: "{{#playground file.rs}}",
|
||||||
},
|
},
|
||||||
Link {
|
Link {
|
||||||
start_index: 47,
|
start_index: 50,
|
||||||
end_index: 68,
|
end_index: 74,
|
||||||
link: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
|
link_type: LinkType::Playground(PathBuf::from("test.rs"), vec![]),
|
||||||
link_text: "{{#playpen test.rs }}",
|
link_text: "{{#playground test.rs }}",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_links_with_special_characters() {
|
||||||
|
let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
|
||||||
|
|
||||||
|
let res = find_links(s).collect::<Vec<_>>();
|
||||||
|
println!("\nOUTPUT: {:?}\n", res);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![Link {
|
||||||
|
start_index: 22,
|
||||||
|
end_index: 57,
|
||||||
|
link_type: LinkType::Playground(PathBuf::from("foo-bar\\baz/_c++.rs"), vec![]),
|
||||||
|
link_text: "{{#playground foo-bar\\baz/_c++.rs}}",
|
||||||
|
},]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_links_with_range() {
|
fn test_find_links_with_range() {
|
||||||
let s = "Some random text with {{#include file.rs:10:20}}...";
|
let s = "Some random text with {{#include file.rs:10:20}}...";
|
||||||
|
@ -414,7 +542,10 @@ mod tests {
|
||||||
vec![Link {
|
vec![Link {
|
||||||
start_index: 22,
|
start_index: 22,
|
||||||
end_index: 48,
|
end_index: 48,
|
||||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
|
link_type: LinkType::Include(
|
||||||
|
PathBuf::from("file.rs"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(9..20))
|
||||||
|
),
|
||||||
link_text: "{{#include file.rs:10:20}}",
|
link_text: "{{#include file.rs:10:20}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -430,7 +561,10 @@ mod tests {
|
||||||
vec![Link {
|
vec![Link {
|
||||||
start_index: 22,
|
start_index: 22,
|
||||||
end_index: 45,
|
end_index: 45,
|
||||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
|
link_type: LinkType::Include(
|
||||||
|
PathBuf::from("file.rs"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(9..10))
|
||||||
|
),
|
||||||
link_text: "{{#include file.rs:10}}",
|
link_text: "{{#include file.rs:10}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -446,7 +580,10 @@ mod tests {
|
||||||
vec![Link {
|
vec![Link {
|
||||||
start_index: 22,
|
start_index: 22,
|
||||||
end_index: 46,
|
end_index: 46,
|
||||||
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
|
link_type: LinkType::Include(
|
||||||
|
PathBuf::from("file.rs"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(9..))
|
||||||
|
),
|
||||||
link_text: "{{#include file.rs:10:}}",
|
link_text: "{{#include file.rs:10:}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -462,7 +599,10 @@ mod tests {
|
||||||
vec![Link {
|
vec![Link {
|
||||||
start_index: 22,
|
start_index: 22,
|
||||||
end_index: 46,
|
end_index: 46,
|
||||||
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
link_type: LinkType::Include(
|
||||||
|
PathBuf::from("file.rs"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(..20))
|
||||||
|
),
|
||||||
link_text: "{{#include file.rs::20}}",
|
link_text: "{{#include file.rs::20}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -478,7 +618,10 @@ mod tests {
|
||||||
vec![Link {
|
vec![Link {
|
||||||
start_index: 22,
|
start_index: 22,
|
||||||
end_index: 44,
|
end_index: 44,
|
||||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
link_type: LinkType::Include(
|
||||||
|
PathBuf::from("file.rs"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(..))
|
||||||
|
),
|
||||||
link_text: "{{#include file.rs::}}",
|
link_text: "{{#include file.rs::}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -494,15 +637,37 @@ mod tests {
|
||||||
vec![Link {
|
vec![Link {
|
||||||
start_index: 22,
|
start_index: 22,
|
||||||
end_index: 42,
|
end_index: 42,
|
||||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
link_type: LinkType::Include(
|
||||||
|
PathBuf::from("file.rs"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(..))
|
||||||
|
),
|
||||||
link_text: "{{#include file.rs}}",
|
link_text: "{{#include file.rs}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_links_with_anchor() {
|
||||||
|
let s = "Some random text with {{#include file.rs:anchor}}...";
|
||||||
|
let res = find_links(s).collect::<Vec<_>>();
|
||||||
|
println!("\nOUTPUT: {:?}\n", res);
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
vec![Link {
|
||||||
|
start_index: 22,
|
||||||
|
end_index: 49,
|
||||||
|
link_type: LinkType::Include(
|
||||||
|
PathBuf::from("file.rs"),
|
||||||
|
RangeOrAnchor::Anchor(String::from("anchor"))
|
||||||
|
),
|
||||||
|
link_text: "{{#include file.rs:anchor}}",
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_links_escaped_link() {
|
fn test_find_links_escaped_link() {
|
||||||
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
|
||||||
|
|
||||||
let res = find_links(s).collect::<Vec<_>>();
|
let res = find_links(s).collect::<Vec<_>>();
|
||||||
println!("\nOUTPUT: {:?}\n", res);
|
println!("\nOUTPUT: {:?}\n", res);
|
||||||
|
@ -510,18 +675,19 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
res,
|
res,
|
||||||
vec![Link {
|
vec![Link {
|
||||||
start_index: 38,
|
start_index: 41,
|
||||||
end_index: 68,
|
end_index: 74,
|
||||||
link: LinkType::Escaped,
|
link_type: LinkType::Escaped,
|
||||||
link_text: "\\{{#playpen file.rs editable}}",
|
link_text: "\\{{#playground file.rs editable}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_playpens_with_properties() {
|
fn test_find_playgrounds_with_properties() {
|
||||||
let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some \
|
let s =
|
||||||
more\n text {{#playpen my.rs editable no_run should_panic}} ...";
|
"Some random text with escaped playground {{#playground file.rs editable }} and some \
|
||||||
|
more\n text {{#playground my.rs editable no_run should_panic}} ...";
|
||||||
|
|
||||||
let res = find_links(s).collect::<Vec<_>>();
|
let res = find_links(s).collect::<Vec<_>>();
|
||||||
println!("\nOUTPUT: {:?}\n", res);
|
println!("\nOUTPUT: {:?}\n", res);
|
||||||
|
@ -529,23 +695,19 @@ mod tests {
|
||||||
res,
|
res,
|
||||||
vec![
|
vec![
|
||||||
Link {
|
Link {
|
||||||
start_index: 38,
|
start_index: 41,
|
||||||
end_index: 68,
|
end_index: 74,
|
||||||
link: LinkType::Playpen(
|
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec!["editable"]),
|
||||||
PathBuf::from("file.rs"),
|
link_text: "{{#playground file.rs editable }}",
|
||||||
vec!["editable"]
|
|
||||||
),
|
|
||||||
link_text: "{{#playpen file.rs editable }}",
|
|
||||||
},
|
},
|
||||||
Link {
|
Link {
|
||||||
start_index: 89,
|
start_index: 95,
|
||||||
end_index: 136,
|
end_index: 145,
|
||||||
link: LinkType::Playpen(
|
link_type: LinkType::Playground(
|
||||||
PathBuf::from("my.rs"),
|
PathBuf::from("my.rs"),
|
||||||
vec!["editable", "no_run", "should_panic"],
|
vec!["editable", "no_run", "should_panic"],
|
||||||
),
|
),
|
||||||
link_text:
|
link_text: "{{#playground my.rs editable no_run should_panic}}",
|
||||||
"{{#playpen my.rs editable no_run should_panic}}",
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -553,8 +715,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_all_link_types() {
|
fn test_find_all_link_types() {
|
||||||
let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are \
|
let s =
|
||||||
insignifficant in escaped link}} some more\n text {{#playpen my.rs editable \
|
"Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
|
||||||
|
insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
|
||||||
no_run should_panic}} ...";
|
no_run should_panic}} ...";
|
||||||
|
|
||||||
let res = find_links(s).collect::<Vec<_>>();
|
let res = find_links(s).collect::<Vec<_>>();
|
||||||
|
@ -563,33 +726,215 @@ mod tests {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
res[0],
|
res[0],
|
||||||
Link {
|
Link {
|
||||||
start_index: 38,
|
start_index: 41,
|
||||||
end_index: 58,
|
end_index: 61,
|
||||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
link_type: LinkType::Include(
|
||||||
|
PathBuf::from("file.rs"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(..))
|
||||||
|
),
|
||||||
link_text: "{{#include file.rs}}",
|
link_text: "{{#include file.rs}}",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
res[1],
|
res[1],
|
||||||
Link {
|
Link {
|
||||||
start_index: 63,
|
start_index: 66,
|
||||||
end_index: 112,
|
end_index: 115,
|
||||||
link: LinkType::Escaped,
|
link_type: LinkType::Escaped,
|
||||||
link_text: "\\{{#contents are insignifficant in escaped link}}",
|
link_text: "\\{{#contents are insignifficant in escaped link}}",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
res[2],
|
res[2],
|
||||||
Link {
|
Link {
|
||||||
start_index: 130,
|
start_index: 133,
|
||||||
end_index: 177,
|
end_index: 183,
|
||||||
link: LinkType::Playpen(
|
link_type: LinkType::Playground(
|
||||||
PathBuf::from("my.rs"),
|
PathBuf::from("my.rs"),
|
||||||
vec!["editable", "no_run", "should_panic"]
|
vec!["editable", "no_run", "should_panic"]
|
||||||
),
|
),
|
||||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
link_text: "{{#playground my.rs editable no_run should_panic}}",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_without_colon_includes_all() {
|
||||||
|
let link_type = parse_include_path("arbitrary");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_nothing_after_colon_includes_all() {
|
||||||
|
let link_type = parse_include_path("arbitrary:");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_two_colons_includes_all() {
|
||||||
|
let link_type = parse_include_path("arbitrary::");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_garbage_after_two_colons_includes_all() {
|
||||||
|
let link_type = parse_include_path("arbitrary::NaN");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_one_number_after_colon_only_that_line() {
|
||||||
|
let link_type = parse_include_path("arbitrary:5");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(4..5))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_one_based_start_becomes_zero_based() {
|
||||||
|
let link_type = parse_include_path("arbitrary:1");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(0..1))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_zero_based_start_stays_zero_based_but_is_probably_an_error() {
|
||||||
|
let link_type = parse_include_path("arbitrary:0");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(0..1))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_start_only_range() {
|
||||||
|
let link_type = parse_include_path("arbitrary:5:");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(4..))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_start_with_garbage_interpreted_as_start_only_range() {
|
||||||
|
let link_type = parse_include_path("arbitrary:5:NaN");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(4..))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_end_only_range() {
|
||||||
|
let link_type = parse_include_path("arbitrary::5");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(..5))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_start_and_end_range() {
|
||||||
|
let link_type = parse_include_path("arbitrary:5:10");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(4..10))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_negative_interpreted_as_anchor() {
|
||||||
|
let link_type = parse_include_path("arbitrary:-5");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Anchor("-5".to_string())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_floating_point_interpreted_as_anchor() {
|
||||||
|
let link_type = parse_include_path("arbitrary:-5.7");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Anchor("-5.7".to_string())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_anchor_followed_by_colon() {
|
||||||
|
let link_type = parse_include_path("arbitrary:some-anchor:this-gets-ignored");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Anchor("some-anchor".to_string())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_with_more_than_three_colons_ignores_everything_after_third_colon() {
|
||||||
|
let link_type = parse_include_path("arbitrary:5:10:17:anything:");
|
||||||
|
assert_eq!(
|
||||||
|
link_type,
|
||||||
|
LinkType::Include(
|
||||||
|
PathBuf::from("arbitrary"),
|
||||||
|
RangeOrAnchor::Range(LineRange::from(4..10))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
//! Book preprocessing.
|
//! Book preprocessing.
|
||||||
|
|
||||||
|
pub use self::cmd::CmdPreprocessor;
|
||||||
pub use self::index::IndexPreprocessor;
|
pub use self::index::IndexPreprocessor;
|
||||||
pub use self::links::LinkPreprocessor;
|
pub use self::links::LinkPreprocessor;
|
||||||
pub use self::cmd::CmdPreprocessor;
|
|
||||||
|
|
||||||
|
mod cmd;
|
||||||
mod index;
|
mod index;
|
||||||
mod links;
|
mod links;
|
||||||
mod cmd;
|
|
||||||
|
|
||||||
use book::Book;
|
use crate::book::Book;
|
||||||
use config::Config;
|
use crate::config::Config;
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Extra information for a `Preprocessor` to give them more context when
|
/// Extra information for a `Preprocessor` to give them more context when
|
||||||
|
@ -26,6 +29,9 @@ pub struct PreprocessorContext {
|
||||||
pub renderer: String,
|
pub renderer: String,
|
||||||
/// The calling `mdbook` version.
|
/// The calling `mdbook` version.
|
||||||
pub mdbook_version: String,
|
pub mdbook_version: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub(crate) chapter_titles: RefCell<HashMap<PathBuf, String>>,
|
||||||
|
#[serde(skip)]
|
||||||
__non_exhaustive: (),
|
__non_exhaustive: (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +42,8 @@ impl PreprocessorContext {
|
||||||
root,
|
root,
|
||||||
config,
|
config,
|
||||||
renderer,
|
renderer,
|
||||||
mdbook_version: ::MDBOOK_VERSION.to_string(),
|
mdbook_version: crate::MDBOOK_VERSION.to_string(),
|
||||||
|
chapter_titles: RefCell::new(HashMap::new()),
|
||||||
__non_exhaustive: (),
|
__non_exhaustive: (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,2 +1,3 @@
|
||||||
pub mod navigation;
|
pub mod navigation;
|
||||||
|
pub mod theme;
|
||||||
pub mod toc;
|
pub mod toc;
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
|
use handlebars::{
|
||||||
use serde_json;
|
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason, Renderable,
|
||||||
|
};
|
||||||
|
|
||||||
use utils;
|
use crate::utils;
|
||||||
|
use log::{debug, trace};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
type StringMap = BTreeMap<String, String>;
|
type StringMap = BTreeMap<String, String>;
|
||||||
|
|
||||||
|
@ -18,23 +21,23 @@ impl Target {
|
||||||
/// Returns target if found.
|
/// Returns target if found.
|
||||||
fn find(
|
fn find(
|
||||||
&self,
|
&self,
|
||||||
base_path: &String,
|
base_path: &str,
|
||||||
current_path: &String,
|
current_path: &str,
|
||||||
current_item: &StringMap,
|
current_item: &StringMap,
|
||||||
previous_item: &StringMap,
|
previous_item: &StringMap,
|
||||||
) -> Result<Option<StringMap>, RenderError> {
|
) -> Result<Option<StringMap>, RenderError> {
|
||||||
match self {
|
match *self {
|
||||||
&Target::Next => {
|
Target::Next => {
|
||||||
let previous_path = previous_item
|
let previous_path = previous_item.get("path").ok_or_else(|| {
|
||||||
.get("path")
|
RenderErrorReason::Other("No path found for chapter in JSON data".to_owned())
|
||||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
|
})?;
|
||||||
|
|
||||||
if previous_path == base_path {
|
if previous_path == base_path {
|
||||||
return Ok(Some(current_item.clone()));
|
return Ok(Some(current_item.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&Target::Previous => {
|
Target::Previous => {
|
||||||
if current_path == base_path {
|
if current_path == base_path {
|
||||||
return Ok(Some(previous_item.clone()));
|
return Ok(Some(previous_item.clone()));
|
||||||
}
|
}
|
||||||
|
@ -47,21 +50,45 @@ impl Target {
|
||||||
|
|
||||||
fn find_chapter(
|
fn find_chapter(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
rc: &mut RenderContext,
|
rc: &mut RenderContext<'_, '_>,
|
||||||
target: Target,
|
target: Target,
|
||||||
) -> Result<Option<StringMap>, RenderError> {
|
) -> Result<Option<StringMap>, RenderError> {
|
||||||
debug!("Get data from context");
|
debug!("Get data from context");
|
||||||
|
|
||||||
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
|
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
|
||||||
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
|
serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone()).map_err(|_| {
|
||||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
|
||||||
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let base_path = rc
|
let base_path = rc
|
||||||
.evaluate_absolute(ctx, "path", true)?
|
.evaluate(ctx, "@root/path")?
|
||||||
|
.as_json()
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
.ok_or_else(|| {
|
||||||
.replace("\"", "");
|
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||||
|
})?
|
||||||
|
.replace('\"', "");
|
||||||
|
|
||||||
|
if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
|
||||||
|
// Special case for index.md which may be a synthetic page.
|
||||||
|
// Target::find won't match because there is no page with the path
|
||||||
|
// "index.md" (unless there really is an index.md in SUMMARY.md).
|
||||||
|
match target {
|
||||||
|
Target::Previous => return Ok(None),
|
||||||
|
Target::Next => match chapters
|
||||||
|
.iter()
|
||||||
|
.filter(|chapter| {
|
||||||
|
// Skip things like "spacer"
|
||||||
|
chapter.contains_key("path")
|
||||||
|
})
|
||||||
|
.nth(1)
|
||||||
|
{
|
||||||
|
Some(chapter) => return Ok(Some(chapter.clone())),
|
||||||
|
None => return Ok(None),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut previous: Option<StringMap> = None;
|
let mut previous: Option<StringMap> = None;
|
||||||
|
|
||||||
|
@ -71,12 +98,12 @@ fn find_chapter(
|
||||||
match item.get("path") {
|
match item.get("path") {
|
||||||
Some(path) if !path.is_empty() => {
|
Some(path) if !path.is_empty() => {
|
||||||
if let Some(previous) = previous {
|
if let Some(previous) = previous {
|
||||||
if let Some(item) = target.find(&base_path, &path, &item, &previous)? {
|
if let Some(item) = target.find(&base_path, path, &item, &previous)? {
|
||||||
return Ok(Some(item));
|
return Ok(Some(item));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
previous = Some(item.clone());
|
previous = Some(item);
|
||||||
}
|
}
|
||||||
_ => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
|
@ -86,62 +113,68 @@ fn find_chapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(
|
fn render(
|
||||||
_h: &Helper,
|
_h: &Helper<'_>,
|
||||||
r: &Handlebars,
|
r: &Handlebars<'_>,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
rc: &mut RenderContext,
|
rc: &mut RenderContext<'_, '_>,
|
||||||
out: &mut Output,
|
out: &mut dyn Output,
|
||||||
chapter: &StringMap,
|
chapter: &StringMap,
|
||||||
) -> Result<(), RenderError> {
|
) -> Result<(), RenderError> {
|
||||||
trace!("Creating BTreeMap to inject in context");
|
trace!("Creating BTreeMap to inject in context");
|
||||||
|
|
||||||
let mut context = BTreeMap::new();
|
let mut context = BTreeMap::new();
|
||||||
let base_path = rc
|
let base_path = rc
|
||||||
.evaluate_absolute(ctx, "path", false)?
|
.evaluate(ctx, "@root/path")?
|
||||||
|
.as_json()
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
.ok_or_else(|| {
|
||||||
.replace("\"", "");
|
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||||
|
})?
|
||||||
|
.replace('\"', "");
|
||||||
|
|
||||||
context.insert(
|
context.insert(
|
||||||
"path_to_root".to_owned(),
|
"path_to_root".to_owned(),
|
||||||
json!(utils::fs::path_to_root(&base_path)),
|
json!(utils::fs::path_to_root(base_path)),
|
||||||
);
|
);
|
||||||
|
|
||||||
chapter
|
chapter
|
||||||
.get("name")
|
.get("name")
|
||||||
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
.ok_or_else(|| {
|
||||||
|
RenderErrorReason::Other("No title found for chapter in JSON data".to_owned())
|
||||||
|
})
|
||||||
.map(|name| context.insert("title".to_owned(), json!(name)))?;
|
.map(|name| context.insert("title".to_owned(), json!(name)))?;
|
||||||
|
|
||||||
chapter
|
chapter
|
||||||
.get("path")
|
.get("path")
|
||||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
|
.ok_or_else(|| {
|
||||||
|
RenderErrorReason::Other("No path found for chapter in JSON data".to_owned())
|
||||||
|
})
|
||||||
.and_then(|p| {
|
.and_then(|p| {
|
||||||
Path::new(p)
|
Path::new(p)
|
||||||
.with_extension("html")
|
.with_extension("html")
|
||||||
.to_str()
|
.to_str()
|
||||||
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
|
.ok_or_else(|| {
|
||||||
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
|
RenderErrorReason::Other("Link could not be converted to str".to_owned())
|
||||||
|
})
|
||||||
|
.map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/"))))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
trace!("Render template");
|
trace!("Render template");
|
||||||
|
|
||||||
_h.template()
|
let t = _h
|
||||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
.template()
|
||||||
.and_then(|t| {
|
.ok_or_else(|| RenderErrorReason::Other("Error with the handlebars template".to_owned()))?;
|
||||||
let mut local_rc = rc.new_for_block();
|
|
||||||
let local_ctx = Context::wraps(&context)?;
|
let local_ctx = Context::wraps(&context)?;
|
||||||
|
let mut local_rc = rc.clone();
|
||||||
t.render(r, &local_ctx, &mut local_rc, out)
|
t.render(r, &local_ctx, &mut local_rc, out)
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous(
|
pub fn previous(
|
||||||
_h: &Helper,
|
_h: &Helper<'_>,
|
||||||
r: &Handlebars,
|
r: &Handlebars<'_>,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
rc: &mut RenderContext,
|
rc: &mut RenderContext<'_, '_>,
|
||||||
out: &mut Output,
|
out: &mut dyn Output,
|
||||||
) -> Result<(), RenderError> {
|
) -> Result<(), RenderError> {
|
||||||
trace!("previous (handlebars helper)");
|
trace!("previous (handlebars helper)");
|
||||||
|
|
||||||
|
@ -153,11 +186,11 @@ pub fn previous(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(
|
pub fn next(
|
||||||
_h: &Helper,
|
_h: &Helper<'_>,
|
||||||
r: &Handlebars,
|
r: &Handlebars<'_>,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
rc: &mut RenderContext,
|
rc: &mut RenderContext<'_, '_>,
|
||||||
out: &mut Output,
|
out: &mut dyn Output,
|
||||||
) -> Result<(), RenderError> {
|
) -> Result<(), RenderError> {
|
||||||
trace!("next (handlebars helper)");
|
trace!("next (handlebars helper)");
|
||||||
|
|
||||||
|
@ -172,7 +205,7 @@ pub fn next(
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
static TEMPLATE: &'static str =
|
static TEMPLATE: &str =
|
||||||
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
|
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
use handlebars::{
|
||||||
|
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason,
|
||||||
|
};
|
||||||
|
use log::trace;
|
||||||
|
|
||||||
|
pub fn theme_option(
|
||||||
|
h: &Helper<'_>,
|
||||||
|
_r: &Handlebars<'_>,
|
||||||
|
ctx: &Context,
|
||||||
|
rc: &mut RenderContext<'_, '_>,
|
||||||
|
out: &mut dyn Output,
|
||||||
|
) -> Result<(), RenderError> {
|
||||||
|
trace!("theme_option (handlebars helper)");
|
||||||
|
|
||||||
|
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
|
||||||
|
RenderErrorReason::ParamTypeMismatchForName(
|
||||||
|
"theme_option",
|
||||||
|
"0".to_owned(),
|
||||||
|
"string".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let default_theme = rc.evaluate(ctx, "@root/default_theme")?;
|
||||||
|
let default_theme_name = default_theme.as_json().as_str().ok_or_else(|| {
|
||||||
|
RenderErrorReason::ParamTypeMismatchForName(
|
||||||
|
"theme_option",
|
||||||
|
"default_theme".to_owned(),
|
||||||
|
"string".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
out.write(param)?;
|
||||||
|
if param.to_lowercase() == default_theme_name.to_lowercase() {
|
||||||
|
out.write(" (default)")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::{cmp::Ordering, collections::BTreeMap};
|
||||||
|
|
||||||
use utils;
|
use crate::utils;
|
||||||
|
use crate::utils::bracket_escape;
|
||||||
|
|
||||||
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
|
use handlebars::{
|
||||||
use pulldown_cmark::{html, Event, Parser, Tag};
|
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
|
||||||
use serde_json;
|
};
|
||||||
|
|
||||||
// Handlebars helper to construct TOC
|
// Handlebars helper to construct TOC
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
@ -16,28 +17,61 @@ pub struct RenderToc {
|
||||||
impl HelperDef for RenderToc {
|
impl HelperDef for RenderToc {
|
||||||
fn call<'reg: 'rc, 'rc>(
|
fn call<'reg: 'rc, 'rc>(
|
||||||
&self,
|
&self,
|
||||||
_h: &Helper,
|
_h: &Helper<'rc>,
|
||||||
_: &Handlebars,
|
_r: &'reg Handlebars<'_>,
|
||||||
ctx: &Context,
|
ctx: &'rc Context,
|
||||||
rc: &mut RenderContext,
|
rc: &mut RenderContext<'reg, 'rc>,
|
||||||
out: &mut Output,
|
out: &mut dyn Output,
|
||||||
) -> Result<(), RenderError> {
|
) -> Result<(), RenderError> {
|
||||||
// get value from context data
|
// get value from context data
|
||||||
// rc.get_path() is current json parent path, you should always use it like this
|
// 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
|
// param is the key of value you want to display
|
||||||
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
|
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
|
||||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone())
|
||||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
.map_err(|_| {
|
||||||
|
RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
|
||||||
|
})
|
||||||
})?;
|
})?;
|
||||||
let current = rc
|
let current_path = rc
|
||||||
.evaluate_absolute(ctx, "path", true)?
|
.evaluate(ctx, "@root/path")?
|
||||||
|
.as_json()
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
.ok_or_else(|| {
|
||||||
.replace("\"", "");
|
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||||
|
})?
|
||||||
|
.replace('\"', "");
|
||||||
|
|
||||||
|
let current_section = rc
|
||||||
|
.evaluate(ctx, "@root/section")?
|
||||||
|
.as_json()
|
||||||
|
.as_str()
|
||||||
|
.map(str::to_owned)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let fold_enable = rc
|
||||||
|
.evaluate(ctx, "@root/fold_enable")?
|
||||||
|
.as_json()
|
||||||
|
.as_bool()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
RenderErrorReason::Other("Type error for `fold_enable`, bool expected".to_owned())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let fold_level = rc
|
||||||
|
.evaluate(ctx, "@root/fold_level")?
|
||||||
|
.as_json()
|
||||||
|
.as_u64()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
RenderErrorReason::Other("Type error for `fold_level`, u64 expected".to_owned())
|
||||||
|
})?;
|
||||||
|
|
||||||
out.write("<ol class=\"chapter\">")?;
|
out.write("<ol class=\"chapter\">")?;
|
||||||
|
|
||||||
let mut current_level = 1;
|
let mut current_level = 1;
|
||||||
|
// The "index" page, which has this attribute set, is supposed to alias the first chapter in
|
||||||
|
// the book, i.e. the first link. There seems to be no easy way to determine which chapter
|
||||||
|
// the "index" is aliasing from within the renderer, so this is used instead to force the
|
||||||
|
// first link to be active. See further below.
|
||||||
|
let mut is_first_chapter = ctx.data().get("is_index").is_some();
|
||||||
|
|
||||||
for item in chapters {
|
for item in chapters {
|
||||||
// Spacer
|
// Spacer
|
||||||
|
@ -46,97 +80,109 @@ impl HelperDef for RenderToc {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let level = if let Some(s) = item.get("section") {
|
let (section, level) = if let Some(s) = item.get("section") {
|
||||||
s.matches('.').count()
|
(s.as_str(), s.matches('.').count())
|
||||||
} else {
|
} else {
|
||||||
1
|
("", 1)
|
||||||
};
|
};
|
||||||
|
|
||||||
if level > current_level {
|
let is_expanded =
|
||||||
|
if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) {
|
||||||
|
// Expand if folding is disabled, or if the section is an
|
||||||
|
// ancestor or the current section itself.
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
// Levels that are larger than this would be folded.
|
||||||
|
level - 1 < fold_level as usize
|
||||||
|
};
|
||||||
|
|
||||||
|
match level.cmp(¤t_level) {
|
||||||
|
Ordering::Greater => {
|
||||||
while level > current_level {
|
while level > current_level {
|
||||||
out.write("<li>")?;
|
out.write("<li>")?;
|
||||||
out.write("<ol class=\"section\">")?;
|
out.write("<ol class=\"section\">")?;
|
||||||
current_level += 1;
|
current_level += 1;
|
||||||
}
|
}
|
||||||
out.write("<li>")?;
|
write_li_open_tag(out, is_expanded, false)?;
|
||||||
} else if level < current_level {
|
}
|
||||||
|
Ordering::Less => {
|
||||||
while level < current_level {
|
while level < current_level {
|
||||||
out.write("</ol>")?;
|
out.write("</ol>")?;
|
||||||
out.write("</li>")?;
|
out.write("</li>")?;
|
||||||
current_level -= 1;
|
current_level -= 1;
|
||||||
}
|
}
|
||||||
out.write("<li>")?;
|
write_li_open_tag(out, is_expanded, false)?;
|
||||||
} else {
|
|
||||||
out.write("<li")?;
|
|
||||||
if item.get("section").is_none() {
|
|
||||||
out.write(" class=\"affix\"")?;
|
|
||||||
}
|
}
|
||||||
out.write(">")?;
|
Ordering::Equal => {
|
||||||
|
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Part title
|
||||||
|
if let Some(title) = item.get("part") {
|
||||||
|
out.write("<li class=\"part-title\">")?;
|
||||||
|
out.write(&bracket_escape(title))?;
|
||||||
|
out.write("</li>")?;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link
|
// Link
|
||||||
let path_exists = if let Some(path) = item.get("path") {
|
let path_exists: bool;
|
||||||
if !path.is_empty() {
|
match item.get("path") {
|
||||||
|
Some(path) if !path.is_empty() => {
|
||||||
out.write("<a href=\"")?;
|
out.write("<a href=\"")?;
|
||||||
|
let tmp = Path::new(path)
|
||||||
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
|
|
||||||
.with_extension("html")
|
.with_extension("html")
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||||
.replace("\\", "/");
|
.replace('\\', "/");
|
||||||
|
|
||||||
// Add link
|
// Add link
|
||||||
out.write(&utils::fs::path_to_root(¤t))?;
|
out.write(&utils::fs::path_to_root(¤t_path))?;
|
||||||
out.write(&tmp)?;
|
out.write(&tmp)?;
|
||||||
out.write("\"")?;
|
out.write("\"")?;
|
||||||
|
|
||||||
if path == ¤t {
|
if path == ¤t_path || is_first_chapter {
|
||||||
|
is_first_chapter = false;
|
||||||
out.write(" class=\"active\"")?;
|
out.write(" class=\"active\"")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.write(">")?;
|
out.write(">")?;
|
||||||
true
|
path_exists = true;
|
||||||
} else {
|
}
|
||||||
false
|
_ => {
|
||||||
|
out.write("<div>")?;
|
||||||
|
path_exists = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if !self.no_section_label {
|
if !self.no_section_label {
|
||||||
// Section does not necessarily exist
|
// Section does not necessarily exist
|
||||||
if let Some(section) = item.get("section") {
|
if let Some(section) = item.get("section") {
|
||||||
out.write("<strong aria-hidden=\"true\">")?;
|
out.write("<strong aria-hidden=\"true\">")?;
|
||||||
out.write(§ion)?;
|
out.write(section)?;
|
||||||
out.write("</strong> ")?;
|
out.write("</strong> ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = item.get("name") {
|
if let Some(name) = item.get("name") {
|
||||||
// Render only inline code blocks
|
out.write(&bracket_escape(name))?
|
||||||
|
|
||||||
// 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
|
|
||||||
out.write(&markdown_parsed_name)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if path_exists {
|
if path_exists {
|
||||||
out.write("</a>")?;
|
out.write("</a>")?;
|
||||||
|
} else {
|
||||||
|
out.write("</div>")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render expand/collapse toggle
|
||||||
|
if let Some(flag) = item.get("has_sub_items") {
|
||||||
|
let has_sub_items = flag.parse::<bool>().unwrap_or_default();
|
||||||
|
if fold_enable && has_sub_items {
|
||||||
|
out.write("<a class=\"toggle\"><div>❱</div></a>")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
out.write("</li>")?;
|
out.write("</li>")?;
|
||||||
}
|
}
|
||||||
while current_level > 1 {
|
while current_level > 1 {
|
||||||
|
@ -149,3 +195,19 @@ impl HelperDef for RenderToc {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_li_open_tag(
|
||||||
|
out: &mut dyn Output,
|
||||||
|
is_expanded: bool,
|
||||||
|
is_affix: bool,
|
||||||
|
) -> Result<(), std::io::Error> {
|
||||||
|
let mut li = String::from("<li class=\"chapter-item ");
|
||||||
|
if is_expanded {
|
||||||
|
li.push_str("expanded ");
|
||||||
|
}
|
||||||
|
if is_affix {
|
||||||
|
li.push_str("affix ");
|
||||||
|
}
|
||||||
|
li.push_str("\">");
|
||||||
|
out.write(&li)
|
||||||
|
}
|
||||||
|
|
|
@ -1,30 +1,45 @@
|
||||||
extern crate ammonia;
|
|
||||||
extern crate elasticlunr;
|
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use self::elasticlunr::Index;
|
use elasticlunr::{Index, IndexBuilder};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use pulldown_cmark::*;
|
use pulldown_cmark::*;
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use book::{Book, BookItem};
|
use crate::book::{Book, BookItem};
|
||||||
use config::Search;
|
use crate::config::Search;
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
use theme::searcher;
|
use crate::theme::searcher;
|
||||||
use utils;
|
use crate::utils;
|
||||||
|
use log::{debug, warn};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
const MAX_WORD_LENGTH_TO_INDEX: usize = 80;
|
||||||
|
|
||||||
|
/// Tokenizes in the same way as elasticlunr-rs (for English), but also drops long tokens.
|
||||||
|
fn tokenize(text: &str) -> Vec<String> {
|
||||||
|
text.split(|c: char| c.is_whitespace() || c == '-')
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.trim().to_lowercase())
|
||||||
|
.filter(|s| s.len() <= MAX_WORD_LENGTH_TO_INDEX)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates all files required for search.
|
/// Creates all files required for search.
|
||||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
||||||
let mut index = Index::new(&["title", "body", "breadcrumbs"]);
|
let mut index = IndexBuilder::new()
|
||||||
|
.add_field_with_tokenizer("title", Box::new(&tokenize))
|
||||||
|
.add_field_with_tokenizer("body", Box::new(&tokenize))
|
||||||
|
.add_field_with_tokenizer("breadcrumbs", Box::new(&tokenize))
|
||||||
|
.build();
|
||||||
|
|
||||||
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
let mut doc_urls = Vec::with_capacity(book.sections.len());
|
||||||
|
|
||||||
for item in book.iter() {
|
for item in book.iter() {
|
||||||
render_item(&mut index, &search_config, &mut doc_urls, item)?;
|
render_item(&mut index, search_config, &mut doc_urls, item)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = write_to_json(index, &search_config, doc_urls)?;
|
let index = write_to_json(index, search_config, doc_urls)?;
|
||||||
debug!("Writing search index ✓");
|
debug!("Writing search index ✓");
|
||||||
if index.len() > 10_000_000 {
|
if index.len() > 10_000_000 {
|
||||||
warn!("searchindex.json is very large ({} bytes)", index.len());
|
warn!("searchindex.json is very large ({} bytes)", index.len());
|
||||||
|
@ -35,7 +50,7 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||||
utils::fs::write_file(
|
utils::fs::write_file(
|
||||||
destination,
|
destination,
|
||||||
"searchindex.js",
|
"searchindex.js",
|
||||||
format!("window.search = {};", index).as_bytes(),
|
format!("Object.assign(window.search, {});", index).as_bytes(),
|
||||||
)?;
|
)?;
|
||||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||||
|
@ -51,10 +66,23 @@ fn add_doc(
|
||||||
index: &mut Index,
|
index: &mut Index,
|
||||||
doc_urls: &mut Vec<String>,
|
doc_urls: &mut Vec<String>,
|
||||||
anchor_base: &str,
|
anchor_base: &str,
|
||||||
section_id: &Option<String>,
|
heading: &str,
|
||||||
|
id_counter: &mut HashMap<String, usize>,
|
||||||
|
section_id: &Option<CowStr<'_>>,
|
||||||
items: &[&str],
|
items: &[&str],
|
||||||
) {
|
) {
|
||||||
let url = if let &Some(ref id) = section_id {
|
// Either use the explicit section id the user specified, or generate one
|
||||||
|
// from the heading content.
|
||||||
|
let section_id = section_id.as_ref().map(|id| id.to_string()).or_else(|| {
|
||||||
|
if heading.is_empty() {
|
||||||
|
// In the case where a chapter has no heading, don't set a section id.
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(utils::unique_id_from_content(heading, id_counter))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let url = if let Some(id) = section_id {
|
||||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||||
} else {
|
} else {
|
||||||
Cow::Borrowed(anchor_base)
|
Cow::Borrowed(anchor_base)
|
||||||
|
@ -74,95 +102,132 @@ fn render_item(
|
||||||
doc_urls: &mut Vec<String>,
|
doc_urls: &mut Vec<String>,
|
||||||
item: &BookItem,
|
item: &BookItem,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let chapter = match item {
|
let chapter = match *item {
|
||||||
&BookItem::Chapter(ref ch) => ch,
|
BookItem::Chapter(ref ch) if !ch.is_draft_chapter() => ch,
|
||||||
_ => return Ok(()),
|
_ => return Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let filepath = Path::new(&chapter.path).with_extension("html");
|
let chapter_path = chapter
|
||||||
|
.path
|
||||||
|
.as_ref()
|
||||||
|
.expect("Checked that path exists above");
|
||||||
|
let filepath = Path::new(&chapter_path).with_extension("html");
|
||||||
let filepath = filepath
|
let filepath = filepath
|
||||||
.to_str()
|
.to_str()
|
||||||
.chain_err(|| "Could not convert HTML path to str")?;
|
.with_context(|| "Could not convert HTML path to str")?;
|
||||||
let anchor_base = utils::fs::normalize_path(filepath);
|
let anchor_base = utils::fs::normalize_path(filepath);
|
||||||
|
|
||||||
let mut opts = Options::empty();
|
let mut p = utils::new_cmark_parser(&chapter.content, false).peekable();
|
||||||
opts.insert(OPTION_ENABLE_TABLES);
|
|
||||||
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
|
||||||
let p = Parser::new_ext(&chapter.content, opts);
|
|
||||||
|
|
||||||
let mut in_header = false;
|
let mut in_heading = false;
|
||||||
let max_section_depth = search_config.heading_split_level as i32;
|
let max_section_depth = u32::from(search_config.heading_split_level);
|
||||||
let mut section_id = None;
|
let mut section_id = None;
|
||||||
let mut heading = String::new();
|
let mut heading = String::new();
|
||||||
let mut body = String::new();
|
let mut body = String::new();
|
||||||
let mut breadcrumbs = chapter.parent_names.clone();
|
let mut breadcrumbs = chapter.parent_names.clone();
|
||||||
let mut footnote_numbers = HashMap::new();
|
let mut footnote_numbers = HashMap::new();
|
||||||
|
|
||||||
for event in p {
|
breadcrumbs.push(chapter.name.clone());
|
||||||
|
|
||||||
|
let mut id_counter = HashMap::new();
|
||||||
|
while let Some(event) = p.next() {
|
||||||
match event {
|
match event {
|
||||||
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
|
Event::Start(Tag::Heading { level, id, .. }) if level as u32 <= max_section_depth => {
|
||||||
if heading.len() > 0 {
|
if !heading.is_empty() {
|
||||||
// Section finished, the next header is following now
|
// Section finished, the next heading is following now
|
||||||
// Write the data to the index, and clear it for the next section
|
// Write the data to the index, and clear it for the next section
|
||||||
add_doc(
|
add_doc(
|
||||||
index,
|
index,
|
||||||
doc_urls,
|
doc_urls,
|
||||||
&anchor_base,
|
&anchor_base,
|
||||||
|
&heading,
|
||||||
|
&mut id_counter,
|
||||||
§ion_id,
|
§ion_id,
|
||||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||||
);
|
);
|
||||||
section_id = None;
|
|
||||||
heading.clear();
|
heading.clear();
|
||||||
body.clear();
|
body.clear();
|
||||||
breadcrumbs.pop();
|
breadcrumbs.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
in_header = true;
|
section_id = id;
|
||||||
|
in_heading = true;
|
||||||
}
|
}
|
||||||
Event::End(Tag::Header(i)) if i <= max_section_depth => {
|
Event::End(TagEnd::Heading(level)) if level as u32 <= max_section_depth => {
|
||||||
in_header = false;
|
in_heading = false;
|
||||||
section_id = Some(utils::id_from_content(&heading));
|
|
||||||
breadcrumbs.push(heading.clone());
|
breadcrumbs.push(heading.clone());
|
||||||
}
|
}
|
||||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||||
let number = footnote_numbers.len() + 1;
|
let number = footnote_numbers.len() + 1;
|
||||||
footnote_numbers.entry(name).or_insert(number);
|
footnote_numbers.entry(name).or_insert(number);
|
||||||
}
|
}
|
||||||
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
|
Event::Html(html) => {
|
||||||
// Insert spaces where HTML output would usually seperate text
|
let mut html_block = html.into_string();
|
||||||
|
|
||||||
|
// As of pulldown_cmark 0.6, html events are no longer contained
|
||||||
|
// in an HtmlBlock tag. We must collect consecutive Html events
|
||||||
|
// into a block ourselves.
|
||||||
|
while let Some(Event::Html(html)) = p.peek() {
|
||||||
|
html_block.push_str(html);
|
||||||
|
p.next();
|
||||||
|
}
|
||||||
|
body.push_str(&clean_html(&html_block));
|
||||||
|
}
|
||||||
|
Event::InlineHtml(html) => {
|
||||||
|
// This is not capable of cleaning inline tags like
|
||||||
|
// `foo <script>…</script>`. The `<script>` tags show up as
|
||||||
|
// individual InlineHtml events, and the content inside is
|
||||||
|
// just a regular Text event. There isn't a very good way to
|
||||||
|
// know how to collect all the content in-between. I'm not
|
||||||
|
// sure if this is easily fixable. It should be extremely
|
||||||
|
// rare, since script and style tags should almost always be
|
||||||
|
// blocks, and worse case you have some noise in the index.
|
||||||
|
body.push_str(&clean_html(&html));
|
||||||
|
}
|
||||||
|
Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => {
|
||||||
|
// Insert spaces where HTML output would usually separate text
|
||||||
// to ensure words don't get merged together
|
// to ensure words don't get merged together
|
||||||
if in_header {
|
if in_heading {
|
||||||
heading.push(' ');
|
heading.push(' ');
|
||||||
} else {
|
} else {
|
||||||
body.push(' ');
|
body.push(' ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Text(text) => {
|
Event::Text(text) | Event::Code(text) => {
|
||||||
if in_header {
|
if in_heading {
|
||||||
heading.push_str(&text);
|
heading.push_str(&text);
|
||||||
} else {
|
} else {
|
||||||
body.push_str(&text);
|
body.push_str(&text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Html(html) | Event::InlineHtml(html) => {
|
|
||||||
body.push_str(&clean_html(&html));
|
|
||||||
}
|
|
||||||
Event::FootnoteReference(name) => {
|
Event::FootnoteReference(name) => {
|
||||||
let len = footnote_numbers.len() + 1;
|
let len = footnote_numbers.len() + 1;
|
||||||
let number = footnote_numbers.entry(name).or_insert(len);
|
let number = footnote_numbers.entry(name).or_insert(len);
|
||||||
body.push_str(&format!(" [{}] ", number));
|
body.push_str(&format!(" [{}] ", number));
|
||||||
}
|
}
|
||||||
|
Event::TaskListMarker(_checked) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if heading.len() > 0 {
|
if !body.is_empty() || !heading.is_empty() {
|
||||||
|
let title = if heading.is_empty() {
|
||||||
|
if let Some(chapter) = breadcrumbs.first() {
|
||||||
|
chapter
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
&heading
|
||||||
|
};
|
||||||
// Make sure the last section is added to the index
|
// Make sure the last section is added to the index
|
||||||
add_doc(
|
add_doc(
|
||||||
index,
|
index,
|
||||||
doc_urls,
|
doc_urls,
|
||||||
&anchor_base,
|
&anchor_base,
|
||||||
|
&heading,
|
||||||
|
&mut id_counter,
|
||||||
§ion_id,
|
§ion_id,
|
||||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
&[title, &body, &breadcrumbs.join(" » ")],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +235,7 @@ fn render_item(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
|
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
|
||||||
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
use elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -193,12 +258,13 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
|
||||||
|
|
||||||
let mut fields = BTreeMap::new();
|
let mut fields = BTreeMap::new();
|
||||||
let mut opt = SearchOptionsField::default();
|
let mut opt = SearchOptionsField::default();
|
||||||
opt.boost = Some(search_config.boost_title);
|
let mut insert_boost = |key: &str, boost| {
|
||||||
fields.insert("title".into(), opt);
|
opt.boost = Some(boost);
|
||||||
opt.boost = Some(search_config.boost_paragraph);
|
fields.insert(key.into(), opt);
|
||||||
fields.insert("body".into(), opt);
|
};
|
||||||
opt.boost = Some(search_config.boost_hierarchy);
|
insert_boost("title", search_config.boost_title);
|
||||||
fields.insert("breadcrumbs".into(), opt);
|
insert_boost("body", search_config.boost_paragraph);
|
||||||
|
insert_boost("breadcrumbs", search_config.boost_hierarchy);
|
||||||
|
|
||||||
let search_options = SearchOptions {
|
let search_options = SearchOptions {
|
||||||
bool: if search_config.use_boolean_and {
|
bool: if search_config.use_boolean_and {
|
||||||
|
@ -231,8 +297,7 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clean_html(html: &str) -> String {
|
fn clean_html(html: &str) -> String {
|
||||||
lazy_static! {
|
static AMMONIA: Lazy<ammonia::Builder<'static>> = Lazy::new(|| {
|
||||||
static ref AMMONIA: ammonia::Builder<'static> = {
|
|
||||||
let mut clean_content = HashSet::new();
|
let mut clean_content = HashSet::new();
|
||||||
clean_content.insert("script");
|
clean_content.insert("script");
|
||||||
clean_content.insert("style");
|
clean_content.insert("style");
|
||||||
|
@ -245,7 +310,6 @@ fn clean_html(html: &str) -> String {
|
||||||
.allowed_classes(HashMap::new())
|
.allowed_classes(HashMap::new())
|
||||||
.clean_content_tags(clean_content);
|
.clean_content_tags(clean_content);
|
||||||
builder
|
builder
|
||||||
};
|
});
|
||||||
}
|
|
||||||
AMMONIA.clean(html).to_string()
|
AMMONIA.clean(html).to_string()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
use crate::book::BookItem;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::renderer::{RenderContext, Renderer};
|
||||||
|
use crate::utils;
|
||||||
|
use log::trace;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
|
||||||
|
/// when debugging preprocessors.
|
||||||
|
pub struct MarkdownRenderer;
|
||||||
|
|
||||||
|
impl MarkdownRenderer {
|
||||||
|
/// Create a new `MarkdownRenderer` instance.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
MarkdownRenderer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderer for MarkdownRenderer {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"markdown"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||||
|
let destination = &ctx.destination;
|
||||||
|
let book = &ctx.book;
|
||||||
|
|
||||||
|
if destination.exists() {
|
||||||
|
utils::fs::remove_dir_content(destination)
|
||||||
|
.with_context(|| "Unable to remove stale Markdown output")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!("markdown render");
|
||||||
|
for item in book.iter() {
|
||||||
|
if let BookItem::Chapter(ref ch) = *item {
|
||||||
|
if !ch.is_draft_chapter() {
|
||||||
|
utils::fs::write_file(
|
||||||
|
&ctx.destination,
|
||||||
|
ch.path.as_ref().expect("Checked path exists before"),
|
||||||
|
ch.content.as_bytes(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_dir_all(destination)
|
||||||
|
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,23 +8,29 @@
|
||||||
//!
|
//!
|
||||||
//! The definition for [RenderContext] may be useful though.
|
//! The definition for [RenderContext] may be useful though.
|
||||||
//!
|
//!
|
||||||
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/lib/index.html
|
//! [For Developers]: https://rust-lang.github.io/mdBook/for_developers/index.html
|
||||||
//! [RenderContext]: struct.RenderContext.html
|
//! [RenderContext]: struct.RenderContext.html
|
||||||
|
|
||||||
pub use self::html_handlebars::HtmlHandlebars;
|
pub use self::html_handlebars::HtmlHandlebars;
|
||||||
|
pub use self::markdown_renderer::MarkdownRenderer;
|
||||||
|
|
||||||
mod html_handlebars;
|
mod html_handlebars;
|
||||||
|
mod markdown_renderer;
|
||||||
|
|
||||||
use serde_json;
|
|
||||||
use shlex::Shlex;
|
use shlex::Shlex;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Read};
|
use std::io::{self, ErrorKind, Read};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
use book::Book;
|
use crate::book::Book;
|
||||||
use config::Config;
|
use crate::config::Config;
|
||||||
use errors::*;
|
use crate::errors::*;
|
||||||
|
use log::{error, info, trace, warn};
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// An arbitrary `mdbook` backend.
|
/// An arbitrary `mdbook` backend.
|
||||||
///
|
///
|
||||||
|
@ -32,12 +38,9 @@ use errors::*;
|
||||||
/// provide your own renderer, there are two main renderer implementations that
|
/// provide your own renderer, there are two main renderer implementations that
|
||||||
/// 99% of users will ever use:
|
/// 99% of users will ever use:
|
||||||
///
|
///
|
||||||
/// - [HtmlHandlebars] - the built-in HTML renderer
|
/// - [`HtmlHandlebars`] - the built-in HTML renderer
|
||||||
/// - [CmdRenderer] - a generic renderer which shells out to a program to do the
|
/// - [`CmdRenderer`] - a generic renderer which shells out to a program to do the
|
||||||
/// actual rendering
|
/// actual rendering
|
||||||
///
|
|
||||||
/// [HtmlHandlebars]: struct.HtmlHandlebars.html
|
|
||||||
/// [CmdRenderer]: struct.CmdRenderer.html
|
|
||||||
pub trait Renderer {
|
pub trait Renderer {
|
||||||
/// The `Renderer`'s name.
|
/// The `Renderer`'s name.
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
@ -64,6 +67,9 @@ pub struct RenderContext {
|
||||||
/// renderers to cache intermediate results, this directory is not
|
/// renderers to cache intermediate results, this directory is not
|
||||||
/// guaranteed to be empty or even exist.
|
/// guaranteed to be empty or even exist.
|
||||||
pub destination: PathBuf,
|
pub destination: PathBuf,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub(crate) chapter_titles: HashMap<PathBuf, String>,
|
||||||
|
#[serde(skip)]
|
||||||
__non_exhaustive: (),
|
__non_exhaustive: (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,11 +81,12 @@ impl RenderContext {
|
||||||
Q: Into<PathBuf>,
|
Q: Into<PathBuf>,
|
||||||
{
|
{
|
||||||
RenderContext {
|
RenderContext {
|
||||||
book: book,
|
book,
|
||||||
config: config,
|
config,
|
||||||
version: ::MDBOOK_VERSION.to_string(),
|
version: crate::MDBOOK_VERSION.to_string(),
|
||||||
root: root.into(),
|
root: root.into(),
|
||||||
destination: destination.into(),
|
destination: destination.into(),
|
||||||
|
chapter_titles: HashMap::new(),
|
||||||
__non_exhaustive: (),
|
__non_exhaustive: (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +98,7 @@ impl RenderContext {
|
||||||
|
|
||||||
/// Load a `RenderContext` from its JSON representation.
|
/// Load a `RenderContext` from its JSON representation.
|
||||||
pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> {
|
pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> {
|
||||||
serde_json::from_reader(reader).chain_err(|| "Unable to deserialize the `RenderContext`")
|
serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,14 +137,44 @@ impl CmdRenderer {
|
||||||
CmdRenderer { name, cmd }
|
CmdRenderer { name, cmd }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compose_command(&self) -> Result<Command> {
|
fn compose_command(&self, root: &Path, destination: &Path) -> Result<Command> {
|
||||||
let mut words = Shlex::new(&self.cmd);
|
let mut words = Shlex::new(&self.cmd);
|
||||||
let executable = match words.next() {
|
let exe = match words.next() {
|
||||||
Some(e) => e,
|
Some(e) => PathBuf::from(e),
|
||||||
None => bail!("Command string was empty"),
|
None => bail!("Command string was empty"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cmd = Command::new(executable);
|
let exe = if exe.components().count() == 1 {
|
||||||
|
// Search PATH for the executable.
|
||||||
|
exe
|
||||||
|
} else {
|
||||||
|
// Relative paths are preferred to be relative to the book root.
|
||||||
|
let abs_exe = root.join(&exe);
|
||||||
|
if abs_exe.exists() {
|
||||||
|
abs_exe
|
||||||
|
} else {
|
||||||
|
// Historically paths were relative to the destination, but
|
||||||
|
// this is not the preferred way.
|
||||||
|
let legacy_path = destination.join(&exe);
|
||||||
|
if legacy_path.exists() {
|
||||||
|
warn!(
|
||||||
|
"Renderer command `{}` uses a path relative to the \
|
||||||
|
renderer output directory `{}`. This was previously \
|
||||||
|
accepted, but has been deprecated. Relative executable \
|
||||||
|
paths should be relative to the book root.",
|
||||||
|
exe.display(),
|
||||||
|
destination.display()
|
||||||
|
);
|
||||||
|
legacy_path
|
||||||
|
} else {
|
||||||
|
// Let this bubble through to later be handled by
|
||||||
|
// handle_render_command_error.
|
||||||
|
abs_exe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmd = Command::new(exe);
|
||||||
|
|
||||||
for arg in words {
|
for arg in words {
|
||||||
cmd.arg(arg);
|
cmd.arg(arg);
|
||||||
|
@ -147,6 +184,40 @@ impl CmdRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CmdRenderer {
|
||||||
|
fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> {
|
||||||
|
if let ErrorKind::NotFound = error.kind() {
|
||||||
|
// Look for "output.{self.name}.optional".
|
||||||
|
// If it exists and is true, treat this as a warning.
|
||||||
|
// Otherwise, fail the build.
|
||||||
|
|
||||||
|
let optional_key = format!("output.{}.optional", self.name);
|
||||||
|
|
||||||
|
let is_optional = match ctx.config.get(&optional_key) {
|
||||||
|
Some(Value::Boolean(value)) => *value,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_optional {
|
||||||
|
warn!(
|
||||||
|
"The command `{}` for backend `{}` was not found, \
|
||||||
|
but was marked as optional.",
|
||||||
|
self.cmd, self.name
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
error!(
|
||||||
|
"The command `{0}` wasn't found, is the \"{1}\" backend installed? \
|
||||||
|
If you want to ignore this error when the \"{1}\" backend is not installed, \
|
||||||
|
set `optional = true` in the `[output.{1}]` section of the book.toml configuration file.",
|
||||||
|
self.cmd, self.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error).with_context(|| "Unable to start the backend")?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Renderer for CmdRenderer {
|
impl Renderer for CmdRenderer {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
&self.name
|
&self.name
|
||||||
|
@ -158,7 +229,7 @@ impl Renderer for CmdRenderer {
|
||||||
let _ = fs::create_dir_all(&ctx.destination);
|
let _ = fs::create_dir_all(&ctx.destination);
|
||||||
|
|
||||||
let mut child = match self
|
let mut child = match self
|
||||||
.compose_command()?
|
.compose_command(&ctx.root, &ctx.destination)?
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::inherit())
|
.stdout(Stdio::inherit())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::inherit())
|
||||||
|
@ -166,20 +237,9 @@ impl Renderer for CmdRenderer {
|
||||||
.spawn()
|
.spawn()
|
||||||
{
|
{
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
|
Err(e) => return self.handle_render_command_error(ctx, e),
|
||||||
warn!(
|
|
||||||
"The command wasn't found, is the \"{}\" backend installed?",
|
|
||||||
self.name
|
|
||||||
);
|
|
||||||
warn!("\tCommand: {}", self.cmd);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(e).chain_err(|| "Unable to start the backend")?;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
|
||||||
let mut stdin = child.stdin.take().expect("Child has stdin");
|
let mut stdin = child.stdin.take().expect("Child has stdin");
|
||||||
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
|
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
|
||||||
// Looks like the backend hung up before we could finish
|
// Looks like the backend hung up before we could finish
|
||||||
|
@ -189,11 +249,10 @@ impl Renderer for CmdRenderer {
|
||||||
|
|
||||||
// explicitly close the `stdin` file handle
|
// explicitly close the `stdin` file handle
|
||||||
drop(stdin);
|
drop(stdin);
|
||||||
}
|
|
||||||
|
|
||||||
let status = child
|
let status = child
|
||||||
.wait()
|
.wait()
|
||||||
.chain_err(|| "Error waiting for the backend to complete")?;
|
.with_context(|| "Error waiting for the backend to complete")?;
|
||||||
|
|
||||||
trace!("{} exited with output: {:?}", self.cmd, status);
|
trace!("{} exited with output: {:?}", self.cmd, status);
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue