mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-28 13:08:57 +00:00
Compare commits
716 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f1d7eb391 | |||
| dd358b8b95 | |||
| 21cf5fcaf4 | |||
| 8c7509759b | |||
| dc9ac0d0ff | |||
| 93b401ca9f | |||
| b137d8270a | |||
| 6db7757451 | |||
| 43fd42b757 | |||
| 5d1c4d2b9f | |||
| 425acd8bf6 | |||
| 414160e0df | |||
| 117c6aefe8 | |||
| 13d584d39a | |||
| 9bc89c24f5 | |||
| 5a8c4d5a4e | |||
| 98da83a552 | |||
| 5344a30a3b | |||
| d5d6b3673e | |||
| d2a2fe88d1 | |||
| ced44c86ce | |||
| ee402ad444 | |||
| 3d2155eb66 | |||
| bd24b03548 | |||
| 5cb55a0f37 | |||
| 163242f80f | |||
| 266bfcb8bf | |||
| d82fa02669 | |||
| 3a5207a0d2 | |||
| e38f3bb884 | |||
| 751cdd9b3b | |||
| 8df6bdca89 | |||
| f9df7545ad | |||
| 9b7a90afef | |||
| 2ae83b3247 | |||
| 5d907f234e | |||
| 3a005c4f13 | |||
| 6c2f50470a | |||
| d233783942 | |||
| 3d9e4c80a2 | |||
| a7f49015c1 | |||
| 68ed2631dc | |||
| 2814fea67e | |||
| 3388be9db6 | |||
| 9b89319820 | |||
| ad34089df5 | |||
| 7a4c6bc326 | |||
| 6ab386ae35 | |||
| 0abbfde912 | |||
| 0521defb63 | |||
| ea4d96610f | |||
| 4bc56564b4 | |||
| 013f7f09da | |||
| f082de1f92 | |||
| ad39dc8c0c | |||
| 481a76319f | |||
| 27a43d5aea | |||
| df1d5a0680 | |||
| 722109faf0 | |||
| d4dca7736a | |||
| 4e90becd52 | |||
| 26890e94eb | |||
| 55c83f9a59 | |||
| b837d82878 | |||
| cbdcdb1e0f | |||
| 4fea85289e | |||
| ba5f9132c9 | |||
| 991681daa2 | |||
| 2faf53e16b | |||
| 5430ed99de | |||
| fef4dfd05e | |||
| 05943db3b5 | |||
| 682fabd1ef | |||
| ee8ccc6d7e | |||
| 8ee0bcac2a | |||
| 0c4ad74788 | |||
| 7d944e4f1f | |||
| 67b2394c16 | |||
| 269d08a35f | |||
| f626e83e48 | |||
| 8ba18cd587 | |||
| 54c12fea89 | |||
| afb5ce71ce | |||
| 6fef6acf10 | |||
| a27411401b | |||
| 3718fc319b | |||
| c6c9463f8c | |||
| b410685318 | |||
| b742b29259 | |||
| df5d27ab61 | |||
| 2870155c0b | |||
| 9ba1075f48 | |||
| 4ae5b2e050 | |||
| 4369bd8f5e | |||
| 321b24a7b9 | |||
| 22e0c1fb54 | |||
| ba65e3c020 | |||
| c19ab952e3 | |||
| ecc29087b5 | |||
| 87e056de64 | |||
| 761200529f | |||
| 770351916e | |||
| 4b7d957b47 | |||
| a1c03c18a5 | |||
| 5d376b054f | |||
| 277d13c2bb | |||
| 4a9389d80a | |||
| 33593c13ae | |||
| eef82c18c2 | |||
| 7363580095 | |||
| 23a46b0597 | |||
| 51c79f294c | |||
| ce1e0e1873 | |||
| c2775b3b63 | |||
| bcb224d1cb | |||
| 8bc5f22866 | |||
| c3f2ef02c4 | |||
| 08e2c90d9d | |||
| d2bb2ba454 | |||
| ba912318cd | |||
| 3d1f057373 | |||
| 0679807517 | |||
| 7f0e4803e2 | |||
| fa76029a17 | |||
| 376c68957f | |||
| 222ec8d4b9 | |||
| 90715c1944 | |||
| 801e1b6059 | |||
| 14bb659b75 | |||
| eab3c50288 | |||
| 74396e7240 | |||
| 6c2539e886 | |||
| aba25d8521 | |||
| f0c97756de | |||
| f14c57d4c2 | |||
| b6f60cc8ad | |||
| 170516d343 | |||
| c931e91bef | |||
| db9151ae36 | |||
| f2d86b6d69 | |||
| 534cdd091b | |||
| fdf4a48638 | |||
| 58a3c5634e | |||
| f0ddb6e89f | |||
| 2b408db7cb | |||
| 4e4bd23e2b | |||
| 62d0eb606e | |||
| 0e9beeae14 | |||
| 5e4dbdee24 | |||
| 9435dae6cd | |||
| 813210bf47 | |||
| c372dfc544 | |||
| ac77200683 | |||
| f5afbc90a4 | |||
| b24760c819 | |||
| 6732957e42 | |||
| 1fd0006859 | |||
| 5a205346e3 | |||
| e193004659 | |||
| fe3a9c33d0 | |||
| d83116b88f | |||
| 5f56098ae7 | |||
| 0f935f4372 | |||
| df5c5a8c01 | |||
| 57a0c3bad8 | |||
| 2a52326a45 | |||
| 87e4ee1eeb | |||
| 45833eea9a | |||
| baf10e9b15 | |||
| 463a947df1 | |||
| cceebb4fd9 | |||
| afd5465a61 | |||
| fe604733de | |||
| da22f9f39f | |||
| 284ebbf75d | |||
| 2c08ccad87 | |||
| fa4e234bab | |||
| 35c4179ca1 | |||
| c14ba7ce34 | |||
| 601ce3add3 | |||
| 08ff39fd0b | |||
| 82a6ee3875 | |||
| 3bf592f870 | |||
| 241e205717 | |||
| fb2720ea8d | |||
| 14853befda | |||
| e7f2803d11 | |||
| 320015dd8f | |||
| 471c124a98 | |||
| aa44a109c6 | |||
| 41e5ca8422 | |||
| 53af8b2bb6 | |||
| bd885b135f | |||
| 62a6dde81a | |||
| 14b72b344f | |||
| ac03668063 | |||
| 2512e176c9 | |||
| b613393866 | |||
| 607a6e95de | |||
| 871f574ff7 | |||
| 377e470b15 | |||
| e0a3f0cea5 | |||
| 66727b2736 | |||
| 8215ad977f | |||
| a611dffbdf | |||
| 39f378d8cf | |||
| f00da58153 | |||
| 09b038b16d | |||
| cf601d97ad | |||
| 80edad53d4 | |||
| a972b32769 | |||
| efcaae3e60 | |||
| e0ee591638 | |||
| 310a6d909f | |||
| cefd8fc50f | |||
| a4422593f2 | |||
| e2c5d22756 | |||
| 84385d9dc3 | |||
| 6cfa4c8ab7 | |||
| c76ccc53e1 | |||
| d48eafb1a2 | |||
| 302d9b4b91 | |||
| a091a7f68b | |||
| 53621f6de3 | |||
| 288b965db3 | |||
| 578c9dcfc6 | |||
| 64de17b248 | |||
| d699dff872 | |||
| 501843d624 | |||
| 508ce4283e | |||
| 4aa8e66549 | |||
| 917a6f70d8 | |||
| 2f5e53da55 | |||
| c3cdefc90a | |||
| 9c580e4989 | |||
| dbc75f7352 | |||
| db841a8578 | |||
| 50c902c4e4 | |||
| 84b6e0bde6 | |||
| abc29c3fba | |||
| 8356c534f5 | |||
| 78ff2687b4 | |||
| 9b6586fb36 | |||
| ea133219d0 | |||
| c9b5d81a3c | |||
| d895c63211 | |||
| 8c86da67ca | |||
| a9a230dfc3 | |||
| 6d53e52771 | |||
| 52e10b7cfe | |||
| addac85696 | |||
| 13fb949d68 | |||
| f1b593cfbd | |||
| f998e2dec9 | |||
| 2c0bd53a91 | |||
| 6dd7f6162f | |||
| 475b0bb2b3 | |||
| e6343935e3 | |||
| 946883fd4c | |||
| b1b7e3148b | |||
| 32203f6438 | |||
| f2d3c5d2ed | |||
| 7e2f0ed4bc | |||
| a877acf905 | |||
| ac756ddf7e | |||
| 36e2236e6b | |||
| 72df665520 | |||
| 07785528a5 | |||
| bb579dd581 | |||
| a7d3064340 | |||
| 1a60778248 | |||
| cda4bb5bda | |||
| 7543781046 | |||
| 82f3ffad06 | |||
| dc4d2a1731 | |||
| 2523925915 | |||
| d5bbd8475c | |||
| 54d195268a | |||
| a0deb9509f | |||
| b0b1433594 | |||
| 17a2c0a37c | |||
| fd68ea1cc2 | |||
| 784874c776 | |||
| f45c6d3466 | |||
| 98af70902f | |||
| 41a67b5c4c | |||
| 83ee903af4 | |||
| e591b0e485 | |||
| bcc9a34742 | |||
| 6b56c7de7f | |||
| 7b42719699 | |||
| 64b8343f67 | |||
| fcc5089fa6 | |||
| 334f782aa8 | |||
| 1776f2bf77 | |||
| e259412f79 | |||
| 999a02f110 | |||
| 604d0fba65 | |||
| 0e8f7b870b | |||
| 1eebc9a46e | |||
| 0a94c80c44 | |||
| c70bb2fbd8 | |||
| 12c06b384f | |||
| 82a58e7ac3 | |||
| dcb56ef433 | |||
| 8da91de514 | |||
| 4b20685562 | |||
| fff7c5cc3a | |||
| 3065742b73 | |||
| ffd69ad1f2 | |||
| 9004011479 | |||
| 1046bb4133 | |||
| 5578505213 | |||
| a61f56662a | |||
| f42e48be03 | |||
| f640e101ba | |||
| b7da276ae0 | |||
| bf7c0989f5 | |||
| 65fdbe7f55 | |||
| c9af209a93 | |||
| 48ab44a776 | |||
| b2e684af8f | |||
| b38a2c11a4 | |||
| c31acef47a | |||
| e9a573e18a | |||
| c07ddc77f7 | |||
| 303fc992d4 | |||
| 4b7797f61b | |||
| f775d41933 | |||
| 7722e7bdfa | |||
| 346c05f0de | |||
| 73c69923b3 | |||
| afbca4bdf5 | |||
| b61371942b | |||
| 61c07e8876 | |||
| d317741144 | |||
| 275739a778 | |||
| cb3a43ed8a | |||
| ec8d1d8fc9 | |||
| 868a3186dd | |||
| 17b546181c | |||
| 2d1aa15af8 | |||
| 63ee94ff89 | |||
| 7d9dc08a38 | |||
| a836bdedb4 | |||
| 15ebae8cb1 | |||
| 315556310f | |||
| 9c0b26304b | |||
| acdaea9f6c | |||
| e481deb5bb | |||
| 6d3ac6074c | |||
| 5e611bde48 | |||
| 9448562156 | |||
| e7e8fe4d08 | |||
| 8320484b88 | |||
| 0e243e6f23 | |||
| 5cd25b0d21 | |||
| 3218ee7455 | |||
| 0552007b8a | |||
| 50bc89d9d9 | |||
| 743a88f53a | |||
| ee92c6dd99 | |||
| b6c54c17d1 | |||
| 992ef1ab74 | |||
| 960848ba34 | |||
| d9aca7a9df | |||
| 05a0de2d86 | |||
| 19e4f498cf | |||
| 3055139297 | |||
| 3d6b099dea | |||
| dd970e33b8 | |||
| 59ba479465 | |||
| 54c9ecf3c2 | |||
| 8308ae69a2 | |||
| bd6c3287ed | |||
| 24fc36deb1 | |||
| 2f44b61cec | |||
| 45ee503db1 | |||
| dc826da99b | |||
| 3ea659d582 | |||
| 34fc2831e0 | |||
| 1bf68a5aa0 | |||
| d72c397f23 | |||
| 2c69525148 | |||
| ee3a0ba801 | |||
| 7a4179a2a2 | |||
| 1d0083c5f4 | |||
| 33a6111a30 | |||
| a5d7a87e31 | |||
| c7f7c5b5ab | |||
| 55a7b99b4d | |||
| 8bfba604b3 | |||
| 2a5fa1c5fb | |||
| a3e95feb26 | |||
| 145b00f590 | |||
| 5f2785f0a8 | |||
| 412529c548 | |||
| 547ec6ade8 | |||
| eac513cfa3 | |||
| 5ec8a1f18c | |||
| c923a7e01a | |||
| 366e6ed105 | |||
| 52078eb666 | |||
| d9ddbff443 | |||
| 01b160f9a1 | |||
| 23fcb817a4 | |||
| f531a3f984 | |||
| c777023e9f | |||
| 62c99759c1 | |||
| 6951c21d19 | |||
| 3a8289483e | |||
| 4079de9587 | |||
| b86d171082 | |||
| 959b272e65 | |||
| 684440e2d1 | |||
| 4740cd3b06 | |||
| b3131d99d2 | |||
| b0fd2a67ea | |||
| 7c83869869 | |||
| 57d5c23eaf | |||
| 8d92b74e61 | |||
| 4e2cda99cc | |||
| dd250e0339 | |||
| 741b158631 | |||
| 843889303e | |||
| 4ad2b040b4 | |||
| f5916b5c59 | |||
| d9da4e9a08 | |||
| 1d0f6370ad | |||
| 12a7a4c4cf | |||
| e4127d961e | |||
| 8e97655040 | |||
| 9b61b87873 | |||
| 4a99e37b2f | |||
| 752b5e9b7c | |||
| b318f2fe94 | |||
| 01c46030b6 | |||
| 9803b05d8a | |||
| 1337858fe6 | |||
| 925b9e88ad | |||
| 7305011a8b | |||
| 489f23f74e | |||
| 370799ac55 | |||
| 4a9adb9440 | |||
| d8898b7a1f | |||
| bb7e081a37 | |||
| c3ffa5e0ef | |||
| 724bc2b59c | |||
| 178d2e2a48 | |||
| 4d710c129c | |||
| e4dbf62c0f | |||
| b0cd23fd34 | |||
| af5764bc3b | |||
| 6309da1214 | |||
| 38c9bfa357 | |||
| f194cb513c | |||
| 03ff892727 | |||
| 8cc0f64f54 | |||
| 532e8b0388 | |||
| 35b3d50012 | |||
| 72ff9996ce | |||
| 0759c4dd78 | |||
| 82a1a151e4 | |||
| ff6e8387fb | |||
| f71601e8d3 | |||
| 234a37726f | |||
| 65a7979874 | |||
| 6a00bf0fc0 | |||
| 853f3d6482 | |||
| 27de7cd914 | |||
| 2318cae1e6 | |||
| cb313c0fab | |||
| 4f25056017 | |||
| 5baebb8754 | |||
| 1eea994e73 | |||
| 8a597ff3f1 | |||
| 42fd7b12c0 | |||
| fd78a06b11 | |||
| 80efc8e086 | |||
| c0dcb886ef | |||
| df39128894 | |||
| 3ab5b57b4f | |||
| 49174d1731 | |||
| ed3b77b655 | |||
| 55a1aee164 | |||
| d7caf6686e | |||
| 71dad602ec | |||
| 30d8719c4b | |||
| ad81d38d07 | |||
| 6740b1b99c | |||
| f5f2cccbf4 | |||
| f45a1faa63 | |||
| 5a0dbe8cb7 | |||
| 2eb45e4699 | |||
| f7039a174a | |||
| 5690704444 | |||
| 8a8ffb6134 | |||
| 5b23dd8ffd | |||
| 5fc0c6b507 | |||
| c8e72092c7 | |||
| 9172948c18 | |||
| 2bec7c5e10 | |||
| 907ce6dbcd | |||
| 898000a580 | |||
| f95beed58b | |||
| 4baaa41ac9 | |||
| 1f7d349aed | |||
| c0d922560d | |||
| f5ded5e5e1 | |||
| 56d1720aae | |||
| 02d7102c68 | |||
| 592b32b036 | |||
| 1213997341 | |||
| 4e620f34ad | |||
| 534edaceac | |||
| ee13b4bf75 | |||
| 924006d508 | |||
| 350f34561a | |||
| 324fbb2f69 | |||
| 5539ab48a5 | |||
| c25157fc40 | |||
| d1b2950993 | |||
| d8995200e6 | |||
| bc389ab4a1 | |||
| cf12d09092 | |||
| 8e18adbe4c | |||
| f1b5e3de0c | |||
| 0e0a23b794 | |||
| d9329295d6 | |||
| 750fb862d4 | |||
| 131c19dee6 | |||
| 870ca4da0c | |||
| 54e1be1e77 | |||
| ce259eb4ec | |||
| 7424395171 | |||
| 6125df5834 | |||
| 99cd3148ba | |||
| 1a1ace0ae1 | |||
| 22bae559db | |||
| 5cee07ee8c | |||
| 4250f559b0 | |||
| 3293c39450 | |||
| 5b447d030b | |||
| f02680dc73 | |||
| cee7643f00 | |||
| a53ec62e4a | |||
| 795da31af8 | |||
| f70c29fea5 | |||
| decd8dbdc5 | |||
| edc954f2ac | |||
| 93bc86a0fe | |||
| 75b217fc24 | |||
| d6b44cc149 | |||
| 0f0ec4cf3d | |||
| 06ab84abe3 | |||
| bb5761e885 | |||
| d60a9763cd | |||
| e366c05238 | |||
| 690cdbe505 | |||
| fd701fc37c | |||
| 4d38baf7a2 | |||
| 7a999e01d5 | |||
| f3a5464306 | |||
| 2c57a37afb | |||
| 244b0f8efc | |||
| b4dc31e82b | |||
| 1583ede3d7 | |||
| 572994a17e | |||
| 796dd4c13a | |||
| e02f29e763 | |||
| 6805b02c63 | |||
| 21949ccd04 | |||
| 7b766f692b | |||
| c9aca86dcc | |||
| 99d9d49610 | |||
| fd98746c2b | |||
| 2efd234824 | |||
| 8be4304b5c | |||
| c984eba2bc | |||
| 2bb2a2a330 | |||
| f12163534d | |||
| e9d7c71911 | |||
| 59b3b0fadd | |||
| b426aba846 | |||
| 89885e17a5 | |||
| b66e9d2bed | |||
| d0f910d65f | |||
| 8e41aca8a7 | |||
| 4fec7760a1 | |||
| e6cbea9c70 | |||
| 7945f19285 | |||
| 5bd1269268 | |||
| 1fdd8a7611 | |||
| 112782d118 | |||
| b88b4a0578 | |||
| a964e19715 | |||
| c47166d700 | |||
| 77834f8166 | |||
| 0fc34e0a35 | |||
| df16f3d90d | |||
| ccd2f6ecdc | |||
| 19f1d31758 | |||
| ee2420cbcc | |||
| 322cf59a34 | |||
| 66841b1bb7 | |||
| 74ea419c53 | |||
| 0c8513801c | |||
| 90789aa5b9 | |||
| 1d13624d3d | |||
| 9e172549d1 | |||
| 0ba36c6f4e | |||
| c47093e82a | |||
| 57bdc102bc | |||
| be651f0138 | |||
| 974819bafb | |||
| f31a272b1f | |||
| a492c66321 | |||
| 85930161f7 | |||
| 4f9ece1ec1 | |||
| 748f004609 | |||
| cb709930bb | |||
| 98027cb008 | |||
| 3d8db17bc3 | |||
| 3c605ea809 | |||
| bd931c5cbd | |||
| c6e3a27284 | |||
| b5d7e7966f | |||
| be2da0fab1 | |||
| 0da50819d6 | |||
| 9f9265ad76 | |||
| a5ebbc34c7 | |||
| 33e32b20d9 | |||
| 238cd9f7f0 | |||
| bac25cdf29 | |||
| c15399613c | |||
| d6f7f1f4fb | |||
| a67dad8cbe | |||
| b549158483 | |||
| 9a2b03b183 | |||
| 2b2b4f46c2 | |||
| 6344928580 | |||
| 2dbf9b0d4a | |||
| be4d2679a4 | |||
| 70189a5ee2 | |||
| ad233c0d8e | |||
| 41c3db4c04 | |||
| 7f01f4b048 | |||
| 5ed5844490 | |||
| 777963c97c | |||
| 2f3d6f8dff | |||
| 2ae3924365 | |||
| d172aaefee | |||
| f0ea7bc53b | |||
| 793600ab2f | |||
| 354b4afba1 | |||
| 8569a74103 | |||
| b314972195 | |||
| 52179163dd | |||
| 8ed9f645fc | |||
| e1594cde2c | |||
| 60993d3123 | |||
| 39b2aaf489 | |||
| 58b04da60f | |||
| d9c020194d | |||
| ec586dd3cd | |||
| 8043216170 | |||
| 3c7372dff5 | |||
| 2cab018791 | |||
| 9467cda131 | |||
| 3382f1b084 | |||
| ae52ef5b78 | |||
| f88951546d | |||
| 7d90cd2a9e | |||
| 94a9db3d54 | |||
| 3f36d83bfb | |||
| c7e3370ac0 | |||
| bcb34dcd79 | |||
| 71905aa0c8 | |||
| dcbf115f25 | |||
| 668f2f9e2c | |||
| 530975b4fb | |||
| d35ce31af2 | |||
| e5f17cd694 | |||
| b50dea9c70 | |||
| 6b76032bb1 | |||
| 85b09eda56 | |||
| c4be78a81f | |||
| e7309f71d2 | |||
| 6c979c9419 | |||
| 7fd7732345 | |||
| 960ef4d945 | |||
| 92523fb4b5 | |||
| 66bb46df22 | |||
| 0454710d3b | |||
| c9a31cc987 | |||
| f03c316274 | |||
| 001a4f91be | |||
| 44a3bfe296 | |||
| a9ce6f808d | |||
| e5c667e0fa | |||
| e36f274dbe | |||
| 2a88427e74 | |||
| ed48f5f3ee | |||
| 7b4accb117 | |||
| d6437d31d4 | |||
| 3dd33f1f3c | |||
| 3a0f00411b | |||
| 5b655bb29a | |||
| faf10605e8 | |||
| c52aecdbd5 | |||
| 5c98b3350b | |||
| 6f4e1d10c1 | |||
| 870886599a | |||
| 1b9801b8a4 | |||
| 897463b22e | |||
| aa4894e94c |
@@ -0,0 +1,10 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
theme/**/*
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"indent": [ 2, 4 ],
|
||||||
|
"quotes": [ 2, "single" ],
|
||||||
|
"linebreak-style": [ 2, "unix" ],
|
||||||
|
"semi": [ 2, "always" ],
|
||||||
|
"no-unused-vars": [ 2, {
|
||||||
|
"vars": "all",
|
||||||
|
"args": "none"
|
||||||
|
} ],
|
||||||
|
"spaced-comment": [ 2, "always" ]
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"mocha": true,
|
||||||
|
"browser": true
|
||||||
|
},
|
||||||
|
"extends": "eslint:recommended"
|
||||||
|
}
|
||||||
@@ -24,8 +24,5 @@ build/Release
|
|||||||
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
|
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
theme/javascript/vendors
|
|
||||||
theme/vendors
|
|
||||||
|
|
||||||
# vim swapfile
|
# vim swapfile
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
+6
-2
@@ -1,4 +1,8 @@
|
|||||||
|
sudo: false
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "0.11"
|
- "stable"
|
||||||
- "0.10"
|
- "4.1"
|
||||||
|
- "0.12"
|
||||||
|
before_install:
|
||||||
|
- npm install svgexport -g
|
||||||
|
|||||||
+49
-4
@@ -7,12 +7,57 @@ Names below are ordered by first contribution.
|
|||||||
Author
|
Author
|
||||||
------
|
------
|
||||||
|
|
||||||
- Samy Pessé <samypesse@gmail.com>
|
- Samy Pessé <samypesse@gmail.com> (@SamyPesse)
|
||||||
- Aaron O'Mullan <aaron.omullan@gmail.com>
|
- Aaron O'Mullan <aaron.omullan@gmail.com> (@AaronO)
|
||||||
|
|
||||||
|
|
||||||
Contributors
|
Contributors
|
||||||
------------
|
------------
|
||||||
|
|
||||||
- Nijiko Yonskai
|
- Nijiko Yonskai (@Nijikokun)
|
||||||
- Herman Starikov
|
- Herman Starikov (@Hermanya)
|
||||||
|
|
||||||
|
Translators
|
||||||
|
------------
|
||||||
|
|
||||||
|
- German
|
||||||
|
- Winnie (@winniehell)
|
||||||
|
- Italian
|
||||||
|
- Giulio Bonanome (@gbonanome)
|
||||||
|
- Russian
|
||||||
|
- Andrey Akinshin (@AndreyAkinshin)
|
||||||
|
- Norwegian
|
||||||
|
- Knut Melvær (@kmelve)
|
||||||
|
- Persian (Farsi)
|
||||||
|
- Mohammad Hossein Mojtahedi (@mhm5000)
|
||||||
|
- Polish
|
||||||
|
- Łukasz Szeremeta (@lszeremeta)
|
||||||
|
- Portuguese
|
||||||
|
- Ryan O'Mullan (@RyanOM)
|
||||||
|
- Spanish
|
||||||
|
- Ryan O'Mullan (@RyanOM)
|
||||||
|
- Simplifiled Chinese
|
||||||
|
- Hu Hao (@howiehu)
|
||||||
|
- Traditional Chinese
|
||||||
|
- Hu Hao (@howiehu)
|
||||||
|
- French
|
||||||
|
- Samy Pessé (@SamyPesse)
|
||||||
|
- Romanian
|
||||||
|
- Iancu Aurel (@awrelll)
|
||||||
|
- Finnish
|
||||||
|
- Tommi Savikko (@savikko)
|
||||||
|
- Japanese
|
||||||
|
- Iancu Aurel (@awrelll)
|
||||||
|
- Korean
|
||||||
|
- Iancu Aurel (@awrelll)
|
||||||
|
- SangYeob Yu (@deminoth)
|
||||||
|
- Vietnamese
|
||||||
|
- Hong Nguyen (@nghong)
|
||||||
|
- Hebrew
|
||||||
|
- @a-moses
|
||||||
|
- Ukrainian
|
||||||
|
- @Karnaukhov
|
||||||
|
- Czech
|
||||||
|
- @mjanda
|
||||||
|
- Swedish
|
||||||
|
- Jacob Burenstam (@buren)
|
||||||
|
|||||||
+238
@@ -1,4 +1,242 @@
|
|||||||
# Release notes
|
# Release notes
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## 2.6.3
|
||||||
|
- Fix parsing bug with inline HTML in Markdown
|
||||||
|
|
||||||
|
## 2.6.2
|
||||||
|
- Fix `gitbook.state.bookRoot`, `gitbook.state.root` now has a trailing slash
|
||||||
|
- Update nunjucks to 2.2.0
|
||||||
|
|
||||||
|
## 2.6.1
|
||||||
|
- Use CamelCase for `gitbook.state.innerLanguage`
|
||||||
|
|
||||||
|
## 2.6.0
|
||||||
|
- Close sidebar after clicking a link on mobile
|
||||||
|
- Add root for multilingual books: `gitbook.state.bookRoot`
|
||||||
|
- Fix color of h6 headings
|
||||||
|
- Fix bottom margin of lists and blockquotes
|
||||||
|
- Add Swedish translation (`sv`)
|
||||||
|
- Add Czech translation (`cs`)
|
||||||
|
- Fix locale names for `zh-hans` and `jp`
|
||||||
|
- Update plugin `search` to fix crashes
|
||||||
|
- Aceept case-incensitive structure files (README, GLOSSARY, etc)
|
||||||
|
|
||||||
|
## 2.5.2
|
||||||
|
- Fix custom stylesheets for ebook generation
|
||||||
|
- Trigger event `start` before `page.change`
|
||||||
|
|
||||||
|
## 2.5.1
|
||||||
|
- Fix calcul of `gitbook.state.root` when serving HTML index as `/`
|
||||||
|
|
||||||
|
## 2.5.0
|
||||||
|
- Font settings, sharing and search are externalized as default plugins
|
||||||
|
- Plugins can define a configuration schema in the manifest, this schema will be used to validate configuration during build
|
||||||
|
- New Node.js API for plugin: `book.formatString(type, content)`
|
||||||
|
- New client side API for website plugins: `gitbook.toolbar.createButton(opts)`
|
||||||
|
- Better header/footer for PDF, CSS wil be inlined to easily style the header/footer
|
||||||
|
- Cleaner table of contents for ebooks
|
||||||
|
- Support for RTL in ebook's table of contents
|
||||||
|
- Better colors for mobi (links and code blocks)
|
||||||
|
- Fix installation of plugins when using a pre-release
|
||||||
|
- Fix support for pre-releases
|
||||||
|
- Update asciidoc parser to remove git dependency
|
||||||
|
- Fix templating in imported content
|
||||||
|
- Fix querystring in image urls
|
||||||
|
- Normalize heading IDs like GitHub
|
||||||
|
- Use Arial as default font for PDFs
|
||||||
|
- Add `root`, `chapterTitle` and `filepath` to `gitbook.state` JS API
|
||||||
|
|
||||||
|
## 2.4.3
|
||||||
|
- Add ukrainian translation (`uk`)
|
||||||
|
- Add `book.json` configuration for maximum size of search index
|
||||||
|
- Improve reliability of summary parser
|
||||||
|
|
||||||
|
## 2.4.2
|
||||||
|
- Default plugins should not be installed by `gitbook install`
|
||||||
|
- Limit search index size to avoid crash during generation
|
||||||
|
- Fix code highlighting for html without language specified
|
||||||
|
- Fix warning message for gitbook version when building a multilingual book
|
||||||
|
|
||||||
|
## 2.4.1
|
||||||
|
- Fix disabling of default plugins, ex: `-highlight`
|
||||||
|
|
||||||
|
## 2.4.0
|
||||||
|
- Fix page being updated when user wants to open a link in a new tab
|
||||||
|
- Plugins can now replaced default code highlighter
|
||||||
|
- Add semantic information for screen readers (web version)
|
||||||
|
- Content references accept absolute paths, resolved to book folder
|
||||||
|
- Improve overall reliability
|
||||||
|
|
||||||
|
## 2.3.3
|
||||||
|
- Fix bug in SUMMARY parsing preventing multiple entries without filenames
|
||||||
|
|
||||||
|
## 2.3.2
|
||||||
|
- Fix blocks (like maths) in Asciidoc
|
||||||
|
- Fix error when checking gitbook version
|
||||||
|
|
||||||
|
## 2.3.1
|
||||||
|
- Fix black font color for ebooks (mobi, pdf and epub)
|
||||||
|
- Fix ISO code for korean language
|
||||||
|
- Fix korean translation
|
||||||
|
- Fix syntax highlighting for asciidoc
|
||||||
|
- Fix inline html escaping in markdown
|
||||||
|
- Add warning for file outside SUMMARY
|
||||||
|
- Force SUMMARY entries to be unique by filename
|
||||||
|
|
||||||
|
## 2.3.0
|
||||||
|
- Fix nunjucks issue with multiple `{% raw %}` blocks
|
||||||
|
- Fix crash when git conref failed
|
||||||
|
- Fix crash when failed to download remote image (better error message)
|
||||||
|
- Fix flicking effect when changing page (big UX improvement)
|
||||||
|
- Add Hebrew translation (`he`)
|
||||||
|
- Add utility method `book.config.get` for plugins
|
||||||
|
- Hooks `page:before` and `page` are no longer deprecated
|
||||||
|
- Remove webfonts to make website lighter
|
||||||
|
- Make glossary's order case insensitive
|
||||||
|
|
||||||
|
## 2.2.0
|
||||||
|
- Fix direction in code blocks (always LTR)
|
||||||
|
- Add options `chapterMark` and `pageBreaksBefore` for PDF
|
||||||
|
- Update code highlighting library
|
||||||
|
- `book.json` accessible as `config` in templating syntax
|
||||||
|
- Add Vietnamese translation (`vi`)
|
||||||
|
|
||||||
|
## 2.1.0
|
||||||
|
- Fix error in calcul of `levels` in table of contents, error introduced a few versions ago
|
||||||
|
- Add optional `styles/print.css` to replace `print.css` used in ebook
|
||||||
|
|
||||||
|
## 2.0.4
|
||||||
|
- Fix `{% raw %}`, got confused with "fake" variable declarations
|
||||||
|
- Fix title of language chooser
|
||||||
|
- Fix the X-UA-Compatible meta tag
|
||||||
|
- Move style sheets to the <head> section
|
||||||
|
|
||||||
|
## 2.0.3
|
||||||
|
- Fix `gitbook init` for SUMMARY with empty entries
|
||||||
|
- Fix escaping of code blocks in markdown
|
||||||
|
|
||||||
|
## 2.0.2
|
||||||
|
- Fix relative links in windows
|
||||||
|
- Improve watcher in serve command (switch to chokidar)
|
||||||
|
- Add Romanian translation (`ro`)
|
||||||
|
- Add Finish translation (`fi`)
|
||||||
|
- Add Japanese translation (`jp`)
|
||||||
|
- Add Korean translation (`kr`)
|
||||||
|
|
||||||
|
## 2.0.1
|
||||||
|
- Improve error logging (display file, line and column)
|
||||||
|
- Add back support for `options.originalInput`
|
||||||
|
- Don't process math in markdown parser (delegated to `mathjax` plugin)
|
||||||
|
- Fix some cases of code blocks escaping
|
||||||
|
- Fix i18n for introduction title in json format
|
||||||
|
- Fix reload when book configuration is updated
|
||||||
|
- Fix backslashes in url when building on windows
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
- Fix page title of introduction
|
||||||
|
- Ignore codeblocks when replacing glossary terms
|
||||||
|
- Improve Unit Tests
|
||||||
|
- Fix scrolling position in website when preparing page
|
||||||
|
|
||||||
|
## 2.0.0-beta.5
|
||||||
|
- Fix progress order in json format
|
||||||
|
|
||||||
|
## 2.0.0-beta.4
|
||||||
|
- Fix default generator for use programmatically
|
||||||
|
- Add option "author" for html meta tags
|
||||||
|
- Fix links normalization (content and hash)
|
||||||
|
|
||||||
|
## 2.0.0-beta.3
|
||||||
|
- Fix odd cases with code blocks escaping
|
||||||
|
|
||||||
|
## 2.0.0-beta.2
|
||||||
|
- Fix definition of entry point title using SUMMARY.md
|
||||||
|
|
||||||
|
## 2.0.0-beta.1
|
||||||
|
- Fix windows incompatibility
|
||||||
|
- Add support for rtl (enabled by default for `ar` and `fa`)
|
||||||
|
- Escape code blocks in markdown parser
|
||||||
|
- Add Persian/Farsi translation (`fa`)
|
||||||
|
- Add Arabic translation (`ar`)
|
||||||
|
- Add Bengali translation (`bn`)
|
||||||
|
- Provide generator name in template context
|
||||||
|
|
||||||
|
## 2.0.0-alpha.9
|
||||||
|
- Fix links in sidebar
|
||||||
|
- Fix normalization of html link (README to index)
|
||||||
|
- Fix html snippets escaping
|
||||||
|
|
||||||
|
## 2.0.0-alpha.8
|
||||||
|
- Improve locale detection for i18n
|
||||||
|
- Fix chapter name for Glossary in pdf
|
||||||
|
- Don't escape html in glossary items
|
||||||
|
- Fix generation of multilingual book as ebook
|
||||||
|
- Add "post" block attribute to post-process
|
||||||
|
|
||||||
|
## 2.0.0-alpha.7
|
||||||
|
- Fix display of glossary in ebook formats
|
||||||
|
- Add default footer and header to pdf
|
||||||
|
- Fix generation of json format compatible with 1.x.x
|
||||||
|
- Add Simplifiled Chinese and Traditional Chinese translations
|
||||||
|
|
||||||
|
## 2.0.0-alpha.6
|
||||||
|
- Add es and pt translations
|
||||||
|
- Fix replacement of glossary terms
|
||||||
|
|
||||||
|
## 2.0.0-alpha.5
|
||||||
|
- Fix copy of files/covers
|
||||||
|
- Add back `finish:before` hook
|
||||||
|
|
||||||
|
## 2.0.0-alpha.4
|
||||||
|
- Fix copy of cover for multilingal books
|
||||||
|
|
||||||
|
## 2.0.0-alpha.3
|
||||||
|
- Norwegian translation
|
||||||
|
- Load plugins from book in priority
|
||||||
|
|
||||||
|
## 2.0.0-alpha.3
|
||||||
|
- Fix init command
|
||||||
|
- Update parsers to fix spaces in summary (`gitbook-parsers@0.3.1`)
|
||||||
|
|
||||||
|
## 2.0.0-alpha.1
|
||||||
|
- Externalize parsing into `gitbook-parsers` module
|
||||||
|
- Supports AsciiDoc and reStructuredText
|
||||||
|
- Hooks for page (`page:*`) are now deprecated, plugins should extend filters and blocks instead
|
||||||
|
- Hooks `summary` and `glossary` (after and before) have been removed
|
||||||
|
- Exercises and Quizzes are no longer parsed in the markdown parser
|
||||||
|
- Support for more markdown extensions: `.markdown`, `.mdown`
|
||||||
|
- Templates are rendered with nunjucks instead of swig, syntax is almost compatible, there is some changes with contexts and filters. `{{ super() }}` should be use instead of `{% parent %}`
|
||||||
|
- Clean output folder on build without removing `.git` and `.svn`
|
||||||
|
- MathJAX is no longer a default plugin
|
||||||
|
- SVG images are converted to PNG during generation of ebooks
|
||||||
|
- i18n in website and ebook (ru, it, de, fr)
|
||||||
|
- New templating syntax
|
||||||
|
- Content references (both internal and external)
|
||||||
|
- Glossary terms are handled during generation (also in ebook format)
|
||||||
|
|
||||||
|
## 1.5.0
|
||||||
|
- Fix `serve` command, broken by `1.4.2`
|
||||||
|
- Add nicer `dark` theme :)
|
||||||
|
|
||||||
|
## 1.4.2
|
||||||
|
- Force `process.exit` after builds, to prevent (possibly) lingering plugins
|
||||||
|
|
||||||
|
## 1.4.1
|
||||||
|
- Fix command 'install' without arguments
|
||||||
|
|
||||||
|
## 1.4.0
|
||||||
|
- Add command `gitbook install` to install plugins from book.json
|
||||||
|
- `package.json` is no longer necessary
|
||||||
|
|
||||||
|
## 1.3.4
|
||||||
|
- Add glossary to ebooks
|
||||||
|
- Fix autocover with new hook "finish:before"
|
||||||
|
- Add X-UA-Compatible meta tag for IE
|
||||||
|
|
||||||
|
## 1.3.3
|
||||||
|
- Fix parsing of lexed content using the client library
|
||||||
|
|
||||||
## 1.3.2
|
## 1.3.2
|
||||||
- ePub files are now passing validation from epubcheck
|
- ePub files are now passing validation from epubcheck
|
||||||
|
|||||||
-129
@@ -1,129 +0,0 @@
|
|||||||
module.exports = function (grunt) {
|
|
||||||
var path = require("path");
|
|
||||||
|
|
||||||
// Load NPM tasks
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-less');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-requirejs');
|
|
||||||
grunt.loadNpmTasks("grunt-bower-install-simple");
|
|
||||||
grunt.loadNpmTasks('grunt-browserify');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
|
||||||
|
|
||||||
// Init GRUNT configuraton
|
|
||||||
grunt.initConfig({
|
|
||||||
pkg: grunt.file.readJSON('package.json'),
|
|
||||||
'bower-install-simple': {
|
|
||||||
options: {
|
|
||||||
color: true,
|
|
||||||
production: false,
|
|
||||||
directory: "theme/vendors"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
less: {
|
|
||||||
development: {
|
|
||||||
options: {
|
|
||||||
compress: true,
|
|
||||||
yuicompress: true,
|
|
||||||
optimization: 2
|
|
||||||
},
|
|
||||||
files: {
|
|
||||||
"theme/assets/style.css": "theme/stylesheets/website.less",
|
|
||||||
"theme/assets/print.css": "theme/stylesheets/ebook.less"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
requirejs: {
|
|
||||||
compile: {
|
|
||||||
options: {
|
|
||||||
name: "gitbook",
|
|
||||||
baseUrl: "theme/javascript/",
|
|
||||||
out: "theme/assets/app.js",
|
|
||||||
preserveLicenseComments: false,
|
|
||||||
optimize: "uglify",
|
|
||||||
include: ["requireLib"],
|
|
||||||
paths: {
|
|
||||||
"jQuery": '../vendors/jquery/dist/jquery',
|
|
||||||
"lodash": '../vendors/lodash/dist/lodash',
|
|
||||||
"requireLib": '../vendors/requirejs/require',
|
|
||||||
"Mousetrap": '../vendors/mousetrap/mousetrap',
|
|
||||||
"lunr": '../vendors/lunr.js/lunr',
|
|
||||||
"URIjs": '../vendors/URIjs/src/',
|
|
||||||
"ace": '../vendors/ace-builds/src-noconflict/'
|
|
||||||
},
|
|
||||||
shim: {
|
|
||||||
'jQuery': {
|
|
||||||
exports: '$'
|
|
||||||
},
|
|
||||||
'lodash': {
|
|
||||||
exports: '_'
|
|
||||||
},
|
|
||||||
'Mousetrap': {
|
|
||||||
exports: 'Mousetrap'
|
|
||||||
},
|
|
||||||
'lunr': {
|
|
||||||
exports: 'lunr'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
copy: {
|
|
||||||
vendors: {
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
expand: true,
|
|
||||||
cwd: 'theme/vendors/fontawesome/fonts/',
|
|
||||||
src: ['**'],
|
|
||||||
dest: 'theme/assets/fonts/fontawesome/',
|
|
||||||
filter: 'isFile'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
browserify: {
|
|
||||||
dist: {
|
|
||||||
files: {
|
|
||||||
'gitbook.js': [
|
|
||||||
'./lib/parse/index.js'
|
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
postBundleCB: function (err, src, next) {
|
|
||||||
return next(null, '(function () { var define = undefined; '+src+'; })();')
|
|
||||||
},
|
|
||||||
browserifyOptions: {
|
|
||||||
'standalone': "gitbook"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
uglify: {
|
|
||||||
dist: {
|
|
||||||
files: {
|
|
||||||
'gitbook.min.js': ['gitbook.js']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
grunt.registerTask("bower-install", [ "bower-install-simple" ]);
|
|
||||||
|
|
||||||
// Build
|
|
||||||
grunt.registerTask('build', [
|
|
||||||
'bower-install',
|
|
||||||
'less',
|
|
||||||
'requirejs',
|
|
||||||
'copy:vendors'
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Bundle the library
|
|
||||||
grunt.registerTask('bundle', [
|
|
||||||
'browserify',
|
|
||||||
'uglify'
|
|
||||||
]);
|
|
||||||
|
|
||||||
grunt.registerTask('default', [
|
|
||||||
'build',
|
|
||||||
'bundle'
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
GitBook
|
GitBook
|
||||||
=======
|
=======
|
||||||
|
|
||||||
[](https://travis-ci.org/GitbookIO/gitbook)
|
|
||||||
[](http://badge.fury.io/js/gitbook)
|
[](http://badge.fury.io/js/gitbook)
|
||||||
|
[](https://travis-ci.org/GitbookIO/gitbook)
|
||||||
|
[](https://ci.appveyor.com/project/GitBook/gitbook)
|
||||||
|
[](https://slack.gitbook.com)
|
||||||
|
|
||||||
GitBook is a command line tool (and Node.js library) for building beautiful books using GitHub/Git and Markdown. Here is an example: [Learn Javascript](https://www.gitbook.io/book/GitBookIO/javascript). You can publish book easily online using [gitbook.io](https://www.gitbook.io) and an [editor](https://github.com/GitbookIO/editor) is available for Windows, Mac and Linux. You can follow [@GitBookIO](https://twitter.com/GitBookIO) on Twitter. Complete documentation is available at [help.gitbook.io](http://help.gitbook.io/).
|
GitBook is a command line tool (and Node.js library) for building beautiful books using GitHub/Git and Markdown (or AsciiDoc). Here is an example: [Learn Javascript](https://www.gitbook.com/book/GitBookIO/javascript).
|
||||||
|
|
||||||
|
You can publish and host book easily online using [gitbook.com](https://www.gitbook.com), a desktop editor is [also available](https://www.gitbook.com/editor).
|
||||||
|
|
||||||
|
Check out the [GitBook Community Slack Channel](https://slack.gitbook.com), Stay updated by following [@GitBookIO](https://twitter.com/GitBookIO) on Twitter or [GitBook](https://www.facebook.com/gitbookcom) on Facebook.
|
||||||
|
|
||||||
|
Complete documentation is available at [help.gitbook.com](http://help.gitbook.com/).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -13,127 +21,52 @@ GitBook is a command line tool (and Node.js library) for building beautiful book
|
|||||||
GitBook can be installed from **NPM** using:
|
GitBook can be installed from **NPM** using:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm install gitbook -g
|
$ npm install gitbook-cli -g
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the directories and files for a book from its [SUMMARY.md](https://github.com/GitbookIO/gitbook#book-format) file (if existing) using
|
||||||
|
|
||||||
|
```
|
||||||
|
$ gitbook init
|
||||||
```
|
```
|
||||||
|
|
||||||
You can serve a repository as a book using:
|
You can serve a repository as a book using:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ gitbook serve ./repository
|
$ gitbook serve
|
||||||
```
|
```
|
||||||
|
|
||||||
Or simply build the static website using:
|
Or simply build the static website using:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ gitbook build ./repository --output=./outputFolder
|
$ gitbook build
|
||||||
```
|
```
|
||||||
|
|
||||||
Options for commands `build` and `serve` are:
|
## Features
|
||||||
|
|
||||||
```
|
* [Output as a website or ebook (pdf, epub, mobi)](http://help.gitbook.com/format/output.html)
|
||||||
-o, --output <directory> Path to output directory, defaults to ./_book
|
* [Multi-Languages](http://help.gitbook.com/format/languages.html)
|
||||||
-f, --format <name> Change generation format, defaults to site, availables are: site, page, ebook, json
|
* [Glossary](http://help.gitbook.com/format/glossary.html)
|
||||||
--config <config file> Configuration file to use, defaults to book.js or book.json
|
* [Cover](http://help.gitbook.com/format/cover.html)
|
||||||
```
|
* [AsciiDoc Support](http://help.gitbook.com/format/asciidoc.html)
|
||||||
|
* [Variables and Templating](http://help.gitbook.com/format/templating.html)
|
||||||
GitBook loads the default configuration from a `book.json` file in the repository if it exists.
|
* [Content References](http://help.gitbook.com/format/conrefs.html)
|
||||||
|
* [Plugins](http://help.gitbook.com/format/plugins.html)
|
||||||
Here are the options that can be stored in this file:
|
* Interractive reader website:
|
||||||
|
* Search
|
||||||
```js
|
* Font Settings (Serif, Sans Serif)
|
||||||
{
|
* Themes: white, sepia, night
|
||||||
// Folders to use for output
|
|
||||||
// Caution: it overrides the value from the command line
|
|
||||||
// It's not advised this option in the book.json
|
|
||||||
"output": null,
|
|
||||||
|
|
||||||
// Generator to use for building
|
|
||||||
// Caution: it overrides the value from the command line
|
|
||||||
// It's not advised this option in the book.json
|
|
||||||
"generator": "site",
|
|
||||||
|
|
||||||
// Book title and description (defaults are extracted from the README)
|
|
||||||
"title": null,
|
|
||||||
"description": null,
|
|
||||||
|
|
||||||
// For ebook format, the extension to use for generation (default is detected from output extension)
|
|
||||||
// "epub", "pdf", "mobi"
|
|
||||||
// Caution: it overrides the value from the command line
|
|
||||||
// It's not advised this option in the book.json
|
|
||||||
"extension": null,
|
|
||||||
|
|
||||||
// Plugins list, can contain "-name" for removing default plugins
|
|
||||||
"plugins": [],
|
|
||||||
|
|
||||||
// Global configuration for plugins
|
|
||||||
"pluginsConfig": {
|
|
||||||
"fontSettings": {
|
|
||||||
"theme": "sepia", "night" or "white",
|
|
||||||
"family": "serif" or "sans",
|
|
||||||
"size": 1 to 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Variables for templating
|
|
||||||
"variables": {},
|
|
||||||
|
|
||||||
// Links in template (null: default, false: remove, string: new value)
|
|
||||||
"links": {
|
|
||||||
// Custom links at top of sidebar
|
|
||||||
"sidebar": {
|
|
||||||
"Custom link name": "https://customlink.com"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Sharing links
|
|
||||||
"sharing": {
|
|
||||||
"google": null,
|
|
||||||
"facebook": null,
|
|
||||||
"twitter": null,
|
|
||||||
"weibo": null,
|
|
||||||
"all": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
// Options for PDF generation
|
|
||||||
"pdf": {
|
|
||||||
// Add page numbers to the bottom of every page
|
|
||||||
"pageNumbers": false,
|
|
||||||
|
|
||||||
// Font size for the fiel content
|
|
||||||
"fontSize": 12,
|
|
||||||
|
|
||||||
// Paper size for the pdf
|
|
||||||
// Choices are [u’a0’, u’a1’, u’a2’, u’a3’, u’a4’, u’a5’, u’a6’, u’b0’, u’b1’, u’b2’, u’b3’, u’b4’, u’b5’, u’b6’, u’legal’, u’letter’]
|
|
||||||
"paperSize": "a4",
|
|
||||||
|
|
||||||
// Margin (in pts)
|
|
||||||
// Note: 72 pts equals 1 inch
|
|
||||||
"margin": {
|
|
||||||
"right": 62,
|
|
||||||
"left": 62,
|
|
||||||
"top": 36,
|
|
||||||
"bottom": 36
|
|
||||||
},
|
|
||||||
|
|
||||||
//Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
|
|
||||||
"headerTemplate": null,
|
|
||||||
|
|
||||||
//Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
|
|
||||||
"footerTemplate": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can publish your books to our index by visiting [GitBook.io](http://www.gitbook.io)
|
|
||||||
|
|
||||||
## Output Formats
|
## Output Formats
|
||||||
|
|
||||||
GitBook can generate your book in the following formats:
|
GitBook can generate your book in the following formats:
|
||||||
|
|
||||||
* **Static Website**: This is the default format. It generates a complete interactive static website that can be, for example, hosted on GitHub Pages.
|
* **Static Website**: This is the default format. It generates a complete interactive static website that can be, for example, hosted on GitHub Pages.
|
||||||
* **eBook**: A complete eBook with exercise solutions at the end of the book. Generate this format using: ```gitbook ebook ./myrepo```. You need to have [ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) installed. The output format could be **PDF**, **ePub** or **MOBI**.
|
* **eBook**: You need to have [ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) installed. You can specify the eBook filename as the second argument, otherwise `book` will be used.
|
||||||
* **JSON**: This format is used for debugging or extracting metadata from a book. Generate this format using: ```gitbook build ./myrepo -f json```.
|
* Generate a **PDF** using: `gitbook pdf ./myrepo ./mybook.pdf`
|
||||||
|
* Generate a **ePub** using: `gitbook epub ./myrepo ./mybook.epub`
|
||||||
|
* Generate a **MOBI** using: `gitbook mobi ./myrepo ./mybook.mobi`
|
||||||
|
* **JSON**: This format is used for debugging or extracting metadata from a book. Generate this format using: ```gitbook build ./myrepo --format=json```.
|
||||||
|
|
||||||
## Book Format
|
## Book Format
|
||||||
|
|
||||||
@@ -187,7 +120,64 @@ Definition for this term
|
|||||||
|
|
||||||
# Another term
|
# Another term
|
||||||
With it's definition, this can contain bold text and all other kinds of inline markup ...
|
With it's definition, this can contain bold text and all other kinds of inline markup ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Variables and Templating
|
||||||
|
|
||||||
|
A set of variables can be defined in the `book.json`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"variables": {
|
||||||
|
"host": "mybook.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These variables can be used in the markdown files:
|
||||||
|
|
||||||
|
```
|
||||||
|
The host is {{ book.host }}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use condition with these variables:
|
||||||
|
|
||||||
|
```
|
||||||
|
{% if book.host == "mybook.com" %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Variables of `book.json` are available in the `book` namespace. You can also access informations about the `file` itself and the `gitbook` version:
|
||||||
|
|
||||||
|
```
|
||||||
|
My file is {{ file.path }}
|
||||||
|
Modified at {{ file.mtime }}
|
||||||
|
Book built with GitBook {{ gitbook.version }}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Content References
|
||||||
|
|
||||||
|
You can use "content references," or conrefs, when writing books or documentation using GitBook.
|
||||||
|
|
||||||
|
Include a file from the same book:
|
||||||
|
|
||||||
|
```
|
||||||
|
{% include "./test.md" %}
|
||||||
|
```
|
||||||
|
|
||||||
|
or from a git repository (with a specific revision):
|
||||||
|
|
||||||
|
```
|
||||||
|
{% include "git+https://github.com/GitbookIO/documentation.git/README.md#1.0.1" %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Includes can be used with variables (see [Variables and Templating](#variables-and-templating)):
|
||||||
|
|
||||||
|
```
|
||||||
|
{% include book.ref_doc_readme %}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Ignoring files & folders
|
#### Ignoring files & folders
|
||||||
@@ -203,47 +193,38 @@ The best resolution is **1800x2360**. The generation of the cover can be done au
|
|||||||
|
|
||||||
A small version of the cover can also be set by creating a file: **/cover_small.jpg**.
|
A small version of the cover can also be set by creating a file: **/cover_small.jpg**.
|
||||||
|
|
||||||
#### Publish your book
|
#### AsciiDoc
|
||||||
|
|
||||||
The platform [GitBook.io](https://www.gitbook.io/) is like an "Heroku for books": you can create a book on it (public, paid, or private) and update it using **git push**.
|
Since version 2.0.0, AsciiDoc can be used instead of Markdown, simply replace the `.md` by the `.adoc` extension. Chapters in the summary are detected from an ordered list in the `SUMMARY.adoc`.
|
||||||
|
|
||||||
#### Plugins
|
## Publish your book
|
||||||
|
|
||||||
|
The platform [GitBook.com](https://www.gitbook.com/) is like an "Heroku for books": you can create a book on it (public, paid, or private) and update it using **git push**.
|
||||||
|
|
||||||
|
## Plugins
|
||||||
|
|
||||||
Plugins can be used to extend your book's functionality. Read [GitbookIO/plugin](https://github.com/GitbookIO/plugin) for more information about how to build a plugin for GitBook.
|
Plugins can be used to extend your book's functionality. Read [GitbookIO/plugin](https://github.com/GitbookIO/plugin) for more information about how to build a plugin for GitBook.
|
||||||
|
|
||||||
##### Official plugins:
|
Plugins needed to build a book can be installed using: `gitbook install ./`. You can find plugins at [plugins.gitbook.com](http://plugins.gitbook.com).
|
||||||
|
|
||||||
| Name | Description |
|
|
||||||
| ----- | ---- |
|
|
||||||
| [exercises](https://github.com/GitbookIO/plugin-exercises) | Add interactive exercises to your book. |
|
|
||||||
| [quizzes](https://github.com/GitbookIO/plugin-quizzes) | Add interactive quizzes to your book. |
|
|
||||||
| [mathjax](https://github.com/GitbookIO/plugin-mathjax) | Displays mathematical notation in the book. |
|
|
||||||
| [mixpanel](https://github.com/GitbookIO/plugin-mixpanel) | Mixpanel tracking for your book |
|
|
||||||
| [infinitescroll](https://github.com/GitbookIO/gitbook-plugin-infinitescroll) | Infinite Scrolling |
|
|
||||||
|
|
||||||
##### Other plugins:
|
## Debugging
|
||||||
|
|
||||||
| Name | Description |
|
You can use the options `--log=debug` and `--debug` to get better error messages (with stack trace). For example:
|
||||||
| ----- | ---- |
|
|
||||||
| [Google Analytics](https://github.com/GitbookIO/plugin-ga) | Google Analytics tracking for your book |
|
|
||||||
| [Disqus](https://github.com/GitbookIO/plugin-disqus) | Disqus comments integration in your book |
|
|
||||||
| [Autocover](https://github.com/GitbookIO/plugin-autocover) | Generate a cover for your book |
|
|
||||||
| [Transform annoted quotes to notes](https://github.com/erixtekila/gitbook-plugin-richquotes) | Allow extra markdown markup to render blockquotes as nice notes |
|
|
||||||
| [Send code to console](https://github.com/erixtekila/gitbook-plugin-toconsole) | Evaluate javascript block in the browser inspector's console |
|
|
||||||
| [Revealable sections](https://github.com/mrpotes/gitbook-plugin-reveal) | Reveal sections of the page using buttons made from the first title in each section |
|
|
||||||
| [Markdown within HTML](https://github.com/mrpotes/gitbook-plugin-nestedmd) | Process markdown within HTML blocks - allows custom layout options for individual pages |
|
|
||||||
| [Bootstrap JavaScript plugins](https://github.com/mrpotes/gitbook-plugin-bootstrapjs) | Use the [Bootstrap JavaScript plugins](http://getbootstrap.com/javascript) in your online GitBook |
|
|
||||||
| [Piwik Open Analytics](https://github.com/emmanuel-keller/gitbook-plugin-piwik) | Piwik Open Analytics tracking for your book |
|
|
||||||
| [Heading Anchors](https://github.com/rlmv/gitbook-plugin-anchors) | Add linkable Github-style anchors to headings |
|
|
||||||
| [JSBin](https://github.com/jcouyang/gitbook-plugin-jsbin) | Embedded jsbin frame into your book |
|
|
||||||
| [gitbook-grvis](https://github.com/romanlytkin/gitbook-grvis) | Gitbook GrViz plugin is used to select from markdown dot and converting it into a picture format svg |
|
|
||||||
| [gitbook-plantuml](https://github.com/romanlytkin/gitbook-plantuml) | Gitbook PlantUml plugin is used to select from markdown uml and converting it into a picture format svg |
|
|
||||||
|
|
||||||
#### Debugging
|
|
||||||
|
|
||||||
You can use the environment variable `DEBUG=true` to get better error messages (with stack trace). For example:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
$ export DEBUG=true
|
$ gitbook build ./ --log=debug --debug
|
||||||
$ gitbook build ./
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### How to use the latest commit from GitBook in gitbook-cli
|
||||||
|
|
||||||
|
To use the latest commit from `GitBook/gitbook` with `gitbook-cli`:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/GitbookIO/gitbook.git ./gitbook
|
||||||
|
$ gitbook versions:link ./gitbook
|
||||||
|
```
|
||||||
|
|
||||||
|
Now `gitbook-cli` will be using the `./gitbook` folder.
|
||||||
|
|
||||||
|
You can uninstall it using: `gitbook versions:uninstall latest`.
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Fix line endings in Windows. (runs before repo cloning)
|
||||||
|
init:
|
||||||
|
- git config --global core.autocrlf input
|
||||||
|
|
||||||
|
# Test against these versions of Node.js.
|
||||||
|
environment:
|
||||||
|
matrix:
|
||||||
|
- nodejs_version: "0.12"
|
||||||
|
- nodejs_version: "4.1"
|
||||||
|
|
||||||
|
# Install scripts. (runs after repo cloning)
|
||||||
|
install:
|
||||||
|
# Get the latest stable version of Node.js or io.js
|
||||||
|
- ps: Install-Product node $env:nodejs_version
|
||||||
|
# install svgexport
|
||||||
|
- npm install svgexport -g
|
||||||
|
# install modules
|
||||||
|
- npm install
|
||||||
|
|
||||||
|
# Post-install test scripts.
|
||||||
|
test_script:
|
||||||
|
# Output useful info for debugging.
|
||||||
|
- node --version
|
||||||
|
- npm --version
|
||||||
|
# run tests
|
||||||
|
- npm test
|
||||||
|
|
||||||
|
# Don't actually build.
|
||||||
|
build: off
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
var path = require('path');
|
|
||||||
var Q = require('q');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var fs = require('fs');
|
|
||||||
|
|
||||||
var utils = require('./utils');
|
|
||||||
var generate = require("../lib/generate");
|
|
||||||
var parse = require("../lib/parse");
|
|
||||||
var generators = require("../lib/generate").generators;
|
|
||||||
|
|
||||||
var buildCommand = function(command) {
|
|
||||||
return command
|
|
||||||
.option('-v, --verbose', 'Activate verbose mode, useful for debugging errors')
|
|
||||||
.option('-o, --output <directory>', 'Path to output directory, defaults to ./_book')
|
|
||||||
.option('-f, --format <name>', 'Change generation format, defaults to site, availables are: '+_.keys(generators).join(", "))
|
|
||||||
.option('--config <config file>', 'Configuration file to use, defaults to book.js or book.json');
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
var buildEbookCommand = function(command) {
|
|
||||||
return buildCommand(command)
|
|
||||||
.option('-c, --cover <path>', 'Cover image, default is cover.jpg if exists');
|
|
||||||
};
|
|
||||||
|
|
||||||
var makeBuildFunc = function(converter) {
|
|
||||||
return function(dir, options) {
|
|
||||||
dir = dir || process.cwd();
|
|
||||||
outputDir = options.output;
|
|
||||||
|
|
||||||
// Set debugging
|
|
||||||
if(options.verbose) {
|
|
||||||
process.env.DEBUG = "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Starting build ...');
|
|
||||||
return converter(
|
|
||||||
_.extend({}, options || {}, {
|
|
||||||
input: dir,
|
|
||||||
output: outputDir,
|
|
||||||
generator: options.format,
|
|
||||||
configFile: options.config
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.then(function(output) {
|
|
||||||
console.log("Successfully built!");
|
|
||||||
return output;
|
|
||||||
})
|
|
||||||
.fail(function(err) {
|
|
||||||
// Log error
|
|
||||||
utils.logError(err);
|
|
||||||
|
|
||||||
// Exit process with failure code
|
|
||||||
process.exit(-1);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
folder: makeBuildFunc(generate.folder),
|
|
||||||
file: makeBuildFunc(generate.file),
|
|
||||||
command: buildCommand,
|
|
||||||
commandEbook: buildEbookCommand
|
|
||||||
};
|
|
||||||
+4
-134
@@ -1,137 +1,7 @@
|
|||||||
#! /usr/bin/env node
|
#! /usr/bin/env node
|
||||||
|
|
||||||
var Q = require('q');
|
var color = require('bash-color');
|
||||||
var _ = require('lodash');
|
|
||||||
var path = require('path');
|
|
||||||
var prog = require('commander');
|
|
||||||
var tinylr = require('tiny-lr-fork');
|
|
||||||
|
|
||||||
var pkg = require('../package.json');
|
console.log(color.red('You need to install "gitbook-cli" to have access to the gitbook command anywhere on your system.'));
|
||||||
var generators = require("../lib/generate").generators;
|
console.log(color.red('If you\'ve installed this package globally, you need to uninstall it.'));
|
||||||
var initDir = require("../lib/generate/init");
|
console.log(color.red('>> Run "npm uninstall -g gitbook" then "npm install -g gitbook-cli"'));
|
||||||
var fs = require('../lib/generate/fs');
|
|
||||||
|
|
||||||
var utils = require('./utils');
|
|
||||||
var build = require('./build');
|
|
||||||
var Server = require('./server');
|
|
||||||
var platform = require("./platform");
|
|
||||||
|
|
||||||
// General options
|
|
||||||
prog
|
|
||||||
.version(pkg.version);
|
|
||||||
|
|
||||||
build.command(prog.command('build [source_dir]'))
|
|
||||||
.description('Build a gitbook from a directory')
|
|
||||||
.action(build.folder);
|
|
||||||
|
|
||||||
build.command(prog.command('serve [source_dir]'))
|
|
||||||
.description('Build then serve a gitbook from a directory')
|
|
||||||
.option('-p, --port <port>', 'Port for server to listen on', 4000)
|
|
||||||
.option('--no-watch', 'Disable restart with file watching')
|
|
||||||
.action(function(dir, options) {
|
|
||||||
var server = new Server();
|
|
||||||
|
|
||||||
// init livereload server
|
|
||||||
var lrOptions = {port: 35729};
|
|
||||||
var lrServer = tinylr(lrOptions);
|
|
||||||
var lrPath = undefined;
|
|
||||||
lrServer.listen(lrOptions.port, function(err) {
|
|
||||||
if (err) { return console.log(err); }
|
|
||||||
console.log('Live reload server started on port: ' + lrOptions.port);
|
|
||||||
});
|
|
||||||
|
|
||||||
var generate = function() {
|
|
||||||
if (server.isRunning()) console.log("Stopping server");
|
|
||||||
|
|
||||||
server.stop()
|
|
||||||
.then(function() {
|
|
||||||
return build.folder(dir, _.extend(options || {}, {
|
|
||||||
defaultsPlugins: ["livereload"]
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
.then(function(_options) {
|
|
||||||
console.log();
|
|
||||||
console.log('Starting server ...');
|
|
||||||
return server.start(_options.output, options.port)
|
|
||||||
.then(function() {
|
|
||||||
console.log('Serving book on http://localhost:'+options.port);
|
|
||||||
|
|
||||||
if (lrPath) {
|
|
||||||
// trigger livereload
|
|
||||||
lrServer.changed({body:{files:[lrPath]}})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.watch) return;
|
|
||||||
return utils.watch(_options.input)
|
|
||||||
.then(function(filepath) {
|
|
||||||
// set livereload path
|
|
||||||
lrPath = filepath;
|
|
||||||
console.log("Restart after change in files");
|
|
||||||
console.log('');
|
|
||||||
return generate();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.fail(utils.logError);
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Press CTRL+C to quit ...');
|
|
||||||
console.log('')
|
|
||||||
generate();
|
|
||||||
});
|
|
||||||
|
|
||||||
build.commandEbook(prog.command('pdf [source_dir]'))
|
|
||||||
.description('Build a gitbook as a PDF')
|
|
||||||
.action(function(dir, options) {
|
|
||||||
build.file(dir, _.extend(options, {
|
|
||||||
extension: "pdf",
|
|
||||||
format: "ebook"
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
build.commandEbook(prog.command('epub [source_dir]'))
|
|
||||||
.description('Build a gitbook as a ePub book')
|
|
||||||
.action(function(dir, options) {
|
|
||||||
build.file(dir, _.extend(options, {
|
|
||||||
extension: "epub",
|
|
||||||
format: "ebook"
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
build.commandEbook(prog.command('mobi [source_dir]'))
|
|
||||||
.description('Build a gitbook as a Mobi book')
|
|
||||||
.action(function(dir, options) {
|
|
||||||
build.file(dir, _.extend(options, {
|
|
||||||
extension: "mobi",
|
|
||||||
format: "ebook"
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
prog
|
|
||||||
.command('init [source_dir]')
|
|
||||||
.description('Create files and folders based on contents of SUMMARY.md')
|
|
||||||
.action(function(dir) {
|
|
||||||
dir = dir || process.cwd();
|
|
||||||
return initDir(dir);
|
|
||||||
});
|
|
||||||
|
|
||||||
prog
|
|
||||||
.command('publish [source_dir]')
|
|
||||||
.description('Publish content to the associated gitbook.io book')
|
|
||||||
.action(function(dir) {
|
|
||||||
dir = dir || process.cwd();
|
|
||||||
return platform.publish(dir);
|
|
||||||
});
|
|
||||||
|
|
||||||
prog
|
|
||||||
.command('git:remote [source_dir] [book_id]')
|
|
||||||
.description('Adds a git remote to a book repository')
|
|
||||||
.action(function(dir, bookId) {
|
|
||||||
dir = dir || process.cwd();
|
|
||||||
return platform.remote(dir, bookId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse and fallback to help if no args
|
|
||||||
if(_.isEmpty(prog.parse(process.argv).args) && process.argv.length === 2) {
|
|
||||||
prog.help();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
var Q = require("q");
|
|
||||||
var utils = require("./utils");
|
|
||||||
|
|
||||||
var publish = function(folder) {
|
|
||||||
if (!folder) {
|
|
||||||
console.log("Need a repository folder");
|
|
||||||
return process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.gitCmd("push", ["gitbook", "master"])
|
|
||||||
.then(function(out) {
|
|
||||||
console.log(out.stdout);
|
|
||||||
}, function(err) {
|
|
||||||
if (err.code == 128) {
|
|
||||||
console.log("No book on gitbook.io is configured with this git repository.");
|
|
||||||
console.log("Run 'gitbook git:remote username/book' to intialize this repository.");
|
|
||||||
} else {
|
|
||||||
console.log(err.message);
|
|
||||||
}
|
|
||||||
process.exit(-1);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var remote = function(folder, bookId) {
|
|
||||||
if (!folder || !bookId) {
|
|
||||||
console.log("Need a repository folder and a book id");
|
|
||||||
return process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var url = "https://push.gitbook.io/"+bookId+".git";
|
|
||||||
var addRemote = function() {
|
|
||||||
return utils.gitCmd("remote", ["add", "gitbook", url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
addRemote()
|
|
||||||
.fail(function(err) {
|
|
||||||
if (err.code == 128) {
|
|
||||||
return utils.gitCmd("remote", ["rm", "gitbook"]).then(addRemote);
|
|
||||||
}
|
|
||||||
return Q.reject(err);
|
|
||||||
})
|
|
||||||
.then(function(out) {
|
|
||||||
console.log("Book remote '"+url+"' added to the folder");
|
|
||||||
}, function(err) {
|
|
||||||
console.log(err.message);
|
|
||||||
process.exit(-1);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
publish: publish,
|
|
||||||
remote: remote
|
|
||||||
};
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
var Q = require('q');
|
|
||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var exec = require('child_process').exec;
|
|
||||||
var http = require('http');
|
|
||||||
var send = require('send');
|
|
||||||
|
|
||||||
var path = require('path');
|
|
||||||
var Gaze = require('gaze').Gaze;
|
|
||||||
|
|
||||||
function watch(dir) {
|
|
||||||
var d = Q.defer();
|
|
||||||
dir = path.resolve(dir);
|
|
||||||
|
|
||||||
var gaze = new Gaze("**/*.md", {
|
|
||||||
cwd: dir
|
|
||||||
});
|
|
||||||
|
|
||||||
gaze.once("all", function(e, filepath) {
|
|
||||||
gaze.close();
|
|
||||||
|
|
||||||
d.resolve(filepath);
|
|
||||||
});
|
|
||||||
gaze.once("error", function(err) {
|
|
||||||
gaze.close();
|
|
||||||
|
|
||||||
d.reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
return d.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
function logError(err) {
|
|
||||||
var message = err.message || err;
|
|
||||||
if (process.env.DEBUG != null) message = err.stack || message;
|
|
||||||
console.log(message);
|
|
||||||
return Q.reject(err);
|
|
||||||
};
|
|
||||||
|
|
||||||
function runGitCommand(command, args) {
|
|
||||||
var d = Q.defer(), child;
|
|
||||||
args = ["git", command].concat(args).join(" ");
|
|
||||||
|
|
||||||
child = exec(args, function (error, stdout, stderr) {
|
|
||||||
if (error !== null) {
|
|
||||||
error.stdout = stdout;
|
|
||||||
error.stderr = stderr;
|
|
||||||
d.reject(error);
|
|
||||||
} else {
|
|
||||||
d.resolve({
|
|
||||||
stdout: stdout,
|
|
||||||
stderr: stderr
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return d.promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
module.exports = {
|
|
||||||
watch: watch,
|
|
||||||
logError: logError,
|
|
||||||
gitCmd: runGitCommand
|
|
||||||
};
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "GitBook",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"jquery": "2.1.1",
|
|
||||||
"lodash": "2.4.1",
|
|
||||||
"requirejs": "2.1.11",
|
|
||||||
"URIjs": "1.13.1",
|
|
||||||
"mousetrap": "1.4.6",
|
|
||||||
"lunr.js": "0.5.2",
|
|
||||||
"fontawesome": "4.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-20724
File diff suppressed because one or more lines are too long
Vendored
-10
File diff suppressed because one or more lines are too long
+47
@@ -0,0 +1,47 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var gulp = require('gulp');
|
||||||
|
var gutil = require('gulp-util');
|
||||||
|
var less = require('gulp-less');
|
||||||
|
var rename = require('gulp-rename');
|
||||||
|
var minifyCSS = require('gulp-minify-css');
|
||||||
|
var browserify = require('browserify');
|
||||||
|
var mergeStream = require('merge-stream');
|
||||||
|
var source = require('vinyl-source-stream');
|
||||||
|
|
||||||
|
gulp.task('css', function() {
|
||||||
|
var merged = mergeStream();
|
||||||
|
|
||||||
|
_.each({
|
||||||
|
'ebook.less': 'ebook/ebook.css',
|
||||||
|
'pdf.less': 'ebook/pdf.css',
|
||||||
|
'mobi.less': 'ebook/mobi.css',
|
||||||
|
'epub.less': 'ebook/epub.css',
|
||||||
|
'website.less': 'website/style.css'
|
||||||
|
}, function(out, input) {
|
||||||
|
gutil.log('compiling', input, 'into', out);
|
||||||
|
merged.add(gulp.src('theme/stylesheets/'+input)
|
||||||
|
.pipe(less())
|
||||||
|
.pipe(minifyCSS())
|
||||||
|
.pipe(rename(out))
|
||||||
|
.pipe(gulp.dest('theme/assets/')));
|
||||||
|
});
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('js', function() {
|
||||||
|
return browserify('./theme/javascript/index.js')
|
||||||
|
.bundle()
|
||||||
|
.pipe(source('app.js'))
|
||||||
|
.pipe(gulp.dest('./theme/assets/website'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('assets', function() {
|
||||||
|
return gulp.src('./node_modules/font-awesome/fonts/*')
|
||||||
|
.pipe(gulp.dest('theme/assets/website/fonts/fontawesome/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('default', ['css', 'js', 'assets'], function() {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// Return non-parsed html
|
||||||
|
// since blocks are by default non-parsable, a simple identity method works fine
|
||||||
|
html: _.identity,
|
||||||
|
|
||||||
|
// Highlight a code block
|
||||||
|
// This block can be extent by plugins
|
||||||
|
code: _.identity
|
||||||
|
};
|
||||||
+817
@@ -0,0 +1,817 @@
|
|||||||
|
var Q = require('q');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var path = require('path');
|
||||||
|
var parsers = require('gitbook-parsers');
|
||||||
|
|
||||||
|
var fs = require('./utils/fs');
|
||||||
|
var parseNavigation = require('./utils/navigation');
|
||||||
|
var parseProgress = require('./utils/progress');
|
||||||
|
var pageUtil = require('./utils/page');
|
||||||
|
var pathUtil = require('./utils/path');
|
||||||
|
var links = require('./utils/links');
|
||||||
|
var i18n = require('./utils/i18n');
|
||||||
|
var logger = require('./utils/logger');
|
||||||
|
|
||||||
|
var Configuration = require('./configuration');
|
||||||
|
var TemplateEngine = require('./template');
|
||||||
|
var PluginsList = require('./pluginslist');
|
||||||
|
|
||||||
|
var generators = require('./generators');
|
||||||
|
|
||||||
|
var Book = function(root, context, parent) {
|
||||||
|
this.context = _.defaults(context || {}, {
|
||||||
|
// Extend book configuration
|
||||||
|
config: {},
|
||||||
|
|
||||||
|
// Log function
|
||||||
|
log: function(msg) {
|
||||||
|
process.stdout.write(msg);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Log level
|
||||||
|
logLevel: 'info'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log
|
||||||
|
this.log = logger(this.context.log, this.context.logLevel);
|
||||||
|
|
||||||
|
// Root folder of the book
|
||||||
|
this.root = path.resolve(root);
|
||||||
|
|
||||||
|
// Parent book
|
||||||
|
this.parent = parent;
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
this.config = new Configuration(this, this.context.config);
|
||||||
|
Object.defineProperty(this, 'options', {
|
||||||
|
get: function () {
|
||||||
|
return this.config.options;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Template
|
||||||
|
this.template = new TemplateEngine(this);
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
this.summary = {};
|
||||||
|
this.navigation = [];
|
||||||
|
|
||||||
|
// Glossary
|
||||||
|
this.glossary = [];
|
||||||
|
|
||||||
|
// Langs
|
||||||
|
this.langs = [];
|
||||||
|
|
||||||
|
// Sub-books
|
||||||
|
this.books = [];
|
||||||
|
|
||||||
|
// Files in the book
|
||||||
|
this.files = [];
|
||||||
|
|
||||||
|
// List of plugins
|
||||||
|
this.plugins = new PluginsList(this);
|
||||||
|
|
||||||
|
// Structure files
|
||||||
|
this.summaryFile = null;
|
||||||
|
this.glossaryFile = null;
|
||||||
|
this.readmeFile = null;
|
||||||
|
this.langsFile = null;
|
||||||
|
|
||||||
|
// Bind methods
|
||||||
|
_.bindAll(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return string representation
|
||||||
|
Book.prototype.toString = function() {
|
||||||
|
return '[Book '+this.root+']';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize and parse the book: config, summary, glossary
|
||||||
|
Book.prototype.parse = function() {
|
||||||
|
var that = this;
|
||||||
|
var multilingual = false;
|
||||||
|
|
||||||
|
return this.parseConfig()
|
||||||
|
|
||||||
|
.then(function() {
|
||||||
|
return that.parsePlugins();
|
||||||
|
})
|
||||||
|
|
||||||
|
.then(function() {
|
||||||
|
return that.parseLangs()
|
||||||
|
.then(function() {
|
||||||
|
multilingual = that.langs.length > 0;
|
||||||
|
if (multilingual) that.log.info.ln('Parsing multilingual book, with', that.langs.length, 'languages');
|
||||||
|
|
||||||
|
// Sub-books that inherit from the current book configuration
|
||||||
|
that.books = _.map(that.langs, function(lang) {
|
||||||
|
that.log.info.ln('Preparing language book', lang.lang);
|
||||||
|
return new Book(
|
||||||
|
path.join(that.root, lang.path),
|
||||||
|
_.merge({}, that.context, {
|
||||||
|
config: _.extend({}, that.options, {
|
||||||
|
'output': path.join(that.options.output, lang.lang),
|
||||||
|
'language': lang.lang
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
that
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
.then(function() {
|
||||||
|
if (multilingual) return;
|
||||||
|
return that.listAllFiles();
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
if (multilingual) return;
|
||||||
|
return that.parseReadme();
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
if (multilingual) return;
|
||||||
|
return that.parseSummary();
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
if (multilingual) return;
|
||||||
|
return that.parseGlossary();
|
||||||
|
})
|
||||||
|
|
||||||
|
.then(function() {
|
||||||
|
// Init sub-books
|
||||||
|
return _.reduce(that.books, function(prev, book) {
|
||||||
|
return prev.then(function() {
|
||||||
|
return book.parse();
|
||||||
|
});
|
||||||
|
}, Q());
|
||||||
|
})
|
||||||
|
|
||||||
|
.thenResolve(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate the output
|
||||||
|
Book.prototype.generate = function(generator) {
|
||||||
|
var that = this;
|
||||||
|
that.options.generator = generator || that.options.generator;
|
||||||
|
|
||||||
|
that.log.info.ln('start generation with', that.options.generator, 'generator');
|
||||||
|
return Q()
|
||||||
|
|
||||||
|
// Clean output folder
|
||||||
|
.then(function() {
|
||||||
|
that.log.info('clean', that.options.generator, 'generator');
|
||||||
|
return fs.clean(that.options.output)
|
||||||
|
.progress(function(p) {
|
||||||
|
that.log.debug.ln('remove', p.file, '('+p.i+'/'+p.count+')');
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
that.log.info.ok();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create generator
|
||||||
|
.then(function() {
|
||||||
|
var Generator = generators[generator];
|
||||||
|
if (!Generator) throw 'Generator \''+that.options.generator+'\' doesn\'t exist';
|
||||||
|
generator = new Generator(that);
|
||||||
|
|
||||||
|
return generator.prepare();
|
||||||
|
})
|
||||||
|
|
||||||
|
// Transform configuration
|
||||||
|
.then(function() {
|
||||||
|
return that.callHook('config', that.config.dump())
|
||||||
|
.then(function(newConfig) {
|
||||||
|
that.config.replace(newConfig);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate content
|
||||||
|
.then(function() {
|
||||||
|
if (that.isMultilingual()) {
|
||||||
|
return that.generateMultiLingual(generator);
|
||||||
|
} else {
|
||||||
|
// Separate list of files into the different operations needed
|
||||||
|
var ops = _.groupBy(that.files, function(file) {
|
||||||
|
if (file[file.length -1] == '/') {
|
||||||
|
return 'directories';
|
||||||
|
} else if (_.contains(parsers.extensions, path.extname(file)) && that.navigation[file]) {
|
||||||
|
return 'content';
|
||||||
|
} else {
|
||||||
|
return 'files';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
|
||||||
|
// First, let's create folder
|
||||||
|
.then(function() {
|
||||||
|
return _.reduce(ops.directories || [], function(prev, folder) {
|
||||||
|
return prev.then(function() {
|
||||||
|
that.log.debug.ln('transferring folder', folder);
|
||||||
|
return Q(generator.transferFolder(folder));
|
||||||
|
});
|
||||||
|
}, Q());
|
||||||
|
})
|
||||||
|
|
||||||
|
// Then, let's copy other files
|
||||||
|
.then(function() {
|
||||||
|
return Q.all(_.map(ops.files || [], function(file) {
|
||||||
|
that.log.debug.ln('transferring file', file);
|
||||||
|
return Q(generator.transferFile(file));
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
|
||||||
|
// Finally let's generate content
|
||||||
|
.then(function() {
|
||||||
|
var nFiles = (ops.content || []).length;
|
||||||
|
return _.reduce(ops.content || [], function(prev, file, i) {
|
||||||
|
return prev.then(function() {
|
||||||
|
var p = ((i*100)/nFiles).toFixed(0)+'%';
|
||||||
|
that.log.debug.ln('processing', file, p);
|
||||||
|
|
||||||
|
return Q(generator.convertFile(file))
|
||||||
|
.fail(function(err) {
|
||||||
|
// Transform error message to signal file
|
||||||
|
throw that.normError(err, {
|
||||||
|
fileName: file
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, Q());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Finish generation
|
||||||
|
.then(function() {
|
||||||
|
return that.callHook('finish:before');
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
return generator.finish();
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
return that.callHook('finish');
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
that.log.info.ln('generation is finished');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate the output for a multilingual book
|
||||||
|
Book.prototype.generateMultiLingual = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
// Generate sub-books
|
||||||
|
return _.reduce(that.books, function(prev, book) {
|
||||||
|
return prev.then(function() {
|
||||||
|
return book.generate(that.options.generator);
|
||||||
|
});
|
||||||
|
}, Q());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract files from ebook generated
|
||||||
|
Book.prototype.generateFile = function(output, options) {
|
||||||
|
var book = this;
|
||||||
|
|
||||||
|
options = _.defaults(options || {}, {
|
||||||
|
ebookFormat: path.extname(output).slice(1)
|
||||||
|
});
|
||||||
|
output = output || path.resolve(book.root, 'book.'+options.ebookFormat);
|
||||||
|
|
||||||
|
return fs.tmp.dir()
|
||||||
|
.then(function(tmpDir) {
|
||||||
|
book.setOutput(tmpDir);
|
||||||
|
|
||||||
|
return book.generate(options.ebookFormat)
|
||||||
|
.then(function() {
|
||||||
|
var copyFile = function(lang) {
|
||||||
|
var _outputFile = output;
|
||||||
|
var _tmpDir = tmpDir;
|
||||||
|
|
||||||
|
if (lang) {
|
||||||
|
_outputFile = _outputFile.slice(0, -path.extname(_outputFile).length)+'_'+lang+path.extname(_outputFile);
|
||||||
|
_tmpDir = path.join(_tmpDir, lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
book.log.debug.ln('copy ebook to', _outputFile);
|
||||||
|
return fs.copy(
|
||||||
|
path.join(_tmpDir, 'index.'+options.ebookFormat),
|
||||||
|
_outputFile
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Multi-langs book
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
if (book.isMultilingual()) {
|
||||||
|
return Q.all(
|
||||||
|
_.map(book.langs, function(lang) {
|
||||||
|
return copyFile(lang.lang);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.thenResolve(book.langs.length);
|
||||||
|
} else {
|
||||||
|
return copyFile().thenResolve(1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function(n) {
|
||||||
|
book.log.info.ok(n+' file(s) generated');
|
||||||
|
|
||||||
|
return fs.remove(tmpDir);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse configuration
|
||||||
|
Book.prototype.parseConfig = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
that.log.info('loading book configuration....');
|
||||||
|
return that.config.load()
|
||||||
|
.then(function() {
|
||||||
|
that.log.info.ok();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse list of plugins
|
||||||
|
Book.prototype.parsePlugins = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
// Load plugins
|
||||||
|
return that.plugins.load(that.options.plugins)
|
||||||
|
.then(function() {
|
||||||
|
if (_.size(that.plugins.failed) > 0) return Q.reject(new Error('Error loading plugins: '+that.plugins.failed.join(',')+'. Run \'gitbook install\' to install plugins from NPM.'));
|
||||||
|
|
||||||
|
that.log.info.ok(that.plugins.count()+' plugins loaded');
|
||||||
|
that.log.debug.ln('normalize plugins list');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse readme to extract defaults title and description
|
||||||
|
Book.prototype.parseReadme = function() {
|
||||||
|
var that = this;
|
||||||
|
var filename = that.config.getStructure('readme', true);
|
||||||
|
that.log.debug.ln('start parsing readme:', filename);
|
||||||
|
|
||||||
|
return that.findFile(filename)
|
||||||
|
.then(function(readme) {
|
||||||
|
if (!readme) throw 'No README file';
|
||||||
|
if (!_.contains(that.files, readme.path)) throw 'README file is ignored';
|
||||||
|
|
||||||
|
that.readmeFile = readme.path;
|
||||||
|
that._defaultsStructure(that.readmeFile);
|
||||||
|
|
||||||
|
that.log.debug.ln('readme located at', that.readmeFile);
|
||||||
|
return that.template.renderFile(that.readmeFile)
|
||||||
|
.then(function(content) {
|
||||||
|
return readme.parser.readme(content)
|
||||||
|
.fail(function(err) {
|
||||||
|
throw that.normError(err, {
|
||||||
|
name: err.name || 'Readme Parse Error',
|
||||||
|
fileName: that.readmeFile
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function(readme) {
|
||||||
|
that.options.title = that.options.title || readme.title;
|
||||||
|
that.options.description = that.options.description || readme.description;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Parse langs to extract list of sub-books
|
||||||
|
Book.prototype.parseLangs = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var filename = that.config.getStructure('langs', true);
|
||||||
|
that.log.debug.ln('start parsing languages index:', filename);
|
||||||
|
|
||||||
|
return that.findFile(filename)
|
||||||
|
.then(function(langs) {
|
||||||
|
if (!langs) return [];
|
||||||
|
|
||||||
|
that.langsFile = langs.path;
|
||||||
|
that._defaultsStructure(that.langsFile);
|
||||||
|
|
||||||
|
that.log.debug.ln('languages index located at', that.langsFile);
|
||||||
|
return that.template.renderFile(that.langsFile)
|
||||||
|
.then(function(content) {
|
||||||
|
return langs.parser.langs(content)
|
||||||
|
.fail(function(err) {
|
||||||
|
throw that.normError(err, {
|
||||||
|
name: err.name || 'Langs Parse Error',
|
||||||
|
fileName: that.langsFile
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function(langs) {
|
||||||
|
that.langs = langs;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse summary to extract list of chapters
|
||||||
|
Book.prototype.parseSummary = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var filename = that.config.getStructure('summary', true);
|
||||||
|
that.log.debug.ln('start parsing summary:', filename);
|
||||||
|
|
||||||
|
return that.findFile(filename)
|
||||||
|
.then(function(summary) {
|
||||||
|
if (!summary) throw 'No SUMMARY file';
|
||||||
|
|
||||||
|
// Remove the summary from the list of files to parse
|
||||||
|
that.summaryFile = summary.path;
|
||||||
|
that._defaultsStructure(that.summaryFile);
|
||||||
|
that.files = _.without(that.files, that.summaryFile);
|
||||||
|
|
||||||
|
that.log.debug.ln('summary located at', that.summaryFile);
|
||||||
|
return that.template.renderFile(that.summaryFile)
|
||||||
|
.then(function(content) {
|
||||||
|
return summary.parser.summary(content, {
|
||||||
|
entryPoint: that.readmeFile,
|
||||||
|
entryPointTitle: that.i18n('SUMMARY_INTRODUCTION'),
|
||||||
|
files: that.files
|
||||||
|
})
|
||||||
|
.fail(function(err) {
|
||||||
|
throw that.normError(err, {
|
||||||
|
name: err.name || 'Summary Parse Error',
|
||||||
|
fileName: that.summaryFile
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function(summary) {
|
||||||
|
that.summary = summary;
|
||||||
|
that.navigation = parseNavigation(that.summary, that.files);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse glossary to extract terms
|
||||||
|
Book.prototype.parseGlossary = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var filename = that.config.getStructure('glossary', true);
|
||||||
|
that.log.debug.ln('start parsing glossary: ', filename);
|
||||||
|
|
||||||
|
return that.findFile(filename)
|
||||||
|
.then(function(glossary) {
|
||||||
|
if (!glossary) return [];
|
||||||
|
|
||||||
|
// Remove the glossary from the list of files to parse
|
||||||
|
that.glossaryFile = glossary.path;
|
||||||
|
that._defaultsStructure(that.glossaryFile);
|
||||||
|
that.files = _.without(that.files, that.glossaryFile);
|
||||||
|
|
||||||
|
that.log.debug.ln('glossary located at', that.glossaryFile);
|
||||||
|
return that.template.renderFile(that.glossaryFile)
|
||||||
|
.then(function(content) {
|
||||||
|
return glossary.parser.glossary(content)
|
||||||
|
.fail(function(err) {
|
||||||
|
throw that.normError(err, {
|
||||||
|
name: err.name || 'Glossary Parse Error',
|
||||||
|
fileName: that.glossaryFile
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function(glossary) {
|
||||||
|
that.glossary = glossary;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse a page
|
||||||
|
Book.prototype.parsePage = function(filename, options) {
|
||||||
|
var that = this, page = {};
|
||||||
|
options = _.defaults(options || {}, {
|
||||||
|
// Transform svg images
|
||||||
|
convertImages: false,
|
||||||
|
|
||||||
|
// Interpolate before templating
|
||||||
|
interpolateTemplate: _.identity,
|
||||||
|
|
||||||
|
// Interpolate after templating
|
||||||
|
interpolateContent: _.identity
|
||||||
|
});
|
||||||
|
|
||||||
|
var interpolate = function(fn) {
|
||||||
|
return Q(fn(page))
|
||||||
|
.then(function(_page) {
|
||||||
|
page = _page || page;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
that.log.debug.ln('start parsing file', filename);
|
||||||
|
|
||||||
|
var extension = path.extname(filename);
|
||||||
|
var filetype = parsers.get(extension);
|
||||||
|
|
||||||
|
if (!filetype) return Q.reject(new Error('Can\'t parse file: '+filename));
|
||||||
|
|
||||||
|
// Type of parser used
|
||||||
|
page.type = filetype.name;
|
||||||
|
|
||||||
|
// Path relative to book
|
||||||
|
page.path = filename;
|
||||||
|
|
||||||
|
// Path absolute in the system
|
||||||
|
page.rawPath = path.resolve(that.root, filename);
|
||||||
|
|
||||||
|
// Progress in the book
|
||||||
|
page.progress = parseProgress(that.navigation, filename);
|
||||||
|
|
||||||
|
that.log.debug.ln('render template', filename);
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
return that.readFile(page.path)
|
||||||
|
.then(function(content) {
|
||||||
|
page.content = content;
|
||||||
|
|
||||||
|
return interpolate(options.interpolateTemplate);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Prepare page markup
|
||||||
|
.then(function() {
|
||||||
|
return filetype.page.prepare(page.content)
|
||||||
|
.then(function(content) {
|
||||||
|
page.content = content;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate template
|
||||||
|
.then(function() {
|
||||||
|
return that.template.renderPage(page);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Prepare and Parse markup
|
||||||
|
.then(function(content) {
|
||||||
|
page.content = content;
|
||||||
|
|
||||||
|
that.log.debug.ln('use file parser', filetype.name, 'for', filename);
|
||||||
|
return filetype.page(page.content);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Post process sections
|
||||||
|
.then(function(_page) {
|
||||||
|
return _.reduce(_page.sections, function(prev, section) {
|
||||||
|
return prev.then(function(_sections) {
|
||||||
|
return that.template.postProcess(section.content || '')
|
||||||
|
.then(function(content) {
|
||||||
|
section.content = content;
|
||||||
|
return _sections.concat([section]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, Q([]));
|
||||||
|
})
|
||||||
|
|
||||||
|
// Prepare html
|
||||||
|
.then(function(_sections) {
|
||||||
|
return pageUtil.normalize(_sections, {
|
||||||
|
book: that,
|
||||||
|
convertImages: options.convertImages,
|
||||||
|
input: filename,
|
||||||
|
navigation: that.navigation,
|
||||||
|
base: path.dirname(filename) || './',
|
||||||
|
output: path.dirname(filename) || './',
|
||||||
|
glossary: that.glossary
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// Interpolate output
|
||||||
|
.then(function(_sections) {
|
||||||
|
page.sections = _sections;
|
||||||
|
return interpolate(options.interpolateContent);
|
||||||
|
})
|
||||||
|
|
||||||
|
.then(function() {
|
||||||
|
return page;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find file that can be parsed with a specific filename
|
||||||
|
Book.prototype.findFile = function(filename) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var ext = path.extname(filename);
|
||||||
|
var basename = path.basename(filename, ext);
|
||||||
|
|
||||||
|
// Ordered list of extensions to test
|
||||||
|
var exts = parsers.extensions;
|
||||||
|
if (ext) exts = _.uniq([ext].concat(exts));
|
||||||
|
|
||||||
|
return _.reduce(exts, function(prev, ext) {
|
||||||
|
return prev.then(function(output) {
|
||||||
|
// Stop if already find a parser
|
||||||
|
if (output) return output;
|
||||||
|
|
||||||
|
var filepath = basename+ext;
|
||||||
|
|
||||||
|
return fs.findFile(that.root, filepath)
|
||||||
|
.then(function(realFilepath) {
|
||||||
|
if (!realFilepath) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
parser: parsers.get(ext),
|
||||||
|
path: realFilepath
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, Q(null));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format a string using a specific markup language
|
||||||
|
Book.prototype.formatString = function(extension, content) {
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
var filetype = parsers.get(extension);
|
||||||
|
if (!filetype) throw new Error('Filetype doesn\'t exist: '+filetype);
|
||||||
|
|
||||||
|
return filetype.page(content);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Merge sections
|
||||||
|
.then(function(page) {
|
||||||
|
return _.reduce(page.sections, function(content, section) {
|
||||||
|
return content + section.content;
|
||||||
|
}, '');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if a file exists in the book
|
||||||
|
Book.prototype.fileExists = function(filename) {
|
||||||
|
return fs.exists(
|
||||||
|
this.resolve(filename)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if a file path is inside the book
|
||||||
|
Book.prototype.fileIsInBook = function(filename) {
|
||||||
|
return pathUtil.isInRoot(this.root, filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read a file
|
||||||
|
Book.prototype.readFile = function(filename) {
|
||||||
|
return fs.readFile(
|
||||||
|
this.resolve(filename),
|
||||||
|
{ encoding: 'utf8' }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return stat for a file
|
||||||
|
Book.prototype.statFile = function(filename) {
|
||||||
|
return fs.stat(this.resolve(filename));
|
||||||
|
};
|
||||||
|
|
||||||
|
// List all files in the book
|
||||||
|
Book.prototype.listAllFiles = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return fs.list(this.root, {
|
||||||
|
ignoreFiles: ['.ignore', '.gitignore', '.bookignore'],
|
||||||
|
ignoreRules: [
|
||||||
|
// Skip Git stuff
|
||||||
|
'.git/',
|
||||||
|
'.gitignore',
|
||||||
|
|
||||||
|
// Skip OS X meta data
|
||||||
|
'.DS_Store',
|
||||||
|
|
||||||
|
// Skip stuff installed by plugins
|
||||||
|
'node_modules',
|
||||||
|
|
||||||
|
// Skip book outputs
|
||||||
|
'_book',
|
||||||
|
'*.pdf',
|
||||||
|
'*.epub',
|
||||||
|
'*.mobi',
|
||||||
|
|
||||||
|
// Skip config files
|
||||||
|
'.ignore',
|
||||||
|
'.bookignore',
|
||||||
|
'book.json',
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(function(_files) {
|
||||||
|
that.files = _files;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return true if the book is a multilingual book
|
||||||
|
Book.prototype.isMultilingual = function() {
|
||||||
|
return this.books.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return root of the parent
|
||||||
|
Book.prototype.parentRoot = function() {
|
||||||
|
if (this.parent) return this.parent.parentRoot();
|
||||||
|
return this.root;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return true if it's a sub-book
|
||||||
|
Book.prototype.isSubBook = function() {
|
||||||
|
return !!this.parent;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test if the file is the entry point
|
||||||
|
Book.prototype.isEntryPoint = function(fp) {
|
||||||
|
return fp == this.readmeFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alias to book.config.get
|
||||||
|
Book.prototype.getConfig = function(key, def) {
|
||||||
|
return this.config.get(key, def);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve a path in the book source
|
||||||
|
// Enforce that the output path in the root folder
|
||||||
|
Book.prototype.resolve = function() {
|
||||||
|
return pathUtil.resolveInRoot.apply(null, [this.root].concat(_.toArray(arguments)));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert an abslute path into a relative path to this
|
||||||
|
Book.prototype.relative = function(p) {
|
||||||
|
return path.relative(this.root, p);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize a path to .html and convert README -> index
|
||||||
|
Book.prototype.contentPath = function(link) {
|
||||||
|
if (
|
||||||
|
path.basename(link, path.extname(link)) == 'README' ||
|
||||||
|
link == this.readmeFile
|
||||||
|
) {
|
||||||
|
link = path.join(path.dirname(link), 'index'+path.extname(link));
|
||||||
|
}
|
||||||
|
|
||||||
|
link = links.changeExtension(link, '.html');
|
||||||
|
return link;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize a link to .html and convert README -> index
|
||||||
|
Book.prototype.contentLink = function(link) {
|
||||||
|
return links.normalize(this.contentPath(link));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default structure paths to an extension
|
||||||
|
Book.prototype._defaultsStructure = function(filename) {
|
||||||
|
var that = this;
|
||||||
|
var extension = path.extname(filename);
|
||||||
|
|
||||||
|
that.readmeFile = that.readmeFile || that.config.getStructure('readme')+extension;
|
||||||
|
that.summaryFile = that.summaryFile || that.config.getStructure('summary')+extension;
|
||||||
|
that.glossaryFile = that.glossaryFile || that.config.getStructure('glossary')+extension;
|
||||||
|
that.langsFile = that.langsFile || that.config.getStructure('langs')+extension;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Change output path
|
||||||
|
Book.prototype.setOutput = function(p) {
|
||||||
|
var that = this;
|
||||||
|
this.options.output = path.resolve(p);
|
||||||
|
|
||||||
|
_.each(this.books, function(book) {
|
||||||
|
book.setOutput(path.join(that.options.output, book.options.language));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Translate a strign according to the book language
|
||||||
|
Book.prototype.i18n = function() {
|
||||||
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
return i18n.__.apply({}, [this.config.normalizeLanguage()].concat(args));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize error
|
||||||
|
Book.prototype.normError = function(err, opts, defs) {
|
||||||
|
if (_.isString(err)) err = new Error(err);
|
||||||
|
|
||||||
|
// Extend err
|
||||||
|
_.extend(err, opts || {});
|
||||||
|
_.defaults(err, defs || {});
|
||||||
|
|
||||||
|
err.lineNumber = err.lineNumber || err.lineno;
|
||||||
|
err.columnNumber = err.columnNumber || err.colno;
|
||||||
|
|
||||||
|
err.toString = function() {
|
||||||
|
var attributes = [];
|
||||||
|
|
||||||
|
if (this.fileName) attributes.push('In file \''+this.fileName+'\'');
|
||||||
|
if (this.lineNumber) attributes.push('Line '+this.lineNumber);
|
||||||
|
if (this.columnNumber) attributes.push('Column '+this.columnNumber);
|
||||||
|
return (this.name || 'Error')+': '+this.message+((attributes.length > 0)? ' ('+attributes.join(', ')+')' : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call a hook in plugins
|
||||||
|
Book.prototype.callHook = function(name, data) {
|
||||||
|
return this.plugins.hook(name, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports= Book;
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// Options that can't be extend
|
||||||
|
'configFile': 'book',
|
||||||
|
'generator': 'website',
|
||||||
|
'extension': null,
|
||||||
|
|
||||||
|
// Book metadats (somes are extracted from the README by default)
|
||||||
|
'title': null,
|
||||||
|
'description': null,
|
||||||
|
'isbn': null,
|
||||||
|
'language': 'en',
|
||||||
|
'direction': null,
|
||||||
|
'author': null,
|
||||||
|
|
||||||
|
// version of gitbook to use
|
||||||
|
'gitbook': '*',
|
||||||
|
|
||||||
|
// Structure
|
||||||
|
'structure': {
|
||||||
|
'langs': 'LANGS.md',
|
||||||
|
'readme': 'README.md',
|
||||||
|
'glossary': 'GLOSSARY.md',
|
||||||
|
'summary': 'SUMMARY.md'
|
||||||
|
},
|
||||||
|
|
||||||
|
// CSS Styles
|
||||||
|
'styles': {
|
||||||
|
'website': 'styles/website.css',
|
||||||
|
'print': 'styles/print.css',
|
||||||
|
'ebook': 'styles/ebook.css',
|
||||||
|
'pdf': 'styles/pdf.css',
|
||||||
|
'mobi': 'styles/mobi.css',
|
||||||
|
'epub': 'styles/epub.css'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Plugins list, can contain '-name' for removing default plugins
|
||||||
|
'plugins': [],
|
||||||
|
|
||||||
|
// Global configuration for plugins
|
||||||
|
'pluginsConfig': {},
|
||||||
|
|
||||||
|
// Variables for templating
|
||||||
|
'variables': {},
|
||||||
|
|
||||||
|
// Set another theme with your own layout
|
||||||
|
// It's recommended to use plugins or add more options for default theme, though
|
||||||
|
// See https://github.com/GitbookIO/gitbook/issues/209
|
||||||
|
'theme': path.resolve(__dirname, '../theme'),
|
||||||
|
|
||||||
|
// Links in template (null: default, false: remove, string: new value)
|
||||||
|
'links': {
|
||||||
|
// Custom links at top of sidebar
|
||||||
|
'sidebar': {
|
||||||
|
// 'Custom link name': 'https://customlink.com'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sharing links
|
||||||
|
'sharing': {
|
||||||
|
'google': null,
|
||||||
|
'facebook': null,
|
||||||
|
'twitter': null,
|
||||||
|
'weibo': null,
|
||||||
|
'all': null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// Options for PDF generation
|
||||||
|
'pdf': {
|
||||||
|
// Add toc at the end of the file
|
||||||
|
'toc': true,
|
||||||
|
|
||||||
|
// Add page numbers to the bottom of every page
|
||||||
|
'pageNumbers': false,
|
||||||
|
|
||||||
|
// Font for the file content
|
||||||
|
'fontSize': 12,
|
||||||
|
'fontFamily': 'Arial',
|
||||||
|
|
||||||
|
// Paper size for the pdf
|
||||||
|
// Choices are [u’a0’, u’a1’, u’a2’, u’a3’, u’a4’, u’a5’, u’a6’, u’b0’, u’b1’, u’b2’, u’b3’, u’b4’, u’b5’, u’b6’, u’legal’, u’letter’]
|
||||||
|
'paperSize': 'a4',
|
||||||
|
|
||||||
|
// How to mark detected chapters.
|
||||||
|
// Choices are “pagebreak”, “rule”, 'both' or “none”.
|
||||||
|
'chapterMark' : 'pagebreak',
|
||||||
|
|
||||||
|
// An XPath expression. Page breaks are inserted before the specified elements.
|
||||||
|
// To disable use the expression: '/'
|
||||||
|
'pageBreaksBefore': '/',
|
||||||
|
|
||||||
|
// Margin (in pts)
|
||||||
|
// Note: 72 pts equals 1 inch
|
||||||
|
'margin': {
|
||||||
|
'right': 62,
|
||||||
|
'left': 62,
|
||||||
|
'top': 56,
|
||||||
|
'bottom': 56
|
||||||
|
},
|
||||||
|
|
||||||
|
// Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
|
||||||
|
'headerTemplate': null,
|
||||||
|
|
||||||
|
// Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
|
||||||
|
'footerTemplate': null
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var Q = require('q');
|
||||||
|
var path = require('path');
|
||||||
|
var semver = require('semver');
|
||||||
|
|
||||||
|
var pkg = require('../package.json');
|
||||||
|
var i18n = require('./utils/i18n');
|
||||||
|
var version = require('./version');
|
||||||
|
|
||||||
|
var DEFAULT_CONFIG = require('./config_default');
|
||||||
|
|
||||||
|
// Default plugins added to each books
|
||||||
|
var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings'];
|
||||||
|
|
||||||
|
// Check if a plugin is a default plugin
|
||||||
|
// Plugin should be in the list
|
||||||
|
// And version from book.json specified for this plugin should be satisfied
|
||||||
|
function isDefaultPlugin(name, version) {
|
||||||
|
if (!_.contains(DEFAULT_PLUGINS, name)) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var pluginPkg = require('gitbook-plugin-'+name+'/package.json');
|
||||||
|
return semver.satisfies(pluginPkg.version, version || '*');
|
||||||
|
} catch(e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize a list of plugins to use
|
||||||
|
function normalizePluginsList(plugins, addDefaults) {
|
||||||
|
// Normalize list to an array
|
||||||
|
plugins = _.isString(plugins) ? plugins.split(',') : (plugins || []);
|
||||||
|
|
||||||
|
// Remove empty parts
|
||||||
|
plugins = _.compact(plugins);
|
||||||
|
|
||||||
|
// Divide as {name, version} to handle format like 'myplugin@1.0.0'
|
||||||
|
plugins = _.map(plugins, function(plugin) {
|
||||||
|
if (plugin.name) return plugin;
|
||||||
|
|
||||||
|
var parts = plugin.split('@');
|
||||||
|
var name = parts[0];
|
||||||
|
var version = parts[1];
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'version': version, // optional
|
||||||
|
'isDefault': isDefaultPlugin(name, version)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// List plugins to remove
|
||||||
|
var toremove = _.chain(plugins)
|
||||||
|
.filter(function(plugin) {
|
||||||
|
return plugin.name.length > 0 && plugin.name[0] == '-';
|
||||||
|
})
|
||||||
|
.map(function(plugin) {
|
||||||
|
return plugin.name.slice(1);
|
||||||
|
})
|
||||||
|
.value();
|
||||||
|
|
||||||
|
// Merge with defaults
|
||||||
|
if (addDefaults !== false) {
|
||||||
|
_.each(DEFAULT_PLUGINS, function(plugin) {
|
||||||
|
if (_.find(plugins, { name: plugin })) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.push({
|
||||||
|
'name': plugin,
|
||||||
|
'isDefault': true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove plugin that start with '-'
|
||||||
|
plugins = _.filter(plugins, function(plugin) {
|
||||||
|
return !_.contains(toremove, plugin.name) && !(plugin.name.length > 0 && plugin.name[0] == '-');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove duplicates
|
||||||
|
plugins = _.uniq(plugins, 'name');
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
var Configuration = function(book, options) {
|
||||||
|
this.book = book;
|
||||||
|
this.replace(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read and parse the configuration
|
||||||
|
Configuration.prototype.load = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
var configPath, _config;
|
||||||
|
|
||||||
|
try {
|
||||||
|
configPath = require.resolve(
|
||||||
|
that.book.resolve(that.options.configFile)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invalidate node.js cache for livreloading
|
||||||
|
delete require.cache[configPath];
|
||||||
|
|
||||||
|
_config = require(configPath);
|
||||||
|
that.options = _.merge(
|
||||||
|
that.options,
|
||||||
|
_.omit(_config, 'configFile', 'defaultsPlugins', 'generator', 'extension')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch(err) {
|
||||||
|
if (err instanceof SyntaxError) return Q.reject(err);
|
||||||
|
return Q();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
if (!that.book.isSubBook()) {
|
||||||
|
if (!version.satisfies(that.options.gitbook)) {
|
||||||
|
throw new Error('GitBook version doesn\'t satisfy version required by the book: '+that.options.gitbook);
|
||||||
|
}
|
||||||
|
if (that.options.gitbook != '*' && !semver.satisfies(semver.inc(pkg.version, 'patch'), that.options.gitbook)) {
|
||||||
|
that.book.log.warn.ln('gitbook version specified in your book.json might be too strict for future patches, \''+(_.first(pkg.version.split('.'))+'.x.x')+'\' is more adequate');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
that.options.output = path.resolve(that.options.output || that.book.resolve('_book'));
|
||||||
|
that.options.plugins = normalizePluginsList(that.options.plugins);
|
||||||
|
that.options.defaultsPlugins = normalizePluginsList(that.options.defaultsPlugins || '', false);
|
||||||
|
that.options.plugins = _.union(that.options.plugins, that.options.defaultsPlugins);
|
||||||
|
that.options.plugins = _.uniq(that.options.plugins, 'name');
|
||||||
|
|
||||||
|
// Default value for text direction (from language)
|
||||||
|
if (!that.options.direction) {
|
||||||
|
var lang = i18n.getCatalog(that.options.language);
|
||||||
|
if (lang) that.options.direction = lang.direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
that.options.gitbook = pkg.version;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extend the configuration
|
||||||
|
Configuration.prototype.extend = function(options) {
|
||||||
|
_.extend(this.options, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replace the whole configuration
|
||||||
|
Configuration.prototype.replace = function(options) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.options = _.cloneDeep(DEFAULT_CONFIG);
|
||||||
|
this.options = _.merge(this.options, options || {});
|
||||||
|
|
||||||
|
// options.input == book.root
|
||||||
|
Object.defineProperty(this.options, 'input', {
|
||||||
|
get: function () {
|
||||||
|
return that.book.root;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// options.originalInput == book.parent.root
|
||||||
|
Object.defineProperty(this.options, 'originalInput', {
|
||||||
|
get: function () {
|
||||||
|
return that.book.parent? that.book.parent.root : undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// options.originalOutput == book.parent.options.output
|
||||||
|
Object.defineProperty(this.options, 'originalOutput', {
|
||||||
|
get: function () {
|
||||||
|
return that.book.parent? that.book.parent.options.output : undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dump configuration as json object
|
||||||
|
Configuration.prototype.dump = function() {
|
||||||
|
return _.cloneDeep(this.options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get structure file
|
||||||
|
Configuration.prototype.getStructure = function(name, dontStripExt) {
|
||||||
|
var filename = this.options.structure[name];
|
||||||
|
if (dontStripExt) return filename;
|
||||||
|
|
||||||
|
filename = filename.split('.').slice(0, -1).join('.');
|
||||||
|
return filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return normalized language
|
||||||
|
Configuration.prototype.normalizeLanguage = function() {
|
||||||
|
return i18n.normalizeLanguage(this.options.language);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return a configuration
|
||||||
|
Configuration.prototype.get = function(key, def) {
|
||||||
|
return _.get(this.options, key, def);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update a configuration
|
||||||
|
Configuration.prototype.set = function(key, value) {
|
||||||
|
return _.set(this.options, key, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default configuration
|
||||||
|
Configuration.DEFAULT = DEFAULT_CONFIG;
|
||||||
|
|
||||||
|
module.exports= Configuration;
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var path = require('path');
|
||||||
|
var nunjucks = require('nunjucks');
|
||||||
|
|
||||||
|
var git = require('./utils/git');
|
||||||
|
var fs = require('./utils/fs');
|
||||||
|
var pathUtil = require('./utils/path');
|
||||||
|
|
||||||
|
// The loader should handle relative and git url
|
||||||
|
var BookLoader = nunjucks.Loader.extend({
|
||||||
|
async: true,
|
||||||
|
|
||||||
|
init: function(book, opts) {
|
||||||
|
this.opts = _.defaults(opts || {}, {
|
||||||
|
interpolate: _.identity
|
||||||
|
});
|
||||||
|
this.book = book;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSource: function(fileurl, callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
git.resolveFile(fileurl)
|
||||||
|
.then(function(filepath) {
|
||||||
|
// Is local file
|
||||||
|
if (!filepath) filepath = path.resolve(fileurl);
|
||||||
|
else that.book.log.debug.ln('resolve from git', fileurl, 'to', filepath);
|
||||||
|
|
||||||
|
// Read file from absolute path
|
||||||
|
return fs.readFile(filepath)
|
||||||
|
.then(function(source) {
|
||||||
|
return that.opts.interpolate(filepath, source.toString());
|
||||||
|
})
|
||||||
|
.then(function(source) {
|
||||||
|
return {
|
||||||
|
src: source,
|
||||||
|
path: filepath,
|
||||||
|
|
||||||
|
// We disable cache sincde content is modified (shortcuts, ...)
|
||||||
|
noCache: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.nodeify(callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve: function(from, to) {
|
||||||
|
// If origin is in the book, we enforce result file to be in the book
|
||||||
|
if (this.book.fileIsInBook(from)) {
|
||||||
|
return this.book.resolve(
|
||||||
|
this.book.relative(path.dirname(from)),
|
||||||
|
to
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If origin is in a git repository, we resolve file in the git repository
|
||||||
|
var gitRoot = git.resolveRoot(from);
|
||||||
|
if (gitRoot) {
|
||||||
|
return pathUtil.resolveInRoot(gitRoot, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If origin is not in the book (include from a git content ref)
|
||||||
|
return path.resolve(path.dirname(from), to);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Handle all files as relative, so that nunjucks pass responsability to 'resolve'
|
||||||
|
// Only git urls are considered as absolute
|
||||||
|
isRelative: function(filename) {
|
||||||
|
return !git.checkUrl(filename);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = BookLoader;
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
// Default configuration for gitbook
|
|
||||||
var CONFIG = {
|
|
||||||
// Folders to use for output
|
|
||||||
// Caution: it overrides the value from the command line
|
|
||||||
// It's not advised this option in the book.json
|
|
||||||
"output": null,
|
|
||||||
|
|
||||||
// Generator to use for building
|
|
||||||
// Caution: it overrides the value from the command line
|
|
||||||
// It's not advised this option in the book.json
|
|
||||||
"generator": "site",
|
|
||||||
|
|
||||||
// Configuration file to use
|
|
||||||
"configFile": "book",
|
|
||||||
|
|
||||||
// Book title and description (defaults are extracted from the README)
|
|
||||||
"title": null,
|
|
||||||
"description": null,
|
|
||||||
|
|
||||||
// For ebook format, the extension to use for generation (default is detected from output extension)
|
|
||||||
// "epub", "pdf", "mobi"
|
|
||||||
// Caution: it overrides the value from the command line
|
|
||||||
// It's not advised this option in the book.json
|
|
||||||
"extension": null,
|
|
||||||
|
|
||||||
// Plugins list, can contain "-name" for removing default plugins
|
|
||||||
"plugins": [],
|
|
||||||
|
|
||||||
// Global configuration for plugins
|
|
||||||
"pluginsConfig": {
|
|
||||||
"fontSettings": {
|
|
||||||
"theme": null, //"sepia", "night" or "white",
|
|
||||||
"family": "sans",// "serif" or "sans",
|
|
||||||
"size": 2 // 1 - 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Variables for templating
|
|
||||||
"variables": {},
|
|
||||||
|
|
||||||
// Set another theme with your own layout
|
|
||||||
// It's recommended to use plugins or add more options for default theme, though
|
|
||||||
// See https://github.com/GitbookIO/gitbook/issues/209
|
|
||||||
"theme": path.resolve(__dirname, '../../theme'),
|
|
||||||
|
|
||||||
// Links in template (null: default, false: remove, string: new value)
|
|
||||||
"links": {
|
|
||||||
// Custom links at top of sidebar
|
|
||||||
"sidebar": {
|
|
||||||
//"Custom link name": "https://customlink.com"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Sharing links
|
|
||||||
"sharing": {
|
|
||||||
"google": null,
|
|
||||||
"facebook": null,
|
|
||||||
"twitter": null,
|
|
||||||
"weibo": null,
|
|
||||||
"all": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
// Options for PDF generation
|
|
||||||
"pdf": {
|
|
||||||
// Add toc at the end of the file
|
|
||||||
"toc": true,
|
|
||||||
|
|
||||||
// Add page numbers to the bottom of every page
|
|
||||||
"pageNumbers": false,
|
|
||||||
|
|
||||||
// Font size for the file content
|
|
||||||
"fontSize": 12,
|
|
||||||
|
|
||||||
// Paper size for the pdf
|
|
||||||
// Choices are [u’a0’, u’a1’, u’a2’, u’a3’, u’a4’, u’a5’, u’a6’, u’b0’, u’b1’, u’b2’, u’b3’, u’b4’, u’b5’, u’b6’, u’legal’, u’letter’]
|
|
||||||
"paperSize": "a4",
|
|
||||||
|
|
||||||
// Margin (in pts)
|
|
||||||
// Note: 72 pts equals 1 inch
|
|
||||||
"margin": {
|
|
||||||
"right": 62,
|
|
||||||
"left": 62,
|
|
||||||
"top": 36,
|
|
||||||
"bottom": 36
|
|
||||||
},
|
|
||||||
|
|
||||||
//Header HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
|
|
||||||
"headerTemplate": "",
|
|
||||||
|
|
||||||
//Footer HTML template. Available variables: _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_.
|
|
||||||
"footerTemplate": ""
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
CONFIG: CONFIG,
|
|
||||||
defaults: function(options) {
|
|
||||||
return _.merge(options || {}, CONFIG, _.defaults);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
var util = require("util");
|
|
||||||
var path = require("path");
|
|
||||||
var Q = require("q");
|
|
||||||
var _ = require("lodash");
|
|
||||||
var exec = require('child_process').exec;
|
|
||||||
|
|
||||||
var fs = require('graceful-fs');
|
|
||||||
var parse = require("../../parse");
|
|
||||||
var BaseGenerator = require("../page");
|
|
||||||
var stringUtils = require("../../utils/string");
|
|
||||||
|
|
||||||
var Generator = function() {
|
|
||||||
BaseGenerator.apply(this, arguments);
|
|
||||||
};
|
|
||||||
util.inherits(Generator, BaseGenerator);
|
|
||||||
|
|
||||||
Generator.prototype.finish = function() {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
return BaseGenerator.prototype.finish.apply(this)
|
|
||||||
.then(function() {
|
|
||||||
var d = Q.defer();
|
|
||||||
var format = that.options.extension || path.extname(that.options.output).replace("\.", "") || "pdf";
|
|
||||||
|
|
||||||
if (!that.options.cover && fs.existsSync(path.join(that.options.output, "cover.jpg"))) {
|
|
||||||
that.options.cover = path.join(that.options.output, "cover.jpg");
|
|
||||||
}
|
|
||||||
|
|
||||||
var _options = {
|
|
||||||
"--cover": that.options.cover,
|
|
||||||
"--title": that.options.title,
|
|
||||||
"--comments": that.options.description,
|
|
||||||
"--authors": that.options.author,
|
|
||||||
"--publisher": "GitBook",
|
|
||||||
"--chapter": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter ')]",
|
|
||||||
"--chapter-mark": "pagebreak",
|
|
||||||
"--page-breaks-before": "/",
|
|
||||||
"--level1-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-1 ')]",
|
|
||||||
"--level2-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-2 ')]",
|
|
||||||
"--level3-toc": "descendant-or-self::*[contains(concat(' ', normalize-space(@class), ' '), ' book-chapter-3 ')]",
|
|
||||||
"--no-chapters-in-toc": true,
|
|
||||||
"--max-levels": "1",
|
|
||||||
"--breadth-first": true
|
|
||||||
};
|
|
||||||
|
|
||||||
if (format == "pdf") {
|
|
||||||
var pdfOptions = that.options.pdf;
|
|
||||||
|
|
||||||
_.extend(_options, {
|
|
||||||
"--margin-left": String(pdfOptions.margin.left),
|
|
||||||
"--margin-right": String(pdfOptions.margin.right),
|
|
||||||
"--margin-top": String(pdfOptions.margin.top),
|
|
||||||
"--margin-bottom": String(pdfOptions.margin.bottom),
|
|
||||||
"--pdf-default-font-size": String(pdfOptions.fontSize),
|
|
||||||
"--pdf-mono-font-size": String(pdfOptions.fontSize),
|
|
||||||
"--paper-size": String(pdfOptions.paperSize),
|
|
||||||
"--pdf-page-numbers": Boolean(pdfOptions.pageNumbers),
|
|
||||||
"--pdf-header-template": String(pdfOptions.headerTemplate),
|
|
||||||
"--pdf-footer-template": String(pdfOptions.footerTemplate)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var command = [
|
|
||||||
"ebook-convert",
|
|
||||||
path.join(that.options.output, "SUMMARY.html"),
|
|
||||||
path.join(that.options.output, "index."+format),
|
|
||||||
stringUtils.optionsToShellArgs(_options)
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
exec(command, function (error, stdout, stderr) {
|
|
||||||
if (error) {
|
|
||||||
if (error.code == 127) {
|
|
||||||
error.message = "Need to install ebook-convert from Calibre";
|
|
||||||
} else {
|
|
||||||
error.message = error.message + " "+stdout;
|
|
||||||
}
|
|
||||||
return d.reject(error);
|
|
||||||
}
|
|
||||||
d.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
return d.promise;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Generator;
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
var Q = require("q");
|
|
||||||
var fs = require('graceful-fs');
|
|
||||||
var fsExtra = require("fs-extra");
|
|
||||||
var Ignore = require("fstream-ignore");
|
|
||||||
|
|
||||||
var getFiles = function(path) {
|
|
||||||
var d = Q.defer();
|
|
||||||
|
|
||||||
// Our list of files
|
|
||||||
var files = [];
|
|
||||||
|
|
||||||
var ig = Ignore({
|
|
||||||
path: path,
|
|
||||||
ignoreFiles: ['.ignore', '.gitignore', '.bookignore']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add extra rules to ignore common folders
|
|
||||||
ig.addIgnoreRules([
|
|
||||||
// Skip Git stuff
|
|
||||||
'.git/',
|
|
||||||
'.gitignore',
|
|
||||||
|
|
||||||
// Skip OS X meta data
|
|
||||||
'.DS_Store',
|
|
||||||
|
|
||||||
// Skip stuff installed by plugins
|
|
||||||
'node_modules',
|
|
||||||
|
|
||||||
// Skip book outputs
|
|
||||||
'*.pdf',
|
|
||||||
'*.epub',
|
|
||||||
'*.mobi',
|
|
||||||
|
|
||||||
// Skip config files
|
|
||||||
'.ignore',
|
|
||||||
'.bookignore',
|
|
||||||
'book.json',
|
|
||||||
], '__custom_stuff');
|
|
||||||
|
|
||||||
// Push each file to our list
|
|
||||||
ig.on('child', function (c) {
|
|
||||||
files.push(
|
|
||||||
c.path.substr(c.root.path.length + 1) + (c.props.Directory === true ? '/' : '')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ig.on('end', function() {
|
|
||||||
// Normalize paths on Windows
|
|
||||||
if(process.platform === 'win32') {
|
|
||||||
return d.resolve(files.map(function(file) {
|
|
||||||
return file.replace(/\\/g, '/');
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simply return paths otherwise
|
|
||||||
return d.resolve(files);
|
|
||||||
});
|
|
||||||
|
|
||||||
ig.on('error', d.reject);
|
|
||||||
|
|
||||||
return d.promise;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
list: getFiles,
|
|
||||||
readFile: Q.denodeify(fs.readFile),
|
|
||||||
writeFile: Q.denodeify(fs.writeFile),
|
|
||||||
mkdirp: Q.denodeify(fsExtra.mkdirp),
|
|
||||||
copy: Q.denodeify(fsExtra.copy),
|
|
||||||
remove: Q.denodeify(fsExtra.remove),
|
|
||||||
symlink: Q.denodeify(fsExtra.symlink),
|
|
||||||
exists: function(path) {
|
|
||||||
var d = Q.defer();
|
|
||||||
fs.exists(path, d.resolve);
|
|
||||||
return d.promise;
|
|
||||||
},
|
|
||||||
readFileSync: fs.readFileSync.bind(fs)
|
|
||||||
};
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
var _ = require("lodash");
|
|
||||||
var path = require("path");
|
|
||||||
var Q = require("q");
|
|
||||||
var fs = require("./fs");
|
|
||||||
|
|
||||||
var Plugin = require("./plugin");
|
|
||||||
|
|
||||||
var BaseGenerator = function(options) {
|
|
||||||
this.options = options;
|
|
||||||
|
|
||||||
// Base for assets in plugins
|
|
||||||
this.pluginAssetsBase = "book";
|
|
||||||
|
|
||||||
this.options.plugins = Plugin.normalizeNames(this.options.plugins);
|
|
||||||
this.options.plugins = _.union(this.options.plugins, this.options.defaultsPlugins);
|
|
||||||
this.plugins = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseGenerator.prototype.callHook = function(name, data) {
|
|
||||||
return this.plugins.hook(name, data);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sets up generator
|
|
||||||
BaseGenerator.prototype.load = function() {
|
|
||||||
return this.loadPlugins();
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseGenerator.prototype.loadPlugins = function() {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
return Plugin.fromList(this.options.plugins, this.options.input, this, {
|
|
||||||
assetsBase: this.pluginAssetsBase
|
|
||||||
})
|
|
||||||
.then(function(_plugins) {
|
|
||||||
that.plugins = _plugins;
|
|
||||||
|
|
||||||
return that.callHook("init");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseGenerator.prototype.convertFile = function(content, input) {
|
|
||||||
return Q.reject(new Error("Could not convert "+input));
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseGenerator.prototype.transferFile = function(input) {
|
|
||||||
return fs.copy(
|
|
||||||
path.join(this.options.input, input),
|
|
||||||
path.join(this.options.output, input)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseGenerator.prototype.transferFolder = function(input) {
|
|
||||||
return fs.mkdirp(
|
|
||||||
path.join(this.options.output, input)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseGenerator.prototype.copyCover = function() {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
return Q.all([
|
|
||||||
fs.copy(path.join(this.options.input, "cover.jpg"), path.join(this.options.output, "cover.jpg")),
|
|
||||||
fs.copy(path.join(this.options.input, "cover_small.jpg"), path.join(this.options.output, "cover_small.jpg"))
|
|
||||||
])
|
|
||||||
.fail(function() {
|
|
||||||
// If orignally from multi-lang, try copy from originalInput
|
|
||||||
if (!that.options.originalInput) return;
|
|
||||||
|
|
||||||
return Q.all([
|
|
||||||
fs.copy(path.join(that.options.originalInput, "cover.jpg"), path.join(that.options.output, "cover.jpg")),
|
|
||||||
fs.copy(path.join(that.options.originalInput, "cover_small.jpg"), path.join(that.options.output, "cover_small.jpg"))
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
.fail(function(err) {
|
|
||||||
return Q();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseGenerator.prototype.langsIndex = function(langs) {
|
|
||||||
return Q.reject(new Error("Langs index is not supported in this generator"));
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseGenerator.prototype.finish = function() {
|
|
||||||
return Q.reject(new Error("Could not finish generation"));
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = BaseGenerator;
|
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
var Q = require("q");
|
|
||||||
var _ = require("lodash");
|
|
||||||
var path = require("path");
|
|
||||||
var tmp = require('tmp');
|
|
||||||
|
|
||||||
var swig = require('./template');
|
|
||||||
var fs = require("./fs");
|
|
||||||
var parse = require("../parse");
|
|
||||||
var Plugin = require("./plugin");
|
|
||||||
var defaultConfig = require("./config");
|
|
||||||
|
|
||||||
var generators = {
|
|
||||||
"site": require("./site"),
|
|
||||||
"page": require("./page"),
|
|
||||||
"ebook": require("./ebook"),
|
|
||||||
"json": require("./json")
|
|
||||||
};
|
|
||||||
|
|
||||||
var defaultDescription = "Book generated using GitBook";
|
|
||||||
|
|
||||||
|
|
||||||
var containsFiles = function(dir, files) {
|
|
||||||
return Q.all(_.map(files, function(file) {
|
|
||||||
return fs.exists(path.join(dir, file));
|
|
||||||
}))
|
|
||||||
.then(_.all);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test if generator exists
|
|
||||||
var checkGenerator = function(options) {
|
|
||||||
if (!generators[options.generator]) {
|
|
||||||
return Q.reject(new Error("Invalid generator (availables are: "+_.keys(generators).join(", ")+")"));
|
|
||||||
}
|
|
||||||
return Q();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the generator and load plugins
|
|
||||||
var loadGenerator = function(options) {
|
|
||||||
return checkGenerator(options)
|
|
||||||
.then(function() {
|
|
||||||
var generator = new generators[options.generator](options);
|
|
||||||
|
|
||||||
return generator.load()
|
|
||||||
.then(_.constant(generator));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var generate = function(options) {
|
|
||||||
// Set defaults to options
|
|
||||||
options = defaultConfig.defaults(options);
|
|
||||||
|
|
||||||
// Validate options
|
|
||||||
if (!options.input) {
|
|
||||||
return Q.reject(new Error("Need option input (book input directory)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check files to get folder type (book, multilanguage book or neither)
|
|
||||||
return checkGenerator(options)
|
|
||||||
|
|
||||||
// Read config file
|
|
||||||
.then(function() {
|
|
||||||
try {
|
|
||||||
var _config = require(path.resolve(options.input, options.configFile));
|
|
||||||
|
|
||||||
options = _.merge(options, _.omit(_config, 'input', 'configFile', 'defaultsPlugins'));
|
|
||||||
}
|
|
||||||
catch(err) {
|
|
||||||
// No config file: not a big deal
|
|
||||||
return Q();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Read readme
|
|
||||||
.then(function() {
|
|
||||||
return fs.readFile(path.join(options.input, "README.md"), "utf-8")
|
|
||||||
.then(function(_readme) {
|
|
||||||
_readme = parse.readme(_readme);
|
|
||||||
|
|
||||||
options.title = options.title || _readme.title;
|
|
||||||
options.description = options.description || _readme.description || defaultDescription;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
// Detect multi-languages book
|
|
||||||
.then(function() {
|
|
||||||
return containsFiles(options.input, ['LANGS.md'])
|
|
||||||
})
|
|
||||||
|
|
||||||
.then(function(isMultiLang) {
|
|
||||||
// Multi language book
|
|
||||||
if(isMultiLang) {
|
|
||||||
return generateMultiLang(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Book
|
|
||||||
return generateBook(options);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
var generateMultiLang = function(options) {
|
|
||||||
var langsSummary;
|
|
||||||
options.output = options.output || path.join(options.input, "_book");
|
|
||||||
|
|
||||||
return checkGenerator(options)
|
|
||||||
|
|
||||||
// Multi-languages book
|
|
||||||
.then(function() {
|
|
||||||
return fs.readFile(path.join(options.input, "LANGS.md"), "utf-8")
|
|
||||||
})
|
|
||||||
|
|
||||||
// Clean output folder
|
|
||||||
.then(function(_langsSummary) {
|
|
||||||
langsSummary = _langsSummary;
|
|
||||||
return fs.remove(options.output);
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
return fs.mkdirp(options.output);
|
|
||||||
})
|
|
||||||
|
|
||||||
// Generate sub-books
|
|
||||||
.then(function() {
|
|
||||||
options.langsSummary = parse.langs(langsSummary);
|
|
||||||
|
|
||||||
// Generated a book for each valid entry
|
|
||||||
return _.reduce(options.langsSummary.list, function(prev, entry) {
|
|
||||||
return prev.then(function() {
|
|
||||||
return generate(_.extend({}, options, {
|
|
||||||
input: path.join(options.input, entry.path),
|
|
||||||
output: path.join(options.output, entry.path),
|
|
||||||
originalInput: options.input,
|
|
||||||
originalOutput: options.output
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
}, Q());
|
|
||||||
})
|
|
||||||
|
|
||||||
.then(function() {
|
|
||||||
return loadGenerator(options);
|
|
||||||
})
|
|
||||||
|
|
||||||
// Generate languages index
|
|
||||||
.then(function(generator) {
|
|
||||||
return generator.langsIndex(options.langsSummary);
|
|
||||||
})
|
|
||||||
|
|
||||||
// Copy cover file
|
|
||||||
.then(function() {
|
|
||||||
return Q.all([
|
|
||||||
fs.copy(path.join(options.input, "cover.jpg"), path.join(options.output, "cover.jpg")),
|
|
||||||
fs.copy(path.join(options.input, "cover_small.jpg"), path.join(options.output, "cover_small.jpg"))
|
|
||||||
])
|
|
||||||
.fail(function() {
|
|
||||||
return Q();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return options to caller
|
|
||||||
.then(_.constant(options));
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Use a specific generator to convert a gitbook to a site/pdf/ebook/
|
|
||||||
* output is always a folder
|
|
||||||
*/
|
|
||||||
var generateBook = function(options) {
|
|
||||||
var files;
|
|
||||||
|
|
||||||
options.output = options.output || path.join(options.input, "_book");
|
|
||||||
|
|
||||||
// Check if it's a book
|
|
||||||
return containsFiles(options.input, ['SUMMARY.md', 'README.md'])
|
|
||||||
|
|
||||||
// Fail if not a book
|
|
||||||
.then(function(isBook) {
|
|
||||||
if(!isBook) {
|
|
||||||
return Q.reject(new Error("Invalid gitbook repository, need SUMMARY.md and README.md"));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Clean output folder
|
|
||||||
.then(function() {
|
|
||||||
return fs.remove(options.output);
|
|
||||||
})
|
|
||||||
|
|
||||||
.then(function() {
|
|
||||||
return fs.mkdirp(options.output);
|
|
||||||
})
|
|
||||||
|
|
||||||
// List all files in the repository
|
|
||||||
.then(function() {
|
|
||||||
return fs.list(options.input)
|
|
||||||
.then(function(_files) {
|
|
||||||
files = _files;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
.then(function() {
|
|
||||||
return loadGenerator(options);
|
|
||||||
})
|
|
||||||
|
|
||||||
// Convert files
|
|
||||||
.then(function(generator) {
|
|
||||||
// Generate the book
|
|
||||||
return Q()
|
|
||||||
|
|
||||||
// Get summary
|
|
||||||
.then(function() {
|
|
||||||
var summary = {
|
|
||||||
path: path.join(options.input, "SUMMARY.md")
|
|
||||||
};
|
|
||||||
|
|
||||||
var _callHook = function(name) {
|
|
||||||
return generator.callHook(name, summary)
|
|
||||||
.then(function(_summary) {
|
|
||||||
summary = _summary;
|
|
||||||
return summary;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return fs.readFile(summary.path, "utf-8")
|
|
||||||
.then(function(_content) {
|
|
||||||
summary.content = _content;
|
|
||||||
return _callHook("summary:before");
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
summary.content = parse.summary(summary.content);
|
|
||||||
return _callHook("summary:after");
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
options.summary = summary.content;
|
|
||||||
options.navigation = parse.navigation(options.summary);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
// Read glossary
|
|
||||||
.then(function() {
|
|
||||||
var glossary = {};
|
|
||||||
|
|
||||||
var _callHook = function(name) {
|
|
||||||
return generator.callHook(name, glossary)
|
|
||||||
.then(function(_glossary) {
|
|
||||||
glossary = _glossary;
|
|
||||||
return glossary;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return fs.readFile(path.join(options.input, "GLOSSARY.md"), "utf-8")
|
|
||||||
.fail(function() {
|
|
||||||
return "";
|
|
||||||
})
|
|
||||||
.then(function(_content) {
|
|
||||||
glossary.content = _content;
|
|
||||||
return _callHook("glossary:before");
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
glossary.content = parse.glossary(glossary.content);
|
|
||||||
return _callHook("glossary:after");
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
options.glossary = glossary.content;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
// Skip processing some files
|
|
||||||
.then(function() {
|
|
||||||
files = _.filter(files, function (file) {
|
|
||||||
return !(
|
|
||||||
file === 'SUMMARY.md' ||
|
|
||||||
file === 'GLOSSARY.md'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
// Copy file and replace markdown file
|
|
||||||
.then(function() {
|
|
||||||
return Q.all(
|
|
||||||
_.chain(files)
|
|
||||||
.map(function(file) {
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
if (file[file.length -1] == "/") {
|
|
||||||
return Q(generator.transferFolder(file));
|
|
||||||
} else if (path.extname(file) == ".md" && options.navigation[file] != null) {
|
|
||||||
return fs.readFile(path.join(options.input, file), "utf-8")
|
|
||||||
.then(function(content) {
|
|
||||||
return Q(generator.convertFile(content, file));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Q(generator.transferFile(file));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.value()
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
// Finish generation
|
|
||||||
.then(function() {
|
|
||||||
return generator.finish();
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
return generator.callHook("finish");
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
// Return all options
|
|
||||||
.then(function() {
|
|
||||||
return options;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Extract files from generate output in a temporary folder
|
|
||||||
*/
|
|
||||||
var generateFile = function(options) {
|
|
||||||
options = _.defaults(options || {}, {
|
|
||||||
input: null,
|
|
||||||
output: null,
|
|
||||||
extension: null
|
|
||||||
});
|
|
||||||
|
|
||||||
return Q.nfcall(tmp.dir)
|
|
||||||
.then(function(tmpDir) {
|
|
||||||
return generate(
|
|
||||||
_.extend({},
|
|
||||||
options,
|
|
||||||
{
|
|
||||||
output: tmpDir
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.then(function(_options) {
|
|
||||||
var ext = options.extension;
|
|
||||||
var outputFile = options.output || path.resolve(options.input, "book."+ext);
|
|
||||||
|
|
||||||
var copyFile = function(lang) {
|
|
||||||
var _outputFile = outputFile;
|
|
||||||
var _tmpDir = tmpDir;
|
|
||||||
|
|
||||||
if (lang) {
|
|
||||||
_outputFile = _outputFile.slice(0, -path.extname(_outputFile).length)+"_"+lang+path.extname(_outputFile);
|
|
||||||
_tmpDir = path.join(_tmpDir, lang);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs.copy(
|
|
||||||
path.join(_tmpDir, "index."+ext),
|
|
||||||
_outputFile
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Multi-langs book
|
|
||||||
return Q()
|
|
||||||
.then(function() {
|
|
||||||
if (_options.langsSummary) {
|
|
||||||
return Q.all(
|
|
||||||
_.map(_options.langsSummary.list, function(lang) {
|
|
||||||
return copyFile(lang.lang);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return copyFile();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
return fs.remove(tmpDir);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
generators: generators,
|
|
||||||
folder: generate,
|
|
||||||
file: generateFile,
|
|
||||||
book: generateBook,
|
|
||||||
Plugin: Plugin,
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
var Q = require('q');
|
|
||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
var fs = require('./fs');
|
|
||||||
var parse = require('../parse');
|
|
||||||
|
|
||||||
|
|
||||||
// Extract paths out of a summary
|
|
||||||
function paths(summary) {
|
|
||||||
return _.reduce(summary.chapters, function(accu, chapter) {
|
|
||||||
return accu.concat(
|
|
||||||
_.filter([chapter.path].concat(_.pluck(chapter.articles, 'path')))
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the parent folders out of a group of files
|
|
||||||
function folders(files) {
|
|
||||||
return _.chain(files)
|
|
||||||
.map(function(file) {
|
|
||||||
return path.dirname(file);
|
|
||||||
})
|
|
||||||
.uniq()
|
|
||||||
.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
function initDir(dir) {
|
|
||||||
return fs.readFile(path.join(dir, 'SUMMARY.md'), 'utf8')
|
|
||||||
.then(function(src) {
|
|
||||||
// Parse summary
|
|
||||||
return parse.summary(src);
|
|
||||||
})
|
|
||||||
.then(function(summary) {
|
|
||||||
// Extract paths from summary
|
|
||||||
return paths(summary);
|
|
||||||
})
|
|
||||||
.then(function(paths) {
|
|
||||||
// Convert to absolute paths
|
|
||||||
return _.map(paths, function(file) {
|
|
||||||
return path.resolve(file);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(function(files) {
|
|
||||||
// Create folders
|
|
||||||
return Q.all(_.map(folders(files), function(folder) {
|
|
||||||
return fs.mkdirp(folder);
|
|
||||||
}))
|
|
||||||
.then(_.constant(files));
|
|
||||||
})
|
|
||||||
.then(function(files) {
|
|
||||||
// Create files that don't exist
|
|
||||||
return Q.all(_.map(files, function(file) {
|
|
||||||
return fs.exists(file)
|
|
||||||
.then(function(exists) {
|
|
||||||
if(exists) return;
|
|
||||||
return fs.writeFile(file, '');
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
.fail(function(err) {
|
|
||||||
console.error(err.stack);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
module.exports = initDir;
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
var util = require("util");
|
|
||||||
var path = require("path");
|
|
||||||
var Q = require("q");
|
|
||||||
var _ = require("lodash");
|
|
||||||
|
|
||||||
var fs = require("../fs");
|
|
||||||
var parse = require("../../parse");
|
|
||||||
var BaseGenerator = require("../generator");
|
|
||||||
|
|
||||||
|
|
||||||
var Generator = function() {
|
|
||||||
BaseGenerator.apply(this, arguments);
|
|
||||||
};
|
|
||||||
util.inherits(Generator, BaseGenerator);
|
|
||||||
|
|
||||||
Generator.prototype.transferFile = function(input) {
|
|
||||||
// ignore
|
|
||||||
};
|
|
||||||
|
|
||||||
Generator.prototype.convertFile = function(content, input) {
|
|
||||||
var that = this;
|
|
||||||
var json = {
|
|
||||||
progress: parse.progress(this.options.navigation, input)
|
|
||||||
};
|
|
||||||
|
|
||||||
return Q()
|
|
||||||
.then(function() {
|
|
||||||
return parse.page(content, {
|
|
||||||
dir: path.dirname(input) || '/'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(function(parsed) {
|
|
||||||
json.lexed = parsed.lexed;
|
|
||||||
json.sections = parsed.sections;
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
return fs.writeFile(
|
|
||||||
path.join(that.options.output, input.replace(".md", ".json")),
|
|
||||||
JSON.stringify(json, null, 4)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate languages index
|
|
||||||
// Contains the first languages readme and langs infos
|
|
||||||
Generator.prototype.langsIndex = function(langs) {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
if (langs.list.length == 0) return Q.reject("Need at least one language");
|
|
||||||
|
|
||||||
var mainLang = _.first(langs.list).lang;
|
|
||||||
console.log("Main language is", mainLang);
|
|
||||||
|
|
||||||
return Q()
|
|
||||||
.then(function() {
|
|
||||||
return fs.readFile(
|
|
||||||
path.join(that.options.output, mainLang, "README.json")
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.then(function(content) {
|
|
||||||
var json = JSON.parse(content);
|
|
||||||
_.extend(json, {
|
|
||||||
langs: langs.list
|
|
||||||
});
|
|
||||||
|
|
||||||
return fs.writeFile(
|
|
||||||
path.join(that.options.output, "README.json"),
|
|
||||||
JSON.stringify(json, null, 4)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Generator.prototype.finish = function() {
|
|
||||||
// ignore
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Generator;
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
var _ = require("lodash");
|
|
||||||
var util = require("util");
|
|
||||||
var path = require("path");
|
|
||||||
var Q = require("q");
|
|
||||||
var swig = require("../template");
|
|
||||||
|
|
||||||
var fs = require("../fs");
|
|
||||||
var parse = require("../../parse");
|
|
||||||
var BaseGenerator = require("../site");
|
|
||||||
|
|
||||||
var Generator = function() {
|
|
||||||
BaseGenerator.apply(this, arguments);
|
|
||||||
|
|
||||||
// Base for assets in plugins
|
|
||||||
this.pluginAssetsBase = "ebook";
|
|
||||||
|
|
||||||
// List of pages content
|
|
||||||
this.pages = {};
|
|
||||||
};
|
|
||||||
util.inherits(Generator, BaseGenerator);
|
|
||||||
|
|
||||||
Generator.prototype.loadTemplates = function() {
|
|
||||||
this.template = swig.compileFile(
|
|
||||||
this.plugins.template("ebook:page") || path.resolve(this.options.theme, 'templates/ebook/page.html')
|
|
||||||
);
|
|
||||||
this.summaryTemplate = swig.compileFile(
|
|
||||||
this.plugins.template("ebook:sumary") || path.resolve(this.options.theme, 'templates/ebook/summary.html')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate table of contents
|
|
||||||
Generator.prototype.writeToc = function() {
|
|
||||||
var that = this;
|
|
||||||
var basePath = ".";
|
|
||||||
|
|
||||||
return this._writeTemplate(this.summaryTemplate, {
|
|
||||||
toc: parse.progress(this.options.navigation, "README.md").chapters,
|
|
||||||
basePath: basePath,
|
|
||||||
staticBase: path.join(basePath, "gitbook"),
|
|
||||||
}, path.join(this.options.output, "SUMMARY.html"));
|
|
||||||
};
|
|
||||||
|
|
||||||
Generator.prototype.finish = function() {
|
|
||||||
var that = this;
|
|
||||||
var basePath = ".";
|
|
||||||
var output = path.join(this.options.output, "index.html");
|
|
||||||
|
|
||||||
var progress = parse.progress(this.options.navigation, "README.md");
|
|
||||||
|
|
||||||
return Q()
|
|
||||||
|
|
||||||
// Write table of contents
|
|
||||||
.then(function() {
|
|
||||||
return that.writeToc();
|
|
||||||
})
|
|
||||||
|
|
||||||
// Copy cover
|
|
||||||
.then(function() {
|
|
||||||
return that.copyCover();
|
|
||||||
})
|
|
||||||
|
|
||||||
// Copy assets
|
|
||||||
.then(function() {
|
|
||||||
return that.copyAssets();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate languages index
|
|
||||||
Generator.prototype.langsIndex = function(langs) {
|
|
||||||
return Q();
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Generator;
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
var _ = require("lodash");
|
|
||||||
var Q = require("q");
|
|
||||||
var semver = require("semver");
|
|
||||||
var path = require("path");
|
|
||||||
var url = require("url");
|
|
||||||
var fs = require("./fs");
|
|
||||||
var resolve = require('resolve');
|
|
||||||
|
|
||||||
var pkg = require("../../package.json");
|
|
||||||
|
|
||||||
var RESOURCES = ["js", "css"];
|
|
||||||
|
|
||||||
var Plugin = function(name, root, generator) {
|
|
||||||
this.name = name;
|
|
||||||
this.root = root;
|
|
||||||
this.packageInfos = {};
|
|
||||||
this.infos = {};
|
|
||||||
this.generator = generator;
|
|
||||||
|
|
||||||
// Bind methods
|
|
||||||
_.bindAll(this);
|
|
||||||
|
|
||||||
_.each([
|
|
||||||
"gitbook-plugin-"+name,
|
|
||||||
"gitbook-theme-"+name,
|
|
||||||
"gitbook-"+name,
|
|
||||||
name,
|
|
||||||
], function(_name) {
|
|
||||||
if (this.load(_name, __dirname)) return false;
|
|
||||||
if (this.load(_name, path.resolve(root))) return false;
|
|
||||||
}, this);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load from a name
|
|
||||||
Plugin.prototype.load = function(name, baseDir) {
|
|
||||||
try {
|
|
||||||
var res = resolve.sync(name+"/package.json", { basedir: baseDir });
|
|
||||||
|
|
||||||
this.baseDir = path.dirname(res);
|
|
||||||
this.packageInfos = require(res);
|
|
||||||
this.infos = require(resolve.sync(name, { basedir: baseDir }));
|
|
||||||
this.name = name;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Plugin.prototype.normalizeResource = function(resource) {
|
|
||||||
// Parse the resource path
|
|
||||||
var parsed = url.parse(resource);
|
|
||||||
|
|
||||||
// This is a remote resource
|
|
||||||
// so we will simply link to using it's URL
|
|
||||||
if (parsed.protocol) {
|
|
||||||
return {
|
|
||||||
"url": resource
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will be copied over from disk
|
|
||||||
// and shipped with the book's build
|
|
||||||
return { "path": this.name+"/"+resource };
|
|
||||||
};
|
|
||||||
|
|
||||||
// Return resources
|
|
||||||
Plugin.prototype._getResources = function(base) {
|
|
||||||
base = base || "book";
|
|
||||||
var book = this.infos[base];
|
|
||||||
|
|
||||||
// Nothing specified, fallback to default
|
|
||||||
if (!book) {
|
|
||||||
return Q({});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic function
|
|
||||||
if(typeof book === "function") {
|
|
||||||
// Call giving it the context of our generator
|
|
||||||
return Q().then(book.bind(this.generator));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plain data object
|
|
||||||
return Q(_.cloneDeep(book));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Normalize resources and return them
|
|
||||||
Plugin.prototype.getResources = function(base) {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
return this._getResources(base)
|
|
||||||
.then(function(resources) {
|
|
||||||
|
|
||||||
_.each(RESOURCES, function(resourceType) {
|
|
||||||
resources[resourceType] = (resources[resourceType] || []).map(that.normalizeResource);
|
|
||||||
});
|
|
||||||
|
|
||||||
return resources;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test if it's a valid plugin
|
|
||||||
Plugin.prototype.isValid = function() {
|
|
||||||
return (
|
|
||||||
this.packageInfos &&
|
|
||||||
this.packageInfos.name &&
|
|
||||||
this.packageInfos.engines &&
|
|
||||||
this.packageInfos.engines.gitbook &&
|
|
||||||
semver.satisfies(pkg.version, this.packageInfos.engines.gitbook)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resolve file path
|
|
||||||
Plugin.prototype.resolveFile = function(filename) {
|
|
||||||
return path.resolve(this.baseDir, filename);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resolve file path
|
|
||||||
Plugin.prototype.callHook = function(name, data) {
|
|
||||||
// Our generator will be the context to apply
|
|
||||||
var context = this.generator;
|
|
||||||
|
|
||||||
var hookFunc = this.infos.hooks? this.infos.hooks[name] : null;
|
|
||||||
data = data || {};
|
|
||||||
|
|
||||||
if (!hookFunc) return Q(data);
|
|
||||||
|
|
||||||
return Q()
|
|
||||||
.then(function() {
|
|
||||||
return hookFunc.apply(context, [data]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Copy plugin assets fodler
|
|
||||||
Plugin.prototype.copyAssets = function(out, options) {
|
|
||||||
var that = this;
|
|
||||||
options = _.defaults(options || {}, {
|
|
||||||
base: "book"
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.getResources(options.base)
|
|
||||||
.get('assets')
|
|
||||||
.then(function(assets) {
|
|
||||||
// Assets are undefined
|
|
||||||
if(!assets) return false;
|
|
||||||
|
|
||||||
return fs.copy(
|
|
||||||
that.resolveFile(assets),
|
|
||||||
out
|
|
||||||
).then(_.constant(true));
|
|
||||||
}, _.constant(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Normalize a list of plugin name to use
|
|
||||||
Plugin.normalizeNames = function(names) {
|
|
||||||
// Normalize list to an array
|
|
||||||
names = _.isString(names) ? names.split(",") : (names || []);
|
|
||||||
|
|
||||||
// List plugins to remove
|
|
||||||
var toremove = _.chain(names)
|
|
||||||
.filter(function(name) {
|
|
||||||
return name.length > 0 && name[0] == "-";
|
|
||||||
})
|
|
||||||
.map(function(name) {
|
|
||||||
return name.slice(1);
|
|
||||||
})
|
|
||||||
.value();
|
|
||||||
|
|
||||||
// Merge with defaults
|
|
||||||
names = _.chain(names)
|
|
||||||
.concat(Plugin.defaults)
|
|
||||||
.uniq()
|
|
||||||
.value();
|
|
||||||
|
|
||||||
// Remove plugins starting with
|
|
||||||
names = _.filter(names, function(name) {
|
|
||||||
return !_.contains(toremove, name) && !(name.length > 0 && name[0] == "-");
|
|
||||||
});
|
|
||||||
|
|
||||||
return names;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract data from a list of plugin
|
|
||||||
Plugin.fromList = function(names, root, generator, options) {
|
|
||||||
options = _.defaults(options || {}, {
|
|
||||||
assetsBase: "book"
|
|
||||||
});
|
|
||||||
|
|
||||||
var failed = [];
|
|
||||||
|
|
||||||
// Load plugins
|
|
||||||
var plugins = _.map(names, function(name) {
|
|
||||||
var plugin = new Plugin(name, root, generator);
|
|
||||||
if (!plugin.isValid()) failed.push(name);
|
|
||||||
return plugin;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (_.size(failed) > 0) return Q.reject(new Error("Error loading plugins: "+failed.join(",")));
|
|
||||||
|
|
||||||
// The raw resources extracted from each plugin
|
|
||||||
var pluginResources;
|
|
||||||
|
|
||||||
// Get resources of plugins
|
|
||||||
return Q.all(_.map(plugins, function(plugin) {
|
|
||||||
return plugin.getResources(options.assetsBase);
|
|
||||||
}))
|
|
||||||
// Extract resources out
|
|
||||||
// css, js, etc ...
|
|
||||||
.then(function(resources) {
|
|
||||||
pluginResources = resources;
|
|
||||||
// Group by resource types
|
|
||||||
return _.chain(RESOURCES)
|
|
||||||
.map(function(resourceType) {
|
|
||||||
// Get resources from all the plugins for this current type
|
|
||||||
return [
|
|
||||||
// Key
|
|
||||||
resourceType,
|
|
||||||
// Value
|
|
||||||
_.chain(resources)
|
|
||||||
.pluck(resourceType)
|
|
||||||
.compact()
|
|
||||||
.flatten()
|
|
||||||
.value()
|
|
||||||
];
|
|
||||||
})
|
|
||||||
.object()
|
|
||||||
.value();
|
|
||||||
})
|
|
||||||
// Extract html snippets
|
|
||||||
.then(function(resources) {
|
|
||||||
// Map of html resources by name added by each plugin
|
|
||||||
resources.html = pluginResources.reduce(function(accu, resource) {
|
|
||||||
var html = (resource && resource.html) || {};
|
|
||||||
_.each(html, function(code, key) {
|
|
||||||
// Turn into function if not one already
|
|
||||||
if (!_.isFunction(code)) code = _.constant(code);
|
|
||||||
// Append
|
|
||||||
accu[key] = (accu[key] || []).concat([code]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return accu;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return resources;
|
|
||||||
})
|
|
||||||
// Return big multi-plugin object
|
|
||||||
.then(function(resources) {
|
|
||||||
return {
|
|
||||||
'list': plugins,
|
|
||||||
'resources': resources,
|
|
||||||
'hook': function(name, data) {
|
|
||||||
return _.reduce(plugins, function(prev, plugin) {
|
|
||||||
return prev.then(function(ret) {
|
|
||||||
return plugin.callHook(name, ret);
|
|
||||||
});
|
|
||||||
}, Q(data));
|
|
||||||
},
|
|
||||||
'template': function(name) {
|
|
||||||
var withTpl = _.find(plugins, function(plugin) {
|
|
||||||
return (
|
|
||||||
plugin.infos.templates &&
|
|
||||||
plugin.infos.templates[name]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!withTpl) return null;
|
|
||||||
return withTpl.resolveFile(withTpl.infos.templates[name]);
|
|
||||||
},
|
|
||||||
'html': function(tag, context, options) {
|
|
||||||
return _.map(resources.html[tag] || [], function(code) {
|
|
||||||
return code.call(context, options);
|
|
||||||
}).join("\n");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Default plugins
|
|
||||||
Plugin.defaults = [
|
|
||||||
"mathjax"
|
|
||||||
];
|
|
||||||
|
|
||||||
module.exports = Plugin;
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
var _ = require("lodash");
|
|
||||||
var kramed = require('kramed');
|
|
||||||
var textRenderer = require('kramed-text-renderer');
|
|
||||||
|
|
||||||
var entryId = require('../../parse/glossary').entryId;
|
|
||||||
|
|
||||||
|
|
||||||
function Indexer(glossary) {
|
|
||||||
if(!(this instanceof Indexer)) {
|
|
||||||
return new Indexer(glossary);
|
|
||||||
}
|
|
||||||
|
|
||||||
_.bindAll(this);
|
|
||||||
|
|
||||||
this.glossary = glossary || [];
|
|
||||||
|
|
||||||
this.glossaryTerms = _.pluck(this.glossary, "id");
|
|
||||||
|
|
||||||
// Regex for searching for terms through body
|
|
||||||
this.termsRegex = new RegExp(
|
|
||||||
// Match any of the terms
|
|
||||||
"("+
|
|
||||||
this.glossaryTerms.map(regexEscape).join('|') +
|
|
||||||
")",
|
|
||||||
|
|
||||||
// Flags
|
|
||||||
"gi"
|
|
||||||
);
|
|
||||||
|
|
||||||
// page url => terms
|
|
||||||
this.idx = {
|
|
||||||
/*
|
|
||||||
"a/b.html": ["one word", "second word"]
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
// term => page urls
|
|
||||||
this.invertedIdx = {
|
|
||||||
/*
|
|
||||||
"word1": ["page1.html", "page2.html"]
|
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use text renderer
|
|
||||||
this.renderer = textRenderer();
|
|
||||||
}
|
|
||||||
|
|
||||||
Indexer.prototype.text = function(nodes) {
|
|
||||||
// Copy section
|
|
||||||
var section = _.toArray(nodes);
|
|
||||||
|
|
||||||
// kramed's Render expects this, we don't use it yet
|
|
||||||
section.links = {};
|
|
||||||
|
|
||||||
var options = _.extend({}, kramed.defaults, {
|
|
||||||
renderer: this.renderer
|
|
||||||
});
|
|
||||||
|
|
||||||
return kramed.parser(section, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add page to glossary index
|
|
||||||
Indexer.prototype.add = function(sections, url) {
|
|
||||||
if(!(this.glossary && this.glossary.length > 0)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var textblob =
|
|
||||||
_.where(sections, { type: 'normal' })
|
|
||||||
.map(this.text)
|
|
||||||
.join('\n');
|
|
||||||
|
|
||||||
var matches = _(textblob.match(this.termsRegex) || [])
|
|
||||||
.map(entryId)
|
|
||||||
.uniq()
|
|
||||||
.value();
|
|
||||||
|
|
||||||
// Add idx for book
|
|
||||||
this.idx[url] = matches;
|
|
||||||
|
|
||||||
// Add to inverted idx
|
|
||||||
matches.forEach(function(match) {
|
|
||||||
if(!this.invertedIdx[match]) {
|
|
||||||
this.invertedIdx[match] = [];
|
|
||||||
}
|
|
||||||
this.invertedIdx[match].push(url);
|
|
||||||
}.bind(this));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dump index as a string
|
|
||||||
Indexer.prototype.dump = function() {
|
|
||||||
return JSON.stringify(this.idx);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function regexEscape(s) {
|
|
||||||
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
module.exports = Indexer;
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
var util = require("util");
|
|
||||||
var path = require("path");
|
|
||||||
var Q = require("q");
|
|
||||||
var _ = require("lodash");
|
|
||||||
var swig = require("../template");
|
|
||||||
|
|
||||||
var fs = require("../fs");
|
|
||||||
var parse = require("../../parse");
|
|
||||||
var BaseGenerator = require("../generator");
|
|
||||||
var links = require("../../utils/links");
|
|
||||||
var indexer = require('./search_indexer');
|
|
||||||
var glossaryIndexer = require('./glossary_indexer');
|
|
||||||
|
|
||||||
|
|
||||||
var Generator = function() {
|
|
||||||
BaseGenerator.apply(this, arguments);
|
|
||||||
|
|
||||||
// Attach methods to instance
|
|
||||||
_.bindAll(this);
|
|
||||||
|
|
||||||
this.revision = Date.now();
|
|
||||||
this.indexer = indexer();
|
|
||||||
};
|
|
||||||
util.inherits(Generator, BaseGenerator);
|
|
||||||
|
|
||||||
// Add template loading to load
|
|
||||||
Generator.prototype.load = function() {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
return BaseGenerator.prototype.load.apply(this)
|
|
||||||
.then(function() {
|
|
||||||
return that.loadTemplates();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load all templates
|
|
||||||
Generator.prototype.loadTemplates = function() {
|
|
||||||
this.template = swig.compileFile(
|
|
||||||
this.plugins.template("site:page") || path.resolve(this.options.theme, 'templates/book/page.html')
|
|
||||||
);
|
|
||||||
this.langsTemplate = swig.compileFile(
|
|
||||||
this.plugins.template("site:langs") || path.resolve(this.options.theme, 'templates/book/langs.html')
|
|
||||||
);
|
|
||||||
this.glossaryTemplate = swig.compileFile(
|
|
||||||
this.plugins.template("site:glossary") || path.resolve(this.options.theme, 'templates/book/glossary.html')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate a template
|
|
||||||
Generator.prototype._writeTemplate = function(tpl, options, output, interpolate) {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
interpolate = interpolate || _.identity;
|
|
||||||
return Q()
|
|
||||||
.then(function(sections) {
|
|
||||||
return tpl(_.extend({
|
|
||||||
revision: that.revision,
|
|
||||||
|
|
||||||
title: that.options.title,
|
|
||||||
description: that.options.description,
|
|
||||||
|
|
||||||
glossary: that.options.glossary,
|
|
||||||
|
|
||||||
summary: that.options.summary,
|
|
||||||
allNavigation: that.options.navigation,
|
|
||||||
|
|
||||||
plugins: that.plugins,
|
|
||||||
pluginsConfig: JSON.stringify(that.options.pluginsConfig),
|
|
||||||
htmlSnippet: _.partialRight(that.plugins.html, that, options),
|
|
||||||
|
|
||||||
options: that.options
|
|
||||||
}, options));
|
|
||||||
})
|
|
||||||
.then(interpolate)
|
|
||||||
.then(function(html) {
|
|
||||||
return fs.writeFile(
|
|
||||||
output,
|
|
||||||
html
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Generator.prototype.indexPage = function(lexed, pagePath) {
|
|
||||||
// Setup glossary indexer if not yet setup
|
|
||||||
if(!this.glossaryIndexer) {
|
|
||||||
this.glossaryIndexer = glossaryIndexer(this.options.glossary);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.indexer.add(lexed, pagePath);
|
|
||||||
this.glossaryIndexer.add(lexed, pagePath);
|
|
||||||
return Q();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert a markdown file into a normalized data set
|
|
||||||
Generator.prototype.prepareFile = function(content, _input) {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
var input = path.join(this.options.input, _input);
|
|
||||||
|
|
||||||
var page = {
|
|
||||||
path: _input,
|
|
||||||
rawPath: input,
|
|
||||||
content: content,
|
|
||||||
progress: parse.progress(this.options.navigation, _input)
|
|
||||||
};
|
|
||||||
|
|
||||||
var _callHook = function(name) {
|
|
||||||
return that.callHook(name, page)
|
|
||||||
.then(function(_page) {
|
|
||||||
page = _page;
|
|
||||||
return page;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return Q()
|
|
||||||
.then(function() {
|
|
||||||
// Send content to plugins
|
|
||||||
return _callHook("page:before");
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
// Lex, parse includes and get
|
|
||||||
// Get HTML generated sections
|
|
||||||
return parse.page(page.content, {
|
|
||||||
// Local files path
|
|
||||||
dir: path.dirname(_input) || '/',
|
|
||||||
|
|
||||||
// Output directory
|
|
||||||
outdir: path.dirname(_input) || '/',
|
|
||||||
|
|
||||||
// Includer for templating
|
|
||||||
includer: parse.includer(that.options.variables, [
|
|
||||||
path.dirname(_input) || '/',
|
|
||||||
path.join(that.options.input, '_includes'),
|
|
||||||
], path.join, fs.readFileSync)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(function(parsed) {
|
|
||||||
page.lexed = parsed.lexed;
|
|
||||||
page.sections = parsed.sections;
|
|
||||||
|
|
||||||
// Use plugin hook
|
|
||||||
return _callHook("page");
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
return page;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert a markdown file to html
|
|
||||||
Generator.prototype.convertFile = function(content, _input) {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
var _output = _input.replace(".md", ".html");
|
|
||||||
if (_output == "README.html") _output = "index.html";
|
|
||||||
var output = path.join(this.options.output, _output);
|
|
||||||
var basePath = path.relative(path.dirname(output), this.options.output) || ".";
|
|
||||||
|
|
||||||
// Bug fix for issue #493 which would occur when relative-links are 2-level or more deep in win32
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
basePath = basePath.replace(/\\/g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prepareFile(content, _input)
|
|
||||||
.then(function(page) {
|
|
||||||
// Index page in search
|
|
||||||
return that.indexPage(page.lexed, _output).thenResolve(page);
|
|
||||||
})
|
|
||||||
.then(function(page) {
|
|
||||||
// Write file
|
|
||||||
return that._writeTemplate(that.template, {
|
|
||||||
progress: page.progress,
|
|
||||||
|
|
||||||
_input: page.path,
|
|
||||||
content: page.sections,
|
|
||||||
|
|
||||||
basePath: basePath,
|
|
||||||
staticBase: links.join(basePath, "gitbook"),
|
|
||||||
}, output, function(html) {
|
|
||||||
page.content = html;
|
|
||||||
|
|
||||||
return that.callHook("page:after", page).get("content")
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate languages index
|
|
||||||
Generator.prototype.langsIndex = function(langs) {
|
|
||||||
var that = this;
|
|
||||||
var basePath = ".";
|
|
||||||
|
|
||||||
return this._writeTemplate(this.langsTemplate, {
|
|
||||||
langs: langs.list,
|
|
||||||
|
|
||||||
basePath: basePath,
|
|
||||||
staticBase: path.join(basePath, "gitbook"),
|
|
||||||
}, path.join(this.options.output, "index.html"))
|
|
||||||
.then(function() {
|
|
||||||
// Copy assets
|
|
||||||
return that.copyAssets();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate glossary
|
|
||||||
Generator.prototype.writeGlossary = function() {
|
|
||||||
var that = this;
|
|
||||||
var basePath = ".";
|
|
||||||
|
|
||||||
// No glossary
|
|
||||||
if (!this.glossaryIndexer) return Q();
|
|
||||||
|
|
||||||
// Transform the glossary to get term, description, files
|
|
||||||
var glossary = _.chain(this.glossaryIndexer.invertedIdx)
|
|
||||||
.map(function(links, id) {
|
|
||||||
var term = _.find(that.options.glossary, { 'id': id });
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: id,
|
|
||||||
name: term.name,
|
|
||||||
description: term.description,
|
|
||||||
files: _.chain(links)
|
|
||||||
.map(function(link) {
|
|
||||||
var name = link.slice(0, -5);
|
|
||||||
|
|
||||||
if (name == "index") {
|
|
||||||
name = "README";
|
|
||||||
}
|
|
||||||
return that.options.navigation[name+".md"];
|
|
||||||
})
|
|
||||||
.sortBy("percent")
|
|
||||||
.value()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sortBy("name")
|
|
||||||
.value();
|
|
||||||
|
|
||||||
return this._writeTemplate(this.glossaryTemplate, {
|
|
||||||
glossaryIndex: glossary,
|
|
||||||
basePath: basePath,
|
|
||||||
staticBase: path.join(basePath, "gitbook"),
|
|
||||||
}, path.join(this.options.output, "GLOSSARY.html"));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Copy assets
|
|
||||||
Generator.prototype.copyAssets = function() {
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
// Copy gitbook assets
|
|
||||||
return fs.copy(
|
|
||||||
path.join(that.options.theme, "assets"),
|
|
||||||
path.join(that.options.output, "gitbook")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Copy plugins assets
|
|
||||||
.then(function() {
|
|
||||||
return Q.all(
|
|
||||||
_.map(that.plugins.list, function(plugin) {
|
|
||||||
var pluginAssets = path.join(that.options.output, "gitbook/plugins/", plugin.name);
|
|
||||||
return plugin.copyAssets(pluginAssets, {
|
|
||||||
base: that.pluginAssetsBase
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dump search index to disk
|
|
||||||
Generator.prototype.writeSearchIndex = function() {
|
|
||||||
return fs.writeFile(
|
|
||||||
path.join(this.options.output, 'search_index.json'),
|
|
||||||
this.indexer.dump()
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dump glossary index to disk
|
|
||||||
Generator.prototype.writeGlossaryIndex = function() {
|
|
||||||
if (!this.glossaryIndexer) return Q();
|
|
||||||
|
|
||||||
return fs.writeFile(
|
|
||||||
path.join(this.options.output, 'glossary_index.json'),
|
|
||||||
JSON.stringify(this.options.glossary)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
Generator.prototype.finish = function() {
|
|
||||||
return this.copyAssets()
|
|
||||||
.then(this.copyCover)
|
|
||||||
.then(this.writeGlossary)
|
|
||||||
.then(this.writeGlossaryIndex)
|
|
||||||
.then(this.writeSearchIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Generator;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
var Q = require("q");
|
|
||||||
var _ = require("lodash");
|
|
||||||
|
|
||||||
var lunr = require('lunr');
|
|
||||||
var kramed = require('kramed');
|
|
||||||
var textRenderer = require('kramed-text-renderer');
|
|
||||||
|
|
||||||
|
|
||||||
function Indexer() {
|
|
||||||
if(!(this instanceof Indexer)) {
|
|
||||||
return new Indexer();
|
|
||||||
}
|
|
||||||
|
|
||||||
_.bindAll(this);
|
|
||||||
|
|
||||||
// Setup lunr index
|
|
||||||
this.idx = lunr(function () {
|
|
||||||
this.ref('url');
|
|
||||||
|
|
||||||
this.field('title', { boost: 10 });
|
|
||||||
this.field('body');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.renderer = textRenderer();
|
|
||||||
}
|
|
||||||
|
|
||||||
Indexer.prototype.text = function(nodes) {
|
|
||||||
// Copy section
|
|
||||||
var section = _.toArray(nodes);
|
|
||||||
|
|
||||||
// kramed's Render expects this, we don't use it yet
|
|
||||||
section.links = {};
|
|
||||||
|
|
||||||
var options = _.extend({}, kramed.defaults, {
|
|
||||||
renderer: this.renderer
|
|
||||||
});
|
|
||||||
|
|
||||||
return kramed.parser(section, options);
|
|
||||||
};
|
|
||||||
|
|
||||||
Indexer.prototype.addSection = function(path, section) {
|
|
||||||
var url = [path, section.id].join('#');
|
|
||||||
|
|
||||||
var title = this.text(
|
|
||||||
_.filter(section, {'type': 'heading'})
|
|
||||||
);
|
|
||||||
|
|
||||||
var body = this.text(
|
|
||||||
_.omit(section, {'type': 'heading'})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add to lunr index
|
|
||||||
this.idx.add({
|
|
||||||
url: url,
|
|
||||||
title: title,
|
|
||||||
body: body,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Indexer.prototype.add = function(lexedPage, url) {
|
|
||||||
var sections = lexedPage;
|
|
||||||
|
|
||||||
_.map(sections, _.partial(this.addSection, url));
|
|
||||||
};
|
|
||||||
|
|
||||||
Indexer.prototype.dump = function() {
|
|
||||||
return JSON.stringify(this.idx);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
module.exports = Indexer;
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
var path = require("path");
|
|
||||||
var swig = require('swig');
|
|
||||||
var hljs = require('highlight.js');
|
|
||||||
|
|
||||||
var links = require('../utils/').links;
|
|
||||||
var pkg = require('../../package.json');
|
|
||||||
|
|
||||||
swig.setDefaults({
|
|
||||||
locals: {
|
|
||||||
gitbook: {
|
|
||||||
version: pkg.version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Swig filter for returning the count of lines in a code section
|
|
||||||
swig.setFilter('lines', function(content) {
|
|
||||||
return content.split('\n').length;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Swig filter for returning a link to the associated html file of a markdown file
|
|
||||||
swig.setFilter('mdLink', function(link) {
|
|
||||||
var link = link.replace(".md", ".html");
|
|
||||||
if (link == "README.html") link = "index.html";
|
|
||||||
return link;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Swig filter: highlight coloration
|
|
||||||
swig.setFilter('code', function(code, lang) {
|
|
||||||
try {
|
|
||||||
return hljs.highlight(lang, code).value;
|
|
||||||
} catch(e) {
|
|
||||||
return hljs.highlightAuto(code).value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert a level into a deep level
|
|
||||||
swig.setFilter('lvl', function(lvl) {
|
|
||||||
return lvl.split(".").length;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Join path
|
|
||||||
swig.setFilter('pathJoin', function(base, _path) {
|
|
||||||
return links.join(base, _path);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Is a link an absolute link
|
|
||||||
swig.setFilter('isExternalLink', function(link) {
|
|
||||||
return links.isExternal(link);
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = swig;
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var path = require('path');
|
||||||
|
var Q = require('q');
|
||||||
|
var fs = require('./utils/fs');
|
||||||
|
|
||||||
|
var BaseGenerator = function(book) {
|
||||||
|
this.book = book;
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'options', {
|
||||||
|
get: function () {
|
||||||
|
return this.book.options;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_.bindAll(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
BaseGenerator.prototype.callHook = function(name, data) {
|
||||||
|
return this.book.callHook(name, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare the genertor
|
||||||
|
BaseGenerator.prototype.prepare = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return that.callHook('init');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write a parsed file to the output
|
||||||
|
BaseGenerator.prototype.convertFile = function(input) {
|
||||||
|
return Q.reject(new Error('Could not convert '+input));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy file to the output (non parsable)
|
||||||
|
BaseGenerator.prototype.transferFile = function(input) {
|
||||||
|
return fs.copy(
|
||||||
|
this.book.resolve(input),
|
||||||
|
path.join(this.options.output, input)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy a folder to the output
|
||||||
|
BaseGenerator.prototype.transferFolder = function(input) {
|
||||||
|
return fs.mkdirp(
|
||||||
|
path.join(this.book.options.output, input)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy the cover picture
|
||||||
|
BaseGenerator.prototype.copyCover = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return Q.all([
|
||||||
|
fs.copy(that.book.resolve('cover.jpg'), path.join(that.options.output, 'cover.jpg')),
|
||||||
|
fs.copy(that.book.resolve('cover_small.jpg'), path.join(that.options.output, 'cover_small.jpg'))
|
||||||
|
])
|
||||||
|
.fail(function() {
|
||||||
|
// If orignaly from multi-lang, try copy from parent
|
||||||
|
if (!that.book.isSubBook()) return;
|
||||||
|
|
||||||
|
return Q.all([
|
||||||
|
fs.copy(path.join(that.book.parentRoot(), 'cover.jpg'), path.join(that.options.output, 'cover.jpg')),
|
||||||
|
fs.copy(path.join(that.book.parentRoot(), 'cover_small.jpg'), path.join(that.options.output, 'cover_small.jpg'))
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
return Q();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// At teh end of the generation
|
||||||
|
BaseGenerator.prototype.finish = function() {
|
||||||
|
return Q.reject(new Error('Could not finish generation'));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = BaseGenerator;
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
var util = require('util');
|
||||||
|
var path = require('path');
|
||||||
|
var Q = require('q');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var juice = require('juice');
|
||||||
|
var exec = require('child_process').exec;
|
||||||
|
|
||||||
|
var fs = require('../utils/fs');
|
||||||
|
var stringUtils = require('../utils/string');
|
||||||
|
var BaseGenerator = require('./website');
|
||||||
|
|
||||||
|
var Generator = function(book, format) {
|
||||||
|
BaseGenerator.apply(this, arguments);
|
||||||
|
|
||||||
|
// eBook format
|
||||||
|
this.ebookFormat = format;
|
||||||
|
|
||||||
|
// Resources namespace
|
||||||
|
this.namespace = 'ebook';
|
||||||
|
|
||||||
|
// Styles to use
|
||||||
|
this.styles = _.compact(['print', 'ebook', this.ebookFormat]);
|
||||||
|
|
||||||
|
// Convert images (svg -> png)
|
||||||
|
this.convertImages = true;
|
||||||
|
};
|
||||||
|
util.inherits(Generator, BaseGenerator);
|
||||||
|
|
||||||
|
Generator.prototype.prepareTemplates = function() {
|
||||||
|
this.templates.page = this.book.plugins.template('ebook:page') || path.resolve(this.options.theme, 'templates/ebook/page.html');
|
||||||
|
this.templates.summary = this.book.plugins.template('ebook:summary') || path.resolve(this.options.theme, 'templates/ebook/summary.html');
|
||||||
|
this.templates.glossary = this.book.plugins.template('ebook:glossary') || path.resolve(this.options.theme, 'templates/ebook/glossary.html');
|
||||||
|
|
||||||
|
return Q();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate table of contents
|
||||||
|
Generator.prototype.writeSummary = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
that.book.log.info.ln('write SUMMARY.html');
|
||||||
|
return this._writeTemplate(this.templates.summary, {}, path.join(this.options.output, 'SUMMARY.html'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return template for footer/header with inlined css
|
||||||
|
Generator.prototype.getPDFTemplate = function(id) {
|
||||||
|
var tpl = this.options.pdf[id+'Template'];
|
||||||
|
var defaultTpl = path.resolve(this.options.theme, 'templates/ebook/'+id+'.html');
|
||||||
|
var defaultCSS = path.resolve(this.options.theme, 'assets/ebook/pdf.css');
|
||||||
|
|
||||||
|
// Default template from theme
|
||||||
|
if (!tpl && fs.existsSync(defaultTpl)) {
|
||||||
|
tpl = fs.readFileSync(defaultTpl, { encoding: 'utf-8' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline CSS using juice
|
||||||
|
var stylesheets = [];
|
||||||
|
|
||||||
|
// From theme
|
||||||
|
if (fs.existsSync(defaultCSS)) {
|
||||||
|
stylesheets.push(fs.readFileSync(defaultCSS, { encoding: 'utf-8' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom PDF style
|
||||||
|
if (this.styles.pdf) {
|
||||||
|
stylesheets.push(fs.readFileSync(this.book.resolve(this.styles.pdf), { encoding: 'utf-8' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl = juice(tpl, {
|
||||||
|
extraCss: stylesheets.join('\n\n')
|
||||||
|
});
|
||||||
|
|
||||||
|
return tpl;
|
||||||
|
};
|
||||||
|
|
||||||
|
Generator.prototype.finish = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
.then(this.copyAssets)
|
||||||
|
.then(this.copyCover)
|
||||||
|
.then(this.writeGlossary)
|
||||||
|
.then(this.writeSummary)
|
||||||
|
.then(function() {
|
||||||
|
if (!that.ebookFormat) return Q();
|
||||||
|
|
||||||
|
if (!that.options.cover && fs.existsSync(path.join(that.options.output, 'cover.jpg'))) {
|
||||||
|
that.options.cover = path.join(that.options.output, 'cover.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
var d = Q.defer();
|
||||||
|
|
||||||
|
var _options = {
|
||||||
|
'--cover': that.options.cover,
|
||||||
|
'--title': that.options.title,
|
||||||
|
'--comments': that.options.description,
|
||||||
|
'--isbn': that.options.isbn,
|
||||||
|
'--authors': that.options.author,
|
||||||
|
'--language': that.options.language,
|
||||||
|
'--book-producer': 'GitBook',
|
||||||
|
'--publisher': 'GitBook',
|
||||||
|
'--chapter': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter \')]',
|
||||||
|
'--level1-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-1 \')]',
|
||||||
|
'--level2-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-2 \')]',
|
||||||
|
'--level3-toc': 'descendant-or-self::*[contains(concat(\' \', normalize-space(@class), \' \'), \' book-chapter-3 \')]',
|
||||||
|
'--no-chapters-in-toc': true,
|
||||||
|
'--max-levels': '1',
|
||||||
|
'--breadth-first': true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (that.ebookFormat == 'pdf') {
|
||||||
|
var pdfOptions = that.options.pdf;
|
||||||
|
|
||||||
|
_.extend(_options, {
|
||||||
|
'--chapter-mark': String(pdfOptions.chapterMark),
|
||||||
|
'--page-breaks-before': String(pdfOptions.pageBreaksBefore),
|
||||||
|
'--margin-left': String(pdfOptions.margin.left),
|
||||||
|
'--margin-right': String(pdfOptions.margin.right),
|
||||||
|
'--margin-top': String(pdfOptions.margin.top),
|
||||||
|
'--margin-bottom': String(pdfOptions.margin.bottom),
|
||||||
|
'--pdf-default-font-size': String(pdfOptions.fontSize),
|
||||||
|
'--pdf-mono-font-size': String(pdfOptions.fontSize),
|
||||||
|
'--paper-size': String(pdfOptions.paperSize),
|
||||||
|
'--pdf-page-numbers': Boolean(pdfOptions.pageNumbers),
|
||||||
|
'--pdf-header-template': that.getPDFTemplate('header'),
|
||||||
|
'--pdf-footer-template': that.getPDFTemplate('footer'),
|
||||||
|
'--pdf-sans-family': String(pdfOptions.fontFamily)
|
||||||
|
});
|
||||||
|
} else if (that.ebookFormat == 'epub') {
|
||||||
|
_.extend(_options, {
|
||||||
|
'--dont-split-on-page-breaks': true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var command = [
|
||||||
|
'ebook-convert',
|
||||||
|
path.join(that.options.output, 'SUMMARY.html'),
|
||||||
|
path.join(that.options.output, 'index.'+that.ebookFormat),
|
||||||
|
stringUtils.optionsToShellArgs(_options)
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
that.book.log.info('start conversion to', that.ebookFormat, '....');
|
||||||
|
|
||||||
|
var child = exec(command, function (error, stdout) {
|
||||||
|
if (error) {
|
||||||
|
that.book.log.info.fail();
|
||||||
|
|
||||||
|
if (error.code == 127) {
|
||||||
|
error.message = 'Need to install ebook-convert from Calibre';
|
||||||
|
} else {
|
||||||
|
error.message = error.message + ' '+stdout;
|
||||||
|
}
|
||||||
|
return d.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
that.book.log.info.ok();
|
||||||
|
d.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdout.on('data', function (data) {
|
||||||
|
that.book.log.debug(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on('data', function (data) {
|
||||||
|
that.book.log.debug(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
return d.promise;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Generator;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
var _ = require("lodash");
|
||||||
|
var EbookGenerator = require("./ebook");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
json: require("./json"),
|
||||||
|
website: require("./website"),
|
||||||
|
ebook: EbookGenerator,
|
||||||
|
pdf: _.partialRight(EbookGenerator, "pdf"),
|
||||||
|
mobi: _.partialRight(EbookGenerator, "mobi"),
|
||||||
|
epub: _.partialRight(EbookGenerator, "epub")
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
var util = require('util');
|
||||||
|
var path = require('path');
|
||||||
|
var Q = require('q');
|
||||||
|
var _ = require('lodash');
|
||||||
|
|
||||||
|
var fs = require('../utils/fs');
|
||||||
|
var BaseGenerator = require('../generator');
|
||||||
|
var links = require('../utils/links');
|
||||||
|
|
||||||
|
var Generator = function() {
|
||||||
|
BaseGenerator.apply(this, arguments);
|
||||||
|
};
|
||||||
|
util.inherits(Generator, BaseGenerator);
|
||||||
|
|
||||||
|
// Ignore some methods
|
||||||
|
Generator.prototype.transferFile = function() { };
|
||||||
|
|
||||||
|
// Convert an input file
|
||||||
|
Generator.prototype.convertFile = function(input) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return that.book.parsePage(input)
|
||||||
|
.then(function(page) {
|
||||||
|
var json = {
|
||||||
|
progress: page.progress,
|
||||||
|
sections: page.sections
|
||||||
|
};
|
||||||
|
|
||||||
|
var output = links.changeExtension(page.path, '.json');
|
||||||
|
output = path.join(that.options.output, output);
|
||||||
|
|
||||||
|
return fs.writeFile(
|
||||||
|
output,
|
||||||
|
JSON.stringify(json, null, 4)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Finish generation
|
||||||
|
Generator.prototype.finish = function() {
|
||||||
|
return this.writeReadme();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write README.json
|
||||||
|
Generator.prototype.writeReadme = function() {
|
||||||
|
var that = this;
|
||||||
|
var mainLang, langs, readme;
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
langs = that.book.langs;
|
||||||
|
mainLang = langs.length > 0? _.first(langs).lang : null;
|
||||||
|
|
||||||
|
readme = links.changeExtension(that.book.readmeFile, '.json');
|
||||||
|
|
||||||
|
// Read readme from main language
|
||||||
|
return fs.readFile(
|
||||||
|
mainLang? path.join(that.options.output, mainLang, readme) : path.join(that.options.output, readme)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(function(content) {
|
||||||
|
// Extend it with infos about the languages
|
||||||
|
var json = JSON.parse(content);
|
||||||
|
_.extend(json, {
|
||||||
|
langs: langs
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write it as README.json
|
||||||
|
return fs.writeFile(
|
||||||
|
path.join(that.options.output, 'README.json'),
|
||||||
|
JSON.stringify(json, null, 4)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Generator;
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
var util = require('util');
|
||||||
|
var path = require('path');
|
||||||
|
var Q = require('q');
|
||||||
|
var _ = require('lodash');
|
||||||
|
|
||||||
|
var nunjucks = require('nunjucks');
|
||||||
|
var AutoEscapeExtension = require('nunjucks-autoescape')(nunjucks);
|
||||||
|
var FilterExtension = require('nunjucks-filter')(nunjucks);
|
||||||
|
|
||||||
|
var fs = require('../utils/fs');
|
||||||
|
var BaseGenerator = require('../generator');
|
||||||
|
var links = require('../utils/links');
|
||||||
|
var i18n = require('../utils/i18n');
|
||||||
|
|
||||||
|
var pkg = require('../../package.json');
|
||||||
|
|
||||||
|
var Generator = function() {
|
||||||
|
BaseGenerator.apply(this, arguments);
|
||||||
|
|
||||||
|
// Revision
|
||||||
|
this.revision = new Date();
|
||||||
|
|
||||||
|
// Resources namespace
|
||||||
|
this.namespace = 'website';
|
||||||
|
|
||||||
|
// Style to integrates in the output
|
||||||
|
this.styles = ['website'];
|
||||||
|
|
||||||
|
// Convert images (svg -> png)
|
||||||
|
this.convertImages = false;
|
||||||
|
|
||||||
|
// Templates
|
||||||
|
this.templates = {};
|
||||||
|
};
|
||||||
|
util.inherits(Generator, BaseGenerator);
|
||||||
|
|
||||||
|
// Prepare the genertor
|
||||||
|
Generator.prototype.prepare = function() {
|
||||||
|
return BaseGenerator.prototype.prepare.apply(this)
|
||||||
|
.then(this.prepareStyles)
|
||||||
|
.then(this.prepareTemplates)
|
||||||
|
.then(this.prepareTemplateEngine);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare all styles
|
||||||
|
Generator.prototype.prepareStyles = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.styles = _.chain(this.styles)
|
||||||
|
.map(function(style) {
|
||||||
|
var stylePath = that.options.styles[style];
|
||||||
|
if (stylePath && fs.existsSync(that.book.resolve(stylePath))) {
|
||||||
|
return [style, stylePath];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.compact()
|
||||||
|
.object()
|
||||||
|
.value();
|
||||||
|
|
||||||
|
return Q();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare templates
|
||||||
|
Generator.prototype.prepareTemplates = function() {
|
||||||
|
this.templates.page = this.book.plugins.template('site:page') || path.resolve(this.options.theme, 'templates/website/page.html');
|
||||||
|
this.templates.langs = this.book.plugins.template('site:langs') || path.resolve(this.options.theme, 'templates/website/langs.html');
|
||||||
|
this.templates.glossary = this.book.plugins.template('site:glossary') || path.resolve(this.options.theme, 'templates/website/glossary.html');
|
||||||
|
|
||||||
|
return Q();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare template engine
|
||||||
|
Generator.prototype.prepareTemplateEngine = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
var language = that.book.config.normalizeLanguage();
|
||||||
|
|
||||||
|
if (!i18n.hasLocale(language)) {
|
||||||
|
that.book.log.warn.ln('Language "'+language+'" is not available as a layout locales (en, '+i18n.getLocales().join(', ')+')');
|
||||||
|
}
|
||||||
|
|
||||||
|
var folders = _.chain(that.templates)
|
||||||
|
.values()
|
||||||
|
.map(path.dirname)
|
||||||
|
.uniq()
|
||||||
|
.value();
|
||||||
|
|
||||||
|
that.env = new nunjucks.Environment(
|
||||||
|
new nunjucks.FileSystemLoader(folders),
|
||||||
|
{
|
||||||
|
autoescape: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add filter
|
||||||
|
that.env.addFilter('contentLink', that.book.contentLink.bind(that.book));
|
||||||
|
that.env.addFilter('lvl', function(lvl) {
|
||||||
|
return lvl.split('.').length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add extension
|
||||||
|
that.env.addExtension('AutoEscapeExtension', new AutoEscapeExtension(that.env));
|
||||||
|
that.env.addExtension('FilterExtension', new FilterExtension(that.env));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Finis generation
|
||||||
|
Generator.prototype.finish = function() {
|
||||||
|
return this.copyAssets()
|
||||||
|
.then(this.copyCover)
|
||||||
|
.then(this.writeGlossary)
|
||||||
|
.then(this.writeLangsIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert an input file
|
||||||
|
Generator.prototype.convertFile = function(input) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return that.book.parsePage(input, {
|
||||||
|
convertImages: that.convertImages,
|
||||||
|
interpolateTemplate: function(page) {
|
||||||
|
return that.callHook('page:before', page);
|
||||||
|
},
|
||||||
|
interpolateContent: function(page) {
|
||||||
|
return that.callHook('page', page);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function(page) {
|
||||||
|
var relativeOutput = that.book.contentPath(page.path);
|
||||||
|
var output = path.join(that.options.output, relativeOutput);
|
||||||
|
|
||||||
|
var basePath = path.relative(path.dirname(output), that.options.output) || '.';
|
||||||
|
if (process.platform === 'win32') basePath = basePath.replace(/\\/g, '/');
|
||||||
|
|
||||||
|
that.book.log.debug.ln('write parsed file', page.path, 'to', relativeOutput);
|
||||||
|
|
||||||
|
return that._writeTemplate(that.templates.page, {
|
||||||
|
progress: page.progress,
|
||||||
|
|
||||||
|
_input: page.path,
|
||||||
|
content: page.sections,
|
||||||
|
|
||||||
|
basePath: basePath,
|
||||||
|
staticBase: links.join(basePath, 'gitbook')
|
||||||
|
}, output);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write the index for langs
|
||||||
|
Generator.prototype.writeLangsIndex = function() {
|
||||||
|
if (!this.book.langs.length) return Q();
|
||||||
|
|
||||||
|
return this._writeTemplate(this.templates.langs, {
|
||||||
|
langs: this.book.langs
|
||||||
|
}, path.join(this.options.output, 'index.html'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write glossary
|
||||||
|
Generator.prototype.writeGlossary = function() {
|
||||||
|
// No glossary
|
||||||
|
if (this.book.glossary.length === 0) return Q();
|
||||||
|
|
||||||
|
return this._writeTemplate(this.templates.glossary, {}, path.join(this.options.output, 'GLOSSARY.html'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert a page into a normalized data set
|
||||||
|
Generator.prototype.normalizePage = function(page) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var _callHook = function(name) {
|
||||||
|
return that.callHook(name, page)
|
||||||
|
.then(function(_page) {
|
||||||
|
page = _page;
|
||||||
|
return page;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
return _callHook('page');
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
return page;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate a template
|
||||||
|
Generator.prototype._writeTemplate = function(tpl, options, output, interpolate) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
interpolate = interpolate || _.identity;
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
return that.env.render(
|
||||||
|
tpl,
|
||||||
|
_.extend({
|
||||||
|
gitbook: {
|
||||||
|
version: pkg.version
|
||||||
|
},
|
||||||
|
|
||||||
|
styles: that.styles,
|
||||||
|
|
||||||
|
revision: that.revision,
|
||||||
|
|
||||||
|
title: that.options.title,
|
||||||
|
description: that.options.description,
|
||||||
|
language: that.book.config.normalizeLanguage(),
|
||||||
|
innerlanguage: that.book.isSubBook()? that.book.config.get('language') : null,
|
||||||
|
|
||||||
|
glossary: that.book.glossary,
|
||||||
|
|
||||||
|
summary: that.book.summary,
|
||||||
|
allNavigation: that.book.navigation,
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
resources: that.book.plugins.resources(that.namespace)
|
||||||
|
},
|
||||||
|
pluginsConfig: JSON.stringify(that.options.pluginsConfig),
|
||||||
|
htmlSnippet: _.partial(_.partialRight(that.book.plugins.html, that, options), that.namespace),
|
||||||
|
|
||||||
|
options: that.options,
|
||||||
|
|
||||||
|
basePath: '.',
|
||||||
|
staticBase: path.join('.', 'gitbook'),
|
||||||
|
|
||||||
|
'__': that.book.i18n.bind(that.book)
|
||||||
|
}, options)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(interpolate)
|
||||||
|
.then(function(html) {
|
||||||
|
return fs.writeFile(
|
||||||
|
output,
|
||||||
|
html
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy assets
|
||||||
|
Generator.prototype.copyAssets = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
// Copy gitbook assets
|
||||||
|
return fs.copy(
|
||||||
|
path.join(that.options.theme, 'assets/'+this.namespace),
|
||||||
|
path.join(that.options.output, 'gitbook')
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copy plugins assets
|
||||||
|
.then(function() {
|
||||||
|
return Q.all(
|
||||||
|
_.map(that.book.plugins.list, function(plugin) {
|
||||||
|
var pluginAssets = path.join(that.options.output, 'gitbook/plugins/', plugin.name);
|
||||||
|
return plugin.copyAssets(pluginAssets, that.namespace);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Generator;
|
||||||
+213
-3
@@ -1,5 +1,215 @@
|
|||||||
module.exports = {
|
/* eslint no-console: 0 */
|
||||||
parse: require('./parse/'),
|
|
||||||
generate: require('./generate/')
|
var Q = require('q');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var path = require('path');
|
||||||
|
var tinylr = require('tiny-lr');
|
||||||
|
var color = require('bash-color');
|
||||||
|
|
||||||
|
var Book = require('./book');
|
||||||
|
var initBook = require('./init');
|
||||||
|
var Server = require('./utils/server');
|
||||||
|
var stringUtils = require('./utils/string');
|
||||||
|
var watch = require('./utils/watch');
|
||||||
|
var logger = require('./utils/logger');
|
||||||
|
|
||||||
|
var LOG_OPTION = {
|
||||||
|
name: 'log',
|
||||||
|
description: 'Minimum log level to display',
|
||||||
|
values: _.chain(logger.LEVELS).keys().map(stringUtils.toLowerCase).value(),
|
||||||
|
defaults: 'info'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var FORMAT_OPTION = {
|
||||||
|
name: 'format',
|
||||||
|
description: 'Format to build to',
|
||||||
|
values: ['website', 'json', 'ebook'],
|
||||||
|
defaults: 'website'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export init to gitbook library
|
||||||
|
Book.init = initBook;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Book: Book,
|
||||||
|
LOG_LEVELS: logger.LEVELS,
|
||||||
|
|
||||||
|
commands: _.flatten([
|
||||||
|
{
|
||||||
|
name: 'build [book] [output]',
|
||||||
|
description: 'build a book',
|
||||||
|
options: [
|
||||||
|
FORMAT_OPTION,
|
||||||
|
LOG_OPTION
|
||||||
|
],
|
||||||
|
exec: function(args, kwargs) {
|
||||||
|
var input = args[0] || process.cwd();
|
||||||
|
var output = args[1] || path.join(input, '_book');
|
||||||
|
|
||||||
|
var book = new Book(input, _.extend({}, {
|
||||||
|
'config': {
|
||||||
|
'output': output
|
||||||
|
},
|
||||||
|
'logLevel': kwargs.log
|
||||||
|
}));
|
||||||
|
|
||||||
|
return book.parse()
|
||||||
|
.then(function() {
|
||||||
|
return book.generate(kwargs.format);
|
||||||
|
})
|
||||||
|
.then(function(){
|
||||||
|
console.log('');
|
||||||
|
console.log(color.green('Done, without error'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_.map(['pdf', 'epub', 'mobi'], function(ebookType) {
|
||||||
|
return {
|
||||||
|
name: ebookType+' [book] [output]',
|
||||||
|
description: 'build a book to '+ebookType,
|
||||||
|
options: [
|
||||||
|
LOG_OPTION
|
||||||
|
],
|
||||||
|
exec: function(args, kwargs) {
|
||||||
|
var input = args[0] || process.cwd();
|
||||||
|
var output = args[1];
|
||||||
|
|
||||||
|
var book = new Book(input, _.extend({}, {
|
||||||
|
'logLevel': kwargs.log
|
||||||
|
}));
|
||||||
|
|
||||||
|
return book.parse()
|
||||||
|
.then(function() {
|
||||||
|
return book.generateFile(output, {
|
||||||
|
ebookFormat: ebookType
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function(){
|
||||||
|
console.log('');
|
||||||
|
console.log(color.green('Done, without error'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'serve [book]',
|
||||||
|
description: 'Build then serve a gitbook from a directory',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'port',
|
||||||
|
description: 'Port for server to listen on',
|
||||||
|
defaults: 4000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lrport',
|
||||||
|
description: 'Port for livereload server to listen on',
|
||||||
|
defaults: 35729
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'watch',
|
||||||
|
description: 'Enable/disable file watcher',
|
||||||
|
defaults: true
|
||||||
|
},
|
||||||
|
FORMAT_OPTION,
|
||||||
|
LOG_OPTION
|
||||||
|
],
|
||||||
|
exec: function(args, kwargs) {
|
||||||
|
var input = args[0] || process.cwd();
|
||||||
|
var server = new Server();
|
||||||
|
|
||||||
|
// Init livereload server
|
||||||
|
var lrServer = tinylr({});
|
||||||
|
var lrPath;
|
||||||
|
|
||||||
|
var generate = function() {
|
||||||
|
if (server.isRunning()) console.log('Stopping server');
|
||||||
|
|
||||||
|
return server.stop()
|
||||||
|
.then(function() {
|
||||||
|
var book = new Book(input, _.extend({}, {
|
||||||
|
'config': {
|
||||||
|
'defaultsPlugins': ['livereload']
|
||||||
|
},
|
||||||
|
'logLevel': kwargs.log
|
||||||
|
}));
|
||||||
|
|
||||||
|
return book.parse()
|
||||||
|
.then(function() {
|
||||||
|
return book.generate(kwargs.format);
|
||||||
|
})
|
||||||
|
.thenResolve(book);
|
||||||
|
})
|
||||||
|
.then(function(book) {
|
||||||
|
console.log();
|
||||||
|
console.log('Starting server ...');
|
||||||
|
return server.start(book.options.output, kwargs.port)
|
||||||
|
.then(function() {
|
||||||
|
console.log('Serving book on http://localhost:'+kwargs.port);
|
||||||
|
|
||||||
|
if (lrPath) {
|
||||||
|
// trigger livereload
|
||||||
|
lrServer.changed({
|
||||||
|
body: {
|
||||||
|
files: [lrPath]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kwargs.watch) return;
|
||||||
|
|
||||||
|
return watch(book.root)
|
||||||
|
.then(function(filepath) {
|
||||||
|
// set livereload path
|
||||||
|
lrPath = filepath;
|
||||||
|
console.log('Restart after change in file', filepath);
|
||||||
|
console.log('');
|
||||||
|
return generate();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return Q.nfcall(lrServer.listen.bind(lrServer), kwargs.lrport)
|
||||||
|
.then(function() {
|
||||||
|
console.log('Live reload server started on port:', kwargs.lrport);
|
||||||
|
console.log('Press CTRL+C to quit ...');
|
||||||
|
console.log('');
|
||||||
|
return generate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'install [book]',
|
||||||
|
description: 'install plugins dependencies',
|
||||||
|
exec: function(args) {
|
||||||
|
var input = args[0] || process.cwd();
|
||||||
|
|
||||||
|
var book = new Book(input);
|
||||||
|
|
||||||
|
return book.config.load()
|
||||||
|
.then(function() {
|
||||||
|
return book.plugins.install();
|
||||||
|
})
|
||||||
|
.then(function(){
|
||||||
|
console.log('');
|
||||||
|
console.log(color.green('Done, without error'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'init [directory]',
|
||||||
|
description: 'create files and folders based on contents of SUMMARY.md',
|
||||||
|
exec: function(args) {
|
||||||
|
return initBook(args[0] || process.cwd())
|
||||||
|
.then(function(){
|
||||||
|
console.log('');
|
||||||
|
console.log(color.green('Done, without error'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
};
|
||||||
|
|||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var Q = require('q');
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
var Book = require('./book');
|
||||||
|
var fs = require('./utils/fs');
|
||||||
|
|
||||||
|
// Initialize folder structure for a book
|
||||||
|
// Read SUMMARY to created the right chapter
|
||||||
|
function initBook(root, opts) {
|
||||||
|
var book = new Book(root, opts);
|
||||||
|
var extensionToUse = '.md';
|
||||||
|
|
||||||
|
var chaptersPaths = function(chapters) {
|
||||||
|
return _.reduce(chapters || [], function(accu, chapter) {
|
||||||
|
var o = {
|
||||||
|
title: chapter.title
|
||||||
|
};
|
||||||
|
if (chapter.path) o.path = chapter.path;
|
||||||
|
|
||||||
|
return accu.concat(
|
||||||
|
[o].concat(chaptersPaths(chapter.articles))
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
book.log.info.ln('init book at', root);
|
||||||
|
return fs.mkdirp(root)
|
||||||
|
.then(function() {
|
||||||
|
book.log.info.ln('detect structure from SUMMARY (if it exists)');
|
||||||
|
return book.parseSummary();
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
return Q();
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
var summary = book.summaryFile || 'SUMMARY.md';
|
||||||
|
var chapters = book.summary.chapters || [];
|
||||||
|
extensionToUse = path.extname(summary);
|
||||||
|
|
||||||
|
if (chapters.length === 0) {
|
||||||
|
chapters = [
|
||||||
|
{
|
||||||
|
title: 'Summary',
|
||||||
|
path: 'SUMMARY'+extensionToUse
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Introduction',
|
||||||
|
path: 'README'+extensionToUse
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Q(chaptersPaths(chapters));
|
||||||
|
})
|
||||||
|
.then(function(chapters) {
|
||||||
|
// Create files that don't exist
|
||||||
|
return Q.all(_.map(chapters, function(chapter) {
|
||||||
|
if (!chapter.path) return Q();
|
||||||
|
var absolutePath = path.resolve(book.root, chapter.path);
|
||||||
|
|
||||||
|
return fs.exists(absolutePath)
|
||||||
|
.then(function(exists) {
|
||||||
|
if(exists) {
|
||||||
|
book.log.info.ln('found', chapter.path);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
book.log.info.ln('create', chapter.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.mkdirp(path.dirname(absolutePath))
|
||||||
|
.then(function() {
|
||||||
|
return fs.writeFile(absolutePath, '# '+chapter.title+'\n');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
book.log.info.ln('initialization is finished');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = initBook;
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
var kramed = require('kramed');
|
|
||||||
|
|
||||||
// Get all the pairs of header + paragraph in a list of nodes
|
|
||||||
function groups(nodes) {
|
|
||||||
// A list of next nodes
|
|
||||||
var next = nodes.slice(1).concat(null);
|
|
||||||
|
|
||||||
return _.reduce(nodes, function(accu, node, idx) {
|
|
||||||
// Skip
|
|
||||||
if(!(
|
|
||||||
node.type === 'heading' &&
|
|
||||||
(next[idx] && next[idx].type === 'paragraph')
|
|
||||||
)) {
|
|
||||||
return accu;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add group
|
|
||||||
accu.push([
|
|
||||||
node,
|
|
||||||
next[idx]
|
|
||||||
]);
|
|
||||||
|
|
||||||
return accu;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseGlossary(src) {
|
|
||||||
var nodes = kramed.lexer(src);
|
|
||||||
|
|
||||||
return groups(nodes)
|
|
||||||
.map(function(pair) {
|
|
||||||
// Simplify each group to a simple object with name/description
|
|
||||||
return {
|
|
||||||
name: pair[0].text,
|
|
||||||
id: entryId(pair[0].text),
|
|
||||||
description: pair[1].text,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalizes a glossary entry's name to create an ID
|
|
||||||
function entryId(name) {
|
|
||||||
return name.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = parseGlossary;
|
|
||||||
module.exports.entryId = entryId;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
module.exports = function(markdown, includer) {
|
|
||||||
// Memoized include function (to cache lookups)
|
|
||||||
var _include = _.memoize(includer);
|
|
||||||
|
|
||||||
return markdown.replace(/{{([\s\S]+?)}}/g, function(match, key) {
|
|
||||||
// If fails leave content as is
|
|
||||||
key = key.trim();
|
|
||||||
return _include(key) || match;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// Return a fs inclduer
|
|
||||||
module.exports = function(ctx, folders, resolveFile, readFile) {
|
|
||||||
return function(name) {
|
|
||||||
return ctx[name] ||
|
|
||||||
folders.map(function(folder) {
|
|
||||||
// Try including snippet from FS
|
|
||||||
try {
|
|
||||||
var fname = resolveFile(folder, name);
|
|
||||||
// Trim trailing newlines/space of imported snippets
|
|
||||||
return readFile(fname, 'utf8').trimRight();
|
|
||||||
} catch(err) {}
|
|
||||||
})
|
|
||||||
.filter(Boolean)[0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
summary: require('./summary'),
|
|
||||||
glossary: require('./glossary'),
|
|
||||||
langs: require('./langs'),
|
|
||||||
page: require('./page'),
|
|
||||||
lex: require('./lex'),
|
|
||||||
progress: require('./progress'),
|
|
||||||
navigation: require('./navigation'),
|
|
||||||
readme: require('./readme'),
|
|
||||||
includer: require('./includer')
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
function isExercise(nodes) {
|
|
||||||
var codeType = { type: 'code' };
|
|
||||||
|
|
||||||
// Number of code nodes in section
|
|
||||||
var len = _.filter(nodes, codeType).length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
// Got 3 or 4 code blocks
|
|
||||||
(len === 3 || len === 4) &&
|
|
||||||
// Ensure all nodes are at the end
|
|
||||||
_.all(_.last(nodes, len), codeType)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = isExercise;
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
function isQuizNode(node) {
|
|
||||||
return (/^[(\[][ x][)\]]/).test(node.text || node);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTableQuestion(nodes) {
|
|
||||||
var block = questionBlock(nodes);
|
|
||||||
return (
|
|
||||||
block.length === 1 &&
|
|
||||||
block[0].type === 'table' &&
|
|
||||||
_.all(block[0].cells[0].slice(1), isQuizNode)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isListQuestion(nodes) {
|
|
||||||
var block = questionBlock(nodes);
|
|
||||||
// Counter of when we go in and out of lists
|
|
||||||
var inlist = 0;
|
|
||||||
// Number of lists we found
|
|
||||||
var lists = 0;
|
|
||||||
// Elements found outside a list
|
|
||||||
var outsiders = 0;
|
|
||||||
// Ensure that we have nothing except lists
|
|
||||||
_.each(block, function(node) {
|
|
||||||
if(node.type === 'list_start') {
|
|
||||||
inlist++;
|
|
||||||
} else if(node.type === 'list_end') {
|
|
||||||
inlist--;
|
|
||||||
lists++;
|
|
||||||
} else if(inlist === 0) {
|
|
||||||
// Found non list_start or list_end whilst outside a list
|
|
||||||
outsiders++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return lists > 0 && outsiders === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isQuestion(nodes) {
|
|
||||||
return isListQuestion(nodes) || isTableQuestion(nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove (optional) paragraph header node and blockquote
|
|
||||||
function questionBlock(nodes) {
|
|
||||||
return nodes.slice(
|
|
||||||
nodes[0].type === 'paragraph' ? 1 : 0,
|
|
||||||
_.findIndex(nodes, { type: 'blockquote_start' })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitQuestions(nodes) {
|
|
||||||
// Represents nodes in current question
|
|
||||||
var buffer = [];
|
|
||||||
return _.reduce(nodes, function(accu, node) {
|
|
||||||
// Add node to buffer
|
|
||||||
buffer.push(node);
|
|
||||||
|
|
||||||
// Flush buffer once we hit the end of a question
|
|
||||||
if(node.type === 'blockquote_end') {
|
|
||||||
accu.push(buffer);
|
|
||||||
// Clear buffer
|
|
||||||
buffer = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return accu;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isQuiz(nodes) {
|
|
||||||
// Extract potential questions
|
|
||||||
var questions = splitQuestions(
|
|
||||||
// Skip quiz title if there
|
|
||||||
nodes.slice(
|
|
||||||
(nodes[0] && nodes[0].type) === 'paragraph' ? 1 : 0
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Nothing that looks like questions
|
|
||||||
if(questions.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure all questions are correctly structured
|
|
||||||
return _.all(questions, isQuestion);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = isQuiz;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
var _ = require("lodash");
|
|
||||||
var parseEntries = require("./summary").entries;
|
|
||||||
|
|
||||||
|
|
||||||
var parseLangs = function(content) {
|
|
||||||
var entries = parseEntries(content);
|
|
||||||
|
|
||||||
return {
|
|
||||||
list: _.chain(entries)
|
|
||||||
.filter(function(entry) {
|
|
||||||
return Boolean(entry.path);
|
|
||||||
})
|
|
||||||
.map(function(entry) {
|
|
||||||
return {
|
|
||||||
title: entry.title,
|
|
||||||
path: entry.path,
|
|
||||||
lang: entry.path.replace("/", "")
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.value()
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = parseLangs;
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
var kramed = require('kramed');
|
|
||||||
|
|
||||||
var isExercise = require('./is_exercise');
|
|
||||||
var isQuiz = require('./is_quiz');
|
|
||||||
|
|
||||||
// Split a page up into sections (lesson, exercises, ...)
|
|
||||||
function splitSections(nodes) {
|
|
||||||
var section = [];
|
|
||||||
|
|
||||||
return _.reduce(nodes, function(sections, el) {
|
|
||||||
if(el.type === 'hr') {
|
|
||||||
sections.push(section);
|
|
||||||
section = [];
|
|
||||||
} else {
|
|
||||||
section.push(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections;
|
|
||||||
}, []).concat([section]); // Add remaining nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
// What is the type of this section
|
|
||||||
function sectionType(nodes, idx) {
|
|
||||||
if(isExercise(nodes)) {
|
|
||||||
return 'exercise';
|
|
||||||
} else if(isQuiz(nodes)) {
|
|
||||||
return 'quiz';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'normal';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a uniqueId to identify this section in our code
|
|
||||||
function sectionId(section, idx) {
|
|
||||||
return _.uniqueId('gitbook_');
|
|
||||||
}
|
|
||||||
|
|
||||||
function lexPage(src) {
|
|
||||||
// Lex file
|
|
||||||
var nodes = kramed.lexer(src);
|
|
||||||
|
|
||||||
return _.chain(splitSections(nodes))
|
|
||||||
.map(function(section, idx) {
|
|
||||||
// Detect section type
|
|
||||||
section.type = sectionType(section, idx);
|
|
||||||
return section;
|
|
||||||
})
|
|
||||||
.map(function(section, idx) {
|
|
||||||
// Give each section an ID
|
|
||||||
section.id = sectionId(section, idx);
|
|
||||||
return section;
|
|
||||||
|
|
||||||
})
|
|
||||||
.filter(function(section) {
|
|
||||||
return !_.isEmpty(section);
|
|
||||||
})
|
|
||||||
.reduce(function(sections, section) {
|
|
||||||
var last = _.last(sections);
|
|
||||||
|
|
||||||
// Merge normal sections together
|
|
||||||
if(last && last.type === section.type && last.type === 'normal') {
|
|
||||||
last.push.apply(last, [{'type': 'hr'}].concat(section));
|
|
||||||
} else {
|
|
||||||
// Add to list of sections
|
|
||||||
sections.push(section);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections;
|
|
||||||
}, [])
|
|
||||||
.map(function(section) {
|
|
||||||
section.links = nodes.links;
|
|
||||||
return section;
|
|
||||||
})
|
|
||||||
.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
module.exports = lexPage;
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
var kramed = require('kramed');
|
|
||||||
var hljs = require('highlight.js');
|
|
||||||
|
|
||||||
var lex = require('./lex');
|
|
||||||
var renderer = require('./renderer');
|
|
||||||
|
|
||||||
var include = require('./include');
|
|
||||||
var lnormalize = require('../utils/lang').normalize;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Render a section using our custom renderer
|
|
||||||
function render(section, _options) {
|
|
||||||
// Copy section
|
|
||||||
var links = section.links || {};
|
|
||||||
section = _.toArray(section);
|
|
||||||
section.links = links;
|
|
||||||
|
|
||||||
// Build options using defaults and our custom renderer
|
|
||||||
var options = _.extend({}, kramed.defaults, {
|
|
||||||
renderer: renderer(null, _options),
|
|
||||||
|
|
||||||
// Synchronous highlighting with highlight.js
|
|
||||||
highlight: function (code, lang) {
|
|
||||||
if(!lang) return code;
|
|
||||||
|
|
||||||
// Normalize lang
|
|
||||||
lang = lnormalize(lang);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return hljs.highlight(lang, code).value;
|
|
||||||
} catch(e) { }
|
|
||||||
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return kramed.parser(section, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function quizQuestion(node) {
|
|
||||||
if (node.text) {
|
|
||||||
node.text = node.text.replace(/^([\[(])x([\])])/, "$1 $2");
|
|
||||||
} else {
|
|
||||||
return node.replace(/^([\[(])x([\])])/, "$1 $2");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePage(src, options) {
|
|
||||||
options = options || {};
|
|
||||||
|
|
||||||
// Lex if not already lexed
|
|
||||||
var parsed = {
|
|
||||||
lexed: (_.isArray(src) ? page.content : lex(include(src, options.includer || function() { return undefined; })))
|
|
||||||
};
|
|
||||||
parsed.sections = parsed.lexed.map(function(section) {
|
|
||||||
// Transform given type
|
|
||||||
if(section.type === 'exercise') {
|
|
||||||
var nonCodeNodes = _.reject(section, {
|
|
||||||
'type': 'code'
|
|
||||||
});
|
|
||||||
|
|
||||||
var codeNodes = _.filter(section, {
|
|
||||||
'type': 'code'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Languages in code blocks
|
|
||||||
var langs = _.pluck(codeNodes, 'lang').map(lnormalize);
|
|
||||||
|
|
||||||
// Check that they are all the same
|
|
||||||
var validLangs = _.all(_.map(langs, function(lang) {
|
|
||||||
return lang && lang === langs[0];
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Main language
|
|
||||||
var lang = validLangs ? langs[0] : null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: section.id,
|
|
||||||
type: section.type,
|
|
||||||
content: render(nonCodeNodes, options),
|
|
||||||
lang: lang,
|
|
||||||
code: {
|
|
||||||
base: codeNodes[0].text,
|
|
||||||
solution: codeNodes[1].text,
|
|
||||||
validation: codeNodes[2].text,
|
|
||||||
// Context is optional
|
|
||||||
context: codeNodes[3] ? codeNodes[3].text : null,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else if (section.type === 'quiz') {
|
|
||||||
var quiz = [], question, foundFeedback = false;
|
|
||||||
var nonQuizNodes = section[0].type === 'paragraph' && section[1].type !== 'list_start' ? [section[0]] : [];
|
|
||||||
var quizNodes = section.slice(0);
|
|
||||||
quizNodes.splice(0, nonQuizNodes.length);
|
|
||||||
|
|
||||||
for (var i = 0; i < quizNodes.length; i++) {
|
|
||||||
var node = quizNodes[i];
|
|
||||||
|
|
||||||
if (question && (((node.type === 'list_end' || node.type === 'blockquote_end') && i === quizNodes.length - 1)
|
|
||||||
|| node.type === 'table' || (node.type === 'paragraph' && !foundFeedback))) {
|
|
||||||
quiz.push({
|
|
||||||
base: render(question.questionNodes, options),
|
|
||||||
solution: render(question.solutionNodes, options),
|
|
||||||
feedback: render(question.feedbackNodes, options)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.type === 'table' || (node.type === 'paragraph' && !foundFeedback)) {
|
|
||||||
question = { questionNodes: [], solutionNodes: [], feedbackNodes: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.type === 'blockquote_start') {
|
|
||||||
foundFeedback = true;
|
|
||||||
} else if (node.type === 'blockquote_end') {
|
|
||||||
foundFeedback = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.type === 'table') {
|
|
||||||
question.solutionNodes.push(_.cloneDeep(node));
|
|
||||||
node.cells = node.cells.map(function(row) {
|
|
||||||
return row.map(quizQuestion);
|
|
||||||
});
|
|
||||||
question.questionNodes.push(node);
|
|
||||||
} else if (!/blockquote/.test(node.type)) {
|
|
||||||
if (foundFeedback) {
|
|
||||||
question.feedbackNodes.push(node);
|
|
||||||
} else if (node.type === 'paragraph' || node.type === 'text'){
|
|
||||||
question.solutionNodes.push(_.cloneDeep(node));
|
|
||||||
quizQuestion(node);
|
|
||||||
question.questionNodes.push(node);
|
|
||||||
} else {
|
|
||||||
question.solutionNodes.push(node);
|
|
||||||
question.questionNodes.push(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: section.id,
|
|
||||||
type: section.type,
|
|
||||||
content: render(nonQuizNodes, options),
|
|
||||||
quiz: quiz
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render normal pages
|
|
||||||
return {
|
|
||||||
id: section.id,
|
|
||||||
type: section.type,
|
|
||||||
content: render(section, options)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
module.exports = parsePage;
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
var kramed = require('kramed');
|
|
||||||
var textRenderer = require('kramed-text-renderer');
|
|
||||||
|
|
||||||
function extractFirstNode(nodes, nType) {
|
|
||||||
return _.chain(nodes)
|
|
||||||
.filter(function(node) {
|
|
||||||
return node.type == nType;
|
|
||||||
})
|
|
||||||
.pluck("text")
|
|
||||||
.first()
|
|
||||||
.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function parseReadme(src) {
|
|
||||||
var nodes, title, description;
|
|
||||||
var renderer = textRenderer();
|
|
||||||
|
|
||||||
// Parse content
|
|
||||||
nodes = kramed.lexer(src);
|
|
||||||
|
|
||||||
title = extractFirstNode(nodes, "heading") || '';
|
|
||||||
description = extractFirstNode(nodes, "paragraph") || '';
|
|
||||||
|
|
||||||
var convert = _.compose(
|
|
||||||
function(text) {
|
|
||||||
return _.unescape(text.replace(/(\r\n|\n|\r)/gm, ""));
|
|
||||||
},
|
|
||||||
function(text) {
|
|
||||||
return kramed.parse(text, _.extend({}, kramed.defaults, {
|
|
||||||
renderer: renderer
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: convert(title),
|
|
||||||
description: convert(description)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
module.exports = parseReadme;
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
var url = require('url');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var inherits = require('util').inherits;
|
|
||||||
var links = require('../utils').links;
|
|
||||||
var kramed = require('kramed');
|
|
||||||
|
|
||||||
var rendererId = 0;
|
|
||||||
|
|
||||||
function GitBookRenderer(options, extra_options) {
|
|
||||||
if(!(this instanceof GitBookRenderer)) {
|
|
||||||
return new GitBookRenderer(options, extra_options);
|
|
||||||
}
|
|
||||||
GitBookRenderer.super_.call(this, options);
|
|
||||||
|
|
||||||
this._extra_options = extra_options;
|
|
||||||
this.quizRowId = 0;
|
|
||||||
this.id = rendererId++;
|
|
||||||
this.quizIndex = 0;
|
|
||||||
}
|
|
||||||
inherits(GitBookRenderer, kramed.Renderer);
|
|
||||||
|
|
||||||
GitBookRenderer.prototype._unsanitized = function(href) {
|
|
||||||
var prot = '';
|
|
||||||
try {
|
|
||||||
prot = decodeURIComponent(unescape(href))
|
|
||||||
.replace(/[^\w:]/g, '')
|
|
||||||
.toLowerCase();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(prot.indexOf('javascript:') === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
GitBookRenderer.prototype.link = function(href, title, text) {
|
|
||||||
// Our "fixed" href
|
|
||||||
var _href = href;
|
|
||||||
|
|
||||||
// Don't build if it looks malicious
|
|
||||||
if (this.options.sanitize && this._unsanitized(href)) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parsed version of the url
|
|
||||||
var parsed = url.parse(href);
|
|
||||||
var o = this._extra_options;
|
|
||||||
var extname = parsed.path? _.last(parsed.path.split(".")) : "";
|
|
||||||
|
|
||||||
// Relative link, rewrite it to point to github repo
|
|
||||||
if(links.isRelative(_href) && extname == "md") {
|
|
||||||
_href = links.toAbsolute(_href, o.dir || "./", o.outdir || "./");
|
|
||||||
_href = _href.replace(".md", ".html");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate HTML for link
|
|
||||||
var out = '<a href="' + _href + '"';
|
|
||||||
// Title if no null
|
|
||||||
if (title) {
|
|
||||||
out += ' title="' + title + '"';
|
|
||||||
}
|
|
||||||
// Target blank if external
|
|
||||||
if(parsed.protocol) {
|
|
||||||
out += ' target="_blank"';
|
|
||||||
}
|
|
||||||
out += '>' + text + '</a>';
|
|
||||||
return out;
|
|
||||||
};
|
|
||||||
|
|
||||||
GitBookRenderer.prototype.image = function(href, title, text) {
|
|
||||||
// Our "fixed" href
|
|
||||||
var _href = href;
|
|
||||||
|
|
||||||
// Parsed version of the url
|
|
||||||
var parsed = url.parse(href);
|
|
||||||
|
|
||||||
// Options
|
|
||||||
var o = this._extra_options;
|
|
||||||
|
|
||||||
// Relative image, rewrite it depending output
|
|
||||||
if(links.isRelative(href) && o && o.dir && o.outdir) {
|
|
||||||
// o.dir: directory parent of the file currently in rendering process
|
|
||||||
// o.outdir: directory parent from the html output
|
|
||||||
|
|
||||||
_href = links.toAbsolute(_href, o.dir, o.outdir);
|
|
||||||
}
|
|
||||||
|
|
||||||
return GitBookRenderer.super_.prototype.image.call(this, _href, title, text);
|
|
||||||
};
|
|
||||||
|
|
||||||
GitBookRenderer.prototype.tablerow = function(content) {
|
|
||||||
this.quizRowId += 1;
|
|
||||||
return GitBookRenderer.super_.prototype.tablerow(content);
|
|
||||||
};
|
|
||||||
|
|
||||||
var fieldRegex = /^([(\[])([ x])[\])]/;
|
|
||||||
GitBookRenderer.prototype._createCheckboxAndRadios = function(text) {
|
|
||||||
var match = fieldRegex.exec(text);
|
|
||||||
if (!match) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
//fix radio input uncheck failed
|
|
||||||
var quizFieldName='quiz-row-' + this.id + '-' + this.quizRowId ;
|
|
||||||
var quizIdentifier = quizFieldName + '-' + this.quizIndex++;
|
|
||||||
var field = "<input name='" + quizFieldName + "' id='" + quizIdentifier + "' type='";
|
|
||||||
field += match[1] === '(' ? "radio" : "checkbox";
|
|
||||||
field += match[2] === 'x' ? "' checked/>" : "'/>";
|
|
||||||
var splittedText = text.split(fieldRegex);
|
|
||||||
var length = splittedText.length;
|
|
||||||
var label = '<label class="quiz-label" for="' + quizIdentifier + '">' + splittedText[length - 1] + '</label>';
|
|
||||||
return text.replace(fieldRegex, field).replace(splittedText[length - 1], label);
|
|
||||||
};
|
|
||||||
|
|
||||||
GitBookRenderer.prototype.tablecell = function(content, flags) {
|
|
||||||
return GitBookRenderer.super_.prototype.tablecell(this._createCheckboxAndRadios(content), flags);
|
|
||||||
};
|
|
||||||
|
|
||||||
GitBookRenderer.prototype.listitem = function(text) {
|
|
||||||
return GitBookRenderer.super_.prototype.listitem(this._createCheckboxAndRadios(text));
|
|
||||||
};
|
|
||||||
|
|
||||||
GitBookRenderer.prototype.code = function(code, lang, escaped) {
|
|
||||||
return GitBookRenderer.super_.prototype.code.call(
|
|
||||||
this,
|
|
||||||
code,
|
|
||||||
lang,
|
|
||||||
escaped
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
GitBookRenderer.prototype.heading = function(text, level, raw) {
|
|
||||||
var id = this.options.headerPrefix + raw.toLowerCase().replace(/[^\w -]+/g, '').replace(/ /g, '-');
|
|
||||||
return '<h' + level + ' id="' + id + '">' + text + '</h' + level + '>\n';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
module.exports = GitBookRenderer;
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
var kramed = require('kramed');
|
|
||||||
|
|
||||||
|
|
||||||
// Utility function for splitting a list into groups
|
|
||||||
function splitBy(list, starter, ender) {
|
|
||||||
var starts = 0;
|
|
||||||
var ends = 0;
|
|
||||||
var group = [];
|
|
||||||
|
|
||||||
// Groups
|
|
||||||
return _.reduce(list, function(groups, value) {
|
|
||||||
// Ignore start and end delimiters in resulted groups
|
|
||||||
if(starter(value)) {
|
|
||||||
starts++;
|
|
||||||
} else if(ender(value)) {
|
|
||||||
ends++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add current value to group
|
|
||||||
group.push(value);
|
|
||||||
|
|
||||||
// We've got a matching
|
|
||||||
if(starts === ends && starts !== 0) {
|
|
||||||
// Add group to end groups
|
|
||||||
// (remove starter and ender token)
|
|
||||||
groups.push(group.slice(1, -1));
|
|
||||||
|
|
||||||
// Reset group
|
|
||||||
group = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function listSplit(nodes, start_type, end_type) {
|
|
||||||
return splitBy(nodes, function(el) {
|
|
||||||
return el.type === start_type;
|
|
||||||
}, function(el) {
|
|
||||||
return el.type === end_type;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the biggest list
|
|
||||||
// out of a list of kramed nodes
|
|
||||||
function filterList(nodes) {
|
|
||||||
return _.chain(nodes)
|
|
||||||
.toArray()
|
|
||||||
.rest(function(el) {
|
|
||||||
// Get everything after list_start
|
|
||||||
return el.type !== 'list_start';
|
|
||||||
})
|
|
||||||
.reverse()
|
|
||||||
.rest(function(el) {
|
|
||||||
// Get everything after list_end (remember we're reversed)
|
|
||||||
return el.type !== 'list_end';
|
|
||||||
})
|
|
||||||
.reverse()
|
|
||||||
.value().slice(1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parses an Article or Chapter title
|
|
||||||
// supports extracting links
|
|
||||||
function parseTitle(src, nums) {
|
|
||||||
// Check if it's a link
|
|
||||||
var matches = kramed.InlineLexer.rules.link.exec(src);
|
|
||||||
|
|
||||||
var level = nums.join('.');
|
|
||||||
|
|
||||||
// Not a link, return plain text
|
|
||||||
if(!matches) {
|
|
||||||
return {
|
|
||||||
title: src,
|
|
||||||
level: level,
|
|
||||||
path: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: matches[1],
|
|
||||||
level: level,
|
|
||||||
|
|
||||||
// Normalize path
|
|
||||||
// 1. Convert Window's "\" to "/"
|
|
||||||
// 2. Remove leading "/" if exists
|
|
||||||
path: matches[2].replace(/\\/g, '/').replace(/^\/+/, ''),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseChapter(nodes, nums) {
|
|
||||||
// Convert single number to an array
|
|
||||||
nums = _.isArray(nums) ? nums : [nums];
|
|
||||||
|
|
||||||
return _.extend(parseTitle(_.first(nodes).text, nums), {
|
|
||||||
articles: _.map(listSplit(filterList(nodes), 'list_item_start', 'list_item_end'), function(nodes, i) {
|
|
||||||
return parseChapter(nodes, nums.concat(i + 1));
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultChapterList(chapterList) {
|
|
||||||
var first = _.first(chapterList);
|
|
||||||
|
|
||||||
// Check if introduction node was specified in SUMMARY.md
|
|
||||||
if (first) {
|
|
||||||
var chapter = parseChapter(first, [0]);
|
|
||||||
|
|
||||||
// Already have README node, we're good to go
|
|
||||||
if(chapter.path === 'README.md') {
|
|
||||||
return chapterList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// It wasn't specified, so add in default
|
|
||||||
return [
|
|
||||||
[ { type: 'text', text: '[Introduction](README.md)' } ]
|
|
||||||
].concat(chapterList);
|
|
||||||
}
|
|
||||||
|
|
||||||
function listGroups(src) {
|
|
||||||
var nodes = kramed.lexer(src);
|
|
||||||
|
|
||||||
// Get out groups of lists
|
|
||||||
return listSplit(
|
|
||||||
filterList(nodes),
|
|
||||||
'list_item_start', 'list_item_end'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSummary(src) {
|
|
||||||
// Split out chapter sections
|
|
||||||
var chapters = defaultChapterList(listGroups(src))
|
|
||||||
.map(parseChapter);
|
|
||||||
|
|
||||||
return {
|
|
||||||
chapters: chapters
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseEntries (src) {
|
|
||||||
return listGroups(src).map(parseChapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
module.exports = parseSummary;
|
|
||||||
module.exports.entries = parseEntries;
|
|
||||||
+241
@@ -0,0 +1,241 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var Q = require('q');
|
||||||
|
var path = require('path');
|
||||||
|
var url = require('url');
|
||||||
|
var fs = require('./utils/fs');
|
||||||
|
var resolve = require('resolve');
|
||||||
|
var mergeDefaults = require('merge-defaults');
|
||||||
|
var jsonschema = require('jsonschema');
|
||||||
|
var jsonSchemaDefaults = require('json-schema-defaults');
|
||||||
|
|
||||||
|
var version = require('./version');
|
||||||
|
|
||||||
|
var PLUGIN_PREFIX = 'gitbook-plugin-';
|
||||||
|
|
||||||
|
// Return an absolute name for the plugin (the one on NPM)
|
||||||
|
function absoluteName(name) {
|
||||||
|
if (name.indexOf(PLUGIN_PREFIX) === 0) return name;
|
||||||
|
return [PLUGIN_PREFIX, name].join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var Plugin = function(book, name) {
|
||||||
|
this.book = book;
|
||||||
|
this.name = absoluteName(name);
|
||||||
|
this.packageInfos = {};
|
||||||
|
this.infos = {};
|
||||||
|
|
||||||
|
// Bind methods
|
||||||
|
_.bindAll(this);
|
||||||
|
|
||||||
|
_.each([
|
||||||
|
absoluteName(name),
|
||||||
|
name
|
||||||
|
], function(_name) {
|
||||||
|
// Load from the book
|
||||||
|
if (this.load(_name, book.root)) return false;
|
||||||
|
|
||||||
|
// Load from default plugins
|
||||||
|
if (this.load(_name, __dirname)) return false;
|
||||||
|
}, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type of plugins resources
|
||||||
|
Plugin.RESOURCES = ['js', 'css'];
|
||||||
|
Plugin.HOOKS = [
|
||||||
|
'init', 'finish', 'finish:before', 'config', 'page', 'page:before'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Return the reduce name for the plugin
|
||||||
|
// "gitbook-plugin-test" -> "test"
|
||||||
|
// Return a relative name for the plugin (the one on GitBook)
|
||||||
|
Plugin.prototype.reducedName = function() {
|
||||||
|
return this.name.replace(PLUGIN_PREFIX, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load from a name
|
||||||
|
Plugin.prototype.load = function(name, baseDir) {
|
||||||
|
try {
|
||||||
|
var res = resolve.sync(name+'/package.json', { basedir: baseDir });
|
||||||
|
|
||||||
|
this.baseDir = path.dirname(res);
|
||||||
|
this.packageInfos = require(res);
|
||||||
|
this.infos = require(resolve.sync(name, { basedir: baseDir }));
|
||||||
|
this.name = this.packageInfos.name;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
this.packageInfos = {};
|
||||||
|
this.infos = {};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Plugin.prototype.normalizeResource = function(resource) {
|
||||||
|
// Parse the resource path
|
||||||
|
var parsed = url.parse(resource);
|
||||||
|
|
||||||
|
// This is a remote resource
|
||||||
|
// so we will simply link to using it's URL
|
||||||
|
if (parsed.protocol) {
|
||||||
|
return {
|
||||||
|
'url': resource
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will be copied over from disk
|
||||||
|
// and shipped with the book's build
|
||||||
|
return { 'path': this.name+'/'+resource };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return resources
|
||||||
|
Plugin.prototype._getResources = function(base) {
|
||||||
|
base = base;
|
||||||
|
var book = this.infos[base];
|
||||||
|
|
||||||
|
// Compatibility with version 1.x.x
|
||||||
|
if (base == 'website') book = book || this.infos.book;
|
||||||
|
|
||||||
|
// Nothing specified, fallback to default
|
||||||
|
if (!book) {
|
||||||
|
return Q({});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic function
|
||||||
|
if(typeof book === 'function') {
|
||||||
|
// Call giving it the context of our book
|
||||||
|
return Q().then(book.bind(this.book));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain data object
|
||||||
|
return Q(_.cloneDeep(book));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize resources and return them
|
||||||
|
Plugin.prototype.getResources = function(base) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return this._getResources(base)
|
||||||
|
.then(function(resources) {
|
||||||
|
|
||||||
|
_.each(Plugin.RESOURCES, function(resourceType) {
|
||||||
|
resources[resourceType] = (resources[resourceType] || []).map(that.normalizeResource);
|
||||||
|
});
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize filters and return them
|
||||||
|
Plugin.prototype.getFilters = function() {
|
||||||
|
return this.infos.filters || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize blocks and return them
|
||||||
|
Plugin.prototype.getBlocks = function() {
|
||||||
|
return this.infos.blocks || {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test if it's a valid plugin
|
||||||
|
Plugin.prototype.isValid = function() {
|
||||||
|
var that = this;
|
||||||
|
var isValid = (
|
||||||
|
this.packageInfos &&
|
||||||
|
this.packageInfos.name &&
|
||||||
|
this.packageInfos.engines &&
|
||||||
|
this.packageInfos.engines.gitbook &&
|
||||||
|
version.satisfies(this.packageInfos.engines.gitbook)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Valid hooks
|
||||||
|
_.each(this.infos.hooks, function(hook, hookName) {
|
||||||
|
if (_.contains(Plugin.HOOKS, hookName)) return;
|
||||||
|
that.book.log.warn.ln('Hook "'+hookName+'"" used by plugin "'+that.packageInfos.name+'" has been removed or is deprecated');
|
||||||
|
});
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize, validate configuration for this plugin using its schema
|
||||||
|
// Throw an error when shcema is not respected
|
||||||
|
Plugin.prototype.validateConfig = function(config) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
var schema = that.packageInfos.gitbook || {};
|
||||||
|
if (!schema) return config;
|
||||||
|
|
||||||
|
// Normalize schema
|
||||||
|
schema.id = '/pluginsConfig.'+that.reducedName();
|
||||||
|
schema.type = 'object';
|
||||||
|
|
||||||
|
// Validate and throw if invalid
|
||||||
|
var v = new jsonschema.Validator();
|
||||||
|
var result = v.validate(config, schema, {
|
||||||
|
propertyName: 'pluginsConfig.'+that.reducedName()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Throw error
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
throw new Error('Configuration Error: '+result.errors[0].stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert default values
|
||||||
|
var defaults = jsonSchemaDefaults(schema);
|
||||||
|
return mergeDefaults(config, defaults);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve file path
|
||||||
|
Plugin.prototype.resolveFile = function(filename) {
|
||||||
|
return path.resolve(this.baseDir, filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve file path
|
||||||
|
Plugin.prototype.callHook = function(name, data) {
|
||||||
|
// Our book will be the context to apply
|
||||||
|
var context = this.book;
|
||||||
|
|
||||||
|
var hookFunc = this.infos.hooks? this.infos.hooks[name] : null;
|
||||||
|
data = data || {};
|
||||||
|
|
||||||
|
if (!hookFunc) return Q(data);
|
||||||
|
|
||||||
|
this.book.log.debug.ln('call hook', name);
|
||||||
|
if (!_.contains(Plugin.HOOKS, name)) this.book.log.warn.ln('hook "'+name+'" used by plugin "'+this.name+'" is deprecated, and will be removed in the coming versions');
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
return hookFunc.apply(context, [data]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy plugin assets fodler
|
||||||
|
Plugin.prototype.copyAssets = function(out, base) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return this.getResources(base)
|
||||||
|
.get('assets')
|
||||||
|
.then(function(assets) {
|
||||||
|
// Assets are undefined
|
||||||
|
if(!assets) return false;
|
||||||
|
|
||||||
|
return fs.copy(
|
||||||
|
that.resolveFile(assets),
|
||||||
|
out
|
||||||
|
).then(_.constant(true));
|
||||||
|
}, _.constant(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get config from book
|
||||||
|
Plugin.prototype.getConfig = function() {
|
||||||
|
return this.book.config.get('pluginsConfig.'+this.reducedName(), {});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set configuration for this plugin
|
||||||
|
Plugin.prototype.setConfig = function(values) {
|
||||||
|
return this.book.config.set('pluginsConfig.'+this.reducedName(), values);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Plugin;
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var Q = require('q');
|
||||||
|
var npmi = require('npmi');
|
||||||
|
var npm = require('npm');
|
||||||
|
var semver = require('semver');
|
||||||
|
|
||||||
|
var Plugin = require('./plugin');
|
||||||
|
var version = require('./version');
|
||||||
|
|
||||||
|
var initNPM = _.memoize(function() {
|
||||||
|
return Q.nfcall(npm.load, { silent: true, loglevel: 'silent' });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var PluginsList = function(book, plugins) {
|
||||||
|
this.book = book;
|
||||||
|
this.log = this.book.log;
|
||||||
|
|
||||||
|
// List of Plugin objects
|
||||||
|
this.list = [];
|
||||||
|
|
||||||
|
// List of names of failed plugins
|
||||||
|
this.failed = [];
|
||||||
|
|
||||||
|
// Namespaces
|
||||||
|
this.namespaces = _.chain(['website', 'ebook'])
|
||||||
|
.map(function(namespace) {
|
||||||
|
return [
|
||||||
|
namespace,
|
||||||
|
{
|
||||||
|
html: {},
|
||||||
|
resources: _.chain(Plugin.RESOURCES)
|
||||||
|
.map(function(type) {
|
||||||
|
return [type, []];
|
||||||
|
})
|
||||||
|
.object()
|
||||||
|
.value()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
})
|
||||||
|
.object()
|
||||||
|
.value();
|
||||||
|
|
||||||
|
// Bind methods
|
||||||
|
_.bindAll(this);
|
||||||
|
|
||||||
|
if (plugins) this.load(plugins);
|
||||||
|
};
|
||||||
|
|
||||||
|
// return count of plugins
|
||||||
|
PluginsList.prototype.count = function() {
|
||||||
|
return this.list.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add and load a plugin
|
||||||
|
PluginsList.prototype.load = function(plugin) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
if (_.isArray(plugin)) {
|
||||||
|
return _.reduce(plugin, function(prev, p) {
|
||||||
|
return prev.then(function() {
|
||||||
|
return that.load(p);
|
||||||
|
});
|
||||||
|
}, Q());
|
||||||
|
}
|
||||||
|
if (_.isObject(plugin) && !(plugin instanceof Plugin)) plugin = plugin.name;
|
||||||
|
if (_.isString(plugin)) plugin = new Plugin(this.book, plugin);
|
||||||
|
|
||||||
|
that.log.info('load plugin', plugin.name, '....');
|
||||||
|
if (!plugin.isValid()) {
|
||||||
|
that.log.info.fail();
|
||||||
|
that.failed.push(plugin.name);
|
||||||
|
return Q();
|
||||||
|
} else {
|
||||||
|
that.log.info.ok();
|
||||||
|
|
||||||
|
// Push in the list
|
||||||
|
that.list.push(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
|
||||||
|
// Validate and normalize configuration
|
||||||
|
.then(function() {
|
||||||
|
var config = plugin.getConfig();
|
||||||
|
return plugin.validateConfig(config);
|
||||||
|
})
|
||||||
|
.then(function(config) {
|
||||||
|
// Update configuration
|
||||||
|
plugin.setConfig(config);
|
||||||
|
|
||||||
|
// Extract filters
|
||||||
|
that.book.template.addFilters(plugin.getFilters());
|
||||||
|
|
||||||
|
// Extract blocks
|
||||||
|
that.book.template.addBlocks(plugin.getBlocks());
|
||||||
|
|
||||||
|
return _.reduce(_.keys(that.namespaces), function(prev, namespaceName) {
|
||||||
|
return prev.then(function() {
|
||||||
|
return plugin.getResources(namespaceName)
|
||||||
|
.then(function(plResources) {
|
||||||
|
var namespace = that.namespaces[namespaceName];
|
||||||
|
|
||||||
|
// Extract js and css
|
||||||
|
_.each(Plugin.RESOURCES, function(resourceType) {
|
||||||
|
namespace.resources[resourceType] = (namespace.resources[resourceType] || []).concat(plResources[resourceType] || []);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map of html resources by name added by each plugin
|
||||||
|
_.each(plResources.html || {}, function(value, tag) {
|
||||||
|
// Turn into function if not one already
|
||||||
|
if (!_.isFunction(value)) value = _.constant(value);
|
||||||
|
|
||||||
|
namespace.html[tag] = namespace.html[tag] || [];
|
||||||
|
namespace.html[tag].push(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, Q());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call a hook
|
||||||
|
PluginsList.prototype.hook = function(name, data) {
|
||||||
|
return _.reduce(this.list, function(prev, plugin) {
|
||||||
|
return prev.then(function(ret) {
|
||||||
|
return plugin.callHook(name, ret);
|
||||||
|
});
|
||||||
|
}, Q(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return a template from a plugin
|
||||||
|
PluginsList.prototype.template = function(name) {
|
||||||
|
var withTpl = _.find(this.list, function(plugin) {
|
||||||
|
return (
|
||||||
|
plugin.infos.templates &&
|
||||||
|
plugin.infos.templates[name]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!withTpl) return null;
|
||||||
|
return withTpl.resolveFile(withTpl.infos.templates[name]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return an html snippet
|
||||||
|
PluginsList.prototype.html = function(namespace, tag, context, options) {
|
||||||
|
var htmlSnippets = this.namespaces[namespace].html[tag];
|
||||||
|
return _.map(htmlSnippets || [], function(code) {
|
||||||
|
return code.call(context, options);
|
||||||
|
}).join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return a resources map for a namespace
|
||||||
|
PluginsList.prototype.resources = function(namespace) {
|
||||||
|
return this.namespaces[namespace].resources;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Install plugins from a book
|
||||||
|
PluginsList.prototype.install = function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
// Remove defaults (no need to install)
|
||||||
|
var plugins = _.reject(that.book.options.plugins, {
|
||||||
|
isDefault: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Install plugins one by one
|
||||||
|
that.book.log.info.ln(plugins.length+' plugins to install');
|
||||||
|
return _.reduce(plugins, function(prev, plugin) {
|
||||||
|
return prev.then(function() {
|
||||||
|
var fullname = 'gitbook-plugin-'+plugin.name;
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
|
||||||
|
// Resolve version if needed
|
||||||
|
.then(function() {
|
||||||
|
if (plugin.version) return plugin.version;
|
||||||
|
|
||||||
|
that.book.log.info.ln('No version specified, resolve plugin', plugin.name);
|
||||||
|
return initNPM()
|
||||||
|
.then(function() {
|
||||||
|
return Q.nfcall(npm.commands.view, [fullname+'@*', 'engines'], true);
|
||||||
|
})
|
||||||
|
.then(function(versions) {
|
||||||
|
return _.chain(versions)
|
||||||
|
.pairs()
|
||||||
|
.map(function(v) {
|
||||||
|
return {
|
||||||
|
version: v[0],
|
||||||
|
gitbook: (v[1].engines || {}).gitbook
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(function(v) {
|
||||||
|
return v.gitbook && version.satisfies(v.gitbook);
|
||||||
|
})
|
||||||
|
.sort(function(v1, v2) {
|
||||||
|
return semver.lt(v1.version, v2.version)? 1 : -1;
|
||||||
|
})
|
||||||
|
.pluck('version')
|
||||||
|
.first()
|
||||||
|
.value();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// Install the plugin with the resolved version
|
||||||
|
.then(function(version) {
|
||||||
|
if (!version) {
|
||||||
|
throw 'Found no satisfactory version for plugin '+plugin.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
that.book.log.info.ln('install plugin', plugin.name, 'from npm ('+fullname+') with version', version);
|
||||||
|
return Q.nfcall(npmi, {
|
||||||
|
'name': fullname,
|
||||||
|
'version': version,
|
||||||
|
'path': that.book.root,
|
||||||
|
'npmLoad': {
|
||||||
|
'loglevel': 'silent',
|
||||||
|
'loaded': true,
|
||||||
|
'prefix': that.book.root
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
that.book.log.info.ok('plugin', plugin.name, 'installed with success');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, Q());
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = PluginsList;
|
||||||
+466
@@ -0,0 +1,466 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var Q = require('q');
|
||||||
|
var path = require('path');
|
||||||
|
var nunjucks = require('nunjucks');
|
||||||
|
var parsers = require('gitbook-parsers');
|
||||||
|
var escapeStringRegexp = require('escape-string-regexp');
|
||||||
|
|
||||||
|
var batch = require('./utils/batch');
|
||||||
|
var pkg = require('../package.json');
|
||||||
|
var defaultBlocks = require('./blocks');
|
||||||
|
var BookLoader = require('./conrefs_loader');
|
||||||
|
|
||||||
|
// Normalize result from a block
|
||||||
|
function normBlockResult(blk) {
|
||||||
|
if (_.isString(blk)) blk = { body: blk };
|
||||||
|
return blk;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var TemplateEngine = function(book) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.book = book;
|
||||||
|
this.log = this.book.log;
|
||||||
|
|
||||||
|
// Template loader
|
||||||
|
this.loader = new BookLoader(book, {
|
||||||
|
// Replace shortcuts in imported files
|
||||||
|
interpolate: function(filepath, source) {
|
||||||
|
var parser = parsers.get(path.extname(filepath));
|
||||||
|
var type = parser? parser.name : null;
|
||||||
|
|
||||||
|
return that.applyShortcuts(type, source);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nunjucks env
|
||||||
|
this.env = new nunjucks.Environment(
|
||||||
|
this.loader,
|
||||||
|
{
|
||||||
|
// Escaping is done after by the markdown parser
|
||||||
|
autoescape: false,
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
tags: {
|
||||||
|
blockStart: '{%',
|
||||||
|
blockEnd: '%}',
|
||||||
|
variableStart: '{{',
|
||||||
|
variableEnd: '}}',
|
||||||
|
commentStart: '{###',
|
||||||
|
commentEnd: '###}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// List of tags shortcuts
|
||||||
|
this.shortcuts = [];
|
||||||
|
|
||||||
|
// Map of blocks bodies (that requires post-processing)
|
||||||
|
this.blockBodies = {};
|
||||||
|
|
||||||
|
// Map of added blocks
|
||||||
|
this.blocks = {};
|
||||||
|
|
||||||
|
// Bind methods
|
||||||
|
_.bindAll(this);
|
||||||
|
|
||||||
|
// Add default blocks
|
||||||
|
this.addBlocks(defaultBlocks);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process the result of block in a context
|
||||||
|
TemplateEngine.prototype.processBlock = function(blk) {
|
||||||
|
blk = _.defaults(blk, {
|
||||||
|
parse: false,
|
||||||
|
post: undefined
|
||||||
|
});
|
||||||
|
blk.id = _.uniqueId('blk');
|
||||||
|
|
||||||
|
var toAdd = (!blk.parse) || (blk.post !== undefined);
|
||||||
|
|
||||||
|
// Add to global map
|
||||||
|
if (toAdd) this.blockBodies[blk.id] = blk;
|
||||||
|
|
||||||
|
// Parsable block, just return it
|
||||||
|
if (blk.parse) {
|
||||||
|
return blk.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return it as a position marker
|
||||||
|
return '@%@'+blk.id+'@%@';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replace position markers of blocks by body after processing
|
||||||
|
// This is done to avoid that markdown/asciidoc processer parse the block content
|
||||||
|
TemplateEngine.prototype.replaceBlocks = function(content) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return content.replace(/\@\%\@([\s\S]+?)\@\%\@/g, function(match, key) {
|
||||||
|
var blk = that.blockBodies[key];
|
||||||
|
if (!blk) return match;
|
||||||
|
|
||||||
|
var body = blk.body;
|
||||||
|
|
||||||
|
return body;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bind a function to a context
|
||||||
|
TemplateEngine.prototype.bindContext = function(func) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
var ctx = {
|
||||||
|
ctx: this.ctx,
|
||||||
|
book: that.book,
|
||||||
|
generator: that.book.options.generator
|
||||||
|
};
|
||||||
|
|
||||||
|
return func.apply(ctx, arguments);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add filter
|
||||||
|
TemplateEngine.prototype.addFilter = function(filterName, func) {
|
||||||
|
try {
|
||||||
|
this.env.getFilter(filterName);
|
||||||
|
this.log.warn.ln('conflict in filters, \''+filterName+'\' is already set');
|
||||||
|
return false;
|
||||||
|
} catch(e) {
|
||||||
|
// Filter doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.debug.ln('add filter \''+filterName+'\'');
|
||||||
|
this.env.addFilter(filterName, this.bindContext(function() {
|
||||||
|
var ctx = this;
|
||||||
|
var args = Array.prototype.slice.apply(arguments);
|
||||||
|
var callback = _.last(args);
|
||||||
|
|
||||||
|
Q()
|
||||||
|
.then(function() {
|
||||||
|
return func.apply(ctx, args.slice(0, -1));
|
||||||
|
})
|
||||||
|
.nodeify(callback);
|
||||||
|
}), true);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add multiple filters
|
||||||
|
TemplateEngine.prototype.addFilters = function(filters) {
|
||||||
|
_.each(filters, function(filter, name) {
|
||||||
|
this.addFilter(name, filter);
|
||||||
|
}, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return nunjucks extension name of a block
|
||||||
|
TemplateEngine.prototype.blockExtName = function(name) {
|
||||||
|
return 'Block'+name+'Extension';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test if a block is defined
|
||||||
|
TemplateEngine.prototype.hasBlock = function(name) {
|
||||||
|
return this.env.hasExtension(this.blockExtName(name));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove a block
|
||||||
|
TemplateEngine.prototype.removeBlock = function(name) {
|
||||||
|
if (!this.hasBlock(name)) return;
|
||||||
|
|
||||||
|
// Remove nunjucks extension
|
||||||
|
this.env.removeExtension(this.blockExtName(name));
|
||||||
|
|
||||||
|
// Cleanup shortcuts
|
||||||
|
this.shortcuts = _.reject(this.shortcuts, {
|
||||||
|
block: name
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a block
|
||||||
|
TemplateEngine.prototype.addBlock = function(name, block) {
|
||||||
|
var that = this, Ext, extName;
|
||||||
|
|
||||||
|
if (_.isFunction(block)) block = { process: block };
|
||||||
|
|
||||||
|
block = _.defaults(block || {}, {
|
||||||
|
shortcuts: [],
|
||||||
|
end: 'end'+name,
|
||||||
|
process: _.identity,
|
||||||
|
blocks: []
|
||||||
|
});
|
||||||
|
|
||||||
|
extName = this.blockExtName(name);
|
||||||
|
|
||||||
|
if (this.hasBlock(name) && !defaultBlocks[name]) {
|
||||||
|
this.log.warn.ln('conflict in blocks, \''+name+'\' is already defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup previous block
|
||||||
|
this.removeBlock(name);
|
||||||
|
|
||||||
|
this.log.debug.ln('add block \''+name+'\'');
|
||||||
|
this.blocks[name] = block;
|
||||||
|
|
||||||
|
Ext = function () {
|
||||||
|
this.tags = [name];
|
||||||
|
|
||||||
|
this.parse = function(parser, nodes) {
|
||||||
|
var body = null;
|
||||||
|
var lastBlockName = null;
|
||||||
|
var lastBlockArgs = null;
|
||||||
|
var allBlocks = block.blocks.concat([block.end]);
|
||||||
|
var subbodies = {};
|
||||||
|
|
||||||
|
var tok = parser.nextToken();
|
||||||
|
var args = parser.parseSignature(null, true);
|
||||||
|
parser.advanceAfterBlockEnd(tok.value);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
// Read body
|
||||||
|
var currentBody = parser.parseUntilBlocks.apply(parser, allBlocks);
|
||||||
|
|
||||||
|
// Handle body with previous block name and args
|
||||||
|
if (lastBlockName) {
|
||||||
|
subbodies[lastBlockName] = subbodies[lastBlockName] || [];
|
||||||
|
subbodies[lastBlockName].push({
|
||||||
|
body: currentBody,
|
||||||
|
args: lastBlockArgs
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
body = currentBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read new block
|
||||||
|
lastBlockName = parser.peekToken().value;
|
||||||
|
if (lastBlockName == block.end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse signature and move to the end of the block
|
||||||
|
lastBlockArgs = parser.parseSignature(null, true);
|
||||||
|
parser.advanceAfterBlockEnd(lastBlockName);
|
||||||
|
}
|
||||||
|
parser.advanceAfterBlockEnd();
|
||||||
|
|
||||||
|
var bodies = [body];
|
||||||
|
_.each(block.blocks, function(blockName) {
|
||||||
|
subbodies[blockName] = subbodies[blockName] || [];
|
||||||
|
if (subbodies[blockName].length === 0) {
|
||||||
|
subbodies[blockName].push({
|
||||||
|
args: new nodes.NodeList(),
|
||||||
|
body: new nodes.NodeList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bodies.push(subbodies[blockName][0].body);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new nodes.CallExtensionAsync(this, 'run', args, bodies);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.run = function(context) {
|
||||||
|
var args = Array.prototype.slice.call(arguments, 1);
|
||||||
|
var callback = args.pop();
|
||||||
|
|
||||||
|
// Extract blocks
|
||||||
|
var blocks = args
|
||||||
|
.concat([])
|
||||||
|
.slice(-block.blocks.length);
|
||||||
|
|
||||||
|
// Eliminate blocks from list
|
||||||
|
if (block.blocks.length > 0) args = args.slice(0, -block.blocks.length);
|
||||||
|
|
||||||
|
// Extract main body and kwargs
|
||||||
|
var body = args.pop();
|
||||||
|
var kwargs = _.isObject(_.last(args))? args.pop() : {};
|
||||||
|
|
||||||
|
// Extract blocks body
|
||||||
|
var _blocks = _.map(block.blocks, function(blockName, i){
|
||||||
|
return {
|
||||||
|
name: blockName,
|
||||||
|
body: blocks[i]()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
Q()
|
||||||
|
.then(function() {
|
||||||
|
return that.applyBlock(name, {
|
||||||
|
body: body(),
|
||||||
|
args: args,
|
||||||
|
kwargs: kwargs,
|
||||||
|
blocks: _blocks
|
||||||
|
}, context);
|
||||||
|
})
|
||||||
|
|
||||||
|
// process the block returned
|
||||||
|
.then(that.processBlock)
|
||||||
|
.nodeify(callback);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the Extension
|
||||||
|
this.env.addExtension(extName, new Ext());
|
||||||
|
|
||||||
|
// Add shortcuts
|
||||||
|
if (!_.isArray(block.shortcuts)) block.shortcuts = [block.shortcuts];
|
||||||
|
_.each(block.shortcuts, function(shortcut) {
|
||||||
|
this.log.debug.ln('add template shortcut from \''+shortcut.start+'\' to block \''+name+'\' for parsers ', shortcut.parsers);
|
||||||
|
this.shortcuts.push({
|
||||||
|
block: name,
|
||||||
|
parsers: shortcut.parsers,
|
||||||
|
start: shortcut.start,
|
||||||
|
end: shortcut.end,
|
||||||
|
tag: {
|
||||||
|
start: name,
|
||||||
|
end: block.end
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add multiple blocks
|
||||||
|
TemplateEngine.prototype.addBlocks = function(blocks) {
|
||||||
|
_.each(blocks, function(block, name) {
|
||||||
|
this.addBlock(name, block);
|
||||||
|
}, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply a block to some content
|
||||||
|
// This method result depends on the type of block (async or sync)
|
||||||
|
TemplateEngine.prototype.applyBlock = function(name, blk, ctx) {
|
||||||
|
var func, block, r;
|
||||||
|
|
||||||
|
block = this.blocks[name];
|
||||||
|
if (!block) throw new Error('Block not found \''+name+'\'');
|
||||||
|
if (_.isString(blk)) {
|
||||||
|
blk = {
|
||||||
|
body: blk
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
blk = _.defaults(blk, {
|
||||||
|
args: [],
|
||||||
|
kwargs: {},
|
||||||
|
blocks: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind and call block processor
|
||||||
|
func = this.bindContext(block.process);
|
||||||
|
r = func.call(ctx || {}, blk);
|
||||||
|
|
||||||
|
if (Q.isPromise(r)) return r.then(normBlockResult);
|
||||||
|
else return normBlockResult(r);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply a shortcut to a string
|
||||||
|
TemplateEngine.prototype._applyShortcut = function(parser, content, shortcut) {
|
||||||
|
if (!_.contains(shortcut.parsers, parser)) return content;
|
||||||
|
var regex = new RegExp(
|
||||||
|
escapeStringRegexp(shortcut.start) + '([\\s\\S]*?[^\\$])' + escapeStringRegexp(shortcut.end),
|
||||||
|
'g'
|
||||||
|
);
|
||||||
|
return content.replace(regex, function(all, match) {
|
||||||
|
return '{% '+shortcut.tag.start+' %}'+ match + '{% '+shortcut.tag.end+' %}';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply all shortcuts to some template string
|
||||||
|
TemplateEngine.prototype.applyShortcuts = function(type, content) {
|
||||||
|
return _.reduce(this.shortcuts, _.partial(this._applyShortcut.bind(this), type), content);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render a string from the book
|
||||||
|
TemplateEngine.prototype.renderString = function(content, context, options) {
|
||||||
|
context = _.extend({}, context, {
|
||||||
|
// Variables from book.json
|
||||||
|
book: this.book.options.variables,
|
||||||
|
|
||||||
|
// Complete book.json
|
||||||
|
config: this.book.options,
|
||||||
|
|
||||||
|
// infos about gitbook
|
||||||
|
gitbook: {
|
||||||
|
version: pkg.version,
|
||||||
|
generator: this.book.options.generator
|
||||||
|
}
|
||||||
|
});
|
||||||
|
options = _.defaults(options || {}, {
|
||||||
|
path: null,
|
||||||
|
type: null
|
||||||
|
});
|
||||||
|
if (options.path) options.path = this.book.resolve(options.path);
|
||||||
|
if (!options.type && options.path) {
|
||||||
|
var parser = parsers.get(path.extname(options.path));
|
||||||
|
options.type = parser? parser.name : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace shortcuts
|
||||||
|
content = this.applyShortcuts(options.type, content);
|
||||||
|
|
||||||
|
return Q.nfcall(this.env.renderString.bind(this.env), content, context, options)
|
||||||
|
.fail(function(err) {
|
||||||
|
if (_.isString(err)) err = new Error(err);
|
||||||
|
err.message = err.message.replace(/^Error: /, '');
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render a file from the book
|
||||||
|
TemplateEngine.prototype.renderFile = function(filename) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return that.book.readFile(filename)
|
||||||
|
.then(function(content) {
|
||||||
|
return that.renderString(content, {}, {
|
||||||
|
path: filename
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render a page from the book
|
||||||
|
TemplateEngine.prototype.renderPage = function(page) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return that.book.statFile(page.path)
|
||||||
|
.then(function(stat) {
|
||||||
|
var context = {
|
||||||
|
// infos about the file
|
||||||
|
file: {
|
||||||
|
path: page.path,
|
||||||
|
mtime: stat.mtime
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return that.renderString(page.content, context, {
|
||||||
|
path: page.path,
|
||||||
|
type: page.type
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Post process content
|
||||||
|
TemplateEngine.prototype.postProcess = function(content) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
return Q(content)
|
||||||
|
.then(that.replaceBlocks)
|
||||||
|
.then(function(_content) {
|
||||||
|
return batch.execEach(that.blockBodies, {
|
||||||
|
max: 20,
|
||||||
|
fn: function(blk, blkId) {
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
if (!blk.post) return Q();
|
||||||
|
return blk.post();
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
delete that.blockBodies[blkId];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.thenResolve(_content);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = TemplateEngine;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
var Q = require("q");
|
||||||
|
var _ = require("lodash");
|
||||||
|
|
||||||
|
// Execute a method for all element
|
||||||
|
function execEach(items, options) {
|
||||||
|
if (_.size(items) === 0) return Q();
|
||||||
|
var concurrents = 0, d = Q.defer(), pending = [];
|
||||||
|
|
||||||
|
options = _.defaults(options || {}, {
|
||||||
|
max: 100,
|
||||||
|
fn: function() {}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function startItem(item, i) {
|
||||||
|
if (concurrents >= options.max) {
|
||||||
|
pending.push([item, i]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
concurrents++;
|
||||||
|
Q()
|
||||||
|
.then(function() {
|
||||||
|
return options.fn(item, i);
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
concurrents--;
|
||||||
|
|
||||||
|
// Next pending
|
||||||
|
var next = pending.shift();
|
||||||
|
|
||||||
|
if (concurrents === 0 && !next) {
|
||||||
|
d.resolve();
|
||||||
|
} else if (next) {
|
||||||
|
startItem.apply(null, next);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function(err) {
|
||||||
|
pending = [];
|
||||||
|
d.reject(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(items, startItem);
|
||||||
|
|
||||||
|
return d.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
execEach: execEach
|
||||||
|
};
|
||||||
|
|
||||||
+193
@@ -0,0 +1,193 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var Q = require('q');
|
||||||
|
var tmp = require('tmp');
|
||||||
|
var path = require('path');
|
||||||
|
var fs = require('graceful-fs');
|
||||||
|
var fsExtra = require('fs-extra');
|
||||||
|
var Ignore = require('fstream-ignore');
|
||||||
|
|
||||||
|
var fsUtils = {
|
||||||
|
tmp: {
|
||||||
|
file: function(opt) {
|
||||||
|
return Q.nfcall(tmp.file.bind(tmp), opt).get(0);
|
||||||
|
},
|
||||||
|
dir: function() {
|
||||||
|
return Q.nfcall(tmp.dir.bind(tmp)).get(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
list: listFiles,
|
||||||
|
stat: Q.denodeify(fs.stat),
|
||||||
|
readdir: Q.denodeify(fs.readdir),
|
||||||
|
readFile: Q.denodeify(fs.readFile),
|
||||||
|
writeFile: writeFile,
|
||||||
|
writeStream: writeStream,
|
||||||
|
mkdirp: Q.denodeify(fsExtra.mkdirp),
|
||||||
|
copy: Q.denodeify(fsExtra.copy),
|
||||||
|
remove: Q.denodeify(fsExtra.remove),
|
||||||
|
symlink: Q.denodeify(fsExtra.symlink),
|
||||||
|
exists: function(path) {
|
||||||
|
var d = Q.defer();
|
||||||
|
fs.exists(path, d.resolve);
|
||||||
|
return d.promise;
|
||||||
|
},
|
||||||
|
findFile: findFile,
|
||||||
|
existsSync: fs.existsSync.bind(fs),
|
||||||
|
readFileSync: fs.readFileSync.bind(fs),
|
||||||
|
clean: cleanFolder,
|
||||||
|
getUniqueFilename: getUniqueFilename
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write a file
|
||||||
|
function writeFile(filename, data, options) {
|
||||||
|
var d = Q.defer();
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(filename, data, options);
|
||||||
|
} catch(err) {
|
||||||
|
d.reject(err);
|
||||||
|
}
|
||||||
|
d.resolve();
|
||||||
|
|
||||||
|
|
||||||
|
return d.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a stream to a file
|
||||||
|
function writeStream(filename, st) {
|
||||||
|
var d = Q.defer();
|
||||||
|
|
||||||
|
var wstream = fs.createWriteStream(filename);
|
||||||
|
|
||||||
|
wstream.on('finish', function () {
|
||||||
|
d.resolve();
|
||||||
|
});
|
||||||
|
wstream.on('error', function (err) {
|
||||||
|
d.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
st.on('error', function(err) {
|
||||||
|
d.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
st.pipe(wstream);
|
||||||
|
|
||||||
|
return d.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a filename available
|
||||||
|
function getUniqueFilename(base, filename) {
|
||||||
|
if (!filename) {
|
||||||
|
filename = base;
|
||||||
|
base = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = path.resolve(base, filename);
|
||||||
|
var ext = path.extname(filename);
|
||||||
|
filename = path.join(path.dirname(filename), path.basename(filename, ext));
|
||||||
|
|
||||||
|
var _filename = filename+ext;
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (fs.existsSync(filename)) {
|
||||||
|
_filename = filename+'_'+i+ext;
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.relative(base, _filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// List files in a directory
|
||||||
|
function listFiles(root, options) {
|
||||||
|
options = _.defaults(options || {}, {
|
||||||
|
ignoreFiles: [],
|
||||||
|
ignoreRules: []
|
||||||
|
});
|
||||||
|
|
||||||
|
var d = Q.defer();
|
||||||
|
|
||||||
|
// Our list of files
|
||||||
|
var files = [];
|
||||||
|
|
||||||
|
var ig = Ignore({
|
||||||
|
path: root,
|
||||||
|
ignoreFiles: options.ignoreFiles
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add extra rules to ignore common folders
|
||||||
|
ig.addIgnoreRules(options.ignoreRules, '__custom_stuff');
|
||||||
|
|
||||||
|
// Push each file to our list
|
||||||
|
ig.on('child', function (c) {
|
||||||
|
files.push(
|
||||||
|
c.path.substr(c.root.path.length + 1) + (c.props.Directory === true ? '/' : '')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ig.on('end', function() {
|
||||||
|
// Normalize paths on Windows
|
||||||
|
if(process.platform === 'win32') {
|
||||||
|
return d.resolve(files.map(function(file) {
|
||||||
|
return file.replace(/\\/g, '/');
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simply return paths otherwise
|
||||||
|
return d.resolve(files);
|
||||||
|
});
|
||||||
|
|
||||||
|
ig.on('error', d.reject);
|
||||||
|
|
||||||
|
return d.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean a folder without removing .git and .svn
|
||||||
|
// Creates it if non existant
|
||||||
|
function cleanFolder(root) {
|
||||||
|
if (!fs.existsSync(root)) return fsUtils.mkdirp(root);
|
||||||
|
|
||||||
|
return listFiles(root, {
|
||||||
|
ignoreFiles: [],
|
||||||
|
ignoreRules: [
|
||||||
|
// Skip Git and SVN stuff
|
||||||
|
'.git/',
|
||||||
|
'.svn/'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.then(function(files) {
|
||||||
|
var d = Q.defer();
|
||||||
|
|
||||||
|
_.reduce(files, function(prev, file, i) {
|
||||||
|
return prev.then(function() {
|
||||||
|
var _file = path.join(root, file);
|
||||||
|
|
||||||
|
d.notify({
|
||||||
|
i: i+1,
|
||||||
|
count: files.length,
|
||||||
|
file: _file
|
||||||
|
});
|
||||||
|
return fsUtils.remove(_file);
|
||||||
|
});
|
||||||
|
}, Q())
|
||||||
|
.then(function() {
|
||||||
|
d.resolve();
|
||||||
|
}, function(err) {
|
||||||
|
d.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return d.promise;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a file in a folder (case incensitive)
|
||||||
|
// Return the real filename
|
||||||
|
function findFile(root, filename) {
|
||||||
|
return Q.nfcall(fs.readdir, root)
|
||||||
|
.then(function(files) {
|
||||||
|
return _.find(files, function(file) {
|
||||||
|
return (file.toLowerCase() == filename.toLowerCase());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = fsUtils;
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
var Q = require('q');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var path = require('path');
|
||||||
|
var crc = require('crc');
|
||||||
|
var exec = Q.denodeify(require('child_process').exec);
|
||||||
|
var URI = require('urijs');
|
||||||
|
var pathUtil = require('./path');
|
||||||
|
|
||||||
|
var fs = require('./fs');
|
||||||
|
|
||||||
|
var GIT_PREFIX = 'git+';
|
||||||
|
var GIT_TMP = null;
|
||||||
|
|
||||||
|
|
||||||
|
// Check if an url is a git dependency url
|
||||||
|
function checkGitUrl(giturl) {
|
||||||
|
return (giturl.indexOf(GIT_PREFIX) === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates a SHA in hexadecimal
|
||||||
|
function validateSha(str) {
|
||||||
|
return (/[0-9a-f]{40}/).test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and extract infos
|
||||||
|
function parseGitUrl(giturl) {
|
||||||
|
var ref, uri, fileParts, filepath;
|
||||||
|
|
||||||
|
if (!checkGitUrl(giturl)) return null;
|
||||||
|
giturl = giturl.slice(GIT_PREFIX.length);
|
||||||
|
|
||||||
|
uri = new URI(giturl);
|
||||||
|
ref = uri.fragment() || 'master';
|
||||||
|
uri.fragment(null);
|
||||||
|
|
||||||
|
// Extract file inside the repo (after the .git)
|
||||||
|
fileParts =uri.path().split('.git');
|
||||||
|
filepath = fileParts.length > 1? fileParts.slice(1).join('.git') : '';
|
||||||
|
if (filepath[0] == '/') filepath = filepath.slice(1);
|
||||||
|
|
||||||
|
// Recreate pathname without the real filename
|
||||||
|
uri.path(_.first(fileParts)+'.git');
|
||||||
|
|
||||||
|
return {
|
||||||
|
host: uri.toString(),
|
||||||
|
ref: ref || 'master',
|
||||||
|
filepath: filepath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone a git repo from a specific ref
|
||||||
|
function cloneGitRepo(host, ref) {
|
||||||
|
var isBranch = false;
|
||||||
|
|
||||||
|
ref = ref || 'master';
|
||||||
|
if (!validateSha(ref)) isBranch = true;
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
|
||||||
|
// Create temporary folder to store git repos
|
||||||
|
.then(function() {
|
||||||
|
if (GIT_TMP) return;
|
||||||
|
return fs.tmp.dir()
|
||||||
|
.then(function(_tmp) {
|
||||||
|
GIT_TMP = _tmp;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// Return or clone the git repo
|
||||||
|
.then(function() {
|
||||||
|
// Unique ID for repo/ref combinaison
|
||||||
|
var repoId = crc.crc32(host+'#'+ref).toString(16);
|
||||||
|
|
||||||
|
// Absolute path to the folder
|
||||||
|
var repoPath = path.resolve(GIT_TMP, repoId);
|
||||||
|
|
||||||
|
return fs.exists(repoPath)
|
||||||
|
.then(function(doExists) {
|
||||||
|
if (doExists) return;
|
||||||
|
|
||||||
|
// Clone repo
|
||||||
|
return exec('git clone '+host+' '+repoPath)
|
||||||
|
.then(function() {
|
||||||
|
return exec('git checkout '+ref, { cwd: repoPath });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.thenResolve(repoPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file from a git repo
|
||||||
|
function resolveFileFromGit(giturl) {
|
||||||
|
if (_.isString(giturl)) giturl = parseGitUrl(giturl);
|
||||||
|
if (!giturl) return Q(null);
|
||||||
|
|
||||||
|
// Clone or get from cache
|
||||||
|
return cloneGitRepo(giturl.host, giturl.ref)
|
||||||
|
.then(function(repo) {
|
||||||
|
|
||||||
|
// Resolve relative path
|
||||||
|
return path.resolve(repo, giturl.filepath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return root of git repo from a filepath
|
||||||
|
function resolveGitRoot(filepath) {
|
||||||
|
var relativeToGit, repoId;
|
||||||
|
|
||||||
|
// No git repo cloned, or file is not in a git repository
|
||||||
|
if (!GIT_TMP || !pathUtil.isInRoot(GIT_TMP, filepath)) return null;
|
||||||
|
|
||||||
|
// Extract first directory (is the repo id)
|
||||||
|
relativeToGit = path.relative(GIT_TMP, filepath);
|
||||||
|
repoId = _.first(relativeToGit.split(path.sep));
|
||||||
|
if (!repoId) return;
|
||||||
|
|
||||||
|
// Return an absolute file
|
||||||
|
return path.resolve(GIT_TMP, repoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
checkUrl: checkGitUrl,
|
||||||
|
parseUrl: parseGitUrl,
|
||||||
|
resolveFile: resolveFileFromGit,
|
||||||
|
resolveRoot: resolveGitRoot
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var path = require('path');
|
||||||
|
var fs = require('fs');
|
||||||
|
|
||||||
|
var i18n = require('i18n');
|
||||||
|
|
||||||
|
var I18N_PATH = path.resolve(__dirname, '../../theme/i18n/');
|
||||||
|
var DEFAULT_LANGUAGE = 'en';
|
||||||
|
var LOCALES = _.map(fs.readdirSync(I18N_PATH), function(lang) {
|
||||||
|
return path.basename(lang, '.json');
|
||||||
|
});
|
||||||
|
|
||||||
|
i18n.configure({
|
||||||
|
locales: LOCALES,
|
||||||
|
directory: I18N_PATH,
|
||||||
|
defaultLocale: DEFAULT_LANGUAGE,
|
||||||
|
updateFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
function compareLocales(lang, locale) {
|
||||||
|
var langMain = _.first(lang.split('-'));
|
||||||
|
var langSecond = _.last(lang.split('-'));
|
||||||
|
|
||||||
|
var localeMain = _.first(locale.split('-'));
|
||||||
|
var localeSecond = _.last(locale.split('-'));
|
||||||
|
|
||||||
|
if (locale == lang) return 100;
|
||||||
|
if (localeMain == langMain) return 50;
|
||||||
|
if (localeSecond == langSecond) return 20;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizeLanguage = _.memoize(function(lang) {
|
||||||
|
var language = _.chain(LOCALES)
|
||||||
|
.values()
|
||||||
|
.map(function(locale) {
|
||||||
|
return {
|
||||||
|
locale: locale,
|
||||||
|
score: compareLocales(lang, locale)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(function(lang) {
|
||||||
|
return lang.score > 0;
|
||||||
|
})
|
||||||
|
.sortBy('score')
|
||||||
|
.pluck('locale')
|
||||||
|
.last()
|
||||||
|
.value();
|
||||||
|
return language || lang;
|
||||||
|
});
|
||||||
|
|
||||||
|
function translate(locale, phrase) {
|
||||||
|
var args = Array.prototype.slice.call(arguments, 2);
|
||||||
|
|
||||||
|
return i18n.__.apply({}, [{
|
||||||
|
locale: locale,
|
||||||
|
phrase: phrase
|
||||||
|
}].concat(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCatalog(locale) {
|
||||||
|
locale = normalizeLanguage(locale);
|
||||||
|
return i18n.getCatalog(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocales() {
|
||||||
|
return LOCALES;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasLocale(locale) {
|
||||||
|
return _.contains(LOCALES, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
__: translate,
|
||||||
|
normalizeLanguage: normalizeLanguage,
|
||||||
|
getCatalog: getCatalog,
|
||||||
|
getLocales: getLocales,
|
||||||
|
hasLocale: hasLocale
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
var _ = require("lodash");
|
||||||
|
var Q = require("q");
|
||||||
|
var fs = require("./fs");
|
||||||
|
var spawn = require("spawn-cmd").spawn;
|
||||||
|
|
||||||
|
// Convert a svg file
|
||||||
|
var convertSVG = function(source, dest, options) {
|
||||||
|
if (!fs.existsSync(source)) return Q.reject(new Error("File doesn't exist: "+source));
|
||||||
|
var d = Q.defer();
|
||||||
|
|
||||||
|
options = _.defaults(options || {}, {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
//var command = shellescape(["svgexport", source, dest]);
|
||||||
|
var child = spawn("svgexport", [source, dest]);
|
||||||
|
|
||||||
|
child.on("error", function(error) {
|
||||||
|
if (error.code == "ENOENT") error = new Error("Need to install \"svgexport\" using \"npm install svgexport -g\"");
|
||||||
|
return d.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", function(code) {
|
||||||
|
if (code === 0 && fs.existsSync(dest)) {
|
||||||
|
d.resolve();
|
||||||
|
} else {
|
||||||
|
d.reject(new Error("Error converting "+source+" into "+dest));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return d.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
convertSVG: convertSVG,
|
||||||
|
INVALID: [".svg"]
|
||||||
|
};
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
lang: require('./lang'),
|
|
||||||
links: require('./links')
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
var MAP = {
|
|
||||||
'py': 'python',
|
|
||||||
'js': 'javascript',
|
|
||||||
'rb': 'ruby',
|
|
||||||
'csharp': 'cs',
|
|
||||||
};
|
|
||||||
|
|
||||||
function normalize(lang) {
|
|
||||||
if(!lang) { return null; }
|
|
||||||
|
|
||||||
var lower = lang.toLowerCase();
|
|
||||||
return MAP[lower] || lower;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exports
|
|
||||||
module.exports = {
|
|
||||||
normalize: normalize,
|
|
||||||
MAP: MAP
|
|
||||||
};
|
|
||||||
+39
-17
@@ -1,5 +1,5 @@
|
|||||||
var url = require('url');
|
var url = require("url");
|
||||||
var path = require('path');
|
var path = require("path");
|
||||||
|
|
||||||
// Is the link an external link
|
// Is the link an external link
|
||||||
var isExternal = function(href) {
|
var isExternal = function(href) {
|
||||||
@@ -15,46 +15,68 @@ var isRelative = function(href) {
|
|||||||
try {
|
try {
|
||||||
var parsed = url.parse(href);
|
var parsed = url.parse(href);
|
||||||
|
|
||||||
return !parsed.protocol && parsed.path && parsed.path[0] != '/';
|
return !!(!parsed.protocol && parsed.path);
|
||||||
} catch(err) {}
|
} catch(err) {}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Return true if the link is an achor
|
||||||
|
var isAnchor = function(href) {
|
||||||
|
try {
|
||||||
|
var parsed = url.parse(href);
|
||||||
|
return !!(!parsed.protocol && !parsed.path && parsed.hash);
|
||||||
|
} catch(err) {}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize a path to be a link
|
||||||
|
var normalizeLink = function(s) {
|
||||||
|
return s.replace(/\\/g, "/");
|
||||||
|
};
|
||||||
|
|
||||||
// Relative to absolute path
|
// Relative to absolute path
|
||||||
// dir: directory parent of the file currently in rendering process
|
// dir: directory parent of the file currently in rendering process
|
||||||
// outdir: directory parent from the html output
|
// outdir: directory parent from the html output
|
||||||
|
|
||||||
var toAbsolute = function(_href, dir, outdir) {
|
var toAbsolute = function(_href, dir, outdir) {
|
||||||
// Absolute file in source
|
if (isExternal(_href)) return _href;
|
||||||
_href = path.join(dir, _href);
|
|
||||||
|
|
||||||
// make it relative to output
|
// Path "_href" inside the base folder
|
||||||
_href = path.relative(outdir, _href);
|
var hrefInRoot = path.normalize(path.join(dir, _href));
|
||||||
|
if (_href[0] == "/") hrefInRoot = path.normalize(_href.slice(1));
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
// Make it relative to output
|
||||||
_href = _href.replace(/\\/g, '/');
|
_href = path.relative(outdir, hrefInRoot);
|
||||||
}
|
|
||||||
|
// Normalize windows paths
|
||||||
|
_href = normalizeLink(_href);
|
||||||
|
|
||||||
return _href;
|
return _href;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Join links
|
// Join links
|
||||||
|
|
||||||
var join = function() {
|
var join = function() {
|
||||||
var _href = path.join.apply(path, arguments);
|
var _href = path.join.apply(path, arguments);
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
return normalizeLink(_href);
|
||||||
_href = _href.replace(/\\/g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return _href;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Change extension
|
||||||
|
var changeExtension = function(filename, newext) {
|
||||||
|
return path.join(
|
||||||
|
path.dirname(filename),
|
||||||
|
path.basename(filename, path.extname(filename))+newext
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
isAnchor: isAnchor,
|
||||||
isRelative: isRelative,
|
isRelative: isRelative,
|
||||||
isExternal: isExternal,
|
isExternal: isExternal,
|
||||||
toAbsolute: toAbsolute,
|
toAbsolute: toAbsolute,
|
||||||
join: join
|
join: join,
|
||||||
|
changeExtension: changeExtension,
|
||||||
|
normalize: normalizeLink
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
var _ = require("lodash");
|
||||||
|
var util = require("util");
|
||||||
|
var color = require("bash-color");
|
||||||
|
|
||||||
|
var LEVELS = {
|
||||||
|
DEBUG: 0,
|
||||||
|
INFO: 1,
|
||||||
|
WARN: 2,
|
||||||
|
ERROR: 3,
|
||||||
|
DISABLED: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
var COLORS = {
|
||||||
|
DEBUG: color.purple,
|
||||||
|
INFO: color.cyan,
|
||||||
|
WARN: color.yellow,
|
||||||
|
ERROR: color.red
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = function(_write, logLevel) {
|
||||||
|
var logger = {};
|
||||||
|
var lastChar = "\n";
|
||||||
|
if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
|
||||||
|
|
||||||
|
// Write a simple message
|
||||||
|
logger.write = function(msg) {
|
||||||
|
msg = msg.toString();
|
||||||
|
lastChar = _.last(msg);
|
||||||
|
return _write(msg);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format a message
|
||||||
|
logger.format = function() {
|
||||||
|
return util.format.apply(util, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write a line
|
||||||
|
logger.writeLn = function(msg) {
|
||||||
|
return this.write((msg || "")+"\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write a message with a certain level
|
||||||
|
logger.log = function(level) {
|
||||||
|
if (level < logLevel) return;
|
||||||
|
|
||||||
|
var levelKey = _.findKey(LEVELS, function(v) { return v == level; });
|
||||||
|
var args = Array.prototype.slice.apply(arguments, [1]);
|
||||||
|
var msg = logger.format.apply(logger, args);
|
||||||
|
|
||||||
|
if (lastChar == "\n") {
|
||||||
|
msg = COLORS[levelKey](levelKey.toLowerCase()+":")+" "+msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return logger.write(msg);
|
||||||
|
};
|
||||||
|
logger.logLn = function() {
|
||||||
|
if (lastChar != "\n") logger.write("\n");
|
||||||
|
|
||||||
|
var args = Array.prototype.slice.apply(arguments);
|
||||||
|
args.push("\n");
|
||||||
|
logger.log.apply(logger, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write a OK
|
||||||
|
logger.ok = function(level) {
|
||||||
|
var args = Array.prototype.slice.apply(arguments, [1]);
|
||||||
|
var msg = logger.format.apply(logger, args);
|
||||||
|
if (arguments.length > 1) {
|
||||||
|
logger.logLn(level, color.green(">> ") + msg.trim().replace(/\n/g, color.green("\n>> ")));
|
||||||
|
} else {
|
||||||
|
logger.log(level, color.green("OK"), "\n");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write an "FAIL"
|
||||||
|
logger.fail = function(level) {
|
||||||
|
return logger.log(level, color.red("ERROR")+"\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
_.each(_.omit(LEVELS, "DISABLED"), function(level, levelKey) {
|
||||||
|
levelKey = levelKey.toLowerCase();
|
||||||
|
|
||||||
|
logger[levelKey] = _.partial(logger.log, level);
|
||||||
|
logger[levelKey].ln = _.partial(logger.logLn, level);
|
||||||
|
logger[levelKey].ok = _.partial(logger.ok, level);
|
||||||
|
logger[levelKey].fail = _.partial(logger.fail, level);
|
||||||
|
logger[levelKey].promise = function(p) {
|
||||||
|
return p.
|
||||||
|
then(function(st) {
|
||||||
|
logger[levelKey].ok();
|
||||||
|
return st;
|
||||||
|
}, function(err) {
|
||||||
|
logger[levelKey].fail();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return logger;
|
||||||
|
};
|
||||||
|
module.exports.LEVELS = LEVELS;
|
||||||
|
module.exports.COLORS = COLORS;
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
var _ = require('lodash');
|
var _ = require("lodash");
|
||||||
|
|
||||||
// Cleans up an article/chapter object
|
// Cleans up an article/chapter object
|
||||||
// remove 'articles' attributes
|
// remove "articles" attributes
|
||||||
function clean(obj) {
|
function clean(obj) {
|
||||||
return obj && _.omit(obj, ['articles']);
|
return obj && _.omit(obj, ["articles"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenChapters(chapters) {
|
function flattenChapters(chapters) {
|
||||||
@@ -27,27 +27,42 @@ function navigation(summary, files) {
|
|||||||
files = _.isArray(files) ? files : (_.isString(files) ? [files] : null);
|
files = _.isArray(files) ? files : (_.isString(files) ? [files] : null);
|
||||||
|
|
||||||
// List of all navNodes
|
// List of all navNodes
|
||||||
// Flatten chapters, then add in default README node if ndeeded etc ...
|
// Flatten chapters
|
||||||
var navNodes = flattenChapters(summary.chapters);
|
var navNodes = flattenChapters(summary.chapters);
|
||||||
var prevNodes = [null].concat(navNodes.slice(0, -1));
|
|
||||||
var nextNodes = navNodes.slice(1).concat([null]);
|
|
||||||
|
|
||||||
// Mapping of prev/next for a give path
|
// Mapping of prev/next for a give path
|
||||||
var mapping = _.chain(_.zip(navNodes, prevNodes, nextNodes))
|
var mapping = _.chain(navNodes)
|
||||||
.map(function(nodes) {
|
.map(function(current, i) {
|
||||||
var current = nodes[0], prev = nodes[1], next = nodes[2];
|
var prev = null, next = null;
|
||||||
|
|
||||||
// Skip if no path
|
// Skip if no path
|
||||||
if(!current.path) return null;
|
if(!current.exists) return null;
|
||||||
|
|
||||||
|
// Find prev
|
||||||
|
prev = _.chain(navNodes.slice(0, i))
|
||||||
|
.reverse()
|
||||||
|
.find(function(node) {
|
||||||
|
return node.exists && !node.external;
|
||||||
|
})
|
||||||
|
.value();
|
||||||
|
|
||||||
|
// Find next
|
||||||
|
next = _.chain(navNodes.slice(i+1))
|
||||||
|
.find(function(node) {
|
||||||
|
return node.exists && !node.external;
|
||||||
|
})
|
||||||
|
.value();
|
||||||
|
|
||||||
return [current.path, {
|
return [current.path, {
|
||||||
|
index: i,
|
||||||
title: current.title,
|
title: current.title,
|
||||||
|
introduction: current.introduction,
|
||||||
prev: prev,
|
prev: prev,
|
||||||
next: next,
|
next: next,
|
||||||
level: current.level,
|
level: current.level,
|
||||||
}];
|
}];
|
||||||
})
|
})
|
||||||
.filter()
|
.compact()
|
||||||
.object()
|
.object()
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
var Q = require('q');
|
||||||
|
var _ = require('lodash');
|
||||||
|
var url = require('url');
|
||||||
|
var path = require('path');
|
||||||
|
var cheerio = require('cheerio');
|
||||||
|
var domSerializer = require('dom-serializer');
|
||||||
|
var request = require('request');
|
||||||
|
var crc = require('crc');
|
||||||
|
var slug = require('github-slugid');
|
||||||
|
|
||||||
|
var links = require('./links');
|
||||||
|
var imgUtils = require('./images');
|
||||||
|
var fs = require('./fs');
|
||||||
|
var batch = require('./batch');
|
||||||
|
|
||||||
|
var parsableExtensions = require('gitbook-parsers').extensions;
|
||||||
|
|
||||||
|
// Map of images that have been converted
|
||||||
|
var imgConversionCache = {};
|
||||||
|
|
||||||
|
// Render a cheerio dom as html
|
||||||
|
function renderDom($, dom, options) {
|
||||||
|
if (!dom && $._root && $._root.children) {
|
||||||
|
dom = $._root.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
options = options|| dom.options || $._options;
|
||||||
|
return domSerializer(dom, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceText($, el, search, replace, text_only ) {
|
||||||
|
return $(el).each(function(){
|
||||||
|
var node = this.firstChild,
|
||||||
|
val,
|
||||||
|
new_val,
|
||||||
|
|
||||||
|
// Elements to be removed at the end.
|
||||||
|
remove = [];
|
||||||
|
|
||||||
|
// Only continue if firstChild exists.
|
||||||
|
if ( node ) {
|
||||||
|
|
||||||
|
// Loop over all childNodes.
|
||||||
|
while (node) {
|
||||||
|
|
||||||
|
// Only process text nodes.
|
||||||
|
if ( node.nodeType === 3 ) {
|
||||||
|
|
||||||
|
// The original node value.
|
||||||
|
val = node.nodeValue;
|
||||||
|
|
||||||
|
// The new value.
|
||||||
|
new_val = val.replace( search, replace );
|
||||||
|
|
||||||
|
// Only replace text if the new value is actually different!
|
||||||
|
if ( new_val !== val ) {
|
||||||
|
|
||||||
|
if ( !text_only && /</.test( new_val ) ) {
|
||||||
|
// The new value contains HTML, set it in a slower but far more
|
||||||
|
// robust way.
|
||||||
|
$(node).before( new_val );
|
||||||
|
|
||||||
|
// Don't remove the node yet, or the loop will lose its place.
|
||||||
|
remove.push( node );
|
||||||
|
} else {
|
||||||
|
// The new value contains no HTML, so it can be set in this
|
||||||
|
// very fast, simple way.
|
||||||
|
node.nodeValue = new_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node = node.nextSibling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time to remove those elements!
|
||||||
|
if (remove.length) $(remove).remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pregQuote( str ) {
|
||||||
|
return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Adapt an html snippet to be relative to a base folder
|
||||||
|
function normalizeHtml(src, options) {
|
||||||
|
var $ = cheerio.load(src, {
|
||||||
|
// We should parse html without trying to normalize too much
|
||||||
|
xmlMode: false,
|
||||||
|
|
||||||
|
// SVG need some attributes to use uppercases
|
||||||
|
lowerCaseAttributeNames: false,
|
||||||
|
lowerCaseTags: false
|
||||||
|
});
|
||||||
|
var toConvert = [];
|
||||||
|
var svgContent = {};
|
||||||
|
var outputRoot = options.book.options.output;
|
||||||
|
|
||||||
|
imgConversionCache[outputRoot] = imgConversionCache[outputRoot] || {};
|
||||||
|
|
||||||
|
// Find svg images to extract and process
|
||||||
|
if (options.convertImages) {
|
||||||
|
$('svg').each(function() {
|
||||||
|
var content = renderDom($, $(this));
|
||||||
|
var svgId = _.uniqueId('svg');
|
||||||
|
var dest = svgId+'.svg';
|
||||||
|
|
||||||
|
// Generate filename
|
||||||
|
dest = '/'+fs.getUniqueFilename(outputRoot, dest);
|
||||||
|
|
||||||
|
svgContent[dest] = '<?xml version="1.0" encoding="UTF-8"?>'+content;
|
||||||
|
$(this).replaceWith($('<img>').attr('src', dest));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate ID for headings
|
||||||
|
$('h1,h2,h3,h4,h5,h6').each(function() {
|
||||||
|
if ($(this).attr('id')) return;
|
||||||
|
|
||||||
|
$(this).attr('id', slug($(this).text()));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find images to normalize
|
||||||
|
$('img').each(function() {
|
||||||
|
var origin;
|
||||||
|
var src = $(this).attr('src');
|
||||||
|
|
||||||
|
if (!src) return;
|
||||||
|
var isExternal = links.isExternal(src);
|
||||||
|
|
||||||
|
// Transform as relative to the bases
|
||||||
|
if (links.isRelative(src)) {
|
||||||
|
src = links.toAbsolute(src, options.base, options.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert if needed
|
||||||
|
if (options.convertImages) {
|
||||||
|
// If image is external and ebook, then downlaod the images
|
||||||
|
if (isExternal) {
|
||||||
|
origin = src;
|
||||||
|
src = '/'+crc.crc32(origin).toString(16)+path.extname(url.parse(origin).pathname);
|
||||||
|
src = links.toAbsolute(src, options.base, options.output);
|
||||||
|
isExternal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ext = path.extname(src);
|
||||||
|
var srcAbs = links.join('/', options.base, src);
|
||||||
|
|
||||||
|
// Test image extension
|
||||||
|
if (_.contains(imgUtils.INVALID, ext)) {
|
||||||
|
if (imgConversionCache[outputRoot][srcAbs]) {
|
||||||
|
// Already converted
|
||||||
|
src = imgConversionCache[outputRoot][srcAbs];
|
||||||
|
} else {
|
||||||
|
// Not converted yet
|
||||||
|
var dest = '';
|
||||||
|
|
||||||
|
// Replace extension
|
||||||
|
dest = links.join(path.dirname(srcAbs), path.basename(srcAbs, ext)+'.png');
|
||||||
|
dest = dest[0] == '/'? dest.slice(1) : dest;
|
||||||
|
|
||||||
|
// Get a name that doesn't exists
|
||||||
|
dest = fs.getUniqueFilename(outputRoot, dest);
|
||||||
|
|
||||||
|
options.book.log.debug.ln('detect invalid image (will be converted to png):', srcAbs);
|
||||||
|
|
||||||
|
// Add to cache
|
||||||
|
imgConversionCache[outputRoot][srcAbs] = '/'+dest;
|
||||||
|
|
||||||
|
// Push to convert
|
||||||
|
toConvert.push({
|
||||||
|
origin: origin,
|
||||||
|
content: svgContent[srcAbs],
|
||||||
|
source: isExternal? srcAbs : path.join('./', srcAbs),
|
||||||
|
dest: path.join('./', dest)
|
||||||
|
});
|
||||||
|
|
||||||
|
src = links.join('/', dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset as relative to output
|
||||||
|
src = links.toAbsolute(src, options.base, options.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (origin) {
|
||||||
|
// Need to downlaod image
|
||||||
|
toConvert.push({
|
||||||
|
origin: origin,
|
||||||
|
source: path.join('./', srcAbs)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).attr('src', src);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Normalize links
|
||||||
|
$('a').each(function() {
|
||||||
|
var href = $(this).attr('href');
|
||||||
|
if (!href) return;
|
||||||
|
|
||||||
|
if (links.isAnchor(href)) {
|
||||||
|
// Keep it as it is
|
||||||
|
} else if (links.isRelative(href)) {
|
||||||
|
var parts = url.parse(href);
|
||||||
|
var absolutePath = links.join(options.base, parts.pathname);
|
||||||
|
var anchor = parts.hash || '';
|
||||||
|
|
||||||
|
|
||||||
|
// If is in navigation relative: transform as content
|
||||||
|
if (options.navigation[absolutePath]) {
|
||||||
|
absolutePath = options.book.contentLink(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If md/adoc/rst files is not in summary
|
||||||
|
// or for ebook, signal all files that are outside the summary
|
||||||
|
else if (_.contains(parsableExtensions, path.extname(absolutePath)) ||
|
||||||
|
_.contains(['epub', 'pdf', 'mobi'], options.book.options.generator)) {
|
||||||
|
options.book.log.warn.ln('page', options.input, 'contains an hyperlink to resource outside spine \''+href+'\'');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform as absolute
|
||||||
|
href = links.toAbsolute('/'+absolutePath, options.base, options.output)+anchor;
|
||||||
|
} else {
|
||||||
|
// External links
|
||||||
|
$(this).attr('target', '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform extension
|
||||||
|
$(this).attr('href', href);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Highlight code blocks
|
||||||
|
$('code').each(function() {
|
||||||
|
// Normalize language
|
||||||
|
var lang = _.chain(
|
||||||
|
($(this).attr('class') || '').split(' ')
|
||||||
|
)
|
||||||
|
.map(function(cl) {
|
||||||
|
// Markdown
|
||||||
|
if (cl.search('lang-') === 0) return cl.slice('lang-'.length);
|
||||||
|
|
||||||
|
// Asciidoc
|
||||||
|
if (cl.search('language-') === 0) return cl.slice('language-'.length);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.compact()
|
||||||
|
.first()
|
||||||
|
.value();
|
||||||
|
|
||||||
|
var source = $(this).text();
|
||||||
|
var blk = options.book.template.applyBlock('code', {
|
||||||
|
body: source,
|
||||||
|
kwargs: {
|
||||||
|
language: lang
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blk.html === false) $(this).text(blk.body);
|
||||||
|
else $(this).html(blk.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace glossary terms
|
||||||
|
var glossary = _.sortBy(options.glossary, function(term) {
|
||||||
|
return -term.name.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
_.each(glossary, function(term) {
|
||||||
|
var r = new RegExp( '\\b(' + pregQuote(term.name.toLowerCase()) + ')\\b' , 'gi' );
|
||||||
|
var includedInFiles = false;
|
||||||
|
|
||||||
|
$('*').each(function() {
|
||||||
|
// Ignore codeblocks
|
||||||
|
if (_.contains(['code', 'pre', 'a'], this.name.toLowerCase())) return;
|
||||||
|
|
||||||
|
replaceText($, this, r, function(match) {
|
||||||
|
// Add to files index in glossary
|
||||||
|
if (!includedInFiles) {
|
||||||
|
includedInFiles = true;
|
||||||
|
term.files = term.files || [];
|
||||||
|
term.files.push(options.navigation[options.input]);
|
||||||
|
}
|
||||||
|
return '<a href=\''+links.toAbsolute('/GLOSSARY.html', options.base, options.output) + '#' + term.id+'\' class=\'glossary-term\' title=\''+_.escape(term.description)+'\'>'+match+'</a>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
html: renderDom($),
|
||||||
|
images: toConvert
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert svg images to png
|
||||||
|
function convertImages(images, options) {
|
||||||
|
if (!options.convertImages) return Q();
|
||||||
|
|
||||||
|
var downloaded = [];
|
||||||
|
options.book.log.debug.ln('convert ', images.length, 'images to png');
|
||||||
|
|
||||||
|
return batch.execEach(images, {
|
||||||
|
max: 100,
|
||||||
|
fn: function(image) {
|
||||||
|
var imgin = path.resolve(options.book.options.output, image.source);
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
|
||||||
|
// Write image if need to be download
|
||||||
|
.then(function() {
|
||||||
|
if (!image.origin && !_.contains(downloaded, image.origin)) return;
|
||||||
|
options.book.log.debug('download image', image.origin, '...');
|
||||||
|
downloaded.push(image.origin);
|
||||||
|
return options.book.log.debug.promise(fs.writeStream(imgin, request(image.origin)))
|
||||||
|
.fail(function(err) {
|
||||||
|
if (!_.isError(err)) err = new Error(err);
|
||||||
|
|
||||||
|
err.message = 'Fail downloading '+image.origin+': '+err.message;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
// Write svg if content
|
||||||
|
.then(function() {
|
||||||
|
if (!image.content) return;
|
||||||
|
return fs.writeFile(imgin, image.content);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert
|
||||||
|
.then(function() {
|
||||||
|
if (!image.dest) return;
|
||||||
|
var imgout = path.resolve(options.book.options.output, image.dest);
|
||||||
|
options.book.log.debug('convert image', image.source, 'to', image.dest, '...');
|
||||||
|
return options.book.log.debug.promise(imgUtils.convertSVG(imgin, imgout));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
options.book.log.debug.ok(images.length+' images converted with success');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapt page content to be relative to a base folder
|
||||||
|
function normalizePage(sections, options) {
|
||||||
|
options = _.defaults(options || {}, {
|
||||||
|
// Current book
|
||||||
|
book: null,
|
||||||
|
|
||||||
|
// Do we need to convert svg?
|
||||||
|
convertImages: false,
|
||||||
|
|
||||||
|
// Current file path
|
||||||
|
input: '.',
|
||||||
|
|
||||||
|
// Navigation to use to transform path
|
||||||
|
navigation: {},
|
||||||
|
|
||||||
|
// Directory parent of the file currently in rendering process
|
||||||
|
base: './',
|
||||||
|
|
||||||
|
// Directory parent from the html output
|
||||||
|
output: './',
|
||||||
|
|
||||||
|
// Glossary terms
|
||||||
|
glossary: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// List of images to convert
|
||||||
|
var toConvert = [];
|
||||||
|
|
||||||
|
sections = _.map(sections, function(section) {
|
||||||
|
if (section.type != 'normal') return section;
|
||||||
|
|
||||||
|
var out = normalizeHtml(section.content, options);
|
||||||
|
|
||||||
|
toConvert = toConvert.concat(out.images);
|
||||||
|
section.content = out.html;
|
||||||
|
return section;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Q()
|
||||||
|
.then(function() {
|
||||||
|
toConvert = _.uniq(toConvert, 'source');
|
||||||
|
return convertImages(toConvert, options);
|
||||||
|
})
|
||||||
|
.thenResolve(sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
normalize: normalizePage
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
var _ = require("lodash");
|
||||||
|
var path = require("path");
|
||||||
|
|
||||||
|
// Return true if file path is inside a folder
|
||||||
|
function isInRoot(root, filename) {
|
||||||
|
filename = path.normalize(filename);
|
||||||
|
return (filename.substr(0, root.length) === root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve paths in a specific folder
|
||||||
|
// Throw error if file is outside this folder
|
||||||
|
function resolveInRoot(root) {
|
||||||
|
var input, result, err;
|
||||||
|
|
||||||
|
input = _.chain(arguments)
|
||||||
|
.toArray()
|
||||||
|
.slice(1)
|
||||||
|
.reduce(function(current, p) {
|
||||||
|
// Handle path relative to book root ("/README.md")
|
||||||
|
if (p[0] == "/" || p[0] == "\\") return p.slice(1);
|
||||||
|
|
||||||
|
return current? path.join(current, p) : path.normalize(p);
|
||||||
|
}, "")
|
||||||
|
.value();
|
||||||
|
|
||||||
|
result = path.resolve(root, input);
|
||||||
|
|
||||||
|
if (!isInRoot(root, result)) {
|
||||||
|
err = new Error("EACCESS: \"" + result + "\" not in \"" + root + "\"");
|
||||||
|
err.code = "EACCESS";
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isInRoot: isInRoot,
|
||||||
|
resolveInRoot: resolveInRoot
|
||||||
|
};
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
var _ = require("lodash");
|
var _ = require('lodash');
|
||||||
|
|
||||||
// Returns from a navigation and a current file, a snapshot of current detailed state
|
// Returns from a navigation and a current file, a snapshot of current detailed state
|
||||||
var calculProgress = function(navigation, current) {
|
function calculProgress(navigation, current) {
|
||||||
var n = _.size(navigation);
|
var n = _.size(navigation);
|
||||||
var percent = 0, prevPercent = 0, currentChapter = null;
|
var percent = 0, prevPercent = 0, currentChapter = null;
|
||||||
var done = true;
|
var done = true;
|
||||||
|
|
||||||
var chapters = _.chain(navigation)
|
var chapters = _.chain(navigation)
|
||||||
|
|
||||||
|
// Transform as array
|
||||||
.map(function(nav, path) {
|
.map(function(nav, path) {
|
||||||
nav.path = path;
|
nav.path = path;
|
||||||
return nav;
|
return nav;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Sort entries
|
||||||
|
.sortBy(function(nav) {
|
||||||
|
return nav.index;
|
||||||
|
})
|
||||||
|
|
||||||
.map(function(nav, i) {
|
.map(function(nav, i) {
|
||||||
// Calcul percent
|
// Calcul percent
|
||||||
nav.percent = (i * 100) / Math.max((n - 1), 1);
|
nav.percent = (i * 100) / Math.max((n - 1), 1);
|
||||||
@@ -44,4 +52,4 @@ var calculProgress = function(navigation, current) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = calculProgress;
|
module.exports = calculProgress;
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
var Q = require('q');
|
var Q = require("q");
|
||||||
var _ = require('lodash');
|
var events = require("events");
|
||||||
|
var http = require("http");
|
||||||
var events = require('events');
|
var send = require("send");
|
||||||
var http = require('http');
|
var util = require("util");
|
||||||
var send = require('send');
|
var url = require("url");
|
||||||
var util = require('util');
|
|
||||||
var url = require('url');
|
|
||||||
|
|
||||||
var Server = function() {
|
var Server = function() {
|
||||||
this.running = null;
|
this.running = null;
|
||||||
@@ -17,7 +15,7 @@ util.inherits(Server, events.EventEmitter);
|
|||||||
|
|
||||||
// Return true if the server is running
|
// Return true if the server is running
|
||||||
Server.prototype.isRunning = function() {
|
Server.prototype.isRunning = function() {
|
||||||
return this.running != null;
|
return !!this.running;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stop the server
|
// Stop the server
|
||||||
@@ -57,25 +55,25 @@ Server.prototype.start = function(dir, port) {
|
|||||||
res.end(err.message);
|
res.end(err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to directory's index.html
|
// Redirect to directory"s index.html
|
||||||
function redirect() {
|
function redirect() {
|
||||||
res.statusCode = 301;
|
res.statusCode = 301;
|
||||||
res.setHeader('Location', req.url + '/');
|
res.setHeader("Location", req.url + "/");
|
||||||
res.end('Redirecting to ' + req.url + '/');
|
res.end("Redirecting to " + req.url + "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send file
|
// Send file
|
||||||
send(req, url.parse(req.url).pathname)
|
send(req, url.parse(req.url).pathname)
|
||||||
.root(dir)
|
.root(dir)
|
||||||
.on('error', error)
|
.on("error", error)
|
||||||
.on('directory', redirect)
|
.on("directory", redirect)
|
||||||
.pipe(res);
|
.pipe(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
that.running.on('connection', function (socket) {
|
that.running.on("connection", function (socket) {
|
||||||
that.sockets.push(socket);
|
that.sockets.push(socket);
|
||||||
socket.setTimeout(4000);
|
socket.setTimeout(4000);
|
||||||
socket.on('close', function () {
|
socket.on("close", function () {
|
||||||
that.sockets.splice(that.sockets.indexOf(socket), 1);
|
that.sockets.splice(that.sockets.indexOf(socket), 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -91,6 +89,6 @@ Server.prototype.start = function(dir, port) {
|
|||||||
|
|
||||||
return d.promise;
|
return d.promise;
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = Server;
|
module.exports = Server;
|
||||||
+4
-3
@@ -1,7 +1,7 @@
|
|||||||
var _ = require("lodash");
|
var _ = require("lodash");
|
||||||
|
|
||||||
function escapeShellArg(arg) {
|
function escapeShellArg(arg) {
|
||||||
var ret = '';
|
var ret = "";
|
||||||
|
|
||||||
ret = arg.replace(/"/g, '\\"');
|
ret = arg.replace(/"/g, '\\"');
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ function escapeShellArg(arg) {
|
|||||||
function optionsToShellArgs(options) {
|
function optionsToShellArgs(options) {
|
||||||
return _.chain(options)
|
return _.chain(options)
|
||||||
.map(function(value, key) {
|
.map(function(value, key) {
|
||||||
if (value == null || value === false) return null;
|
if (value === null || value === undefined || value === false) return null;
|
||||||
if (value === true) return key;
|
if (value === true) return key;
|
||||||
return key+"="+escapeShellArg(value);
|
return key+"="+escapeShellArg(value);
|
||||||
})
|
})
|
||||||
@@ -22,5 +22,6 @@ function optionsToShellArgs(options) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
escapeShellArg: escapeShellArg,
|
escapeShellArg: escapeShellArg,
|
||||||
optionsToShellArgs: optionsToShellArgs
|
optionsToShellArgs: optionsToShellArgs,
|
||||||
|
toLowerCase: String.prototype.toLowerCase.call.bind(String.prototype.toLowerCase)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
var Q = require("q");
|
||||||
|
var _ = require("lodash");
|
||||||
|
var path = require("path");
|
||||||
|
var chokidar = require("chokidar");
|
||||||
|
|
||||||
|
var parsers = require("gitbook-parsers");
|
||||||
|
|
||||||
|
function watch(dir) {
|
||||||
|
var d = Q.defer();
|
||||||
|
dir = path.resolve(dir);
|
||||||
|
|
||||||
|
var toWatch = [
|
||||||
|
"book.json", "book.js"
|
||||||
|
];
|
||||||
|
|
||||||
|
_.each(parsers.extensions, function(ext) {
|
||||||
|
toWatch.push("**/*"+ext);
|
||||||
|
});
|
||||||
|
|
||||||
|
var watcher = chokidar.watch(toWatch, {
|
||||||
|
cwd: dir,
|
||||||
|
ignored: "_book/**",
|
||||||
|
ignoreInitial: true
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher.once("all", function(e, filepath) {
|
||||||
|
watcher.close();
|
||||||
|
|
||||||
|
d.resolve(filepath);
|
||||||
|
});
|
||||||
|
watcher.once("error", function(err) {
|
||||||
|
watcher.close();
|
||||||
|
|
||||||
|
d.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return d.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = watch;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
var semver = require('semver');
|
||||||
|
var pkg = require('../package.json');
|
||||||
|
|
||||||
|
var VERSION = pkg.version;
|
||||||
|
var VERSION_STABLE = VERSION.replace(/\-(\S+)/g, '');
|
||||||
|
|
||||||
|
// Test if current current gitbook version satisfies a condition
|
||||||
|
// We can't directly use samver.satisfies since it will break all plugins when gitbook version is a prerelease (beta, alpha)
|
||||||
|
function satisfies(condition) {
|
||||||
|
// Test with real version
|
||||||
|
if (semver.satisfies(VERSION, condition)) return true;
|
||||||
|
|
||||||
|
// Test with future stable release
|
||||||
|
return semver.satisfies(VERSION_STABLE, condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
satisfies: satisfies
|
||||||
|
};
|
||||||
+59
-38
@@ -1,72 +1,93 @@
|
|||||||
{
|
{
|
||||||
"name": "gitbook",
|
"name": "gitbook",
|
||||||
"version": "1.3.2",
|
"version": "2.6.3",
|
||||||
"homepage": "http://www.gitbook.io/",
|
"homepage": "https://www.gitbook.com",
|
||||||
"description": "Library and cmd utility to generate GitBooks",
|
"description": "Library and cmd utility to generate GitBooks",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"q": "1.0.1",
|
"q": "1.0.1",
|
||||||
"lodash": "2.4.1",
|
"lodash": "3.10.1",
|
||||||
"kramed": "0.4.3",
|
"graceful-fs": "3.0.5",
|
||||||
"kramed-text-renderer": "0.2.1",
|
|
||||||
"lunr": "0.5.2",
|
|
||||||
"swig": "1.3.2",
|
|
||||||
"send": "0.2.0",
|
|
||||||
"fstream-ignore": "0.0.7",
|
|
||||||
"commander": "2.2.0",
|
|
||||||
"graceful-fs": "3.0.2",
|
|
||||||
"fs-extra": "0.10.0",
|
|
||||||
"highlight.js": "8.3.0",
|
|
||||||
"tmp": "0.0.23",
|
|
||||||
"semver": "2.2.1",
|
|
||||||
"gaze": "~0.5.1",
|
|
||||||
"resolve": "0.6.3",
|
"resolve": "0.6.3",
|
||||||
"tiny-lr-fork": "0.0.5",
|
"fs-extra": "0.16.5",
|
||||||
"gitbook-plugin": "0.0.2",
|
"fstream-ignore": "1.0.2",
|
||||||
"gitbook-plugin-mathjax": "0.0.6",
|
"gitbook-parsers": "0.8.8",
|
||||||
|
"gitbook-plugin-highlight": "1.0.3",
|
||||||
|
"gitbook-plugin-sharing": "1.0.1",
|
||||||
|
"gitbook-plugin-search": "1.1.0",
|
||||||
|
"gitbook-plugin-fontsettings": "1.0.2",
|
||||||
|
"nunjucks": "2.2.0",
|
||||||
|
"nunjucks-autoescape": "1.0.0",
|
||||||
|
"nunjucks-filter": "1.0.0",
|
||||||
|
"i18n": "0.5.0",
|
||||||
|
"semver": "5.0.1",
|
||||||
|
"npmi": "0.1.1",
|
||||||
|
"cheerio": "0.19.0",
|
||||||
"gitbook-plugin-livereload": "0.0.1",
|
"gitbook-plugin-livereload": "0.0.1",
|
||||||
"gitbook-plugin-exercises": "1.0.0",
|
"chokidar": "~1.0.5",
|
||||||
"gitbook-plugin-quizzes": "1.0.0"
|
"send": "0.2.0",
|
||||||
|
"tiny-lr": "0.2.1",
|
||||||
|
"tmp": "0.0.24",
|
||||||
|
"crc": "3.2.1",
|
||||||
|
"bash-color": "0.0.3",
|
||||||
|
"urijs": "1.17.0",
|
||||||
|
"request": "2.51.0",
|
||||||
|
"npm": "2.4.1",
|
||||||
|
"dom-serializer": "0.1.0",
|
||||||
|
"spawn-cmd": "0.0.2",
|
||||||
|
"escape-string-regexp": "1.0.3",
|
||||||
|
"juice": "1.5.0",
|
||||||
|
"jsonschema": "1.0.2",
|
||||||
|
"json-schema-defaults": "0.1.1",
|
||||||
|
"merge-defaults": "0.2.1",
|
||||||
|
"github-slugid": "1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mocha": "1.18.2",
|
"eslint": "1.5.0",
|
||||||
"grunt": "~0.4.2",
|
"mocha": "2.3.2",
|
||||||
"grunt-cli": "0.1.11",
|
"should": "7.1.0",
|
||||||
"grunt-contrib-copy": "0.5.0",
|
"should-promised": "0.3.1",
|
||||||
"grunt-contrib-less": "~0.5.0",
|
"gulp": "^3.8.11",
|
||||||
"grunt-contrib-requirejs": "0.4.1",
|
"gulp-rename": "^1.2.2",
|
||||||
"grunt-bower-install-simple": "0.9.2",
|
"gulp-uglify": "1.1.0",
|
||||||
"grunt-browserify": "3.1.0",
|
"gulp-less": "3.0.2",
|
||||||
"grunt-contrib-uglify": "0.6.0"
|
"gulp-minify-css": "1.0.0",
|
||||||
|
"gulp-util": "3.0.6",
|
||||||
|
"browserify": "11.0.1",
|
||||||
|
"merge-stream": "0.1.7",
|
||||||
|
"vinyl-source-stream": "1.1.0",
|
||||||
|
"jquery": "2.1.4",
|
||||||
|
"mousetrap": "1.5.3",
|
||||||
|
"font-awesome": "4.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "export TESTING=true; mocha --reporter list"
|
"test": "node_modules/.bin/mocha --reporter spec --timeout 15000"
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"gitbook": "./bin/gitbook.js"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/GitbookIO/gitbook.git"
|
"url": "https://github.com/GitbookIO/gitbook.git"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"gitbook": "./bin/gitbook.js"
|
||||||
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"git",
|
"git",
|
||||||
"book",
|
"book",
|
||||||
"gitbook"
|
"gitbook"
|
||||||
],
|
],
|
||||||
"author": "FriendCode <contact@friendco.de>",
|
"author": "FriendCode Inc. <contact@gitbook.com>",
|
||||||
"license": "Apache 2",
|
"license": "Apache-2.0",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/GitbookIO/gitbook/issues"
|
"url": "https://github.com/GitbookIO/gitbook/issues"
|
||||||
},
|
},
|
||||||
"contributors": [
|
"contributors": [
|
||||||
{
|
{
|
||||||
"name": "Aaron O'Mullan",
|
"name": "Aaron O'Mullan",
|
||||||
"email": "aaron.omullan@gmail.com"
|
"email": "aaron@gitbook.com"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Samy Pessé",
|
"name": "Samy Pessé",
|
||||||
"email": "samypesse@gmail.com"
|
"email": "samy@gitbook.com"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
var _ = require('lodash');
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var should = require('should');
|
||||||
|
var cheerio = require('cheerio');
|
||||||
|
|
||||||
|
require('should-promised');
|
||||||
|
|
||||||
|
should.Assertion.add('file', function(file, description) {
|
||||||
|
this.params = { actual: this.obj.toString(), operator: 'have file ' + file, message: description };
|
||||||
|
|
||||||
|
this.obj.should.have.property('options').which.is.an.Object();
|
||||||
|
this.obj.options.should.have.property('output').which.is.a.String();
|
||||||
|
this.assert(fs.existsSync(path.resolve(this.obj.options.output, file)));
|
||||||
|
});
|
||||||
|
|
||||||
|
should.Assertion.add('jsonfile', function(file, description) {
|
||||||
|
this.params = { actual: this.obj.toString(), operator: 'have valid jsonfile ' + file, message: description };
|
||||||
|
|
||||||
|
this.obj.should.have.property('options').which.is.an.Object();
|
||||||
|
this.obj.options.should.have.property('output').which.is.a.String();
|
||||||
|
this.assert(JSON.parse(fs.readFileSync(path.resolve(this.obj.options.output, file), { encoding: 'utf-8' })));
|
||||||
|
});
|
||||||
|
|
||||||
|
should.Assertion.add('html', function(rules, description) {
|
||||||
|
this.params = { actual: 'HTML string', operator: 'valid html', message: description };
|
||||||
|
var $ = cheerio.load(this.obj);
|
||||||
|
|
||||||
|
_.each(rules, function(validations, query) {
|
||||||
|
validations = _.defaults(validations || {}, {
|
||||||
|
// Select a specific element in the list of matched elements
|
||||||
|
index: null,
|
||||||
|
|
||||||
|
// Check that there is the correct count of elements
|
||||||
|
count: 1,
|
||||||
|
|
||||||
|
// Check attribute values
|
||||||
|
attributes: {},
|
||||||
|
|
||||||
|
// Trim inner text
|
||||||
|
trim: false,
|
||||||
|
|
||||||
|
// Check inner text
|
||||||
|
text: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
var $el = $(query);
|
||||||
|
|
||||||
|
// Select correct element
|
||||||
|
if (_.isNumber(validations.index)) $el = $($el.get(validations.index));
|
||||||
|
|
||||||
|
// Test number of elements
|
||||||
|
$el.length.should.be.equal(validations.count);
|
||||||
|
|
||||||
|
// Test text
|
||||||
|
if (validations.text !== undefined) {
|
||||||
|
var text = $el.text();
|
||||||
|
if (validations.trim) text = text.trim();
|
||||||
|
text.should.be.equal(validations.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test attributes
|
||||||
|
_.each(validations.attributes, function(value, name) {
|
||||||
|
var attr = $el.attr(name);
|
||||||
|
should(attr).be.ok();
|
||||||
|
attr.should.be.equal(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Readme
|
||||||
|
|
||||||
|
Default description for the book.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Summary
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Readme
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Summary
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
"title": "js-config"
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Readme
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Summary
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"title": "json-config"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Readme
|
||||||
|
|
||||||
|
<p id="test-plugin-block-shortcuts-1">$$test_block1$$</p>
|
||||||
|
<p id="test-plugin-block-shortcuts-2">{% include "./block.md" %}</p>
|
||||||
|
|
||||||
|
### Relative
|
||||||
|
|
||||||
|
<p id="t1">{% include "./hello.md" %}</p>
|
||||||
|
<p id="t2">{% include "/hello.md" %}</p>
|
||||||
|
|
||||||
|
### Git
|
||||||
|
|
||||||
|
<p id="t3">{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test.md" %}</p>
|
||||||
|
<p id="t4">{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test2.md" %}</p>
|
||||||
|
<p id="t5">{% include "git+https://gist.github.com/69ea4542e4c8967d2fa7.git/test3.md" %}</p>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Summary
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
$$test_block2$$
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Hello World
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Glossary
|
||||||
|
|
||||||
|
## test
|
||||||
|
|
||||||
|
Just a simple and easy to understand test.
|
||||||
|
|
||||||
|
## hakunamatata
|
||||||
|
|
||||||
|
This word is not present in the content.
|
||||||
|
|
||||||
|
## test long
|
||||||
|
|
||||||
|
This is a test with a longer text that the first entry to test order.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Readme
|
||||||
|
|
||||||
|
The word test should be replaced by a glossary link.
|
||||||
|
|
||||||
|
```
|
||||||
|
But the word test should not be replaced in code blocks
|
||||||
|
```
|
||||||
|
|
||||||
|
The two words test long should be replaced by the correct glossary links.
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Summary
|
||||||
|
|
||||||
|
* [Page](folder/PAGE.md)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Page
|
||||||
|
|
||||||
|
Just a page in a sub-directory to test relative link to glossary.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Hello World
|
||||||
|
|
||||||
|
## Hello {#hello-custom}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Readme
|
||||||
|
|
||||||
|
Block without language
|
||||||
|
|
||||||
|
```
|
||||||
|
test 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Block with a language
|
||||||
|
|
||||||
|
```lang
|
||||||
|
test 2
|
||||||
|
```
|
||||||
|
|
||||||
|
Inline code: `test 3`
|
||||||
|
Inline code with html: `<test>`
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Summary
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Tests for Images
|
||||||
|
|
||||||
|
# SVG relative image
|
||||||
|
|
||||||
|
|
||||||
|
### Convert SVG to PNG
|
||||||
|

|
||||||
|
|
||||||
|
### Should only convert it once
|
||||||
|

|
||||||
|
|
||||||
|
### Download remote images
|
||||||
|

|
||||||
|
|
||||||
|
### Remote images with same filename
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Summary
|
||||||
|
|
||||||
|
* [Page](./folder/PAGE.md)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
### Images from other folder
|
||||||
|

|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user