Compare commits
1257 Commits
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 | cffc385b0c | |
Michael Bryan | e73928f933 | |
Michael Bryan | 41071a5dd9 | |
Michael Bryan | f6a7432569 | |
Michael Bryan | 89ea60e7a5 | |
Jason Liquorish | 10b69e60c8 | |
Jason Liquorish | 336e08fe50 | |
Jason Liquorish | 5bfdf9fcc8 | |
Michael Bryan | 29f8b791f1 | |
xyh | 877bf37d18 | |
Jason Liquorish | d2565af000 | |
Jason Liquorish | 599e47f1f1 | |
Jason Liquorish | 0c31ab2953 | |
Michael Bryan | b1c7c54108 | |
Jan-Erik Rediger | f654c42426 | |
Jan-Erik Rediger | 0c926b3e88 | |
mwilbur | e4eddb3f26 | |
Michael Bryan | adec78e7f5 | |
Michael Bryan | 5cd5e4764c | |
Michael Bryan | 132f4fd358 | |
Michael Bryan | 1d72cea972 | |
Michael Bryan | 1aa1194d79 | |
Michael Bryan | 304234c122 | |
Michael Bryan | 729c94a7e4 | |
Ramon Buckland | df874cdbdb | |
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).
|
153
CONTRIBUTING.md
153
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,20 +49,127 @@ 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
|
||||||
|
|
||||||
|
We love code quality and Rust has some excellent tools to assist you with contributions.
|
||||||
|
|
||||||
|
#### Formatting Code with rustfmt
|
||||||
|
|
||||||
|
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
|
||||||
|
This will ensure we have good quality source code that is better for us all to maintain.
|
||||||
|
|
||||||
|
[rustfmt](https://github.com/rust-lang/rustfmt) has a lot more information on the project.
|
||||||
|
The quick guide is
|
||||||
|
|
||||||
|
1. Install it (`rustfmt` is usually installed by default via [rustup](https://rustup.rs/)):
|
||||||
|
```
|
||||||
|
rustup component add rustfmt
|
||||||
|
```
|
||||||
|
1. You can now run `rustfmt` on a single file simply by...
|
||||||
|
```
|
||||||
|
rustfmt src/path/to/your/file.rs
|
||||||
|
```
|
||||||
|
... or you can format the entire project with
|
||||||
|
```
|
||||||
|
cargo fmt
|
||||||
|
```
|
||||||
|
When run through `cargo` it will format all bin and lib files in the current package.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
[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 help us maintain awesome code.
|
||||||
|
|
||||||
|
1. To install
|
||||||
|
```
|
||||||
|
rustup component add clippy
|
||||||
|
```
|
||||||
|
2. Running clippy
|
||||||
|
```
|
||||||
|
cargo clippy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
189
README.md
189
README.md
|
@ -1,191 +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.
|
|
||||||
|
|
||||||
|
|
||||||
### 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/
|
|
||||||
|
|
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:
|
|
||||||
- cd path/to/mybook && mdbook build && mdbook test
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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,109 +0,0 @@
|
||||||
# Preprocessors
|
|
||||||
|
|
||||||
A *preprocessor* is simply a bit of code which gets run immediately after the
|
|
||||||
book is loaded and before it gets rendered, allowing you to update and mutate
|
|
||||||
the book. Possible use cases are:
|
|
||||||
|
|
||||||
- Creating custom helpers like `\{{#include /path/to/file.md}}`
|
|
||||||
- Updating links so `[some chapter](some_chapter.md)` is automatically changed
|
|
||||||
to `[some chapter](some_chapter.html)` for the HTML renderer
|
|
||||||
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
|
|
||||||
mathjax equivalents
|
|
||||||
|
|
||||||
|
|
||||||
## Implementing a Preprocessor
|
|
||||||
|
|
||||||
A preprocessor is represented by the `Preprocessor` trait.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub trait Preprocessor {
|
|
||||||
fn name(&self) -> &str;
|
|
||||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
|
|
||||||
fn supports_renderer(&self, _renderer: &str) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Where the `PreprocessorContext` is defined as
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct PreprocessorContext {
|
|
||||||
pub root: PathBuf,
|
|
||||||
pub config: Config,
|
|
||||||
/// The `Renderer` this preprocessor is being used with.
|
|
||||||
pub renderer: String,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `renderer` value allows you react accordingly, for example, PDF or HTML.
|
|
||||||
|
|
||||||
## A complete Example
|
|
||||||
|
|
||||||
The magic happens within the `run(...)` method of the
|
|
||||||
[`Preprocessor`][preprocessor-docs] trait implementation.
|
|
||||||
|
|
||||||
As direct access to the chapters is not possible, you will probably end up
|
|
||||||
iterating them using `for_each_mut(...)`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
book.for_each_mut(|item: &mut BookItem| {
|
|
||||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
|
||||||
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
|
|
||||||
res = Some(
|
|
||||||
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
|
|
||||||
Ok(md) => {
|
|
||||||
chapter.content = md;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(err) => Err(err),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
The `chapter.content` is just a markdown formatted string, and you will have to
|
|
||||||
process it in some way. Even though it's entirely possible to implement some
|
|
||||||
sort of manual find & replace operation, if that feels too unsafe you can use
|
|
||||||
[`pulldown-cmark`][pc] to parse the string into events and work on them instead.
|
|
||||||
|
|
||||||
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events
|
|
||||||
back to a string.
|
|
||||||
|
|
||||||
The following code block shows how to remove all emphasis from markdown, and do
|
|
||||||
so safely.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn remove_emphasis(
|
|
||||||
num_removed_items: &mut 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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For everything else, have a look [at the complete example][example].
|
|
||||||
|
|
||||||
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
|
|
||||||
[pc]: https://crates.io/crates/pulldown-cmark
|
|
||||||
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
|
||||||
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs
|
|
|
@ -1,259 +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
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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 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
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 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,98 +0,0 @@
|
||||||
//! This program removes all forms of emphasis from the markdown of the book.
|
|
||||||
extern crate mdbook;
|
|
||||||
extern crate pulldown_cmark;
|
|
||||||
extern crate pulldown_cmark_to_cmark;
|
|
||||||
|
|
||||||
use mdbook::book::{Book, BookItem, Chapter};
|
|
||||||
use mdbook::errors::{Error, Result};
|
|
||||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
|
||||||
use mdbook::MDBook;
|
|
||||||
use pulldown_cmark::{Event, Parser, Tag};
|
|
||||||
use pulldown_cmark_to_cmark::fmt::cmark;
|
|
||||||
|
|
||||||
use std::env::{args, args_os};
|
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::process;
|
|
||||||
|
|
||||||
const NAME: &str = "md-links-to-html-links";
|
|
||||||
|
|
||||||
fn do_it(book: OsString) -> Result<()> {
|
|
||||||
let mut book = MDBook::load(book)?;
|
|
||||||
book.with_preprecessor(Deemphasize);
|
|
||||||
book.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
if args_os().count() != 2 {
|
|
||||||
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
|
|
||||||
eprintln!("{}", e);
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Deemphasize;
|
|
||||||
|
|
||||||
impl Preprocessor for Deemphasize {
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
NAME
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
|
||||||
eprintln!("Running '{}' preprocessor", self.name());
|
|
||||||
let mut num_removed_items = 0;
|
|
||||||
|
|
||||||
process(&mut book.sections, &mut num_removed_items)?;
|
|
||||||
|
|
||||||
eprintln!(
|
|
||||||
"{}: removed {} events from markdown stream.",
|
|
||||||
self.name(),
|
|
||||||
num_removed_items
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(book)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process<'a, I>(items: I, num_removed_items: &mut usize) -> Result<()>
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = &'a mut BookItem> + 'a,
|
|
||||||
{
|
|
||||||
for item in items {
|
|
||||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
|
||||||
eprintln!("{}: processing chapter '{}'", NAME, chapter.name);
|
|
||||||
|
|
||||||
let md = remove_emphasis(num_removed_items, chapter)?;
|
|
||||||
chapter.content = md;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_emphasis(
|
|
||||||
num_removed_items: &mut usize,
|
|
||||||
chapter: &mut Chapter,
|
|
||||||
) -> Result<String> {
|
|
||||||
let mut buf = String::with_capacity(chapter.content.len());
|
|
||||||
|
|
||||||
let events = Parser::new(&chapter.content).filter(|e| {
|
|
||||||
let should_keep = match *e {
|
|
||||||
Event::Start(Tag::Emphasis)
|
|
||||||
| Event::Start(Tag::Strong)
|
|
||||||
| Event::End(Tag::Emphasis)
|
|
||||||
| Event::End(Tag::Strong) => false,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
if !should_keep {
|
|
||||||
*num_removed_items += 1;
|
|
||||||
}
|
|
||||||
should_keep
|
|
||||||
});
|
|
||||||
|
|
||||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
|
||||||
Error::from(format!("Markdown serialization failed: {}", err))
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
use crate::nop_lib::Nop;
|
||||||
|
use clap::{Arg, ArgMatches, Command};
|
||||||
|
use mdbook::book::Book;
|
||||||
|
use mdbook::errors::Error;
|
||||||
|
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||||
|
use semver::{Version, VersionReq};
|
||||||
|
use std::io;
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
pub fn make_app() -> Command {
|
||||||
|
Command::new("nop-preprocessor")
|
||||||
|
.about("A mdbook preprocessor which does precisely nothing")
|
||||||
|
.subcommand(
|
||||||
|
Command::new("supports")
|
||||||
|
.arg(Arg::new("renderer").required(true))
|
||||||
|
.about("Check whether a renderer is supported by this preprocessor"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let matches = make_app().get_matches();
|
||||||
|
|
||||||
|
// Users will want to construct their own preprocessor here
|
||||||
|
let preprocessor = Nop::new();
|
||||||
|
|
||||||
|
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||||
|
handle_supports(&preprocessor, sub_args);
|
||||||
|
} else if let Err(e) = handle_preprocessing(&preprocessor) {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
||||||
|
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
||||||
|
|
||||||
|
let book_version = Version::parse(&ctx.mdbook_version)?;
|
||||||
|
let version_req = VersionReq::parse(mdbook::MDBOOK_VERSION)?;
|
||||||
|
|
||||||
|
if !version_req.matches(&book_version) {
|
||||||
|
eprintln!(
|
||||||
|
"Warning: The {} plugin was built against version {} of mdbook, \
|
||||||
|
but we're being called from version {}",
|
||||||
|
pre.name(),
|
||||||
|
mdbook::MDBOOK_VERSION,
|
||||||
|
ctx.mdbook_version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let processed_book = pre.run(&ctx, book)?;
|
||||||
|
serde_json::to_writer(io::stdout(), &processed_book)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
|
||||||
|
let renderer = sub_args
|
||||||
|
.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.
|
||||||
|
if supported {
|
||||||
|
process::exit(0);
|
||||||
|
} else {
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The actual implementation of the `Nop` preprocessor. This would usually go
|
||||||
|
/// in your main `lib.rs` file.
|
||||||
|
mod nop_lib {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A no-op preprocessor.
|
||||||
|
pub struct Nop;
|
||||||
|
|
||||||
|
impl Nop {
|
||||||
|
pub fn new() -> Nop {
|
||||||
|
Nop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Preprocessor for Nop {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"nop-preprocessor"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book, Error> {
|
||||||
|
// In testing we want to tell the preprocessor to blow up by setting a
|
||||||
|
// particular config value
|
||||||
|
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
|
||||||
|
if nop_cfg.contains_key("blow-up") {
|
||||||
|
anyhow::bail!("Boom!!1!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we *are* a no-op preprocessor after all
|
||||||
|
Ok(book)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||||
|
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]: http://rust-lang-nursery.github.io/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
|
|
@ -0,0 +1,134 @@
|
||||||
|
# Preprocessors
|
||||||
|
|
||||||
|
A *preprocessor* is simply a bit of code which gets run immediately after the
|
||||||
|
book is loaded and before it gets rendered, allowing you to update and mutate
|
||||||
|
the book. Possible use cases are:
|
||||||
|
|
||||||
|
- Creating custom helpers like `\{{#include /path/to/file.md}}`
|
||||||
|
- Substituting in latex-style expressions (`$$ \frac{1}{3} $$`) with their
|
||||||
|
mathjax equivalents
|
||||||
|
|
||||||
|
See [Configuring Preprocessors](../format/configuration/preprocessors.md) for more information about using preprocessors.
|
||||||
|
|
||||||
|
## Hooking Into MDBook
|
||||||
|
|
||||||
|
MDBook uses a fairly simple mechanism for discovering third party plugins.
|
||||||
|
A new table is added to `book.toml` (e.g. `[preprocessor.foo]` for the `foo`
|
||||||
|
preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
|
||||||
|
part of the build process.
|
||||||
|
|
||||||
|
Once the preprocessor has been defined and the build process starts, mdBook executes the command defined in the `preprocessor.foo.command` key twice.
|
||||||
|
The first time it runs the preprocessor to determine if it supports the given renderer.
|
||||||
|
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.
|
||||||
|
|
||||||
|
If the preprocessor supports the renderer, then mdbook runs it a second time, passing JSON data into stdin.
|
||||||
|
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.
|
||||||
|
|
||||||
|
The preprocessor should return the JSON format of the [`Book`] object to stdout, with any modifications it wishes to perform.
|
||||||
|
|
||||||
|
The easiest way to get started is by creating your own implementation of the
|
||||||
|
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
|
||||||
|
translates inputs to the correct `Preprocessor` method. For convenience, there
|
||||||
|
is [an example no-op preprocessor] in the `examples/` directory which can easily
|
||||||
|
be adapted for other preprocessors.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Example no-op preprocessor</summary>
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// nop-preprocessors.rs
|
||||||
|
|
||||||
|
{{#include ../../../examples/nop-preprocessor.rs}}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Hints For Implementing A Preprocessor
|
||||||
|
|
||||||
|
By pulling in `mdbook` as a library, preprocessors can have access to the
|
||||||
|
existing infrastructure for dealing with books.
|
||||||
|
|
||||||
|
For example, a custom preprocessor could use the
|
||||||
|
[`CmdPreprocessor::parse_input()`] function to deserialize the JSON written to
|
||||||
|
`stdin`. Then each chapter of the `Book` can be mutated in-place via
|
||||||
|
[`Book::for_each_mut()`], and then written to `stdout` with the `serde_json`
|
||||||
|
crate.
|
||||||
|
|
||||||
|
Chapters can be accessed either directly (by recursively iterating over
|
||||||
|
chapters) or via the `Book::for_each_mut()` convenience method.
|
||||||
|
|
||||||
|
The `chapter.content` is just a string which happens to be markdown. While it's
|
||||||
|
entirely possible to use regular expressions or do a manual find & replace,
|
||||||
|
you'll probably want to process the input into something more computer-friendly.
|
||||||
|
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
|
||||||
|
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] crate allowing you to
|
||||||
|
translate events back into markdown text.
|
||||||
|
|
||||||
|
The following code block shows how to remove all emphasis from markdown,
|
||||||
|
without accidentally breaking the document.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
[pc]: https://crates.io/crates/pulldown-cmark
|
||||||
|
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
||||||
|
[example]: https://github.com/rust-lang/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
|
||||||
|
[`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
|
||||||
|
@ -45,53 +45,57 @@ at your disposal.
|
||||||
|
|
||||||
### 1. toc
|
### 1. toc
|
||||||
|
|
||||||
The toc helper is used like this
|
The toc helper is used like this
|
||||||
|
|
||||||
```handlebars
|
```handlebars
|
||||||
{{#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">
|
||||||
<li><a href="link/to/file.html">Some chapter</a></li>
|
<li><a href="link/to/file.html">Some chapter</a></li>
|
||||||
<li>
|
<li>
|
||||||
<ul class="section">
|
<ul class="section">
|
||||||
<li><a href="link/to/other_file.html">Some other Chapter</a></li>
|
<li><a href="link/to/other_file.html">Some other Chapter</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</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>
|
||||||
var chapters = {{chapters}};
|
var chapters = {{chapters}};
|
||||||
// Processing here
|
// Processing here
|
||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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}}
|
||||||
```
|
```
|
||||||
|
|
||||||
The inner html will only be rendered if the previous / next chapter exists.
|
The inner html will only be rendered if the previous / next chapter exists.
|
||||||
Of course the inner html can be changed to your liking.
|
Of course the inner html can be changed to your liking.
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
*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.
|
212
src/book/book.rs
212
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,21 +39,23 @@ 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 {
|
||||||
if !filename.exists() {
|
let filename = src_dir.join(location);
|
||||||
if let Some(parent) = filename.parent() {
|
if !filename.exists() {
|
||||||
if !parent.exists() {
|
if let Some(parent) = filename.parent() {
|
||||||
fs::create_dir_all(parent)?;
|
if !parent.exists() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
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 {
|
||||||
|
src_dir.join(link_location)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut f = File::open(&location)
|
||||||
|
.with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
|
||||||
|
|
||||||
|
let mut content = String::new();
|
||||||
|
f.read_to_string(&mut content).with_context(|| {
|
||||||
|
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
|
||||||
|
.strip_prefix(src_dir)
|
||||||
|
.expect("Chapters are always inside a book");
|
||||||
|
|
||||||
|
Chapter::new(&link.name, content, stripped, parent_names.clone())
|
||||||
} else {
|
} else {
|
||||||
src_dir.join(&link.location)
|
Chapter::new_draft(&link.name, parent_names.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut f = File::open(&location)
|
let mut sub_item_parents = parent_names;
|
||||||
.chain_err(|| format!("Chapter file not found, {}", link.location.display()))?;
|
|
||||||
|
|
||||||
let mut content = String::new();
|
|
||||||
f.read_to_string(&mut content)
|
|
||||||
.chain_err(|| format!("Unable to read \"{}\" ({})", link.name, location.display()))?;
|
|
||||||
|
|
||||||
let stripped = location
|
|
||||||
.strip_prefix(&src_dir)
|
|
||||||
.expect("Chapters are always inside a book");
|
|
||||||
|
|
||||||
let mut sub_item_parents = parent_names.clone();
|
|
||||||
let mut ch = Chapter::new(&link.name, content, stripped, parent_names);
|
|
||||||
ch.number = link.number.clone();
|
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");
|
||||||
fs::create_dir(&cssdir)?;
|
if !cssdir.exists() {
|
||||||
|
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)?;
|
||||||
|
|
||||||
let mut print_css = File::create(cssdir.join("print.css"))?;
|
if html_config.print.enable {
|
||||||
print_css.write_all(theme::PRINT_CSS)?;
|
let mut print_css = File::create(cssdir.join("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() {
|
||||||
writeln!(f, "# Summary")?;
|
trace!("No summary found creating stub summary and chapter_1.md.");
|
||||||
writeln!(f, "")?;
|
let mut f = File::create(&summary).with_context(|| "Unable to create SUMMARY.md")?;
|
||||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
writeln!(f, "# Summary")?;
|
||||||
|
writeln!(f)?;
|
||||||
let chapter_1 = src_dir.join("chapter_1.md");
|
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||||
let mut f = File::create(&chapter_1).chain_err(|| "Unable to create chapter_1.md")?;
|
|
||||||
writeln!(f, "# Chapter 1")?;
|
|
||||||
|
|
||||||
|
let chapter_1 = src_dir.join("chapter_1.md");
|
||||||
|
let mut f = File::create(chapter_1).with_context(|| "Unable to create chapter_1.md")?;
|
||||||
|
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(())
|
||||||
}
|
}
|
||||||
|
|
622
src/book/mod.rs
622
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,18 +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, PreprocessorContext};
|
use crate::preprocess::{
|
||||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
|
||||||
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 {
|
||||||
|
@ -34,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 {
|
||||||
|
@ -53,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() {
|
||||||
|
@ -65,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);
|
||||||
}
|
}
|
||||||
|
@ -74,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)?;
|
||||||
|
@ -94,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) => {}
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
|
@ -118,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,115 +202,176 @@ 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 preprocess_ctx = PreprocessorContext::new(
|
||||||
|
self.root.clone(),
|
||||||
|
self.config.clone(),
|
||||||
|
renderer.name().to_string(),
|
||||||
|
);
|
||||||
let mut preprocessed_book = self.book.clone();
|
let mut preprocessed_book = self.book.clone();
|
||||||
let preprocess_ctx = PreprocessorContext::new(self.root.clone(),
|
|
||||||
self.config.clone(),
|
|
||||||
renderer.name().to_string());
|
|
||||||
|
|
||||||
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()?;
|
||||||
|
|
||||||
// FIXME: Is "test" the proper renderer name to use here?
|
let mut chapter_found = false;
|
||||||
let preprocess_context = PreprocessorContext::new(self.root.clone(),
|
|
||||||
self.config.clone(),
|
|
||||||
"test".to_string());
|
|
||||||
|
|
||||||
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
|
struct TestRenderer;
|
||||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
impl Renderer for TestRenderer {
|
||||||
// actual markdown files.
|
// FIXME: Is "test" the proper renderer name to use here?
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"test"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self, _: &RenderContext) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
};
|
||||||
|
|
||||||
// write preprocessed file to tempdir
|
if let Some(chapter) = chapter {
|
||||||
let path = temp_dir.path().join(&ch.path);
|
if ch.name != chapter && chapter_path.to_str() != Some(chapter) {
|
||||||
let mut tmpf = utils::fs::create_file(&path)?;
|
if chapter == "?" {
|
||||||
tmpf.write_all(content.as_bytes())?;
|
info!("Skipping chapter '{}'...", ch.name);
|
||||||
|
}
|
||||||
let output = Command::new("rustdoc")
|
continue;
|
||||||
.arg(&path)
|
|
||||||
.arg("--test")
|
|
||||||
.args(&library_args)
|
|
||||||
.output()?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
bail!(ErrorKind::Subprocess(
|
|
||||||
"Rustdoc returned an error".to_string(),
|
|
||||||
output
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
chapter_found = true;
|
||||||
|
info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
|
||||||
|
|
||||||
|
// write preprocessed file to tempdir
|
||||||
|
let path = temp_dir.path().join(chapter_path);
|
||||||
|
let mut tmpf = utils::fs::create_file(&path)?;
|
||||||
|
tmpf.write_all(ch.content.as_bytes())?;
|
||||||
|
|
||||||
|
let mut cmd = Command::new("rustdoc");
|
||||||
|
cmd.current_dir(temp_dir.path())
|
||||||
|
.arg(&chapter_path)
|
||||||
|
.arg("--test")
|
||||||
|
.args(&library_args);
|
||||||
|
|
||||||
|
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() {
|
||||||
|
failed = true;
|
||||||
|
error!(
|
||||||
|
"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(())
|
||||||
|
@ -274,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):
|
||||||
|
@ -319,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
|
||||||
|
@ -342,60 +449,149 @@ 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 preprocessor_keys = config.get("preprocessor")
|
// Collect the names of all preprocessors intended to be run, and the order
|
||||||
.and_then(|value| value.as_table())
|
// in which they should be run.
|
||||||
.map(|table| table.keys());
|
let mut preprocessor_names = TopologicalSort::<String>::new();
|
||||||
|
|
||||||
let mut preprocessors = if config.build.use_default_preprocessors {
|
if config.build.use_default_preprocessors {
|
||||||
default_preprocessors()
|
for name in DEFAULT_PREPROCESSORS {
|
||||||
} else {
|
preprocessor_names.insert(name.to_string());
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let preprocessor_keys = match preprocessor_keys {
|
|
||||||
Some(keys) => keys,
|
|
||||||
// If no preprocessor field is set, default to the LinkPreprocessor and
|
|
||||||
// IndexPreprocessor. This allows you to disable default preprocessors
|
|
||||||
// by setting "preprocess" to an empty list.
|
|
||||||
None => return Ok(preprocessors),
|
|
||||||
};
|
|
||||||
|
|
||||||
for key in preprocessor_keys {
|
|
||||||
match key.as_ref() {
|
|
||||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
|
||||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
|
||||||
_ => bail!("{:?} is not a recognised preprocessor", key),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(preprocessors)
|
if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::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())
|
||||||
|
{
|
||||||
|
// The `topological_sort` crate does not guarantee a stable order for ties, even across
|
||||||
|
// runs of the same program. Thus, we break ties manually by sorting.
|
||||||
|
// Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
|
||||||
|
// values ([1]), which may not be an alphabetical sort.
|
||||||
|
// As mentioned in [1], doing so depends on locale, which is not desirable for deciding
|
||||||
|
// preprocessor execution order.
|
||||||
|
// [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
|
||||||
|
names.sort();
|
||||||
|
for name in names {
|
||||||
|
let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
|
||||||
|
"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)
|
||||||
|
} else {
|
||||||
|
Err(Error::msg("Cyclic dependency detected in preprocessors"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
|
fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
|
||||||
|
table
|
||||||
|
.get("command")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.unwrap_or_else(|| format!("mdbook-{}", key))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||||
// look for the `command` field, falling back to using the key
|
// look for the `command` field, falling back to using the key
|
||||||
// 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 = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
let command = 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
|
||||||
|
@ -404,7 +600,11 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
|
||||||
///
|
///
|
||||||
/// 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());
|
||||||
|
@ -414,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() {
|
||||||
|
@ -477,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]
|
||||||
|
@ -492,8 +693,8 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn config_complains_if_unimplemented_preprocessor() {
|
fn can_determine_third_party_preprocessors() {
|
||||||
let cfg_str: &'static str = r#"
|
let cfg_str = r#"
|
||||||
[book]
|
[book]
|
||||||
title = "Some Book"
|
title = "Some Book"
|
||||||
|
|
||||||
|
@ -509,14 +710,142 @@ mod tests {
|
||||||
// make sure the `preprocessor.random` table exists
|
// make sure the `preprocessor.random` table exists
|
||||||
assert!(cfg.get_preprocessor("random").is_some());
|
assert!(cfg.get_preprocessor("random").is_some());
|
||||||
|
|
||||||
let got = determine_preprocessors(&cfg);
|
let got = determine_preprocessors(&cfg).unwrap();
|
||||||
|
|
||||||
assert!(got.is_err());
|
assert!(got.into_iter().any(|p| p.name() == "random"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preprocessors_can_provide_their_own_commands() {
|
||||||
|
let cfg_str = r#"
|
||||||
|
[preprocessor.random]
|
||||||
|
command = "python random.py"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let cfg = Config::from_str(cfg_str).unwrap();
|
||||||
|
|
||||||
|
// make sure the `preprocessor.random` table exists
|
||||||
|
let random = cfg.get_preprocessor("random").unwrap();
|
||||||
|
let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
|
||||||
|
|
||||||
|
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"]
|
||||||
"#;
|
"#;
|
||||||
|
@ -524,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 {
|
||||||
|
@ -134,12 +142,13 @@ impl From<Link> for SummaryItem {
|
||||||
///
|
///
|
||||||
/// ```text
|
/// ```text
|
||||||
/// summary ::= title prefix_chapters numbered_chapters
|
/// summary ::= title prefix_chapters numbered_chapters
|
||||||
/// suffix_chapters
|
/// suffix_chapters
|
||||||
/// title ::= "# " TEXT
|
/// title ::= "# " TEXT
|
||||||
/// | 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,85 +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![];
|
||||||
|
|
||||||
|
// We want the section numbers to be continues through all parts.
|
||||||
|
let mut root_number = SectionNumber::default();
|
||||||
|
let mut root_items = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// 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 name = stringify_events(link_content);
|
||||||
|
|
||||||
if href.is_empty() {
|
let path = if href.is_empty() {
|
||||||
Err(self.parse_error("You can't have an empty link."))
|
None
|
||||||
} else {
|
} else {
|
||||||
Ok(Link {
|
Some(PathBuf::from(href))
|
||||||
name: name,
|
};
|
||||||
location: PathBuf::from(href.to_string()),
|
|
||||||
number: None,
|
Link {
|
||||||
nested_items: Vec::new(),
|
name,
|
||||||
})
|
location: path,
|
||||||
|
number: None,
|
||||||
|
nested_items: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the numbered chapters. This assumes the opening list tag has
|
/// Parse the numbered chapters.
|
||||||
/// already been consumed by a previous parser.
|
fn parse_numbered(
|
||||||
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
|
&mut self,
|
||||||
|
root_items: &mut u32,
|
||||||
|
root_number: &mut SectionNumber,
|
||||||
|
) -> Result<Vec<SummaryItem>> {
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
let root_number = SectionNumber::default();
|
|
||||||
|
|
||||||
// we need to do this funny loop-match-if-let dance because a rule will
|
// For the first iteration, we want to just skip any opening paragraph tags, as that just
|
||||||
// close off any currently running list. Therefore we try to read the
|
// marks the start of the list. But after that, another opening paragraph indicates that we
|
||||||
// list items before the rule, then if we encounter a rule we'll add a
|
// have started a new part or the suffix chapters.
|
||||||
// separator and try to resume parsing numbered chapters if we start a
|
let mut first = true;
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
// if we've resumed after something like a rule the root sections
|
|
||||||
// will be numbered from 1. We need to manually go back and update
|
|
||||||
// them
|
|
||||||
update_section_numbers(&mut bunch_of_items, 0, items.len() as u32);
|
|
||||||
items.extend(bunch_of_items);
|
|
||||||
|
|
||||||
match self.next_event() {
|
match self.next_event() {
|
||||||
Some(Event::Start(Tag::Paragraph)) => {
|
Some(ev @ Event::Start(Tag::Paragraph)) => {
|
||||||
// we're starting the suffix chapters
|
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;
|
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
|
||||||
|
// will be numbered from 1. We need to manually go back and update
|
||||||
|
// them
|
||||||
|
update_section_numbers(&mut bunch_of_items, 0, *root_items);
|
||||||
|
*root_items += bunch_of_items.len() as u32;
|
||||||
|
items.extend(bunch_of_items);
|
||||||
|
}
|
||||||
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Some(Event::Rule) => {
|
||||||
|
items.push(SummaryItem::Separator);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
|
// something else... ignore
|
||||||
continue;
|
Some(_) => {}
|
||||||
} else {
|
|
||||||
break;
|
// EOF, bail...
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(_) => {
|
|
||||||
// something else... ignore
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
None => {
|
None => {
|
||||||
// EOF, bail...
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From now on, we cannot accept any new paragraph opening tags.
|
||||||
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -367,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
|
||||||
|
@ -378,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,
|
||||||
}
|
}
|
||||||
|
@ -395,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);
|
||||||
|
@ -404,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);
|
||||||
|
@ -423,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 {
|
||||||
debug!("Found a h1 in the SUMMARY");
|
match self.next_event() {
|
||||||
|
Some(Event::Start(Tag::Heading {
|
||||||
|
level: HeadingLevel::H1,
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -461,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
|
||||||
|
@ -484,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 {
|
||||||
|
@ -544,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";
|
||||||
|
@ -574,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);
|
||||||
|
@ -595,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);
|
||||||
|
@ -607,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());
|
||||||
|
@ -618,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -639,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);
|
||||||
}
|
}
|
||||||
|
@ -660,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);
|
||||||
}
|
}
|
||||||
|
@ -695,33 +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/mdBook/issues/779
|
||||||
|
/// Ensure section numbers are correctly incremented after a horizontal separator.
|
||||||
|
#[test]
|
||||||
|
fn keep_numbering_after_separator() {
|
||||||
|
let src =
|
||||||
|
"- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
|
||||||
|
let should_be = vec![
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from("First"),
|
||||||
|
location: Some(PathBuf::from("./first.md")),
|
||||||
|
number: Some(SectionNumber(vec![1])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
}),
|
||||||
|
SummaryItem::Separator,
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from("Second"),
|
||||||
|
location: Some(PathBuf::from("./second.md")),
|
||||||
|
number: Some(SectionNumber(vec![2])),
|
||||||
|
nested_items: Vec::new(),
|
||||||
|
}),
|
||||||
|
SummaryItem::Separator,
|
||||||
|
SummaryItem::Link(Link {
|
||||||
|
name: String::from("Third"),
|
||||||
|
location: Some(PathBuf::from("./third.md")),
|
||||||
|
number: Some(SectionNumber(vec![3])),
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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");
|
||||||
|
println!();
|
||||||
|
println!("Copying the default theme to {}", theme_dir.display());
|
||||||
// Skip this if `--force` is present
|
// Skip this if `--force` is present
|
||||||
if !args.is_present("force") {
|
if !args.get_flag("force") && theme_dir.exists() {
|
||||||
// Print warning
|
|
||||||
println!();
|
|
||||||
println!(
|
|
||||||
"Copying the default theme to {}",
|
|
||||||
builder.config().book.src.display()
|
|
||||||
);
|
|
||||||
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<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
if let Some(ignore) = args.get_one::<String>("ignore").map(|s| s.as_str()) {
|
||||||
|
match ignore {
|
||||||
if confirm() {
|
"git" => builder.create_gitignore(true),
|
||||||
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)");
|
||||||
|
if confirm() {
|
||||||
|
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;
|
||||||
|
|
201
src/cmd/serve.rs
201
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);
|
|
||||||
book.config
|
|
||||||
.set("output.html.livereload-url", &livereload_url)?;
|
|
||||||
|
|
||||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
|
||||||
book.config.build.build_dir = dest_dir.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let update_config = |book: &mut MDBook| {
|
||||||
|
book.config
|
||||||
|
.set("output.html.live-reload-endpoint", LIVE_RELOAD_ENDPOINT)
|
||||||
|
.expect("live-reload-endpoint update failed");
|
||||||
|
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||||
|
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')
|
||||||
|
.long("library-path")
|
||||||
|
.value_name("dir")
|
||||||
|
.value_delimiter(',')
|
||||||
|
.value_parser(NonEmptyStringValueParser::new())
|
||||||
|
.action(ArgAction::Append)
|
||||||
|
.help(
|
||||||
|
"A comma-separated list of directories to add to the crate \
|
||||||
|
search path when building tests",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.arg(Arg::with_name("library-path")
|
|
||||||
.short("L")
|
|
||||||
.long("library-path")
|
|
||||||
.value_name("dir")
|
|
||||||
.takes_value(true)
|
|
||||||
.require_delimiter(true)
|
|
||||||
.multiple(true)
|
|
||||||
.empty_values(false)
|
|
||||||
.help("A comma-separated list of directories to add to {n}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(())
|
||||||
}
|
}
|
||||||
|
|
215
src/cmd/watch.rs
215
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
828
src/config.rs
828
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();
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,208 @@
|
||||||
|
use super::{Preprocessor, PreprocessorContext};
|
||||||
|
use crate::book::Book;
|
||||||
|
use crate::errors::*;
|
||||||
|
use log::{debug, trace, warn};
|
||||||
|
use shlex::Shlex;
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
|
||||||
|
/// A custom preprocessor which will shell out to a 3rd-party program.
|
||||||
|
///
|
||||||
|
/// # Preprocessing Protocol
|
||||||
|
///
|
||||||
|
/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
|
||||||
|
/// execute the shell command `$cmd supports $renderer`. If the renderer is
|
||||||
|
/// supported, custom preprocessors should exit with a exit code of `0`,
|
||||||
|
/// any other exit code be considered as unsupported.
|
||||||
|
///
|
||||||
|
/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
|
||||||
|
/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
|
||||||
|
/// should then "return" a processed book by printing it to `stdout` as JSON.
|
||||||
|
/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
|
||||||
|
/// to parse the input provided by `mdbook`.
|
||||||
|
///
|
||||||
|
/// Exiting with a non-zero exit code while preprocessing is considered an
|
||||||
|
/// error. `stderr` is passed directly through to the user, so it can be used
|
||||||
|
/// for logging or emitting warnings if desired.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// An example preprocessor is available in this project's `examples/`
|
||||||
|
/// directory.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct CmdPreprocessor {
|
||||||
|
name: String,
|
||||||
|
cmd: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CmdPreprocessor {
|
||||||
|
/// Create a new `CmdPreprocessor`.
|
||||||
|
pub fn new(name: String, cmd: String) -> CmdPreprocessor {
|
||||||
|
CmdPreprocessor { name, cmd }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience function custom preprocessors can use to parse the input
|
||||||
|
/// written to `stdin` by a `CmdRenderer`.
|
||||||
|
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
|
||||||
|
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
|
||||||
|
let stdin = child.stdin.take().expect("Child has stdin");
|
||||||
|
|
||||||
|
if let Err(e) = self.write_input(stdin, book, ctx) {
|
||||||
|
// Looks like the backend hung up before we could finish
|
||||||
|
// sending it the render context. Log the error and keep going
|
||||||
|
warn!("Error writing the RenderContext to the backend, {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_input<W: Write>(
|
||||||
|
&self,
|
||||||
|
writer: W,
|
||||||
|
book: &Book,
|
||||||
|
ctx: &PreprocessorContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The command this `Preprocessor` will invoke.
|
||||||
|
pub fn cmd(&self) -> &str {
|
||||||
|
&self.cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command(&self) -> Result<Command> {
|
||||||
|
let mut words = Shlex::new(&self.cmd);
|
||||||
|
let executable = match words.next() {
|
||||||
|
Some(e) => e,
|
||||||
|
None => bail!("Command string was empty"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmd = Command::new(executable);
|
||||||
|
|
||||||
|
for arg in words {
|
||||||
|
cmd.arg(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Preprocessor for CmdPreprocessor {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
|
||||||
|
let mut cmd = self.command()?;
|
||||||
|
|
||||||
|
let mut child = cmd
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.spawn()
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Unable to start the \"{}\" preprocessor. Is it installed?",
|
||||||
|
self.name()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.write_input_to_child(&mut child, &book, ctx);
|
||||||
|
|
||||||
|
let output = child.wait_with_output().with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Error waiting for the \"{}\" preprocessor to complete",
|
||||||
|
self.name
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
trace!("{} exited with output: {:?}", self.cmd, output);
|
||||||
|
ensure!(
|
||||||
|
output.status.success(),
|
||||||
|
format!(
|
||||||
|
"The \"{}\" preprocessor exited unsuccessfully with {} status",
|
||||||
|
self.name, output.status
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
debug!(
|
||||||
|
"Checking if the \"{}\" preprocessor supports \"{}\"",
|
||||||
|
self.name(),
|
||||||
|
renderer
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cmd = match self.command() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"Unable to create the command for the \"{}\" preprocessor, {}",
|
||||||
|
self.name(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let outcome = cmd
|
||||||
|
.arg("supports")
|
||||||
|
.arg(renderer)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::inherit())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.status()
|
||||||
|
.map(|status| status.code() == Some(0));
|
||||||
|
|
||||||
|
if let Err(ref e) = outcome {
|
||||||
|
if e.kind() == io::ErrorKind::NotFound {
|
||||||
|
warn!(
|
||||||
|
"The command wasn't found, is the \"{}\" preprocessor installed?",
|
||||||
|
self.name
|
||||||
|
);
|
||||||
|
warn!("\tCommand: {}", self.cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::MDBook;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn 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) {
|
||||||
if index_md.exists() {
|
let mut index_md = source_dir.join(path.with_file_name("index.md"));
|
||||||
warn_readme_name_conflict(&ch.path, &index_md);
|
if index_md.exists() {
|
||||||
}
|
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();
|
||||||
ch.content = content;
|
let content =
|
||||||
|
replace_all(&ch.content, base, chapter_path, 0, &mut chapter_title);
|
||||||
|
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,14 +90,20 @@ 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(&new_content, rel_path, source, depth + 1));
|
replaced.push_str(&replace_all(
|
||||||
|
&new_content,
|
||||||
|
rel_path,
|
||||||
|
source,
|
||||||
|
depth + 1,
|
||||||
|
chapter_title,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
replaced.push_str(&new_content);
|
replaced.push_str(&new_content);
|
||||||
}
|
}
|
||||||
|
@ -79,13 +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.chain().skip(1) {
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,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> {
|
||||||
|
@ -109,11 +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, _) => Some(return_relative_path(base, &p)),
|
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
|
||||||
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
|
LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
|
||||||
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
|
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
|
||||||
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
|
LinkType::Title(_) => None,
|
||||||
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,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();
|
|
||||||
// subtract 1 since line numbers usually begin with 1
|
let next_element = parts.next();
|
||||||
let start = parts
|
let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
|
||||||
.next()
|
// subtract 1 since line numbers usually begin with 1
|
||||||
.and_then(|s| s.parse::<usize>().ok())
|
Some(value.saturating_sub(1))
|
||||||
.map(|val| val.saturating_sub(1));
|
} else if let Some("") = next_element {
|
||||||
|
None
|
||||||
|
} else if let Some(anchor) = next_element {
|
||||||
|
return RangeOrAnchor::Anchor(String::from(anchor));
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let end = parts.next();
|
let end = parts.next();
|
||||||
let has_end = end.is_some();
|
// If `end` is empty string or any other value that can't be parsed as a usize, treat this
|
||||||
let end = end.and_then(|s| s.parse::<usize>().ok());
|
// include as a range with only a start bound. However, if end isn't specified, include only
|
||||||
match start {
|
// the single line specified by `start`.
|
||||||
Some(start) => match end {
|
let end = end.map(|s| s.parse::<usize>());
|
||||||
Some(end) => LinkType::IncludeRange(
|
|
||||||
path,
|
match (start, end) {
|
||||||
Range {
|
(Some(start), Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(start..end)),
|
||||||
start: start,
|
(Some(start), Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(start..)),
|
||||||
end: end,
|
(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)),
|
||||||
None => if has_end {
|
|
||||||
LinkType::IncludeRangeFrom(path, RangeFrom { start: start })
|
|
||||||
} else {
|
|
||||||
LinkType::IncludeRange(
|
|
||||||
path,
|
|
||||||
Range {
|
|
||||||
start: start,
|
|
||||||
end: start + 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
None => match end {
|
|
||||||
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end: end }),
|
|
||||||
None => LinkType::IncludeRangeFull(path, RangeFull),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
@ -182,7 +290,16 @@ 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)),
|
||||||
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
|
("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)),
|
||||||
|
("playpen", Some(pth)) => {
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,43 +309,86 @@ impl<'a> Link<'a> {
|
||||||
_ => 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) => file_to_string(base.join(pat))
|
LinkType::Include(ref pat, ref range_or_anchor) => {
|
||||||
.map(|s| take_lines(&s, range.clone()))
|
let target = base.join(pat);
|
||||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
|
||||||
LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(base.join(pat))
|
fs::read_to_string(&target)
|
||||||
.map(|s| take_lines(&s, range.clone()))
|
.map(|s| match range_or_anchor {
|
||||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
|
||||||
LinkType::IncludeRangeTo(ref pat, ref range) => file_to_string(base.join(pat))
|
RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
|
||||||
.map(|s| take_lines(&s, range.clone()))
|
})
|
||||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
.with_context(|| {
|
||||||
LinkType::IncludeRangeFull(ref pat, _) => file_to_string(base.join(pat))
|
format!(
|
||||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
"Could not read file for link {} ({})",
|
||||||
LinkType::Playpen(ref pat, ref attrs) => {
|
self.link_text,
|
||||||
let contents = file_to_string(base.join(pat))
|
target.display(),
|
||||||
.chain_err(|| format!("Could not read file for link {}", self.link_text))?;
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
|
||||||
|
let target = base.join(pat);
|
||||||
|
|
||||||
|
fs::read_to_string(&target)
|
||||||
|
.map(|s| match range_or_anchor {
|
||||||
|
RangeOrAnchor::Range(range) => {
|
||||||
|
take_rustdoc_include_lines(&s, range.clone())
|
||||||
|
}
|
||||||
|
RangeOrAnchor::Anchor(anchor) => {
|
||||||
|
take_rustdoc_include_anchored_lines(&s, anchor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Could not read file for link {} ({})",
|
||||||
|
self.link_text,
|
||||||
|
target.display(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
LinkType::Playground(ref pat, ref attrs) => {
|
||||||
|
let target = base.join(pat);
|
||||||
|
|
||||||
|
let mut contents = fs::read_to_string(&target).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Could not read file for link {} ({})",
|
||||||
|
self.link_text,
|
||||||
|
target.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,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]
|
||||||
|
@ -292,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![]);
|
||||||
|
@ -302,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);
|
||||||
|
@ -324,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}}...";
|
||||||
|
@ -348,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}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -364,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}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -380,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:}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -396,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}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -412,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::}}",
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
|
@ -428,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);
|
||||||
|
@ -444,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);
|
||||||
|
@ -463,19 +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(PathBuf::from("file.rs"), vec!["editable"]),
|
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec!["editable"]),
|
||||||
link_text: "{{#playpen file.rs editable }}",
|
link_text: "{{#playground 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: "{{#playpen my.rs editable no_run should_panic}}",
|
link_text: "{{#playground my.rs editable no_run should_panic}}",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -483,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<_>>();
|
||||||
|
@ -493,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,19 +1,25 @@
|
||||||
//! 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;
|
||||||
|
|
||||||
|
mod cmd;
|
||||||
mod index;
|
mod index;
|
||||||
mod links;
|
mod links;
|
||||||
|
|
||||||
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
|
||||||
/// processing a book.
|
/// processing a book.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct PreprocessorContext {
|
pub struct PreprocessorContext {
|
||||||
/// The location of the book directory on disk.
|
/// The location of the book directory on disk.
|
||||||
pub root: PathBuf,
|
pub root: PathBuf,
|
||||||
|
@ -23,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: (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,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,29 +205,29 @@ 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]
|
||||||
fn test_next_previous() {
|
fn test_next_previous() {
|
||||||
let data = json!({
|
let data = json!({
|
||||||
"name": "two",
|
"name": "two",
|
||||||
"path": "two.path",
|
"path": "two.path",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
"name": "one",
|
"name": "one",
|
||||||
"path": "one.path"
|
"path": "one.path"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "two",
|
"name": "two",
|
||||||
"path": "two.path",
|
"path": "two.path",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "three",
|
"name": "three",
|
||||||
"path": "three.path"
|
"path": "three.path"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut h = Handlebars::new();
|
let mut h = Handlebars::new();
|
||||||
h.register_helper("previous", Box::new(previous));
|
h.register_helper("previous", Box::new(previous));
|
||||||
|
@ -209,23 +242,23 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_first() {
|
fn test_first() {
|
||||||
let data = json!({
|
let data = json!({
|
||||||
"name": "one",
|
"name": "one",
|
||||||
"path": "one.path",
|
"path": "one.path",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
"name": "one",
|
"name": "one",
|
||||||
"path": "one.path"
|
"path": "one.path"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "two",
|
"name": "two",
|
||||||
"path": "two.path",
|
"path": "two.path",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "three",
|
"name": "three",
|
||||||
"path": "three.path"
|
"path": "three.path"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut h = Handlebars::new();
|
let mut h = Handlebars::new();
|
||||||
h.register_helper("previous", Box::new(previous));
|
h.register_helper("previous", Box::new(previous));
|
||||||
|
@ -239,23 +272,23 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_last() {
|
fn test_last() {
|
||||||
let data = json!({
|
let data = json!({
|
||||||
"name": "three",
|
"name": "three",
|
||||||
"path": "three.path",
|
"path": "three.path",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{
|
{
|
||||||
"name": "one",
|
"name": "one",
|
||||||
"path": "one.path"
|
"path": "one.path"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "two",
|
"name": "two",
|
||||||
"path": "two.path",
|
"path": "two.path",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "three",
|
"name": "three",
|
||||||
"path": "three.path"
|
"path": "three.path"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut h = Handlebars::new();
|
let mut h = Handlebars::new();
|
||||||
h.register_helper("previous", Box::new(previous));
|
h.register_helper("previous", Box::new(previous));
|
||||||
|
|
|
@ -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 =
|
||||||
while level > current_level {
|
if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) {
|
||||||
out.write("<li>")?;
|
// Expand if folding is disabled, or if the section is an
|
||||||
out.write("<ol class=\"section\">")?;
|
// ancestor or the current section itself.
|
||||||
current_level += 1;
|
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 {
|
||||||
|
out.write("<li>")?;
|
||||||
|
out.write("<ol class=\"section\">")?;
|
||||||
|
current_level += 1;
|
||||||
|
}
|
||||||
|
write_li_open_tag(out, is_expanded, false)?;
|
||||||
}
|
}
|
||||||
out.write("<li>")?;
|
Ordering::Less => {
|
||||||
} else if level < current_level {
|
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;
|
}
|
||||||
|
write_li_open_tag(out, is_expanded, false)?;
|
||||||
}
|
}
|
||||||
out.write("<li>")?;
|
Ordering::Equal => {
|
||||||
} else {
|
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
|
||||||
out.write("<li")?;
|
|
||||||
if item.get("section").is_none() {
|
|
||||||
out.write(" class=\"affix\"")?;
|
|
||||||
}
|
}
|
||||||
out.write(">")?;
|
}
|
||||||
|
|
||||||
|
// 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
|
|
||||||
}
|
}
|
||||||
} else {
|
_ => {
|
||||||
false
|
out.write("<div>")?;
|
||||||
};
|
path_exists = 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,21 +297,19 @@ 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");
|
let mut builder = ammonia::Builder::new();
|
||||||
let mut builder = ammonia::Builder::new();
|
builder
|
||||||
builder
|
.tags(HashSet::new())
|
||||||
.tags(HashSet::new())
|
.tag_attributes(HashMap::new())
|
||||||
.tag_attributes(HashMap::new())
|
.generic_attributes(HashSet::new())
|
||||||
.generic_attributes(HashSet::new())
|
.link_rel(None)
|
||||||
.link_rel(None)
|
.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,34 +237,22 @@ 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
|
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// explicitly close the `stdin` file handle
|
|
||||||
drop(stdin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// explicitly close the `stdin` file handle
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue