mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-26 12:18:01 +00:00
Compare commits
1486 Commits
0.2.0
...
3.0.0-pre.2
| Author | SHA1 | Date | |
|---|---|---|---|
| e436115a4a | |||
| 1c61f93b5f | |||
| 0494f7d1fd | |||
| 4aa66338fc | |||
| 8cda844ef1 | |||
| dde1ef92f0 | |||
| 999f8b481f | |||
| 294951f22a | |||
| 2619737f64 | |||
| 445a90defb | |||
| d3d64f636c | |||
| eef072693b | |||
| 43c5f8d340 | |||
| b344d65302 | |||
| ce967f35ca | |||
| 80718cc783 | |||
| 73f22332ec | |||
| e963b6b65d | |||
| 327e90e8ec | |||
| 9253b764fa | |||
| 393aed6270 | |||
| b240d6a8fc | |||
| 3cda0643fb | |||
| 1ba63675d5 | |||
| 32f52f2d95 | |||
| 9215edc0a0 | |||
| 25ae21dae3 | |||
| 94393bef4b | |||
| 4f29183685 | |||
| edecf56dd6 | |||
| 08754bc626 | |||
| 702542f408 | |||
| 6990b371a6 | |||
| 8020d9045b | |||
| 34bb52a051 | |||
| a2e0bc267c | |||
| d86a95b5e1 | |||
| 585ed46034 | |||
| 82a4f80cd2 | |||
| f47a3968b7 | |||
| 0b592dd5c6 | |||
| addf6ade9a | |||
| d7d0429169 | |||
| 4bf5dc6646 | |||
| 5b20027f2e | |||
| 4499f71dba | |||
| 81b734c151 | |||
| 0b4a6b5362 | |||
| 3a0636046e | |||
| 52eccb29e4 | |||
| 86e4a935e2 | |||
| 1fa952b930 | |||
| 78e11bc6be | |||
| 40cc20e06e | |||
| c223f970ec | |||
| daa80dc949 | |||
| 255a78a534 | |||
| 6faa469bf5 | |||
| eeefac4964 | |||
| eb4770085a | |||
| 1ed63ce4f5 | |||
| e85ad95bcf | |||
| bd73c79787 | |||
| b244d506bd | |||
| 44f03865c0 | |||
| 72dfb4f11d | |||
| 6b0a4f20bf | |||
| f052aacee1 | |||
| 13e2ff8035 | |||
| 199e61e01c | |||
| 1bf6abf338 | |||
| c7b1d72bf0 | |||
| 0b3bdcb219 | |||
| 6653307fda | |||
| 642ba72a1d | |||
| 71144db09c | |||
| 395bd62663 | |||
| d8fd6430ed | |||
| 1e243281c7 | |||
| de23494409 | |||
| b23d3a9f37 | |||
| de31b90080 | |||
| ccc585ca1f | |||
| 4dbe81cb66 | |||
| d139d1d1bd | |||
| a186544fad | |||
| 7de152539d | |||
| 1e486a16bd | |||
| 7f9c24eedb | |||
| 8a3396deba | |||
| 96d5b37196 | |||
| 7b2e332f6c | |||
| ab541c1653 | |||
| 5f058f2ceb | |||
| 360381dd9f | |||
| cef4096b39 | |||
| a9c96c1afa | |||
| a446deaff3 | |||
| e5dd987d03 | |||
| 51fd8ae08c | |||
| 884540e565 | |||
| ca1313e891 | |||
| 8df18746fa | |||
| 40f829ce54 | |||
| e16b3e35f4 | |||
| 2f15e26ffa | |||
| b4cdbc6534 | |||
| 0a93ffd76b | |||
| 98e9848c4a | |||
| 42bb5fccb8 | |||
| fb921788b9 | |||
| 847c6b9aa2 | |||
| cafbcd4097 | |||
| a22539a91e | |||
| 89378c3883 | |||
| f763e1dc42 | |||
| ea13e3cec4 | |||
| 3d3302e547 | |||
| 95d81d02a1 | |||
| 8401a674c3 | |||
| 3fcf1a7723 | |||
| 1a3ee78459 | |||
| b99c499d2e | |||
| 709b388dfc | |||
| 6e83240233 | |||
| 202c235c66 | |||
| 576d773fec | |||
| 58a930e456 | |||
| 52206ce9d0 | |||
| 1de9d5683b | |||
| b66242db98 | |||
| 004ac88516 | |||
| 3d1eac234a | |||
| a78e52812f | |||
| 5993e6c791 | |||
| 1ec8b7c324 | |||
| e75002a96d | |||
| a68364d4d7 | |||
| 2b5e836a33 | |||
| 9caa9c39e5 | |||
| 096bdaba7a | |||
| d22c1f57e5 | |||
| dac00be7b5 | |||
| 4e7cb9b23f | |||
| 5fdee2cb5e | |||
| 1db3f3deab | |||
| b2af55ac63 | |||
| 40cdc285b6 | |||
| 46f017e68b | |||
| 1769ac2c2d | |||
| 067bdcda2d | |||
| 3bc1934966 | |||
| 6ee9a3a64c | |||
| c0196c97a5 | |||
| 96105d6ba0 | |||
| 853ba6079c | |||
| b6104c64fe | |||
| e95678cf3a | |||
| e2c21051aa | |||
| 7c575e1b43 | |||
| c89c253f29 | |||
| f54685e216 | |||
| 8141bcb3b6 | |||
| db3d21db49 | |||
| ca85c0ecf3 | |||
| 702db46f20 | |||
| 327d0bf5c5 | |||
| 2375296e67 | |||
| b4fcd2da14 | |||
| e3c92cce77 | |||
| 1141d34f07 | |||
| 354fe12742 | |||
| 1b19c66603 | |||
| d7077fc7db | |||
| d6bd4a0d67 | |||
| 8628c87be4 | |||
| fef822145d | |||
| cfefa7d579 | |||
| def115fe0f | |||
| 01a7c6f4e0 | |||
| 0e86cdeb4a | |||
| a65f888a77 | |||
| 71059e3ea3 | |||
| ee3faaad11 | |||
| d2aad34935 | |||
| d8ed34beba | |||
| 3154d37e41 | |||
| 272f30af53 | |||
| 4c6717e234 | |||
| 39b6562d14 | |||
| 0d966fe197 | |||
| 82f94b010f | |||
| 4555c541a8 | |||
| a853a51a0b | |||
| 756694c029 | |||
| 669f3b3984 | |||
| e7eed2abbe | |||
| 98a13cf487 | |||
| 7f9aa214a4 | |||
| 347a536a4e | |||
| 5cf0abaf2b | |||
| 09c6f025a1 | |||
| f47208528b | |||
| 87d90a65ac | |||
| 02cff8c72c | |||
| a67993ac0b | |||
| e540d25665 | |||
| 52c4dc49cf | |||
| 44ad70320a | |||
| 2b8e1c0b11 | |||
| 85a2ce9065 | |||
| a24aa79dbf | |||
| 633de85180 | |||
| f32d03eb88 | |||
| e83d63c2aa | |||
| ff30ba62ba | |||
| ea98444dc0 | |||
| 328c3a7d51 | |||
| 4b465bedea | |||
| 461ef2c68e | |||
| 46efdacd6d | |||
| a063cc1a53 | |||
| 36f2f5d52e | |||
| ddc99104d4 | |||
| 8d277e9108 | |||
| f305d57ab7 | |||
| 877f2e477b | |||
| c8e2fc0e57 | |||
| 4cb9cbb5ae | |||
| 6f3be56223 | |||
| c638c108d2 | |||
| d8f5aa680d | |||
| f2b585e446 | |||
| 78f91e0037 | |||
| 3f3c156697 | |||
| 4627bf5d1f | |||
| 93a144984a | |||
| 2d0784440c | |||
| 05a083437f | |||
| d4b5f59f92 | |||
| ff53fb6db0 | |||
| 534b3908cc | |||
| 7ea2369810 | |||
| 4d96ec10c1 | |||
| a37ee3457a | |||
| 6969bb9a5c | |||
| 4567041456 | |||
| 49f09e46da | |||
| becd027563 | |||
| 6eb1605be6 | |||
| 5c16aec096 | |||
| 835e2dc1fb | |||
| e2378ab29a | |||
| 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 | |||
| 8a0180ae6b | |||
| d6776b98d3 | |||
| 6a04ba89a5 | |||
| 05eaeeb0d0 | |||
| 77043464b2 | |||
| b90f74a821 | |||
| f804eb42ac | |||
| 74fd8c159b | |||
| a741ec1350 | |||
| 4b26c31b3b | |||
| dfd487692a | |||
| 195374ecee | |||
| 1659778a77 | |||
| 40c4035afb | |||
| 1502d65286 | |||
| 0feb672d70 | |||
| 2a4ef5bbf4 | |||
| c4540332a7 | |||
| 2104404cb3 | |||
| 002d685ebb | |||
| 9da10e2b25 | |||
| 1e4632080b | |||
| 5191106396 | |||
| 6ab5cafaf3 | |||
| d1c0cb9c71 | |||
| 38c750101c | |||
| ba0f379a89 | |||
| 1d9509b3b6 | |||
| 3c8de4a8ab | |||
| 591d8fc9ee | |||
| d480e9cea4 | |||
| 32a71869da | |||
| fdbfda64a6 | |||
| a6f48be39e | |||
| ce50b893e9 | |||
| 225ff27dd1 | |||
| 52e8361846 | |||
| 49e3bbc3d6 | |||
| c9ec911d0e | |||
| 84d7662fd8 | |||
| 676b2287ee | |||
| 32a64be407 | |||
| 138cddf3d6 | |||
| d730a3aac6 | |||
| ffb0b12012 | |||
| 78e25f8252 | |||
| 09f42f57e1 | |||
| 950ad1d060 | |||
| ed6b29c1a4 | |||
| 36785120b1 | |||
| 26fad5b888 | |||
| f049576575 | |||
| a7ecbb87e4 | |||
| 1da341741b | |||
| 2dd6192576 | |||
| 52c9347b9c | |||
| e2295591ed | |||
| 0c1cb724d7 | |||
| 7cee248426 | |||
| 6ea5b2b539 | |||
| a56b559d43 | |||
| df1a82e0e4 | |||
| f15a1efe71 | |||
| b8379fc6fa | |||
| d69cc74647 | |||
| f83874a9ca | |||
| 1968bb79c2 | |||
| 60c721db40 | |||
| 11b299653f | |||
| c8c07fe8ce | |||
| be0a4f08e9 | |||
| 4da4e28d07 | |||
| 9dc19026c6 | |||
| 31ca9f7fcb | |||
| 6925d39e8d | |||
| a24cafd975 | |||
| e3fdf8b404 | |||
| c861223378 | |||
| f5c6d8df71 | |||
| 452c846e90 | |||
| aedd875ab7 | |||
| d2ec2f8f02 | |||
| 5af0df93c3 | |||
| 5b4b5e1af7 | |||
| 21704c4ac2 | |||
| 71aff10092 | |||
| 4b390667e5 | |||
| 14dda3148d | |||
| 511dc9f805 | |||
| d0a14dcbcc | |||
| ec8dc2cdb0 | |||
| 6f1ef585be | |||
| d84acd0b82 | |||
| e1723ad3d5 | |||
| f77404afe1 | |||
| fbd7522748 | |||
| 90320fd26c | |||
| 4f9b474fdd | |||
| 4c921bdc09 | |||
| 93e0864063 | |||
| 80e2f23714 | |||
| 00496dbd09 | |||
| 54fb9a58d7 | |||
| 6b0ad22b19 | |||
| 17f40540ae | |||
| d337a500dc | |||
| cbe4e9edec | |||
| 402e88f866 | |||
| e0aed828e0 | |||
| 323facbdc8 | |||
| 2a61c858e4 | |||
| d216661f27 | |||
| 50dfee99b6 | |||
| 9e8ed9ac68 | |||
| 3a54554713 | |||
| fc113ce0c3 | |||
| f605f0b6c8 | |||
| 08788edfd3 | |||
| 4a0e19935f | |||
| a3454bb7a8 | |||
| 7245fa8ad7 | |||
| d771ff341d | |||
| 8171241666 | |||
| 1125e89b49 | |||
| 67fd5a060b | |||
| 4f60eda3ee | |||
| 6c62d75eae | |||
| bcfcf3d979 | |||
| b4497029db | |||
| 23ac4b846d | |||
| 691d04e994 | |||
| a54959ee4f | |||
| 76ebca8c55 | |||
| 786c4f147d | |||
| 7028fc7118 | |||
| 1c3210e789 | |||
| c28b804352 | |||
| 6d841c909b | |||
| e917fd28e0 | |||
| ab88459a56 | |||
| fe95c361be | |||
| 7e78546516 | |||
| d07b9450b1 | |||
| c30b431b7b | |||
| 0fe54fbd57 | |||
| 1bec19029f | |||
| 21ccf7757e | |||
| ceb766a124 | |||
| 046b789fa8 | |||
| cd50a1402f | |||
| 1b0ae5a827 | |||
| 01b6ca76e8 | |||
| 06b9c345c9 | |||
| 152276c1d7 | |||
| 275eedf076 | |||
| 5e959d65a2 | |||
| 66c1b89da2 | |||
| 78c68c7b9e | |||
| d914eae40a | |||
| 74f95f4d0d | |||
| d803052461 | |||
| 633e52c4be | |||
| 1512bcf808 | |||
| 4e46d088ca | |||
| 388c7ffd32 | |||
| 7ba8653935 | |||
| a4403aad1e | |||
| b629b78dba | |||
| f62c218735 | |||
| 725510028f | |||
| ad7927242d | |||
| 5692bdd229 | |||
| 3402746719 | |||
| 30a8414b06 | |||
| 9d46899ea7 | |||
| 58f14fd9ca | |||
| b6af14b13e | |||
| ba2134c480 | |||
| 4705548860 | |||
| 7d51ae3689 | |||
| b9360de3e3 | |||
| 9f241457bb | |||
| 60a7c28e66 | |||
| 5dff9b795e | |||
| 0134677e0e | |||
| 39ef4fa199 | |||
| b507a30301 | |||
| 539c2d0c57 | |||
| aa6f3530f9 | |||
| 98057af219 | |||
| bb48c8233c | |||
| 8f14db64df | |||
| 37d40dc66c | |||
| 6c96680cca | |||
| 4e475857d5 | |||
| f8ff0453dd | |||
| 3cc5c47535 | |||
| a2046814b4 | |||
| 82f814fdbd | |||
| cd382dc8fc | |||
| 3842e5bf4a | |||
| f05b138fd6 | |||
| 1b87cec893 | |||
| 4a894b1e18 | |||
| c7c98009e7 | |||
| 356322e0ed | |||
| 7a47049367 | |||
| a9f473260c | |||
| d16577e8bf | |||
| 15b4053d6b | |||
| 925a9b1388 | |||
| fc942fdeca | |||
| 75a6ecbf52 | |||
| 520045441b | |||
| 00c3e966d3 | |||
| 8b6ebe86b8 | |||
| d5bd3453e1 | |||
| ddacbbe73b | |||
| f7d52f0eff | |||
| 39c50640ea | |||
| 16128c7e60 | |||
| 8b6a80cffb | |||
| 86f8a9d8fb | |||
| 790dfd601e | |||
| 3426428142 | |||
| 6c5c8e6eb7 | |||
| 3aefcaea73 | |||
| 04c025774d | |||
| 80aac7f581 | |||
| 266f7ca73d | |||
| 6bbefad24e | |||
| d6eb8e4c60 | |||
| c87edb155a | |||
| ef0468ad5d | |||
| f03153eb0c | |||
| 3d8b6e550a | |||
| 4c3dbca5ca | |||
| a258feea37 | |||
| 3a3332d0b9 | |||
| 4259283f32 | |||
| 243d977cd6 | |||
| 94bbbecedf | |||
| f49eab42a9 | |||
| ce6b2bc47d | |||
| 3245d7e430 | |||
| 6ee17c32ae | |||
| bf7ffe6d4c | |||
| ec7bc6b347 | |||
| b425db0979 | |||
| 30c6d1c9de | |||
| cee1d90fc6 | |||
| 522342667c | |||
| d1c1277bb9 | |||
| 9a66f56c79 | |||
| 087ca28ca2 | |||
| 3d5281ff3a | |||
| f62cf340c5 | |||
| 7e12bd3588 | |||
| 4dbdb95ebe | |||
| 2c5ddbcd8f | |||
| be4fd74f90 | |||
| 57491e3a8b | |||
| f7a442d297 | |||
| cfebdc6f6b | |||
| 65319e8fd6 | |||
| 55db44a3b4 | |||
| 13b6045300 | |||
| b7ff879c31 | |||
| 57ade87fe1 | |||
| 907e9ea5a7 | |||
| 215488dc45 | |||
| a195b3eac2 | |||
| 77c6f0e967 | |||
| f0e18afec1 | |||
| b6d11d88b7 | |||
| c803270af8 | |||
| 5d51263a58 | |||
| 7e30720b2c | |||
| 771c5e8b68 | |||
| 8f53afab6b | |||
| 0ad0403d41 | |||
| 2515c1dd73 | |||
| 7dfcd102dd | |||
| a91245d575 | |||
| 5f207f5e14 | |||
| 718692de3b | |||
| 9856a51255 | |||
| 58eaaf5514 | |||
| c0ae0138f8 | |||
| b1b9dc5b0a | |||
| f85c5a6498 | |||
| 32eafefc45 | |||
| 336999a34e | |||
| 1bf5f3b984 | |||
| c76d070237 | |||
| 8c7da1351f | |||
| 34588c6092 | |||
| 9f00bf8060 | |||
| f4a17b182f | |||
| 5c3b345fa6 | |||
| 46321fffd9 | |||
| 1f4cf33dd0 | |||
| 7274aa6940 | |||
| 125e804cd5 | |||
| 89ba17de84 | |||
| 115fba837c | |||
| 4df51546b2 | |||
| f68aaf7ff9 | |||
| e3d1746ba3 | |||
| a0d5646034 | |||
| 2f8af5f136 | |||
| 8b7abbd6f4 | |||
| f228f646eb | |||
| 45891d80c7 | |||
| ac900aa37f | |||
| 422cbed796 | |||
| 38cbbe46aa | |||
| c1b57a387b | |||
| aaa9ae8bb7 | |||
| 6340a64cff | |||
| 96bcfa4e20 | |||
| af0f56b26b | |||
| aed5befa48 | |||
| 8e37da672d | |||
| c0f68c45e2 | |||
| 47fdd73791 | |||
| 7c0b88cf2e | |||
| bf21633b6c | |||
| ad9b1fd6f7 | |||
| d245895085 | |||
| c7439d12f7 | |||
| 8d6923ce2c | |||
| b21fb24a82 | |||
| e7be132194 | |||
| 0e1717f4bb | |||
| 276dbe29c7 | |||
| 21fd5924b8 | |||
| 443def6cfd | |||
| 75464574d7 | |||
| 86f05c68c5 | |||
| 8dd407eaa8 | |||
| ea7ad10e8a | |||
| 62ec695dc8 | |||
| ca37ac73a9 | |||
| 96edd50c8f | |||
| 3993c1e35a | |||
| 5298558925 | |||
| c6ba147917 | |||
| 7efecea3d6 | |||
| a73fed3ebd | |||
| 50ef7bcd23 | |||
| 82e422cb89 | |||
| dab46c779d | |||
| 7c65adc9b2 | |||
| 9b96ef9bae | |||
| e97d1941dc | |||
| 0b0ac46e34 | |||
| 622bbcb8ea | |||
| 58a5245d21 | |||
| 612f9a6dc7 | |||
| 90d4dff263 | |||
| 0f38385c7d | |||
| 2b50616251 | |||
| 5b7f37f0d4 | |||
| 6ca8e26741 | |||
| a3ada05598 | |||
| 42eb11aaff | |||
| b581859afd | |||
| 2e22fae52a | |||
| 603ac8b91d | |||
| 5faa4841ca | |||
| 7416b826e0 | |||
| 82afc34755 | |||
| da39631a5a | |||
| fc8685a7c1 | |||
| 5e0204a980 | |||
| 6bb0f33f39 | |||
| ed4fd565b2 | |||
| 79cff28abd | |||
| 27329239f7 | |||
| 9de86fc0e4 | |||
| 7fa7826372 | |||
| 96ecfd098b | |||
| f7fe976c7e | |||
| 756629b91d | |||
| e640d124d4 | |||
| 7f41466297 | |||
| 57dfc062d2 | |||
| 799815d85a | |||
| 2946246834 | |||
| 6d276d1e79 | |||
| 6a3a369b3e | |||
| 5d8afa2135 | |||
| ab3e6aa8ce | |||
| 822092736e | |||
| 7497acd2e3 | |||
| ae137885b8 | |||
| b964cc7009 | |||
| c2ea126c5f | |||
| b668dba479 | |||
| 2cae5a0310 | |||
| b6a0f3ff67 | |||
| fa00b52fb5 | |||
| 2999d90907 | |||
| b6696bcad2 | |||
| 728c87c525 | |||
| ace394ee2d | |||
| 076ee98f64 | |||
| 2e472654ca | |||
| d46bd14678 | |||
| b3e5b3d106 | |||
| b830ef5dca | |||
| 45326c4ad2 | |||
| 603b61e8c8 | |||
| efb066c8e3 | |||
| c30db427b1 | |||
| 23056fc85d | |||
| 5f1bfc0280 | |||
| b715901f92 | |||
| 38fea06cf9 | |||
| 4481b5f556 | |||
| 0e0d051f7c | |||
| a1b649a279 | |||
| e6bc0a0fb7 | |||
| 93e4016c09 | |||
| 20c741a248 | |||
| 54f530c7c3 | |||
| 9fdc5da22d | |||
| 99ce48b146 | |||
| 6a33ad9ef7 | |||
| ca94055adf | |||
| 36a67e4c91 | |||
| 69431e1bbb | |||
| a3fc4ee2b0 | |||
| e505fd6d54 | |||
| 93a3785e4c | |||
| a02de84812 | |||
| c0521f0ec7 | |||
| 83d8c16853 | |||
| 47e9506397 | |||
| b8a3d0e254 | |||
| 001d7ec44e | |||
| 5cb0ec447a | |||
| 67e7d77302 | |||
| 8cbd8b2b48 | |||
| 7c404dbf96 | |||
| c4a67b6f2f | |||
| 3b70347625 | |||
| bb0a7935e9 | |||
| 8f4b799d3c | |||
| 931e0c66c9 | |||
| 0212084862 | |||
| 9abbabbce3 | |||
| ac610487e8 | |||
| a9a0f89892 | |||
| 305f221077 | |||
| 53514f2602 | |||
| 00327778cb | |||
| 37fadec787 | |||
| ada11d0b59 | |||
| 4c90fdb7b2 | |||
| 98c133df3c | |||
| f86c27b090 | |||
| b5b8ed72a5 | |||
| 5beff86548 | |||
| dd395edae1 | |||
| d1c1d7b75b | |||
| 4a380e9279 | |||
| 878c105def | |||
| 74d628676b | |||
| 6fdd98ec3a | |||
| 2ed1be3533 | |||
| 0ac04e1584 | |||
| 2a38d33d8a | |||
| fb14caf5b3 | |||
| cf51772f54 | |||
| cc8516df83 | |||
| c6a624bc6b | |||
| 8be8b825ee | |||
| 56eb4a2897 | |||
| 7a461eaa61 | |||
| fd64c05809 | |||
| 6bce93d893 | |||
| c9db068227 | |||
| 8a6d36ac4d | |||
| 64622c5945 | |||
| 837813edb0 | |||
| 0d9d62f1a9 | |||
| f7e9abf984 | |||
| dd0579b60d | |||
| b5bb953e47 | |||
| ef4a8ea1b6 | |||
| 49cde96ee7 | |||
| 627be20e48 | |||
| e45bb84b02 | |||
| 4e8876c725 | |||
| 6d5be15ec9 | |||
| 1a8db646f2 | |||
| d3aba2c906 | |||
| 457a83282f | |||
| 48e94c937d | |||
| 6e613e9f71 | |||
| b698d43d7d | |||
| a36b9a6088 | |||
| 20acd89c8a | |||
| 7bed9a257d | |||
| 19c83aa5a8 | |||
| 52610d7d61 | |||
| 9e4b393b7a | |||
| a0ae34a3b7 | |||
| 413dc5d004 | |||
| c9eb27bfe7 | |||
| b78505c290 | |||
| 95f9ae83d0 | |||
| 9304289315 | |||
| 1b726ca293 | |||
| 656ca18929 |
@@ -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,2 @@
|
||||
docs/**/*
|
||||
test/node_modules/**/*
|
||||
@@ -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"
|
||||
}
|
||||
+11
-1
@@ -22,4 +22,14 @@ build/Release
|
||||
# Dependency directory
|
||||
# Deployed apps should consider commenting this line out:
|
||||
# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
/node_modules
|
||||
|
||||
# vim swapfile
|
||||
*.swp
|
||||
|
||||
# Output of documentation
|
||||
docs/_book
|
||||
|
||||
book.pdf
|
||||
book.epub
|
||||
book.mobi
|
||||
|
||||
+8
-3
@@ -1,5 +1,10 @@
|
||||
sudo: false
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.11"
|
||||
- "0.10"
|
||||
- "0.8"
|
||||
- "stable"
|
||||
- "4.1"
|
||||
- "0.12"
|
||||
before_install:
|
||||
- npm install svgexport -g
|
||||
after_success:
|
||||
- npm run lint
|
||||
|
||||
+53
-3
@@ -7,11 +7,61 @@ Names below are ordered by first contribution.
|
||||
Author
|
||||
------
|
||||
|
||||
- Samy Pessé <samypesse@gmail.com>
|
||||
- Aaron O'Mullan <aaron.omullan@gmail.com>
|
||||
- Samy Pessé <samypesse@gmail.com> (@SamyPesse)
|
||||
- Aaron O'Mullan <aaron.omullan@gmail.com> (@AaronO)
|
||||
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
- Nijiko Yonskai
|
||||
- Nijiko Yonskai (@Nijikokun)
|
||||
- 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)
|
||||
- Turkish
|
||||
- Turan Konan (@turankonan)
|
||||
- Catalan
|
||||
- Esaú García Sánchez-Torija (@egasato)
|
||||
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
# Release notes
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 3.0.0 (pre)
|
||||
- Summary can contain external links and anchors (Fix [#776](https://github.com/GitbookIO/gitbook/issues/776))
|
||||
- Summary can contain differents entitled sections
|
||||
- Glossary is generated as a normal page
|
||||
- Headings are no longer annotated with glossary terms
|
||||
- Themes are now published as a plugin, with ability to extend it from the book source
|
||||
- `links.sidebar` configuration is no longer supported, use summary sections instead
|
||||
- `pdf.headerTemplate` and `pdf.footerTemplate` have been replaced by a template in theme/book: `_layout/ebook/pdf_header.html` and `_layout/ebook/pdf_footer.html`
|
||||
- Markdown parser is now using CommonMark
|
||||
- Root folder for the book can be specified in a `.gitbook` file
|
||||
- Multi-lingual books share assets folder
|
||||
- YAML front matter is parsed and page's description can be specified in it
|
||||
- Fix `uk` translation
|
||||
|
||||
## 2.6.7
|
||||
- Fix bug with filenames including spaces
|
||||
- Add Turkish and Catalan translations
|
||||
|
||||
## 2.6.6
|
||||
- Fix custom CSS that are generated by plugins for PDF output
|
||||
|
||||
## 2.6.5
|
||||
- Fix support for plugins generating custom stylesheets (`styles-less` and `styles-sass`)
|
||||
- Fix glossary terms being replaced in script (ex: math)
|
||||
|
||||
## 2.6.4
|
||||
- Fix regression introduced by `2.6.3` of single HTML tags in markdown
|
||||
|
||||
## 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
|
||||
- ePub files are now passing validation from epubcheck
|
||||
- Fix replacement of multiple glossary terms in a single sentence
|
||||
- Fix on windows deep relative links
|
||||
- Fix search indexer
|
||||
|
||||
## 1.3.1
|
||||
- Fix error with links in markdown
|
||||
|
||||
## 1.3.0
|
||||
- Bundle gitbook parsing library as a client side library in `gitbook.js` and `gitbook.min.js`
|
||||
|
||||
## 1.2.0
|
||||
- Improvements on ebook generation
|
||||
- Fix incorrect follow of links in ebook generation
|
||||
- Move Table of Contents at the beginning of the ebook
|
||||
- Update to last highlight.js (includes Swift)
|
||||
- Includes of templates and variables (from book.json)
|
||||
|
||||
## 1.1.1
|
||||
- Rewrite quiz logic to be more robust
|
||||
- Improve integration of glossary
|
||||
- Improve generation of ebook by using a multiple HTML pages input source
|
||||
- Fix incorrect page breaks after h1 and h2 divs
|
||||
- New options to set header and footer in PDF generation
|
||||
|
||||
## 1.1.0
|
||||
- Plugins can now extend the ebook generation (pdf, epub, mobi)
|
||||
- Update `kramed` to version 0.4.3
|
||||
|
||||
## 1.0.3
|
||||
- Update `mathjax` plugin and MathJAx to version 2.4
|
||||
- Update `highlight.js` to 8.2.0
|
||||
|
||||
## 1.0.2
|
||||
- Update `mathjax` plugin, fixes issues with inline math rendering (no longer wanted)
|
||||
|
||||
## 1.0.1
|
||||
- New inline math convention (kramdown's), using `$$` rather than `$` as delimiters
|
||||
- Fix instapaper sharing
|
||||
- The `exercises` & `quizzes` plugins are now by default
|
||||
|
||||
## 1.0.0
|
||||
- New design
|
||||
- Support for glossary
|
||||
- Support for sharing to instapaper
|
||||
- Support for footnotes
|
||||
|
||||
## 0.7.1
|
||||
- Update `fs-extra` to `0.10.0` (fixes potential race conditions)
|
||||
|
||||
## 0.7.0
|
||||
- Add page break in ebook (pdf, epub, mobi) between chapters/articles
|
||||
- Start using kramed instead of marked
|
||||
- Fix display of inline math
|
||||
- Switch to graceful-fs to fix EMFILE errors
|
||||
- Add sharing to weibo.com
|
||||
|
||||
## 0.6.2
|
||||
- Support generating a plugin's book info dynamically
|
||||
- Improve navigation on dark theme
|
||||
- Improve path normalization when parsing SUMMARY.md
|
||||
|
||||
## 0.6.0
|
||||
- Generate header id the same as github
|
||||
- Custom links can be added at top of sidebar
|
||||
- Summary can now be transformed by plugins
|
||||
- Support importing code snippets
|
||||
@@ -1,72 +0,0 @@
|
||||
module.exports = function (grunt) {
|
||||
var path = require("path");
|
||||
|
||||
// Load NPM tasks
|
||||
grunt.loadNpmTasks('grunt-contrib-less');
|
||||
grunt.loadNpmTasks('grunt-contrib-requirejs');
|
||||
|
||||
// Init GRUNT configuraton
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
less: {
|
||||
development: {
|
||||
options: {
|
||||
compress: true,
|
||||
yuicompress: true,
|
||||
optimization: 2
|
||||
},
|
||||
files: {
|
||||
"theme/assets/style.css": "theme/stylesheets/main.less",
|
||||
"theme/assets/print.css": "theme/stylesheets/print.less"
|
||||
}
|
||||
}
|
||||
},
|
||||
requirejs: {
|
||||
compile: {
|
||||
options: {
|
||||
name: "app",
|
||||
baseUrl: "theme/javascript/",
|
||||
out: "theme/assets/app.js",
|
||||
preserveLicenseComments: false,
|
||||
optimize: "uglify", //"uglify",
|
||||
include: ["requireLib"],
|
||||
paths: {
|
||||
"jQuery": 'vendors/jquery',
|
||||
"lodash": 'vendors/lodash',
|
||||
"requireLib": 'vendors/require',
|
||||
"Mousetrap": 'vendors/mousetrap',
|
||||
"mixpanel": 'vendors/mixpanel',
|
||||
"lunr": path.join(__dirname, "node_modules/lunr/lunr")
|
||||
},
|
||||
shim: {
|
||||
'jQuery': {
|
||||
exports: '$'
|
||||
},
|
||||
'lodash': {
|
||||
exports: '_'
|
||||
},
|
||||
'Mousetrap': {
|
||||
exports: 'Mousetrap'
|
||||
},
|
||||
'mixpanel': {
|
||||
exports: 'mixpanel'
|
||||
},
|
||||
'lunr': {
|
||||
exports: 'lunr'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Build
|
||||
grunt.registerTask('build', [
|
||||
'less',
|
||||
'requirejs'
|
||||
]);
|
||||
|
||||
grunt.registerTask('default', [
|
||||
'build'
|
||||
]);
|
||||
};
|
||||
@@ -186,7 +186,7 @@ Apache License
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
Copyright 2014 FriendCode Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -198,4 +198,4 @@ Apache License
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
limitations under the License.
|
||||
|
||||
@@ -1,134 +1,51 @@
|
||||
GitBook
|
||||
=======
|
||||
|
||||
[](https://travis-ci.org/GitbookIO/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 programming books and exercises using GitHub/Git and Markdown. You can see an example: [Learn Javascript](http://gitbookio.github.io/javascript/).
|
||||
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 books 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/).
|
||||
|
||||

|
||||
|
||||
## How to use it:
|
||||
## Getting started
|
||||
|
||||
GitBook can be installed from **NPM** using:
|
||||
GitBook can be used either on your computer for building local books or on GitBook.com for hosting them. To get started, check out [the installation instructions in the documentation](docs/setup.md).
|
||||
|
||||
```
|
||||
$ npm install gitbook -g
|
||||
```
|
||||
## Usage examples
|
||||
|
||||
You can serve a repository as a book using:
|
||||
GitBook can be used to create book, public documentation, enterprise manual, thesis, research papers, etc.
|
||||
|
||||
```
|
||||
$ gitbook serve ./repository
|
||||
```
|
||||
You can find a [list of real-world examples](docs/examples.md) in the documentation.
|
||||
|
||||
Or simply build the static website using:
|
||||
## Help and Support
|
||||
|
||||
```
|
||||
$ gitbook build ./repository --output=./outputFolder
|
||||
```
|
||||
We're always happy to help out with your books or any other questions you might have. You can ask a question on the following contact form at [gitbook.com/contact](https://www.gitbook.com/contact) or signal an issue on [GitHub](https://github.com/GitbookIO/gitbook).
|
||||
|
||||
Options for commands `build` and `serve` are:
|
||||
## Features
|
||||
|
||||
```
|
||||
-t, --title <name> Name of the book to generate, defaults to repo name
|
||||
-i, --intro <intro> Description of the book to generate
|
||||
-g, --github <repo_path> ID of github repo like : username/repo
|
||||
-o, --output <directory> Path to output directory, defaults to ./_book
|
||||
-f, --format <name> Change generation format, defaults to site, availables are: site, page, pdf, json
|
||||
-i, --intro <intro> Description of the book to generate
|
||||
--githubHost <url> The url of the github host (defaults to https://github.com/)
|
||||
--theme <path> Path to theme directory
|
||||
```
|
||||
* [Output as a website or ebook (pdf, epub, mobi)](http://help.gitbook.com/format/output.html)
|
||||
* [Multi-Languages](http://help.gitbook.com/format/languages.html)
|
||||
* [Glossary](http://help.gitbook.com/format/glossary.html)
|
||||
* [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)
|
||||
* [Content References](http://help.gitbook.com/format/conrefs.html)
|
||||
* [Plugins](http://help.gitbook.com/format/plugins.html)
|
||||
* [Beautiful default theme](https://github.com/GitbookIO/theme-default)
|
||||
|
||||
## Output Formats
|
||||
## Publish your book
|
||||
|
||||
GitBook can generate your book in the following formats:
|
||||
The platform [GitBook.com](https://www.gitbook.com/) is like an "Heroku for books": you can create a book on it (public, or private) and update it using **git push**.
|
||||
|
||||
* **Static Website**: This is the default format, it generates a complete interactive static website that can be for example hosted on GitHub Pages.
|
||||
* **PDF**: A complete PDF book with exercise solutions at the end of the book. Generate to this format using: ```gitbook pdf ./myrepo```, you need to have [gitbook-pdf](https://github.com/GitbookIO/gitbook-pdf) installed.
|
||||
* **eBook**: A complete eBook with exercise solutions at the end of the book. Generate to this format using: ```gitbook ebook ./myrepo```, you need to have [ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) installed.
|
||||
* **Single Page**: The book will be stored in a single printable HTML page, this format is used for conversion to PDF or eBook. Generate to this format using: ```gitbook build ./myrepo -f page```.
|
||||
* **JSON**: This format is used for debugging or extracting metadata from a book. Generate to this format using: ```gitbook build ./myrepo -f json```.
|
||||
## Licensing
|
||||
|
||||
## Book Format
|
||||
|
||||
A book is a GitHub repository containing at least 2 files: `README.md` and `SUMMARY.md`.
|
||||
|
||||
#### README.md
|
||||
|
||||
As usual, it should contains an introduction for your book. It will be automatically added to the final summary.
|
||||
|
||||
#### SUMMARY.md
|
||||
|
||||
The `SUMMARY.md` defines your book's structure. It should contain a list of chapters, linking to their respective pages.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
# Summary
|
||||
|
||||
This is the summary of my book.
|
||||
|
||||
* [section 1](section1/README.md)
|
||||
* [example 1](section1/example1.md)
|
||||
* [example 2](section1/example2.md)
|
||||
* [section 2](section2/README.md)
|
||||
* [example 1](section2/example1.md)
|
||||
```
|
||||
|
||||
Files that are not included in the `SUMMARY.md` will not be processed by `gitbook`.
|
||||
|
||||
#### Exercises
|
||||
|
||||
A book can contain interactive exercises (currently only in Javascript but Python and Ruby are coming soon ;) ). An exercise is a code challenge provided to the reader, which is given a code editor to write a solution which is checked against the book author's validation code.
|
||||
|
||||
An exercise is defined by 4 simple parts:
|
||||
|
||||
* Exercise **Message**/Goals (in markdown/text)
|
||||
* **Initial** code to show to the user, providing a starting point
|
||||
* **Solution** code, being a correct solution to the exercise
|
||||
* **Validation** code that tests the correctness of the user's input
|
||||
|
||||
Exercises need to start and finish with a separation bar (```---``` or ```***```). It should contain 3 code elements (**base**, **solution** and **validation**). It can contain a 4th element that provides **context** code (functions, imports of libraries etc ... that shouldn't be displayed to the user).
|
||||
|
||||
---
|
||||
|
||||
Define a variable `x` equal to 10.
|
||||
|
||||
```js
|
||||
var x =
|
||||
```
|
||||
|
||||
```js
|
||||
var x = 10;
|
||||
```
|
||||
|
||||
```js
|
||||
assert(x == 10);
|
||||
```
|
||||
|
||||
```js
|
||||
// This is context code available everywhere
|
||||
// The user will be able to call magicFunc in his code
|
||||
function magicFunc() {
|
||||
return 3;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Multi-Languages
|
||||
|
||||
GitBook supports building books written in multiple languages. Each language should be a sub-directory following the normal GitBook format, and a file named `LANGS.md` should be present at the root of the repository with the following format:
|
||||
|
||||
```
|
||||
* [English](en/)
|
||||
* [French](fr/)
|
||||
* [Español](es/)
|
||||
```
|
||||
|
||||
You can see a complete example with the [Learn Git](https://github.com/GitbookIO/git) book.
|
||||
|
||||
#### Ignoring files & folders
|
||||
|
||||
GitBook will read the `.gitignore`, `.bookignore` and `.ignore` files to get a list of files and folders to skip. (The format inside those files, follows the same convention as `.gitignore`)
|
||||
GitBook is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text.
|
||||
|
||||
@@ -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,50 +0,0 @@
|
||||
var path = require('path');
|
||||
var Q = require('q');
|
||||
var _ = require('lodash');
|
||||
|
||||
var utils = require('./utils');
|
||||
|
||||
var generate = require("../lib/generate");
|
||||
var parse = require("../lib/parse");
|
||||
var fs = require('../lib/generate/fs');
|
||||
var generators = require("../lib/generate").generators;
|
||||
|
||||
var makeBuildFunc = function(converter) {
|
||||
return function(dir, options) {
|
||||
dir = dir || process.cwd();
|
||||
outputDir = options.output
|
||||
|
||||
console.log('Starting build ...');
|
||||
// Get repo's URL
|
||||
return utils.gitURL(dir)
|
||||
.then(function(url) {
|
||||
// Get ID of repo
|
||||
return utils.githubID(url);
|
||||
}, function(err) {
|
||||
return null;
|
||||
})
|
||||
.then(function(repoID) {
|
||||
return converter(
|
||||
_.extend(options || {}, {
|
||||
input: dir,
|
||||
output: outputDir,
|
||||
title: options.title,
|
||||
description: options.intro,
|
||||
github: options.github || repoID,
|
||||
githubHost: options.githubHost,
|
||||
generator: options.format,
|
||||
theme: options.theme
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function(output) {
|
||||
console.log("Successfuly built !");
|
||||
return output;
|
||||
}, utils.logError);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
folder: makeBuildFunc(generate.folder),
|
||||
file: makeBuildFunc(generate.file)
|
||||
};
|
||||
+5
-83
@@ -1,86 +1,8 @@
|
||||
#! /usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
var Q = require('q');
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var prog = require('commander');
|
||||
var color = require('bash-color');
|
||||
|
||||
var pkg = require('../package.json');
|
||||
var generators = require("../lib/generate").generators;
|
||||
var initDir = require("../lib/generate/init");
|
||||
var fs = require('../lib/generate/fs');
|
||||
|
||||
var utils = require('./utils');
|
||||
var build = require('./build');
|
||||
|
||||
// General options
|
||||
prog
|
||||
.version(pkg.version);
|
||||
|
||||
var buildCommand = function(command) {
|
||||
return command
|
||||
.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('-t, --title <name>', 'Name of the book to generate, default is extracted from readme')
|
||||
.option('-i, --intro <intro>', 'Description of the book to generate, default is extracted from readme')
|
||||
.option('-g, --github <repo_path>', 'ID of github repo like : username/repo')
|
||||
.option('--githubHost <url>', 'The url of the github host (defaults to https://github.com/')
|
||||
.option('--theme <path>', 'Path to theme directory');
|
||||
};
|
||||
|
||||
buildCommand(prog.command('build [source_dir]'))
|
||||
.description('Build a gitbook from a directory')
|
||||
.action(build.folder);
|
||||
|
||||
buildCommand(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)
|
||||
.action(function(dir, options) {
|
||||
build.folder(dir, options || {})
|
||||
.then(function(_options) {
|
||||
console.log();
|
||||
console.log('Starting server ...');
|
||||
return utils.serveDir(_options.output, options.port)
|
||||
.fail(utils.logError);
|
||||
})
|
||||
.then(function() {
|
||||
console.log('Serving book on http://localhost:'+options.port);
|
||||
console.log();
|
||||
console.log('Press CTRL+C to quit ...');
|
||||
});
|
||||
});
|
||||
|
||||
buildCommand(prog.command('pdf [source_dir]'))
|
||||
.description('Build a gitbook as a PDF')
|
||||
.option('-pf, --paperformat <format>', 'PDF paper format (default is A4): "5in*7.5in", "10cm*20cm", "A4", "Letter"')
|
||||
.action(function(dir, options) {
|
||||
build.file(dir, _.extend(options, {
|
||||
extension: "pdf",
|
||||
format: "pdf"
|
||||
}));
|
||||
});
|
||||
|
||||
buildCommand(prog.command('ebook [source_dir]'))
|
||||
.description('Build a gitbook as a eBook')
|
||||
.option('-c, --cover <path>', 'Cover image, default is cover.png if exists')
|
||||
.action(function(dir, options) {
|
||||
var ext = options.output ? path.extname(options.output) : "epub";
|
||||
|
||||
build.file(dir, _.extend(options, {
|
||||
extension: ext,
|
||||
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);
|
||||
});
|
||||
|
||||
// Parse and fallback to help if no args
|
||||
if(_.isEmpty(prog.parse(process.argv).args) && process.argv.length === 2) {
|
||||
prog.help();
|
||||
}
|
||||
console.log(color.red('You need to install "gitbook-cli" to have access to the gitbook command anywhere on your system.'));
|
||||
console.log(color.red('If you\'ve installed this package globally, you need to uninstall it.'));
|
||||
console.log(color.red('>> Run "npm uninstall -g gitbook" then "npm install -g gitbook-cli"'));
|
||||
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
var Q = require('q');
|
||||
var _ = require('lodash');
|
||||
|
||||
var http = require('http');
|
||||
var send = require('send');
|
||||
|
||||
var url = require('url');
|
||||
var cp = require('child_process');
|
||||
|
||||
|
||||
// Get the remote of a given repo
|
||||
function gitURL(path) {
|
||||
var d = Q.defer();
|
||||
|
||||
cp.exec("git config --get remote.origin.url", {
|
||||
cwd: path,
|
||||
env: process.env,
|
||||
}, function(err, stdout, stderr) {
|
||||
if(err) {
|
||||
return d.reject(err);
|
||||
}
|
||||
|
||||
return d.resolve(stdout);
|
||||
});
|
||||
|
||||
return d.promise
|
||||
.then(function(output) {
|
||||
return output.replace(/(\r\n|\n|\r)/gm, "");
|
||||
});
|
||||
}
|
||||
|
||||
// Poorman's parsing
|
||||
// Parse a git URL to a github ID : username/reponame
|
||||
function githubID(_url) {
|
||||
// Remove .git if it's in _url
|
||||
var sliceEnd = _url.slice(-4) === '.git' ? -4 : _url.length;
|
||||
|
||||
// Detect HTTPS repos
|
||||
var parsed = url.parse(_url);
|
||||
if(parsed.protocol === 'https:' && parsed.host === 'github.com') {
|
||||
return parsed.path.slice(1, sliceEnd);
|
||||
}
|
||||
|
||||
// Detect SSH repos
|
||||
if(_url.indexOf('git@') === 0) {
|
||||
return _url.split(':', 2)[1].slice(0, sliceEnd);
|
||||
}
|
||||
|
||||
// None found
|
||||
return null;
|
||||
}
|
||||
|
||||
function titleCase(str)
|
||||
{
|
||||
return str.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
|
||||
}
|
||||
|
||||
function serveDir(dir, port) {
|
||||
var d = Q.defer();
|
||||
|
||||
var server = http.createServer(function(req, res){
|
||||
// Render error
|
||||
function error(err) {
|
||||
res.statusCode = err.status || 500;
|
||||
res.end(err.message);
|
||||
}
|
||||
|
||||
// Redirect to directory's index.html
|
||||
function redirect() {
|
||||
res.statusCode = 301;
|
||||
res.setHeader('Location', req.url + '/');
|
||||
res.end('Redirecting to ' + req.url + '/');
|
||||
}
|
||||
|
||||
// Send file
|
||||
send(req, url.parse(req.url).pathname)
|
||||
.root(dir)
|
||||
.on('error', error)
|
||||
.on('directory', redirect)
|
||||
.pipe(res);
|
||||
}).listen(port);
|
||||
|
||||
d.resolve(server);
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
function logError(err) {
|
||||
console.log(err.stack || err.message || err);
|
||||
return Q.reject(err);
|
||||
};
|
||||
|
||||
|
||||
// Exports
|
||||
module.exports = {
|
||||
gitURL: gitURL,
|
||||
githubID: githubID,
|
||||
titleCase: titleCase,
|
||||
serveDir: serveDir,
|
||||
logError: logError
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
# GitBook Format Documentation
|
||||
|
||||
> This documentation is for GitBook version **{{ book.version }}**
|
||||
|
||||
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). This documentation has been generated using GitBook.
|
||||
|
||||
GitBook can output your content as a website ([customizable](themes.md) and [extensibles](plugins.md)) or as an ebook (PDF, ePub or Mobi).
|
||||
|
||||
[GitBook.com](https://www.gitbook.com) is the online platform to create and host books built using the GitBook format. It offers hosting, collaboration features and an [easy-to-use editor](https://www.gitbook.com/editor).
|
||||
|
||||
### Help and Support
|
||||
|
||||
We're always happy to help out with your books or any other questions you might have. You can ask a question on the following contact form at [gitbook.com/contact](https://www.gitbook.com/contact) or signal an issue on [GitHub](https://github.com/GitbookIO/gitbook).
|
||||
|
||||
### FAQ
|
||||
|
||||
There are questions that are asked quite often, [check this out before creating an issue](faq.md).
|
||||
|
||||
### Contribute to this documentation
|
||||
|
||||
You can contribute to improve this documentation on [GitHub](https://github.com/GitbookIO/gitbook) by signaling issues or proposing changes.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Summary
|
||||
|
||||
### Getting Started
|
||||
|
||||
* [About this documentation](README.md)
|
||||
* [Installation and Setup](setup.md)
|
||||
|
||||
### Your Content
|
||||
|
||||
* [Directory structure](structure.md)
|
||||
* [Pages and Summary](pages.md)
|
||||
* [Glossary](lexicon.md)
|
||||
* [Multi-Lingual](languages.md)
|
||||
* [Configuration](config.md)
|
||||
* [AsciiDoc](asciidoc.md)
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* [Templating](templating.md)
|
||||
* [Content References](conrefs.md)
|
||||
* [Variables](variables.md)
|
||||
|
||||
### Customization
|
||||
|
||||
* [Plugins](plugins.md)
|
||||
* [Theming](themes.md)
|
||||
|
||||
--
|
||||
|
||||
* [FAQ](faq.md)
|
||||
* [Release notes](https://github.com/GitbookIO/gitbook/blob/master/CHANGES.md)
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends template.theme %}
|
||||
|
||||
{% block header_nav %}
|
||||
<a href="https://github.com/GitbookIO/gitbook/blob/master/docs/{{ file.path }}" target="_blank" class="btn btn-link pull-right hidden-xs">
|
||||
<i class="octicon octicon-mark-github"></i> Edit on GitHub
|
||||
</a>
|
||||
<a href="https://github.com/GitbookIO/gitbook/blob/master/CHANGES.md" target="_blank" class="btn btn-link pull-right hidden-xs">
|
||||
{{ book.version }}
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block page %}
|
||||
{{ super() }}
|
||||
<hr>
|
||||
<div class="btn-group btn-group-justified">
|
||||
{% if page.previous and page.previous.path %}
|
||||
<a class="btn" href="{{ page.previous.path|resolveFile }}"><b>Previous:</b> {{ page.previous.title }}</a>
|
||||
{% endif %}
|
||||
{% if page.next and page.next.path %}
|
||||
<a class="btn" href="{{ page.next.path|resolveFile }}"><b>Next:</b> {{ page.next.title }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,60 @@
|
||||
# AsciiDoc
|
||||
|
||||
Since version `2.0.0`, GitBook can also accept AsciiDoc as an input format.
|
||||
|
||||
Please refer to the [AsciiDoc Syntax Quick Reference](http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/) for more informations about the format.
|
||||
|
||||
Just like for markdown, GitBook is using some special files to extract structures: `README.adoc`, `SUMMARY.adoc`, `LANGS.adoc` and `GLOSSARY.adoc`.
|
||||
|
||||
### README.adoc
|
||||
|
||||
This is the main entry of your book: the introduction. This file is **non optional**.
|
||||
|
||||
### SUMMARY.adoc
|
||||
|
||||
This file defines the list of chapters and subchapters. Just like [for markdown](./pages.md), the `SUMMARY.adoc`'s format is simply a list of links, the name of the link is used as the chapter's name, and the target is a path to that chapter's file.
|
||||
|
||||
Subchapters are defined simply by adding a nested list to a parent chapter.
|
||||
|
||||
```asciidoc
|
||||
= Summary
|
||||
|
||||
. link:chapter-1/README.adoc[Chapter 1]
|
||||
.. link:chapter-1/ARTICLE1.adoc[Article 1]
|
||||
.. link:chapter-1/ARTICLE2.adoc[Article 2]
|
||||
... link:chapter-1/ARTICLE-1-2-1.adoc[Article 1.2.1]
|
||||
. link:chapter-2/README.adoc[Chapter 2]
|
||||
. link:chapter-3/README.adoc[Chapter 3]
|
||||
. link:chapter-4/README.adoc[Chapter 4]
|
||||
.. Unfinished article
|
||||
. Unfinished Chapter
|
||||
```
|
||||
|
||||
### LANGS.adoc
|
||||
|
||||
For [Multi-Languages](./languages.md) books, this file is used to define the different supported languages and translations.
|
||||
|
||||
This file is following the same syntax as the `SUMMARY.adoc`:
|
||||
|
||||
```asciidoc
|
||||
= Languages
|
||||
|
||||
. link:en/[English]
|
||||
. link:fr/[French]
|
||||
```
|
||||
|
||||
### GLOSSARY.adoc
|
||||
|
||||
This file is used to define terms. [See the glossary section](./lexicon.md).
|
||||
|
||||
```asciidoc
|
||||
= Glossary
|
||||
|
||||
== Magic
|
||||
Sufficiently advanced technology, beyond the understanding of the observer producing a sense of wonder.
|
||||
|
||||
== PHP
|
||||
An atrocious language, invented for the sole purpose of inflicting pain and suffering amongst the programming wizards of this world.
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
var pkg = require('../package.json');
|
||||
|
||||
module.exports = {
|
||||
title: 'GitBook Documentation',
|
||||
|
||||
plugins: ['theme-official'],
|
||||
theme: 'official',
|
||||
variables: {
|
||||
version: pkg.version
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
# Configuration
|
||||
|
||||
GitBook allows you to customize your book using a flexible configuration. These options are specified in a `book.json` file.
|
||||
|
||||
### Configuration Settings
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `title` | Title of your book, default value is extracted from the README. On GitBook.com this field is pre-filled. |
|
||||
| `description` | Description of your book, default value is extracted from the README. On GitBook.com this field is pre-filled. |
|
||||
| `author` | Name of the author. On GitBook.com this field is pre-filled. |
|
||||
| `isbn` | ISBN of the book |
|
||||
| `language` | ISO code of the book's language, default value is `en` |
|
||||
| `direction` | `rtl` or `ltr`, default value depends on the value of `language` |
|
||||
| `gitbook` | [SemVer](http://semver.org) condition to validate which GitBook version should be used |
|
||||
| `plugins` | List of plugins to load, See [the plugins section](plugins.md) for more details |
|
||||
| `pluginsConfig` |Configuration for plugins, See [the plugins section](plugins.md) for more details |
|
||||
|
||||
### PDF Options
|
||||
|
||||
PDF Output can be customized using a set of options in the `book.json`:
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `pdf.pageNumbers` | Add page numbers to the bottom of every page (default is `true`) |
|
||||
| `pdf.fontSize` | Base font size (default is `12`) |
|
||||
| `pdf.fontFamily` | Base font family (default is `Arial`) |
|
||||
| `pdf.paperSize` | Paper size, options are `'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'legal', 'letter'` (default is `a4`) |
|
||||
| `pdf.margin.top` | Top margin (default is `56`) |
|
||||
| `pdf.margin.bottom` | Bottom margin (default is `56`) |
|
||||
| `pdf.margin.right` | Right margin (default is `62`) |
|
||||
| `pdf.margin.left` | Left margin (default is `62`) |
|
||||
|
||||
### Plugins
|
||||
|
||||
Plugins and their configurations are specified in the `book.json`. See [the plugins section](plugins.md) for more details.
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Content References
|
||||
|
||||
Content referencing (conref) is a convenient mechanism for reuse of content from other files or books.
|
||||
|
||||
### Importing local files
|
||||
|
||||
Importing an other file's content is really easy using the `include` tag:
|
||||
|
||||
```
|
||||
{% include "./test.md" %}
|
||||
```
|
||||
|
||||
### Importing file from another book
|
||||
|
||||
GitBook can also resolve the include path by using git:
|
||||
|
||||
```
|
||||
{% include "git+https://github.com/GitbookIO/documentation.git/README.md#0.0.1" %}
|
||||
```
|
||||
|
||||
The format of git url is:
|
||||
|
||||
```
|
||||
git+https://user@hostname/project/blah.git/file#commit-ish
|
||||
```
|
||||
|
||||
The real git url part should finish with `.git`, the filename to import is extracted after the `.git` till the fragment of the url.
|
||||
|
||||
The `commit-ish` can be any tag, sha, or branch which can be supplied as an argument to `git checkout`. The default is `master`.
|
||||
|
||||
### Inheritance
|
||||
|
||||
Template inheritance is a way to make it easy to reuse templates. When writing a template, you can define "blocks" that child templates can override. The inheritance chain can be as long as you like.
|
||||
|
||||
`block` defines a section on the template and identifies it with a name. Base templates can specify blocks and child templates can override them with new content.
|
||||
|
||||
```
|
||||
{% extends "./mypage.md" %}
|
||||
|
||||
{% block pageContent %}
|
||||
# This is my page content
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
In the file `mypage.md`, you should specify the blocks that can be extent:
|
||||
|
||||
```
|
||||
{% block pageContent %}
|
||||
This is the default content
|
||||
{% endblock %}
|
||||
|
||||
# License
|
||||
|
||||
{% import "./LICENSE" %}
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
description: Real world examples of content published using GitBook.
|
||||
---
|
||||
|
||||
# Examples
|
||||
|
||||
More than 50,000 books have benn published on [GitBook.com](https://www.gitbook.com/explore).
|
||||
|
||||
### Books
|
||||
|
||||
- [Front-end Handbook](https://www.gitbook.com/book/frontendmasters/front-end-handbook/details) by [Cody Lindley](http://codylindley.com)
|
||||
- [How to make an Operating System](https://www.gitbook.com/book/samypesse/how-to-create-an-operating-system/details) by [@SamyPesse](https://github.com/SamyPesse)
|
||||
- [Building Web Apps with Go](https://www.gitbook.com/book/codegangsta/building-web-apps-with-go/details) by [@codegangsta](https://github.com/codegangsta)
|
||||
- [Django Girls Tutorial](http://tutorial.djangogirls.org/en/index.html) by [Django Girls](https://djangogirls.org)
|
||||
- [Linux Inside](https://www.gitbook.com/book/0xax/linux-insides/details) by [0xAX](https://twitter.com/0xAX)
|
||||
- [Learn Javascript](https://www.gitbook.com/book/gitbookio/javascript/details) by [GitBook](https://twitter.com/GitbookIO)
|
||||
|
||||
### Research Papers
|
||||
|
||||
- [TowCenter Collection](https://www.gitbook.com/@towcenter) by [Columbia Journalism School]()
|
||||
- [Block Relaxation Algorithms in Statistics](https://www.gitbook.com/@jandeleeuw) by Jan de Leeuw
|
||||
|
||||
### Documentation
|
||||
|
||||
- [DuckDuckHack Documentation](http://docs.duckduckhack.com) by DuckDuckGo
|
||||
- This documentation
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# GitBook FAQ
|
||||
|
||||
#### How can I host/publish my book?
|
||||
|
||||
Books can easily be published and hosted on [GitBook.com](https://www.gitbook.com). But GitBook output can be hosted on any static file hosting solution.
|
||||
|
||||
#### What can I use to edit my content?
|
||||
|
||||
Any text editor should work! But we advise using the [GitBook Editor](https://www.gitbook.com/editor). [GitBook.com](https://www.gitbook.com) also provides a web version of this editor.
|
||||
|
||||
---
|
||||
|
||||
#### Should I use an `.html` or `.md` extensions in my links?
|
||||
|
||||
You should always use `.md` extensions for your relative links, GitBook will automatically replace these links by the right value when the pointing file is referenced in the Table of Contents.
|
||||
|
||||
#### Can I create a GitBook in a sub-directory of my repository?
|
||||
|
||||
Yes, GitBooks can be created in [sub-directories](structure.md#subdirectory). GitBook.com and the CLI also looks by default in a serie of [folders](structure.md).
|
||||
|
||||
---
|
||||
|
||||
#### Does GitBook support Math equations?
|
||||
|
||||
GitBook supports math equations and TeX thanks to plugins. There are currently 2 official plugins to display math: [mathjax](https://plugins.gitbook.com/plugin/mathjax) and [katex](https://plugins.gitbook.com/plugin/katex).
|
||||
|
||||
#### Can I customize/theme the output?
|
||||
|
||||
Yes, both the website and ebook outputs can be customized using [themes](themes.md).
|
||||
|
||||
#### Can I add interactive content (videos, etc)?
|
||||
|
||||
GitBook is very [extensible](plugins.md). You can use [existing plugins](https://plugins.gitbook.com) or create your own!
|
||||
@@ -0,0 +1,15 @@
|
||||
# Multi-Languages
|
||||
|
||||
GitBook supports building books written in multiple languages. Each language should be a sub-directory following the normal GitBook format, and a file named `LANGS.md` should be present at the root of the repository with the following format:
|
||||
|
||||
```markdown
|
||||
* [English](en/)
|
||||
* [French](fr/)
|
||||
* [Español](es/)
|
||||
```
|
||||
|
||||
### Configuration for each language
|
||||
|
||||
When a language book (ex: `en`) has a `book.json`, its configuration will extend the main configuration.
|
||||
|
||||
The only exception is plugins, plugins are specify globally relative to the book, and language specific plugins can not be specified.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Glossary
|
||||
|
||||
Allows you to specify terms and their respective definitions to be displayed as annotations. Based on those terms, gitbook will automatically build an index and highlight those terms in pages.
|
||||
|
||||
The `GLOSSARY.md` format is very simple :
|
||||
|
||||
```markdown
|
||||
# Term
|
||||
Definition for this term
|
||||
|
||||
# Another term
|
||||
With it's definition, this can contain bold text and all other kinds of inline markup ...
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
# Pages and Summary
|
||||
|
||||
GitBook uses a `SUMMARY.md` file to define the structure of chapters and subchapters of the book. The `SUMMARY.md` file is used to generate the book's table of contents.
|
||||
|
||||
### Summary
|
||||
|
||||
The `SUMMARY.md`'s format is simply a list of links, the title of the link is used as the chapter's title, and the target is a path to that chapter's file.
|
||||
|
||||
Subchapters are defined simply by adding a nested list to a parent chapter.
|
||||
|
||||
##### Simple example
|
||||
|
||||
```markdown
|
||||
# Summary
|
||||
|
||||
* [Part I](part1/README.md)
|
||||
* [Writing is nice](part1/writing.md)
|
||||
* [GitBook is nice](part1/gitbook.md)
|
||||
* [Part II](part2/README.md)
|
||||
* [We love feedback](part2/feedback_please.md)
|
||||
* [Better tools for authors](part2/better_tools.md)
|
||||
```
|
||||
|
||||
##### Example with subchapters split into parts
|
||||
|
||||
```markdown
|
||||
# Summary
|
||||
|
||||
### Part 1
|
||||
|
||||
* [Writing is nice](part1/writing.md)
|
||||
* [GitBook is nice](part1/gitbook.md)
|
||||
|
||||
### Part 2
|
||||
|
||||
* [We love feedback](part2/feedback_please.md)
|
||||
* [Better tools for authors](part2/better_tools.md)
|
||||
```
|
||||
|
||||
### Front Matter
|
||||
|
||||
Pages can contain an optional front matter. It can be used to define the page's description. The front matter must be the first thing in the file and must take the form of valid YAML set between triple-dashed lines. Here is a basic example:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: This is a short description of my page
|
||||
---
|
||||
|
||||
# The content of my page
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Plugins
|
||||
|
||||
Plugins are the best way to extend GitBook functionalities (ebook and website). There exist plugins to do a lot of things: bring math formulas display support, track visits using Google Analytic, etc.
|
||||
|
||||
### How to find plugins?
|
||||
|
||||
Plugins can be easily searched on [plugins.gitbook.com](https://plugins.gitbook.com).
|
||||
|
||||
|
||||
### How to install a plugin?
|
||||
|
||||
Once you find a plugin that you want to install, you need to add it to your `book.json`:
|
||||
|
||||
```
|
||||
{
|
||||
"plugins": ["myPlugin", "anotherPlugin"]
|
||||
}
|
||||
```
|
||||
|
||||
You can also specify a specific version using: `"myPlugin@0.3.1"`. By default GitBook will resolve the latest version of the plugin compatbile with the current GitBook version.
|
||||
|
||||
Plugins are automatically installed on [GitBook.com](https://www.gitbook.com). Locally, run `gitbook install` to install and prepare all plugins for your books.
|
||||
|
||||
### Configuring plugins
|
||||
|
||||
PLugins specific configurations are stored in `pluginsConfig`. You have to refer to the documentation of the plugin itself for details about the available options.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Setup and Installation of GitBook
|
||||
|
||||
Getting GitBook installed and ready-to-go should only take a few minutes.
|
||||
|
||||
### GitBook.com
|
||||
|
||||
[GitBook.com](https://www.gitbook.com) is an easy to use solution to write, publish and host books. It's best and easier solution for publishing your content and collaborate on it.
|
||||
|
||||
It integrates well with the [GitBook Editor](https://www.gitbook.com/editor).
|
||||
|
||||
### Local Installation
|
||||
|
||||
##### Requirements
|
||||
|
||||
Installing GitBook is easy and straight-forward, but there are a few requirements you’ll need to make sure your system has before you start.
|
||||
|
||||
* NodeJS (v4.0.0 and above are adviced)
|
||||
* Windows, Linux, Unix, or Mac OS X
|
||||
|
||||
##### Install with NPM
|
||||
|
||||
The best way to install GitBook is via **NPM**. At the terminal prompt, simply run the following command to install GitBook:
|
||||
|
||||
```
|
||||
$ npm install gitbook-cli -g
|
||||
```
|
||||
|
||||
`gitbook-cli` is an utility to install and use multiple versions of GitBook on the same system. It will automatically install the required version to build a book.
|
||||
|
||||
##### Create a book
|
||||
|
||||
GitBook can setup a boilerplate book:
|
||||
|
||||
```
|
||||
$ gitbook init
|
||||
```
|
||||
|
||||
If you wish to create the book into a new directory, you can do so by running `gitbook init ./directory`
|
||||
|
||||
Preview and serve your book using:
|
||||
|
||||
```
|
||||
$ gitbook serve
|
||||
```
|
||||
|
||||
or build the static website using:
|
||||
|
||||
```
|
||||
$ gitbook build
|
||||
```
|
||||
|
||||
##### Install pre-releases
|
||||
|
||||
`gitbook-cli` makes it easy to install and test other versions of GitBook with your book:
|
||||
|
||||
```
|
||||
$ gitbook fetch beta
|
||||
```
|
||||
|
||||
Use `gitbook ls-remote` to list remote versions available for install.
|
||||
|
||||
##### Debugging
|
||||
|
||||
You can use the options `--log=debug` and `--debug` to get better error messages (with stack trace). For example:
|
||||
|
||||
```
|
||||
$ gitbook build ./ --log=debug --debug
|
||||
```
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Directory structure
|
||||
|
||||
GitBook uses a very simple and obvious directory sttructure:
|
||||
|
||||
```
|
||||
.
|
||||
├── book.json
|
||||
├── README.md
|
||||
├── SUMMARY.md
|
||||
├── chapter-1/
|
||||
| ├── README.md
|
||||
| └── something.md
|
||||
└── chapter-2/
|
||||
├── README.md
|
||||
└── something.md
|
||||
```
|
||||
|
||||
An overview of what each of these does:
|
||||
|
||||
| File | Description |
|
||||
| -------- | ----------- |
|
||||
| `book.json` | Stores [configuration](config.md) data (__optional__) |
|
||||
| `README.md` | Preface / Introduction for your book (**required**) |
|
||||
| `SUMMARY.md` | Table of Contents |
|
||||
|
||||
|
||||
### Static files and Images
|
||||
|
||||
A static file is a file that is not listed in the `SUMMARY.md`. All static files, not [ignored](#ignore), are copied to the output.
|
||||
|
||||
### Ignoring files & folders {#ignore}
|
||||
|
||||
GitBook will read the `.gitignore`, `.bookignore` and `.ignore` files to get a list of files and folders to skip.
|
||||
The format inside those files, follows the same convention as `.gitignore`:
|
||||
|
||||
```markdown
|
||||
# This is a comment
|
||||
|
||||
# Ignore the file test.md
|
||||
test.md
|
||||
|
||||
# Ignore everything in the directory "bin"
|
||||
bin/*
|
||||
```
|
||||
|
||||
### Project documentation / Sub-directory {#subdirectory}
|
||||
|
||||
For project documentaiton, it sometimes better to use a diretcory (like `docs/`) to store the prject's documentation. You can use a `.gitbook` file to indicate to GitBook in which folder the book is stored:
|
||||
|
||||
```
|
||||
.
|
||||
├── .gitbook
|
||||
└── docs/
|
||||
├── README.md
|
||||
└── SUMMARY.md
|
||||
```
|
||||
|
||||
With `.gitbook` containing:
|
||||
|
||||
```
|
||||
./docs/
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Templating
|
||||
|
||||
GitBook uses the Nunjucks templating language to process pages and theme's templates.
|
||||
|
||||
The Nunjucks syntax is very similar to **Jinja2** or **Liquid**.
|
||||
|
||||
### Variables
|
||||
|
||||
A variable looks up a value from the template context. If you wanted to simply display a variable, you would do:
|
||||
|
||||
```twig
|
||||
{{ username }}
|
||||
```
|
||||
|
||||
This looks up username from the context and displays it. Variable names can have dots in them which lookup properties, just like javascript. You can also use the square bracket syntax.
|
||||
|
||||
```twig
|
||||
{{ foo.bar }}
|
||||
{{ foo["bar"] }}
|
||||
```
|
||||
|
||||
If a value is undefined, nothing is displayed. The following all output nothing if foo is undefined: `{{ foo }}`, `{{ foo.bar }}`, `{{ foo.bar.baz }}`.
|
||||
|
||||
GitBook provides a set of [context variables](variables.md).
|
||||
|
||||
### Filters
|
||||
|
||||
Filters are essentially functions that can be applied to variables. They are called with a pipe operator (`|`) and can take arguments.
|
||||
|
||||
```twig
|
||||
{{ foo | title }}
|
||||
{{ foo | join(",") }}
|
||||
{{ foo | replace("foo", "bar") | capitalize }}
|
||||
```
|
||||
|
||||
The third example shows how you can chain filters. It would display "Bar", by first replacing "foo" with "bar" and then capitalizing it.
|
||||
|
||||
### Tags
|
||||
|
||||
##### if
|
||||
|
||||
`if` tests a condition and lets you selectively display content. It behaves exactly as javascript's if behaves.
|
||||
|
||||
```twig
|
||||
{% if variable %}
|
||||
It is true
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
If variable is defined and evaluates to true, "It is true" will be displayed. Otherwise, nothing will be.
|
||||
|
||||
You can specify alternate conditions with elif and else:
|
||||
|
||||
```twig
|
||||
{% if hungry %}
|
||||
I am hungry
|
||||
{% elif tired %}
|
||||
I am tired
|
||||
{% else %}
|
||||
I am good!
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
##### for
|
||||
|
||||
`for` iterates over arrays and dictionaries.
|
||||
|
||||
```twig
|
||||
# Chapters about GitBook
|
||||
|
||||
{% for article in glossary.terms['gitbook'].articles %}
|
||||
* [{{ article.title }}]({{ article.path }})
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
##### set
|
||||
|
||||
`set` lets you create/modify a variable.
|
||||
|
||||
```twig
|
||||
{% set softwareVersion = "1.0.0" %}
|
||||
|
||||
Current version is {{ softwareVersion }}.
|
||||
[Download it](website.com/download/{{ softwareVersion }})
|
||||
```
|
||||
|
||||
##### include and block
|
||||
|
||||
Inclusion and inheritance is detailled in the [ConRefs](conrefs.md) section.
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Theming
|
||||
|
||||
Since version 3.0.0, GitBook can be easily themed. Books are using by default the [theme-default](https://github.com/GitbookIO/theme-default).
|
||||
|
||||
The theme to use is specified in the [book's configuration](config.md) using key `theme`.
|
||||
|
||||
> **Caution**: Custom theming can block some plugins from working correctly.
|
||||
|
||||
### Structure of a theme
|
||||
|
||||
A theme is a folder containing templates and assets. All the templates are optionnal, since theme are always extending the default theme.
|
||||
|
||||
| Folder | Description |
|
||||
| -------- | ----------- |
|
||||
| `_layouts` | Main folder containing all the templates |
|
||||
| `_layouts/website/page.html` | Template for a normal page |
|
||||
| `_layouts/ebook/page.html` | Template for a normal page during ebook generation (PDF< ePub, Mobi) |
|
||||
|
||||
|
||||
### Extend/Customize theme in a book
|
||||
|
||||
Authors can extend the templates of a theme directly from the book source (without creating an external theme). Templates will be resolved in the `_layouts` folder of the book first, then in
|
||||
|
||||
### Publish a theme
|
||||
|
||||
Themes are published as plugins ([see related docs](plugins.md)) with a `theme-` prefix. For example the theme `awesome` will be loaded from `theme-awesome` plugin, and then from `gitbook-plugin-theme-awesome` NPM package.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Variables
|
||||
|
||||
The following is a reference of the available data during book's parsing and theme generation.
|
||||
|
||||
### Global Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `book` | Bookwide information + configuration settings from `book.json`. See below for details. |
|
||||
| `gitbook` | GitBook specific information |
|
||||
| `page` | Current page specific information |
|
||||
| `file` | File associated with the current page specific information |
|
||||
| `summary` | Information about the table of contents |
|
||||
| `languages` | List of languages for multi-lingual books |
|
||||
| `config` | Dump of the `book.json` |
|
||||
|
||||
### Book Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `book.[CONFIGURATION_DATA]` | All the `variables` set via the `book.json` are available through the book variable. |
|
||||
| `book.language` | Current language for a multilingual book |
|
||||
|
||||
### GitBook Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `gitbook.time` | The current time (when you run the `gitbook` command). |
|
||||
| `gitbook.version` | Version of GitBook used to generate the book |
|
||||
|
||||
### File Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `file.path` | The path to the raw page |
|
||||
| `file.mtime` | Modified Time, Time when file data last modified |
|
||||
| `file.type` | The name of the parser used to compile this file (ex: `markdown`, `asciidoc`, etc) |
|
||||
|
||||
#### Page Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `page.title` | Title of the page |
|
||||
| `page.previous` | Previous page in the Table of Contents (can be `null`) |
|
||||
| `page.next` | Next page in the Table of Contents (can be `null`) |
|
||||
| `page.dir` | Text direction, based on configuration or detected from content (`rtl` or `ltr`) |
|
||||
|
||||
#### Table of Contents Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `summary.parts` | List of sections in the Table of Contents |
|
||||
|
||||
Thw whole table of contents (`SUMMARY.md`) can be accessed:
|
||||
|
||||
`summary.parts[0].articles[0].title` will return the title of the first article.
|
||||
|
||||
#### Multi-lingual book Variable
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `languages.list` | List of languages for this book |
|
||||
|
||||
Languages are defined by `{ id: 'en', title: 'English' }`.
|
||||
@@ -0,0 +1,69 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
function BackboneFile(book) {
|
||||
if (!(this instanceof BackboneFile)) return new BackboneFile(book);
|
||||
|
||||
this.book = book;
|
||||
this.log = this.book.log;
|
||||
|
||||
// Filename in the book
|
||||
this.path = '';
|
||||
this.parser;
|
||||
|
||||
_.bindAll(this);
|
||||
}
|
||||
|
||||
// Type of the backbone file
|
||||
BackboneFile.prototype.type = '';
|
||||
|
||||
// Parse a backbone file
|
||||
BackboneFile.prototype.parse = function() {
|
||||
// To be implemented by each child
|
||||
};
|
||||
|
||||
// Handle case where file doesn't exists
|
||||
BackboneFile.prototype.parseNotFound = function() {
|
||||
|
||||
};
|
||||
|
||||
// Return true if backbone file exists
|
||||
BackboneFile.prototype.exists = function() {
|
||||
return Boolean(this.path);
|
||||
};
|
||||
|
||||
// Locate a backbone file, could be .md, .asciidoc, etc
|
||||
BackboneFile.prototype.locate = function() {
|
||||
var that = this;
|
||||
var filename = this.book.config.getStructure(this.type, true);
|
||||
this.log.debug.ln('locating', this.type, ':', filename);
|
||||
|
||||
return this.book.findParsableFile(filename)
|
||||
.then(function(result) {
|
||||
if (!result) return;
|
||||
|
||||
that.path = result.path;
|
||||
that.parser = result.parser;
|
||||
});
|
||||
};
|
||||
|
||||
// Read and parse the file
|
||||
BackboneFile.prototype.load = function() {
|
||||
var that = this;
|
||||
this.log.debug.ln('loading', this.type, ':', that.path);
|
||||
|
||||
return this.locate()
|
||||
.then(function() {
|
||||
if (!that.path) return that.parseNotFound();
|
||||
|
||||
that.log.debug.ln(that.type, 'located at', that.path);
|
||||
|
||||
return that.book.readFile(that.path)
|
||||
|
||||
// Parse it
|
||||
.then(function(content) {
|
||||
return that.parse(content);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = BackboneFile;
|
||||
@@ -0,0 +1,99 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
var BackboneFile = require('./file');
|
||||
|
||||
// Normalize a glossary entry name into a unique id
|
||||
function nameToId(name) {
|
||||
return name.toLowerCase()
|
||||
.replace(/[\/\\\?\%\*\:\;\|\"\'\\<\\>\#\$\(\)\!\.\@]/g, '')
|
||||
.replace(/ /g, '_')
|
||||
.trim();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
A glossary entry is represented by a name and a short description
|
||||
An unique id for the entry is generated using its name
|
||||
*/
|
||||
function GlossaryEntry(name, description) {
|
||||
if (!(this instanceof GlossaryEntry)) return new GlossaryEntry(name, description);
|
||||
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
|
||||
Object.defineProperty(this, 'id', {
|
||||
get: _.bind(this.getId, this)
|
||||
});
|
||||
}
|
||||
|
||||
// Normalizes a glossary entry's name to create an ID
|
||||
GlossaryEntry.prototype.getId = function() {
|
||||
return nameToId(this.name);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
A glossary is a list of entries stored in a GLOSSARY.md file
|
||||
*/
|
||||
function Glossary() {
|
||||
BackboneFile.apply(this, arguments);
|
||||
|
||||
this.entries = [];
|
||||
}
|
||||
util.inherits(Glossary, BackboneFile);
|
||||
|
||||
Glossary.prototype.type = 'glossary';
|
||||
|
||||
// Get templating context
|
||||
Glossary.prototype.getContext = function() {
|
||||
if (!this.path) return {};
|
||||
|
||||
return {
|
||||
glossary: {
|
||||
path: this.path
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Parse the readme content
|
||||
Glossary.prototype.parse = function(content) {
|
||||
var that = this;
|
||||
|
||||
return this.parser.glossary(content)
|
||||
.then(function(entries) {
|
||||
that.entries = _.map(entries, function(entry) {
|
||||
return new GlossaryEntry(entry.name, entry.description);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Return an entry by its id
|
||||
Glossary.prototype.get = function(id) {
|
||||
return _.find(this.entries, {
|
||||
id: id
|
||||
});
|
||||
};
|
||||
|
||||
// Find an entry by its name
|
||||
Glossary.prototype.find = function(name) {
|
||||
return this.get(nameToId(name));
|
||||
};
|
||||
|
||||
// Return false if glossary has entries (and exists)
|
||||
Glossary.prototype.isEmpty = function(id) {
|
||||
return _.size(this.entries) === 0;
|
||||
};
|
||||
|
||||
// Convert the glossary to a list of annotations
|
||||
Glossary.prototype.annotations = function() {
|
||||
return _.map(this.entries, function(entry) {
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
description: entry.description,
|
||||
href: '/' + this.path + '#' + entry.id
|
||||
};
|
||||
}, this);
|
||||
};
|
||||
|
||||
module.exports = Glossary;
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
module.exports = {
|
||||
Readme: require('./readme'),
|
||||
Summary: require('./summary'),
|
||||
Glossary: require('./glossary'),
|
||||
Langs: require('./langs')
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var util = require('util');
|
||||
var BackboneFile = require('./file');
|
||||
|
||||
function Language(title, folder) {
|
||||
var that = this;
|
||||
|
||||
this.title = title;
|
||||
this.folder = folder;
|
||||
|
||||
Object.defineProperty(this, 'id', {
|
||||
get: function() {
|
||||
return path.basename(that.folder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
A Langs is a list of languages stored in a LANGS.md file
|
||||
*/
|
||||
function Langs() {
|
||||
BackboneFile.apply(this, arguments);
|
||||
|
||||
this.languages = [];
|
||||
}
|
||||
util.inherits(Langs, BackboneFile);
|
||||
|
||||
Langs.prototype.type = 'langs';
|
||||
|
||||
// Parse the readme content
|
||||
Langs.prototype.parse = function(content) {
|
||||
var that = this;
|
||||
|
||||
return this.parser.langs(content)
|
||||
.then(function(langs) {
|
||||
that.languages = _.map(langs, function(entry) {
|
||||
return new Language(entry.title, entry.path);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Return the list of languages
|
||||
Langs.prototype.list = function() {
|
||||
return this.languages;
|
||||
};
|
||||
|
||||
// Return default/main language for the book
|
||||
Langs.prototype.getDefault = function() {
|
||||
return _.first(this.languages);
|
||||
};
|
||||
|
||||
// Return true if a language is the default one
|
||||
// "lang" cam be a string (id) or a Language entry
|
||||
Langs.prototype.isDefault = function(lang) {
|
||||
lang = lang.id || lang;
|
||||
return (this.cound() > 0 && this.getDefault().id == lang);
|
||||
};
|
||||
|
||||
// Return the count of languages
|
||||
Langs.prototype.count = function() {
|
||||
return _.size(this.languages);
|
||||
};
|
||||
|
||||
// Return templating context for the languages list
|
||||
Langs.prototype.getContext = function() {
|
||||
if (this.count() == 0) return {};
|
||||
|
||||
return {
|
||||
languages: {
|
||||
list: _.map(this.languages, function(lang) {
|
||||
return {
|
||||
id: lang.id,
|
||||
title: lang.title
|
||||
};
|
||||
})
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Langs;
|
||||
@@ -0,0 +1,26 @@
|
||||
var util = require('util');
|
||||
var BackboneFile = require('./file');
|
||||
|
||||
function Readme() {
|
||||
BackboneFile.apply(this, arguments);
|
||||
|
||||
this.title;
|
||||
this.description;
|
||||
}
|
||||
util.inherits(Readme, BackboneFile);
|
||||
|
||||
Readme.prototype.type = 'readme';
|
||||
|
||||
// Parse the readme content
|
||||
Readme.prototype.parse = function(content) {
|
||||
var that = this;
|
||||
|
||||
return this.parser.readme(content)
|
||||
.then(function(out) {
|
||||
that.title = out.title;
|
||||
that.description = out.description;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports = Readme;
|
||||
@@ -0,0 +1,353 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
var url = require('url');
|
||||
|
||||
var location = require('../utils/location');
|
||||
var error = require('../utils/error');
|
||||
var BackboneFile = require('./file');
|
||||
|
||||
/*
|
||||
An article represent an entry in the Summary.
|
||||
It's defined by a title, a reference, and children articles,
|
||||
the reference (ref) can be a filename + anchor or an external file (optional)
|
||||
*/
|
||||
function TOCArticle(def, parent) {
|
||||
// Title
|
||||
this.title = def.title;
|
||||
|
||||
// Parent TOCPart or TOCArticle
|
||||
this.parent = parent;
|
||||
|
||||
// As string indicating the overall position
|
||||
// ex: '1.0.0'
|
||||
this.level;
|
||||
this._next;
|
||||
this._prev;
|
||||
|
||||
// When README has been automatically added
|
||||
this.isAutoIntro = def.isAutoIntro;
|
||||
this.isIntroduction = def.isIntroduction;
|
||||
|
||||
this.validate();
|
||||
|
||||
// Path can be a relative path or an url, or nothing
|
||||
this.ref = def.path;
|
||||
if (this.ref) {
|
||||
var parts = url.parse(this.ref);
|
||||
|
||||
if (!this.isExternal()) {
|
||||
var parts = this.ref.split('#');
|
||||
this.path = (parts.length > 1? parts.slice(0, -1).join('#') : this.ref);
|
||||
this.anchor = (parts.length > 1? '#' + _.last(parts) : null);
|
||||
|
||||
// Normalize path to remove ('./', etc)
|
||||
this.path = location.normalize(this.path);
|
||||
}
|
||||
}
|
||||
|
||||
this.articles = _.map(def.articles || [], function(article) {
|
||||
if (article instanceof TOCArticle) return article;
|
||||
return new TOCArticle(article, this);
|
||||
}, this);
|
||||
}
|
||||
|
||||
// Validate the article
|
||||
TOCArticle.prototype.validate = function() {
|
||||
if (!this.title) {
|
||||
throw error.ParsingError(new Error('SUMMARY entries should have an non-empty title'));
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over all articles in this articles
|
||||
TOCArticle.prototype.walk = function(iter, base) {
|
||||
base = base || this.level;
|
||||
|
||||
_.each(this.articles, function(article, i) {
|
||||
var level = levelId(base, i);
|
||||
|
||||
if (iter(article, level) === false) {
|
||||
return false;
|
||||
}
|
||||
article.walk(iter, level);
|
||||
});
|
||||
};
|
||||
|
||||
// Return templating context for an article
|
||||
TOCArticle.prototype.getContext = function() {
|
||||
return {
|
||||
level: this.level,
|
||||
title: this.title,
|
||||
depth: this.depth(),
|
||||
path: this.isExternal()? undefined : this.path,
|
||||
anchor: this.isExternal()? undefined : this.anchor,
|
||||
url: this.isExternal()? this.ref : undefined
|
||||
};
|
||||
};
|
||||
|
||||
// Return true if is pointing to a file
|
||||
TOCArticle.prototype.hasLocation = function() {
|
||||
return Boolean(this.path);
|
||||
};
|
||||
|
||||
// Return true if is pointing to an external location
|
||||
TOCArticle.prototype.isExternal = function() {
|
||||
return location.isExternal(this.ref);
|
||||
};
|
||||
|
||||
// Return true if this article is the introduction
|
||||
TOCArticle.prototype.isIntro = function() {
|
||||
return Boolean(this.isIntroduction);
|
||||
};
|
||||
|
||||
// Return true if has children
|
||||
TOCArticle.prototype.hasChildren = function() {
|
||||
return this.articles.length > 0;
|
||||
};
|
||||
|
||||
// Return true if has an article as parent
|
||||
TOCArticle.prototype.hasParent = function() {
|
||||
return !(this.parent instanceof TOCPart);
|
||||
};
|
||||
|
||||
// Return depth of this article
|
||||
TOCArticle.prototype.depth = function() {
|
||||
return this.level.split('.').length;
|
||||
};
|
||||
|
||||
// Return next article in the TOC
|
||||
TOCArticle.prototype.next = function() {
|
||||
return this._next;
|
||||
};
|
||||
|
||||
// Return previous article in the TOC
|
||||
TOCArticle.prototype.prev = function() {
|
||||
return this._prev;
|
||||
};
|
||||
|
||||
// Map over all articles
|
||||
TOCArticle.prototype.map = function(iter) {
|
||||
return _.map(this.articles, iter);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
A part of a ToC is a composed of a tree of articles and an optiona title
|
||||
*/
|
||||
function TOCPart(part, parent) {
|
||||
if (!(this instanceof TOCPart)) return new TOCPart(part, parent);
|
||||
|
||||
TOCArticle.apply(this, arguments);
|
||||
}
|
||||
util.inherits(TOCPart, TOCArticle);
|
||||
|
||||
// Validate the part
|
||||
TOCPart.prototype.validate = function() { };
|
||||
|
||||
// Return a sibling (next or prev) of this part
|
||||
TOCPart.prototype.sibling = function(direction) {
|
||||
var parts = this.parent.parts;
|
||||
var pos = _.findIndex(parts, this);
|
||||
|
||||
if (parts[pos + direction]) {
|
||||
return parts[pos + direction];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Iterate over all entries of the part
|
||||
TOCPart.prototype.walk = function(iter, base) {
|
||||
var articles = this.articles;
|
||||
|
||||
if (articles.length == 0) return;
|
||||
|
||||
// Has introduction?
|
||||
if (articles[0].isIntro()) {
|
||||
if (iter(articles[0], '0') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
articles = articles.slice(1);
|
||||
}
|
||||
|
||||
|
||||
_.each(articles, function(article, i) {
|
||||
var level = levelId(base, i);
|
||||
|
||||
if (iter(article, level) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
article.walk(iter, level);
|
||||
});
|
||||
};
|
||||
|
||||
// Return templating context for a part
|
||||
TOCPart.prototype.getContext = function(onArticle) {
|
||||
onArticle = onArticle || function(article) {
|
||||
return article.getContext();
|
||||
};
|
||||
|
||||
return {
|
||||
title: this.title,
|
||||
articles: this.map(onArticle)
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
A summary is composed of a list of parts, each composed wit a tree of articles.
|
||||
*/
|
||||
function Summary() {
|
||||
BackboneFile.apply(this, arguments);
|
||||
|
||||
this.parts = [];
|
||||
this._length = 0;
|
||||
}
|
||||
util.inherits(Summary, BackboneFile);
|
||||
|
||||
Summary.prototype.type = 'summary';
|
||||
|
||||
// Prepare summary when non existant
|
||||
Summary.prototype.parseNotFound = function() {
|
||||
this.update([]);
|
||||
};
|
||||
|
||||
// Parse the summary content
|
||||
Summary.prototype.parse = function(content) {
|
||||
var that = this;
|
||||
|
||||
return this.parser.summary(content)
|
||||
|
||||
.then(function(summary) {
|
||||
that.update(summary.parts);
|
||||
});
|
||||
};
|
||||
|
||||
// Return templating context for the summary
|
||||
Summary.prototype.getContext = function() {
|
||||
function onArticle(article) {
|
||||
var result = article.getContext();
|
||||
if (article.hasChildren()) {
|
||||
result.articles = article.map(onArticle);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
summary: {
|
||||
parts: _.map(this.parts, function(part) {
|
||||
return part.getContext(onArticle);
|
||||
})
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Iterate over all entries of the summary
|
||||
// iter is called with an TOCArticle
|
||||
Summary.prototype.walk = function(iter) {
|
||||
var hasMultipleParts = this.parts.length > 1;
|
||||
|
||||
_.each(this.parts, function(part, i) {
|
||||
part.walk(iter, hasMultipleParts? levelId('', i) : null);
|
||||
});
|
||||
};
|
||||
|
||||
// Find a specific article using a filter
|
||||
Summary.prototype.find = function(filter) {
|
||||
var result;
|
||||
|
||||
this.walk(function(article) {
|
||||
if (filter(article)) {
|
||||
result = article;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Flatten the list of articles
|
||||
Summary.prototype.flatten = function() {
|
||||
var result = [];
|
||||
|
||||
this.walk(function(article) {
|
||||
result.push(article);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Return the first TOCArticle for a specific page (or path)
|
||||
Summary.prototype.getArticle = function(page) {
|
||||
if (!_.isString(page)) page = page.path;
|
||||
|
||||
return this.find(function(article) {
|
||||
return article.path == page;
|
||||
});
|
||||
};
|
||||
|
||||
// Return the first TOCArticle for a specific level
|
||||
Summary.prototype.getArticleByLevel = function(lvl) {
|
||||
return this.find(function(article) {
|
||||
return article.level == lvl;
|
||||
});
|
||||
};
|
||||
|
||||
// Return the count of articles in the summary
|
||||
Summary.prototype.count = function() {
|
||||
return this._length;
|
||||
};
|
||||
|
||||
// Prepare the summary
|
||||
Summary.prototype.update = function(parts) {
|
||||
var that = this;
|
||||
|
||||
|
||||
that.parts = _.map(parts, function(part) {
|
||||
return new TOCPart(part, that);
|
||||
});
|
||||
|
||||
// Create first part if none
|
||||
if (that.parts.length == 0) {
|
||||
that.parts.push(new TOCPart({}, that));
|
||||
}
|
||||
|
||||
// Add README as first entry
|
||||
var firstArticle = that.parts[0].articles[0];
|
||||
if (!firstArticle || firstArticle.path != that.book.readme.path) {
|
||||
that.parts[0].articles.unshift(new TOCArticle({
|
||||
title: 'Introduction',
|
||||
path: that.book.readme.path,
|
||||
isAutoIntro: true
|
||||
}, that.parts[0]));
|
||||
}
|
||||
that.parts[0].articles[0].isIntroduction = true;
|
||||
|
||||
|
||||
// Update the count and indexing of "level"
|
||||
var prev = undefined;
|
||||
|
||||
that._length = 0;
|
||||
that.walk(function(article, level) {
|
||||
// Index level
|
||||
article.level = level;
|
||||
|
||||
// Chain articles
|
||||
article._prev = prev;
|
||||
if (prev) prev._next = article;
|
||||
|
||||
prev = article;
|
||||
|
||||
that._length += 1;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Return a level string from a base level and an index
|
||||
function levelId(base, i) {
|
||||
i = i + 1;
|
||||
return (base? [base || '', i] : [i]).join('.');
|
||||
}
|
||||
|
||||
module.exports = Summary;
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var Ignore = require('ignore');
|
||||
|
||||
var Config = require('./config');
|
||||
var Readme = require('./backbone/readme');
|
||||
var Glossary = require('./backbone/glossary');
|
||||
var Summary = require('./backbone/summary');
|
||||
var Langs = require('./backbone/langs');
|
||||
var Page = require('./page');
|
||||
var pathUtil = require('./utils/path');
|
||||
var error = require('./utils/error');
|
||||
var Promise = require('./utils/promise');
|
||||
var Logger = require('./utils/logger');
|
||||
var parsers = require('./parsers');
|
||||
var initBook = require('./init');
|
||||
|
||||
|
||||
/*
|
||||
The Book class is an interface for parsing books content.
|
||||
It does not require to run on Node.js, isnce it only depends on the fs implementation
|
||||
*/
|
||||
|
||||
function Book(opts) {
|
||||
if (!(this instanceof Book)) return new Book(opts);
|
||||
|
||||
this.opts = _.defaults(opts || {}, {
|
||||
fs: null,
|
||||
|
||||
// Root path for the book
|
||||
root: '',
|
||||
|
||||
// Extend book configuration
|
||||
config: {},
|
||||
|
||||
// Log function
|
||||
log: function(msg) {
|
||||
process.stdout.write(msg);
|
||||
},
|
||||
|
||||
// Log level
|
||||
logLevel: 'info'
|
||||
});
|
||||
|
||||
if (!opts.fs) throw error.ParsingError(new Error('Book requires a fs instance'));
|
||||
|
||||
// Root path for the book
|
||||
this.root = opts.root;
|
||||
|
||||
// If multi-lingual, book can have a parent
|
||||
this.parent = opts.parent;
|
||||
if (this.parent) {
|
||||
this.language = path.relative(this.parent.root, this.root);
|
||||
}
|
||||
|
||||
// A book is linked to an fs, to access its content
|
||||
this.fs = opts.fs;
|
||||
|
||||
// Rules to ignore some files
|
||||
this.ignore = Ignore();
|
||||
this.ignore.addPattern([
|
||||
// Skip Git stuff
|
||||
'.git/',
|
||||
|
||||
// Skip OS X meta data
|
||||
'.DS_Store',
|
||||
|
||||
// Skip stuff installed by plugins
|
||||
'node_modules',
|
||||
|
||||
// Skip book outputs
|
||||
'_book',
|
||||
'*.pdf',
|
||||
'*.epub',
|
||||
'*.mobi'
|
||||
]);
|
||||
|
||||
// Create a logger for the book
|
||||
this.log = new Logger(opts.log, opts.logLevel);
|
||||
|
||||
// Create an interface to access the configuration
|
||||
this.config = new Config(this, opts.config);
|
||||
|
||||
// Interfaces for the book structure
|
||||
this.readme = new Readme(this);
|
||||
this.summary = new Summary(this);
|
||||
this.glossary = new Glossary(this);
|
||||
|
||||
// Multilinguals book
|
||||
this.langs = new Langs(this);
|
||||
this.books = [];
|
||||
|
||||
// List of page in the book
|
||||
this.pages = {};
|
||||
|
||||
// Deprecation for templates
|
||||
Object.defineProperty(this, 'options', {
|
||||
get: function () {
|
||||
this.log.warn.ln('"options" property is deprecated, use config.get(key) instead');
|
||||
var cfg = this.config.dump();
|
||||
error.deprecateField(cfg, 'book', (this.output? this.output.name : null), '"options.generator" property is deprecated, use "output.name" instead');
|
||||
|
||||
return cfg;
|
||||
}
|
||||
});
|
||||
|
||||
_.bindAll(this);
|
||||
|
||||
// Loop for template filters/blocks
|
||||
error.deprecateField(this, 'book', this, '"book" property is deprecated, use "this" directly instead');
|
||||
}
|
||||
|
||||
// Return templating context for the book
|
||||
Book.prototype.getContext = function() {
|
||||
var variables = this.config.get('variables', {});
|
||||
|
||||
return {
|
||||
book: _.extend({
|
||||
language: this.language
|
||||
}, variables)
|
||||
};
|
||||
};
|
||||
|
||||
// Parse and prepare the configuration, fail if invalid
|
||||
Book.prototype.prepareConfig = function() {
|
||||
return this.config.load();
|
||||
};
|
||||
|
||||
// Resolve a path in the book source
|
||||
// Enforce that the output path is in the scope
|
||||
Book.prototype.resolve = function() {
|
||||
var filename = path.resolve.apply(path, [this.root].concat(_.toArray(arguments)));
|
||||
if (!this.isFileInScope(filename)) {
|
||||
throw error.FileOutOfScopeError({
|
||||
filename: filename,
|
||||
root: this.root
|
||||
});
|
||||
}
|
||||
|
||||
return filename;
|
||||
};
|
||||
|
||||
// Return false if a file is outside the book' scope
|
||||
Book.prototype.isFileInScope = function(filename) {
|
||||
filename = path.resolve(this.root, filename);
|
||||
|
||||
// Is the file in the scope of the parent?
|
||||
if (this.parent && this.parent.isFileInScope(filename)) return true;
|
||||
|
||||
// Is file in the root folder?
|
||||
return pathUtil.isInRoot(this.root, filename);
|
||||
};
|
||||
|
||||
// Parse .gitignore, etc to extract rules
|
||||
Book.prototype.parseIgnoreRules = function() {
|
||||
var that = this;
|
||||
|
||||
return Promise.serie([
|
||||
'.ignore',
|
||||
'.gitignore',
|
||||
'.bookignore'
|
||||
], function(filename) {
|
||||
return that.readFile(filename)
|
||||
.then(function(content) {
|
||||
that.ignore.addPattern(content.toString().split(/\r?\n/));
|
||||
}, function() {
|
||||
return Promise();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Parse the whole book
|
||||
Book.prototype.parse = function() {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(this.prepareConfig)
|
||||
.then(this.parseIgnoreRules)
|
||||
|
||||
// Parse languages
|
||||
.then(function() {
|
||||
return that.langs.load();
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
if (that.isMultilingual()) {
|
||||
if (that.isLanguageBook()) {
|
||||
throw error.ParsingError(new Error('A multilingual book as a language book is forbidden'));
|
||||
}
|
||||
|
||||
that.log.info.ln('Parsing multilingual book, with', that.langs.count(), 'languages');
|
||||
|
||||
// Create a new book for each language and parse it
|
||||
return Promise.serie(that.langs.list(), function(lang) {
|
||||
that.log.debug.ln('Preparing book for language', lang.id);
|
||||
var langBook = new Book(_.extend({}, that.opts, {
|
||||
parent: that,
|
||||
config: that.config.dump(),
|
||||
root: that.resolve(lang.id)
|
||||
}));
|
||||
|
||||
that.books.push(langBook);
|
||||
|
||||
return langBook.parse();
|
||||
});
|
||||
}
|
||||
|
||||
return Promise()
|
||||
|
||||
// Parse the readme
|
||||
.then(that.readme.load)
|
||||
.then(function() {
|
||||
if (!that.readme.exists()) {
|
||||
throw new error.FileNotFoundError({ filename: 'README' });
|
||||
}
|
||||
|
||||
// Default configuration to infos extracted from readme
|
||||
if (!that.config.get('title')) that.config.set('title', that.readme.title);
|
||||
if (!that.config.get('description')) that.config.set('description', that.readme.description);
|
||||
})
|
||||
|
||||
// Parse the summary
|
||||
.then(that.summary.load)
|
||||
.then(function() {
|
||||
if (!that.summary.exists()) {
|
||||
that.log.warn.ln('no summary file in this book');
|
||||
}
|
||||
|
||||
// Index summary's articles
|
||||
that.summary.walk(function(article) {
|
||||
if (!article.hasLocation() || article.isExternal()) return;
|
||||
that.addPage(article.path);
|
||||
});
|
||||
})
|
||||
|
||||
// Parse the glossary
|
||||
.then(that.glossary.load)
|
||||
|
||||
// Add the glossary as a page
|
||||
.then(function() {
|
||||
if (!that.glossary.exists()) return;
|
||||
that.addPage(that.glossary.path);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Mark a filename as being parsable
|
||||
Book.prototype.addPage = function(filename) {
|
||||
if (this.hasPage(filename)) return this.getPage(filename);
|
||||
|
||||
filename = pathUtil.normalize(filename);
|
||||
this.pages[filename] = new Page(this, filename);
|
||||
return this.pages[filename];
|
||||
};
|
||||
|
||||
// Return a page by its filename (or undefined)
|
||||
Book.prototype.getPage = function(filename) {
|
||||
filename = pathUtil.normalize(filename);
|
||||
return this.pages[filename];
|
||||
};
|
||||
|
||||
|
||||
// Return true, if has a specific page
|
||||
Book.prototype.hasPage = function(filename) {
|
||||
return Boolean(this.getPage(filename));
|
||||
};
|
||||
|
||||
// Test if a file is ignored, return true if it is
|
||||
Book.prototype.isFileIgnored = function(filename) {
|
||||
return this.ignore.filter([filename]).length == 0;
|
||||
};
|
||||
|
||||
// Read a file in the book, throw error if ignored
|
||||
Book.prototype.readFile = function(filename) {
|
||||
if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
|
||||
return this.fs.readAsString(this.resolve(filename));
|
||||
};
|
||||
|
||||
// Get stat infos about a file
|
||||
Book.prototype.statFile = function(filename) {
|
||||
if (this.isFileIgnored(filename)) return Promise.reject(new error.FileNotFoundError({ filename: filename }));
|
||||
return this.fs.stat(this.resolve(filename));
|
||||
};
|
||||
|
||||
// Find a parsable file using a filename
|
||||
Book.prototype.findParsableFile = 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 that.fs.findFile(that.root, filepath)
|
||||
.then(function(realFilepath) {
|
||||
if (!realFilepath) return null;
|
||||
|
||||
return {
|
||||
parser: parsers.get(ext),
|
||||
path: realFilepath
|
||||
};
|
||||
});
|
||||
});
|
||||
}, Promise(null));
|
||||
};
|
||||
|
||||
// Return true if book is associated to a language
|
||||
Book.prototype.isLanguageBook = function() {
|
||||
return Boolean(this.parent);
|
||||
};
|
||||
Book.prototype.isSubBook = Book.prototype.isLanguageBook;
|
||||
|
||||
// Return true if the book is main instance of a multilingual book
|
||||
Book.prototype.isMultilingual = function() {
|
||||
return this.langs.count() > 0;
|
||||
};
|
||||
|
||||
// Return true if file is in the scope of this book
|
||||
Book.prototype.isInBook = function(filename) {
|
||||
return pathUtil.isInRoot(
|
||||
this.root,
|
||||
filename
|
||||
);
|
||||
};
|
||||
|
||||
// Return true if file is in the scope of a child book
|
||||
Book.prototype.isInLanguageBook = function(filename) {
|
||||
var that = this;
|
||||
|
||||
return _.some(this.langs.list(), function(lang) {
|
||||
return pathUtil.isInRoot(
|
||||
that.resolve(lang.id),
|
||||
that.resolve(filename)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Locate a book in a folder
|
||||
// - Read the ".gitbook" is exists
|
||||
// - Try the folder itself
|
||||
// - Try a "docs" folder
|
||||
Book.locate = function(fs, root) {
|
||||
return fs.readAsString(path.join(root, '.gitbook'))
|
||||
.then(function(content) {
|
||||
return path.join(root, content);
|
||||
}, function() {
|
||||
// .gitbook doesn't exists, fall back to the root folder
|
||||
return Promise(root);
|
||||
});
|
||||
};
|
||||
|
||||
// Locate and setup a book
|
||||
Book.setup = function(fs, root, opts) {
|
||||
return Book.locate(fs, root)
|
||||
.then(function(_root) {
|
||||
return new Book(_.extend(opts || {}, {
|
||||
root: _root,
|
||||
fs: fs
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize a book
|
||||
Book.init = function(fs, root, opts) {
|
||||
return Book.setup(fs, root, opts)
|
||||
.then(initBook);
|
||||
};
|
||||
|
||||
|
||||
module.exports = Book;
|
||||
@@ -0,0 +1,139 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
|
||||
var Book = require('../book');
|
||||
var NodeFS = require('../fs/node');
|
||||
var Logger = require('../utils/logger');
|
||||
var Promise = require('../utils/promise');
|
||||
var fs = require('../utils/fs');
|
||||
var JSONOutput = require('../output/json');
|
||||
var WebsiteOutput = require('../output/website');
|
||||
var EBookOutput = require('../output/ebook');
|
||||
|
||||
var nodeFS = new NodeFS();
|
||||
|
||||
var LOG_OPTION = {
|
||||
name: 'log',
|
||||
description: 'Minimum log level to display',
|
||||
values: _.chain(Logger.LEVELS)
|
||||
.keys()
|
||||
.map(function(s) {
|
||||
return s.toLowerCase();
|
||||
})
|
||||
.value(),
|
||||
defaults: 'info'
|
||||
};
|
||||
|
||||
var FORMAT_OPTION = {
|
||||
name: 'format',
|
||||
description: 'Format to build to',
|
||||
values: ['website', 'json', 'ebook'],
|
||||
defaults: 'website'
|
||||
};
|
||||
|
||||
var FORMATS = {
|
||||
json: JSONOutput,
|
||||
website: WebsiteOutput,
|
||||
ebook: EBookOutput
|
||||
};
|
||||
|
||||
// Commands which is processing a book
|
||||
// the root of the book is the first argument (or current directory)
|
||||
function bookCmd(fn) {
|
||||
return function(args, kwargs) {
|
||||
var input = path.resolve(args[0] || process.cwd());
|
||||
return Book.setup(nodeFS, input, {
|
||||
logLevel: kwargs.log
|
||||
})
|
||||
.then(function(book) {
|
||||
return fn(book, args.slice(1), kwargs);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Commands which is working on a Output instance
|
||||
function outputCmd(fn) {
|
||||
return bookCmd(function(book, args, kwargs) {
|
||||
var Out = FORMATS[kwargs.format];
|
||||
var outputFolder = undefined;
|
||||
|
||||
// Set output folder
|
||||
if (args[0]) {
|
||||
outputFolder = path.resolve(process.cwd(), args[0]);
|
||||
}
|
||||
|
||||
return fn(new Out(book, {
|
||||
root: outputFolder
|
||||
}), args);
|
||||
});
|
||||
}
|
||||
|
||||
// Command to generate an ebook
|
||||
function ebookCmd(format) {
|
||||
return {
|
||||
name: format + ' [book] [output] [file]',
|
||||
description: 'generates ebook '+format,
|
||||
options: [
|
||||
LOG_OPTION
|
||||
],
|
||||
exec: bookCmd(function(book, args, kwargs) {
|
||||
return fs.tmpDir()
|
||||
.then(function(dir) {
|
||||
var ext = '.'+format;
|
||||
var outputFile = path.resolve(process.cwd(), args[1] || ('book' + ext));
|
||||
var output = new EBookOutput(book, {
|
||||
root: dir,
|
||||
format: format
|
||||
});
|
||||
|
||||
return output.book.parse()
|
||||
.then(function() {
|
||||
return output.generate();
|
||||
})
|
||||
|
||||
// Copy the ebook files
|
||||
.then(function() {
|
||||
if (output.book.isMultilingual()) {
|
||||
return Promise.serie(output.book.langs.list(), function(lang) {
|
||||
var _outputFile = path.join(
|
||||
path.dirname(outputFile),
|
||||
path.basename(outputFile, ext) + '_' + lang.id + ext
|
||||
);
|
||||
|
||||
return fs.copy(
|
||||
path.resolve(dir, lang.id, 'index' + ext),
|
||||
_outputFile
|
||||
);
|
||||
})
|
||||
.thenResolve(output.book.langs.count());
|
||||
} else {
|
||||
return fs.copy(
|
||||
path.resolve(dir, 'index' + ext),
|
||||
outputFile
|
||||
).thenResolve(1);
|
||||
}
|
||||
})
|
||||
.then(function(n) {
|
||||
output.book.log.info.ok(n+' file(s) generated');
|
||||
|
||||
output.book.log.info('cleaning up... ');
|
||||
return output.book.log.info.promise(fs.rmDir(dir));
|
||||
});
|
||||
});
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
nodeFS: nodeFS,
|
||||
bookCmd: bookCmd,
|
||||
outputCmd: outputCmd,
|
||||
ebookCmd: ebookCmd,
|
||||
|
||||
options: {
|
||||
log: LOG_OPTION,
|
||||
format: FORMAT_OPTION
|
||||
},
|
||||
|
||||
FORMATS: FORMATS
|
||||
};
|
||||
@@ -0,0 +1,199 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var tinylr = require('tiny-lr');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var PluginsManager = require('../plugins');
|
||||
var Book = require('../book');
|
||||
var initBook = require('../init');
|
||||
|
||||
var helper = require('./helper');
|
||||
var Server = require('./server');
|
||||
var watch = require('./watch');
|
||||
|
||||
module.exports = {
|
||||
commands: [
|
||||
{
|
||||
name: 'init [book]',
|
||||
description: 'setup and create files for chapters',
|
||||
options: [
|
||||
helper.options.log
|
||||
],
|
||||
exec: function(args) {
|
||||
var input = path.resolve(args[0] || process.cwd());
|
||||
return Book.init(helper.nodeFS, input);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'parse [book]',
|
||||
description: 'parse and returns debug information for a book',
|
||||
options: [
|
||||
helper.options.log
|
||||
],
|
||||
exec: helper.bookCmd(function(book) {
|
||||
return book.parse()
|
||||
.then(function() {
|
||||
book.log.info.ln('Book located in:', book.root);
|
||||
book.log.info.ln('');
|
||||
|
||||
if (book.config.exists()) book.log.info.ln('Configuration:', book.config.path);
|
||||
|
||||
if (book.isMultilingual()) {
|
||||
book.log.info.ln('Multilingual book detected:', book.langs.path);
|
||||
} else {
|
||||
book.log.info.ln('Readme:', book.readme.path);
|
||||
book.log.info.ln('Summary:', book.summary.path);
|
||||
if (book.glossary.exists()) book.log.info.ln('Glossary:', book.glossary.path);
|
||||
|
||||
book.log.info.ln('Pages:');
|
||||
_.each(book.pages, function(page) {
|
||||
book.log.info.ln('\t-', page.path);
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
{
|
||||
name: 'install [book]',
|
||||
description: 'install all plugins dependencies',
|
||||
options: [
|
||||
helper.options.log
|
||||
],
|
||||
exec: helper.bookCmd(function(book, args) {
|
||||
var plugins = new PluginsManager(book);
|
||||
|
||||
return book.config.load()
|
||||
.then(function() {
|
||||
return plugins.install();
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
{
|
||||
name: 'build [book] [output]',
|
||||
description: 'build a book',
|
||||
options: [
|
||||
helper.options.log,
|
||||
helper.options.format
|
||||
],
|
||||
exec: helper.outputCmd(function(output, args, kwargs) {
|
||||
return output.book.parse()
|
||||
.then(function() {
|
||||
return output.generate();
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
helper.ebookCmd('pdf'),
|
||||
helper.ebookCmd('epub'),
|
||||
helper.ebookCmd('mobi'),
|
||||
|
||||
{
|
||||
name: 'serve [book]',
|
||||
description: 'Build then serve a book 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
|
||||
},
|
||||
helper.options.format,
|
||||
helper.options.log
|
||||
],
|
||||
exec: function(args, kwargs) {
|
||||
var input = path.resolve(args[0] || process.cwd());
|
||||
var server = new Server();
|
||||
|
||||
// Init livereload server
|
||||
var lrServer = tinylr({});
|
||||
var port = kwargs.port;
|
||||
var lrPath;
|
||||
|
||||
var generate = function() {
|
||||
|
||||
// Stop server if running
|
||||
if (server.isRunning()) console.log('Stopping server');
|
||||
return server.stop()
|
||||
|
||||
// Generate the book
|
||||
.then(function() {
|
||||
return Book.setup(helper.nodeFS, input, {
|
||||
'logLevel': kwargs.log
|
||||
})
|
||||
.then(function(book) {
|
||||
return book.parse()
|
||||
.then(function() {
|
||||
// Add livereload plugin
|
||||
book.config.set('plugins',
|
||||
book.config.get('plugins')
|
||||
.concat([
|
||||
{ name: 'livereload' }
|
||||
])
|
||||
);
|
||||
|
||||
var Out = helper.FORMATS[kwargs.format];
|
||||
var output = new Out(book);
|
||||
|
||||
return output.generate()
|
||||
.thenResolve(output);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
// Start server and watch changes
|
||||
.then(function(output) {
|
||||
console.log();
|
||||
console.log('Starting server ...');
|
||||
return server.start(output.root(), port)
|
||||
.then(function() {
|
||||
console.log('Serving book on http://localhost:'+port);
|
||||
|
||||
if (lrPath) {
|
||||
// trigger livereload
|
||||
lrServer.changed({
|
||||
body: {
|
||||
files: [lrPath]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!kwargs.watch) return;
|
||||
|
||||
return watch(output.book.root)
|
||||
.then(function(filepath) {
|
||||
// set livereload path
|
||||
lrPath = filepath;
|
||||
console.log('Restart after change in file', filepath);
|
||||
console.log('');
|
||||
return generate();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return Promise.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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
var events = require('events');
|
||||
var http = require('http');
|
||||
var send = require('send');
|
||||
var util = require('util');
|
||||
var url = require('url');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
|
||||
var Server = function() {
|
||||
this.running = null;
|
||||
this.dir = null;
|
||||
this.port = 0;
|
||||
this.sockets = [];
|
||||
};
|
||||
util.inherits(Server, events.EventEmitter);
|
||||
|
||||
// Return true if the server is running
|
||||
Server.prototype.isRunning = function() {
|
||||
return !!this.running;
|
||||
};
|
||||
|
||||
// Stop the server
|
||||
Server.prototype.stop = function() {
|
||||
var that = this;
|
||||
if (!this.isRunning()) return Promise();
|
||||
|
||||
var d = Promise.defer();
|
||||
this.running.close(function(err) {
|
||||
that.running = null;
|
||||
that.emit('state', false);
|
||||
|
||||
if (err) d.reject(err);
|
||||
else d.resolve();
|
||||
});
|
||||
|
||||
for (var i = 0; i < this.sockets.length; i++) {
|
||||
this.sockets[i].destroy();
|
||||
}
|
||||
|
||||
return d.promise;
|
||||
};
|
||||
|
||||
Server.prototype.start = function(dir, port) {
|
||||
var that = this, pre = Promise();
|
||||
port = port || 8004;
|
||||
|
||||
if (that.isRunning()) pre = this.stop();
|
||||
return pre
|
||||
.then(function() {
|
||||
var d = Promise.defer();
|
||||
|
||||
that.running = http.createServer(function(req, res){
|
||||
// Render error
|
||||
function error(err) {
|
||||
res.statusCode = err.status || 500;
|
||||
res.end(err.message);
|
||||
}
|
||||
|
||||
// Redirect to directory's index.html
|
||||
function redirect() {
|
||||
res.statusCode = 301;
|
||||
res.setHeader('Location', req.url + '/');
|
||||
res.end('Redirecting to ' + req.url + '/');
|
||||
}
|
||||
|
||||
// Send file
|
||||
send(req, url.parse(req.url).pathname)
|
||||
.root(dir)
|
||||
.on('error', error)
|
||||
.on('directory', redirect)
|
||||
.pipe(res);
|
||||
});
|
||||
|
||||
that.running.on('connection', function (socket) {
|
||||
that.sockets.push(socket);
|
||||
socket.setTimeout(4000);
|
||||
socket.on('close', function () {
|
||||
that.sockets.splice(that.sockets.indexOf(socket), 1);
|
||||
});
|
||||
});
|
||||
|
||||
that.running.listen(port, function(err) {
|
||||
if (err) return d.reject(err);
|
||||
|
||||
that.port = port;
|
||||
that.dir = dir;
|
||||
that.emit('state', true);
|
||||
d.resolve();
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Server;
|
||||
@@ -0,0 +1,42 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var chokidar = require('chokidar');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var parsers = require('../parsers');
|
||||
|
||||
// Watch a folder and resolve promise once a file is modified
|
||||
function watch(dir) {
|
||||
var d = Promise.defer();
|
||||
dir = path.resolve(dir);
|
||||
|
||||
var toWatch = [
|
||||
'book.json', 'book.js'
|
||||
];
|
||||
|
||||
// Watch all parsable files
|
||||
_.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,132 @@
|
||||
var _ = require('lodash');
|
||||
var semver = require('semver');
|
||||
|
||||
var gitbook = require('../gitbook');
|
||||
var Promise = require('../utils/promise');
|
||||
var validator = require('./validator');
|
||||
var plugins = require('./plugins');
|
||||
|
||||
// Config files to tested (sorted)
|
||||
var CONFIG_FILES = [
|
||||
'book.js',
|
||||
'book.json'
|
||||
];
|
||||
|
||||
/*
|
||||
Config is an interface for the book's configuration stored in "book.json" (or "book.js")
|
||||
*/
|
||||
|
||||
function Config(book, baseConfig) {
|
||||
this.book = book;
|
||||
this.fs = book.fs;
|
||||
this.log = book.log;
|
||||
this.path = '';
|
||||
|
||||
this.baseConfig = baseConfig || {};
|
||||
this.replace({});
|
||||
}
|
||||
|
||||
// Load configuration of the book
|
||||
// and verify that the configuration is satisfying
|
||||
Config.prototype.load = function() {
|
||||
var that = this;
|
||||
var isLanguageBook = this.book.isLanguageBook();
|
||||
|
||||
// Try all potential configuration file
|
||||
return Promise.some(CONFIG_FILES, function(filename) {
|
||||
that.log.debug.ln('try loading configuration from', filename);
|
||||
|
||||
return that.fs.loadAsObject(that.book.resolve(filename))
|
||||
.then(function(_config) {
|
||||
that.log.debug.ln('configuration loaded from', filename);
|
||||
|
||||
that.path = filename;
|
||||
return that.replace(_config);
|
||||
})
|
||||
.fail(function(err) {
|
||||
if (err.code != 'MODULE_NOT_FOUND') throw(err);
|
||||
else return Promise(false);
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
if (!isLanguageBook) {
|
||||
if (!gitbook.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(gitbook.version, 'patch'), that.options.gitbook)) {
|
||||
that.log.warn.ln('gitbook version specified in your book.json might be too strict for future patches, \''+(_.first(gitbook.version.split('.'))+'.x.x')+'\' is more adequate');
|
||||
}
|
||||
|
||||
that.options.plugins = plugins.toList(that.options.plugins);
|
||||
} else {
|
||||
// Multilingual book should inherits the plugins list from parent
|
||||
that.options.plugins = that.book.parent.config.get('plugins');
|
||||
}
|
||||
|
||||
that.options.gitbook = gitbook.version;
|
||||
});
|
||||
};
|
||||
|
||||
// Replace the whole configuration
|
||||
Config.prototype.replace = function(options) {
|
||||
var that = this;
|
||||
|
||||
// Extend base config
|
||||
options = _.defaults(_.cloneDeep(options), this.baseConfig);
|
||||
|
||||
// Validate the config
|
||||
this.options = validator.validate(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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Return true if book has a configuration file
|
||||
Config.prototype.exists = function() {
|
||||
return Boolean(this.path);
|
||||
};
|
||||
|
||||
// Return path to a structure file
|
||||
// Strip the extension by default
|
||||
Config.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 a configuration using a key and a default value
|
||||
Config.prototype.get = function(key, def) {
|
||||
return _.get(this.options, key, def);
|
||||
};
|
||||
|
||||
// Update a configuration
|
||||
Config.prototype.set = function(key, value) {
|
||||
return _.set(this.options, key, value);
|
||||
};
|
||||
|
||||
// Return a dump of the configuration
|
||||
Config.prototype.dump = function() {
|
||||
return _.cloneDeep(this.options);
|
||||
};
|
||||
|
||||
// Return templating context
|
||||
Config.prototype.getContext = function() {
|
||||
return {
|
||||
config: this.book.config.dump()
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Config;
|
||||
@@ -0,0 +1,67 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
// Default plugins added to each books
|
||||
var DEFAULT_PLUGINS = ['highlight', 'search', 'sharing', 'fontsettings', 'theme-default'];
|
||||
|
||||
// Return true if a plugin is a default plugin
|
||||
function isDefaultPlugin(name, version) {
|
||||
return _.contains(DEFAULT_PLUGINS, name);
|
||||
}
|
||||
|
||||
// Normalize a list of plugins to use
|
||||
function normalizePluginsList(plugins) {
|
||||
// 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
|
||||
};
|
||||
});
|
||||
|
||||
// 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
|
||||
_.each(DEFAULT_PLUGINS, function(plugin) {
|
||||
if (_.find(plugins, { name: plugin })) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugins.push({
|
||||
'name': plugin
|
||||
});
|
||||
});
|
||||
// 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;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isDefaultPlugin: isDefaultPlugin,
|
||||
toList: normalizePluginsList
|
||||
};
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
module.exports = {
|
||||
'$schema': 'http://json-schema.org/schema#',
|
||||
'id': 'https://gitbook.com/schemas/book.json',
|
||||
'title': 'GitBook Configuration',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'title': {
|
||||
'type': 'string',
|
||||
'title': 'Title of the book, default is extracted from README'
|
||||
},
|
||||
'title': {
|
||||
'type': 'string',
|
||||
'title': 'Description of the book, default is extracted from README'
|
||||
},
|
||||
'isbn': {
|
||||
'type': 'string',
|
||||
'title': 'ISBN for published book'
|
||||
},
|
||||
'author': {
|
||||
'type': 'string',
|
||||
'title': 'Name of the author'
|
||||
},
|
||||
'gitbook': {
|
||||
'type': 'string',
|
||||
'default': '*',
|
||||
'title': 'GitBook version to match'
|
||||
},
|
||||
'direction': {
|
||||
'type': 'string',
|
||||
'enum': ['ltr', 'rtl'],
|
||||
'title': 'Direction of texts, default is detected in the pages'
|
||||
},
|
||||
'theme': {
|
||||
'type': 'string',
|
||||
'default': 'default',
|
||||
'title': 'Name of the theme plugin to use'
|
||||
},
|
||||
'variables': {
|
||||
'type': 'object',
|
||||
'title': 'Templating context variables'
|
||||
},
|
||||
'plugins': {
|
||||
'oneOf': [
|
||||
{ '$ref': '#/definitions/pluginsArray' },
|
||||
{ '$ref': '#/definitions/pluginsString' }
|
||||
],
|
||||
'default': []
|
||||
},
|
||||
'pluginsConfig': {
|
||||
'type': 'object',
|
||||
'title': 'Configuration for plugins'
|
||||
},
|
||||
'structure': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'langs': {
|
||||
'default': 'LANGS.md',
|
||||
'type': 'string',
|
||||
'title': 'File to use as languages index',
|
||||
'pattern': '^[0-9a-zA-Z ... ]+$'
|
||||
},
|
||||
'readme': {
|
||||
'default': 'README.md',
|
||||
'type': 'string',
|
||||
'title': 'File to use as preface',
|
||||
'pattern': '^[0-9a-zA-Z ... ]+$'
|
||||
},
|
||||
'glossary': {
|
||||
'default': 'GLOSSARY.md',
|
||||
'type': 'string',
|
||||
'title': 'File to use as glossary index',
|
||||
'pattern': '^[0-9a-zA-Z ... ]+$'
|
||||
},
|
||||
'summary': {
|
||||
'default': 'SUMMARY.md',
|
||||
'type': 'string',
|
||||
'title': 'File to use as table of contents',
|
||||
'pattern': '^[0-9a-zA-Z ... ]+$'
|
||||
}
|
||||
},
|
||||
'additionalProperties': false
|
||||
},
|
||||
'pdf': {
|
||||
'type': 'object',
|
||||
'title': 'PDF specific configurations',
|
||||
'properties': {
|
||||
'pageNumbers': {
|
||||
'type': 'boolean',
|
||||
'default': true,
|
||||
'title': 'Add page numbers to the bottom of every page'
|
||||
},
|
||||
'fontSize': {
|
||||
'type': 'integer',
|
||||
'minimum': 8,
|
||||
'maximum': 30,
|
||||
'default': 12,
|
||||
'title': 'Font size for the PDF output'
|
||||
},
|
||||
'fontFamily': {
|
||||
'type': 'string',
|
||||
'default': 'Arial',
|
||||
'title': 'Font family for the PDF output'
|
||||
},
|
||||
'paperSize': {
|
||||
'type': 'string',
|
||||
'enum': ['a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'legal', 'letter'],
|
||||
'default': 'a4',
|
||||
'title': 'Paper size for the PDF'
|
||||
},
|
||||
'chapterMark': {
|
||||
'type': 'string',
|
||||
'enum': ['pagebreak', 'rule', 'both', 'none'],
|
||||
'default': 'pagebreak',
|
||||
'title': 'How to mark detected chapters'
|
||||
},
|
||||
'pageBreaksBefore': {
|
||||
'type': 'string',
|
||||
'default': '/',
|
||||
'title': 'An XPath expression. Page breaks are inserted before the specified elements. To disable use the expression: "/"'
|
||||
},
|
||||
'margin': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'right': {
|
||||
'type': 'integer',
|
||||
'title': 'Right Margin',
|
||||
'minimum': 0,
|
||||
'maximum': 100,
|
||||
'default': 62
|
||||
},
|
||||
'left': {
|
||||
'type': 'integer',
|
||||
'title': 'Left Margin',
|
||||
'minimum': 0,
|
||||
'maximum': 100,
|
||||
'default': 62
|
||||
},
|
||||
'top': {
|
||||
'type': 'integer',
|
||||
'title': 'Top Margin',
|
||||
'minimum': 0,
|
||||
'maximum': 100,
|
||||
'default': 56
|
||||
},
|
||||
'bottom': {
|
||||
'type': 'integer',
|
||||
'title': 'Bottom Margin',
|
||||
'minimum': 0,
|
||||
'maximum': 100,
|
||||
'default': 56
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'required': [],
|
||||
'definitions': {
|
||||
'pluginsArray': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'oneOf': [
|
||||
{ '$ref': '#/definitions/pluginObject' },
|
||||
{ '$ref': '#/definitions/pluginString' }
|
||||
]
|
||||
}
|
||||
},
|
||||
'pluginsString': {
|
||||
'type': 'string'
|
||||
},
|
||||
'pluginString': {
|
||||
'type': 'string'
|
||||
},
|
||||
'pluginObject': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {
|
||||
'type': 'string'
|
||||
},
|
||||
'version': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'additionalProperties': false,
|
||||
'required': ['name']
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
var jsonschema = require('jsonschema');
|
||||
var jsonSchemaDefaults = require('json-schema-defaults');
|
||||
var mergeDefaults = require('merge-defaults');
|
||||
|
||||
var schema = require('./schema');
|
||||
var error = require('../utils/error');
|
||||
|
||||
// Validate a book.json content
|
||||
// And return a mix with the default value
|
||||
function validate(bookJson) {
|
||||
var v = new jsonschema.Validator();
|
||||
var result = v.validate(bookJson, schema, {
|
||||
propertyName: 'config'
|
||||
});
|
||||
|
||||
// Throw error
|
||||
if (result.errors.length > 0) {
|
||||
throw new error.ConfigurationError(new Error(result.errors[0].stack));
|
||||
}
|
||||
|
||||
// Insert default values
|
||||
var defaults = jsonSchemaDefaults(schema);
|
||||
return mergeDefaults(bookJson, defaults);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validate: validate
|
||||
};
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
|
||||
/*
|
||||
A filesystem is an interface to read files
|
||||
GitBook can works with a virtual filesystem, for example in the browser.
|
||||
*/
|
||||
|
||||
// .readdir return files/folder as a list of string, folder ending with '/'
|
||||
function pathIsFolder(filename) {
|
||||
return _.last(filename) == '/' || _.last(filename) == '\\';
|
||||
}
|
||||
|
||||
|
||||
function FS() {
|
||||
|
||||
}
|
||||
|
||||
// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
|
||||
FS.prototype.exists = function(filename) {
|
||||
// To implement for each fs
|
||||
};
|
||||
|
||||
// Read a file and returns a promise with the content as a buffer
|
||||
FS.prototype.read = function(filename) {
|
||||
// To implement for each fs
|
||||
};
|
||||
|
||||
// Read stat infos about a file
|
||||
FS.prototype.stat = function(filename) {
|
||||
// To implement for each fs
|
||||
};
|
||||
|
||||
// List files/directories in a directory
|
||||
FS.prototype.readdir = function(folder) {
|
||||
// To implement for each fs
|
||||
};
|
||||
|
||||
// These methods don't require to be redefined, by default it uses .exists, .read, .write, .list
|
||||
// For optmization, it can be redefined:
|
||||
|
||||
// List files in a directory
|
||||
FS.prototype.listFiles = function(folder) {
|
||||
return this.readdir(folder)
|
||||
.then(function(files) {
|
||||
return _.reject(files, pathIsFolder);
|
||||
});
|
||||
};
|
||||
|
||||
// List all files in the fs
|
||||
FS.prototype.listAllFiles = function(folder) {
|
||||
var that = this;
|
||||
|
||||
return this.readdir(folder)
|
||||
.then(function(files) {
|
||||
return _.reduce(files, function(prev, file) {
|
||||
return prev.then(function(output) {
|
||||
var isDirectory = pathIsFolder(file);
|
||||
|
||||
if (!isDirectory) {
|
||||
output.push(file);
|
||||
return output;
|
||||
} else {
|
||||
return that.listAllFiles(path.join(folder, file))
|
||||
.then(function(files) {
|
||||
return output.concat(_.map(files, function(_file) {
|
||||
return path.join(file, _file);
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
}, Promise([]));
|
||||
});
|
||||
};
|
||||
|
||||
// Read a file as a string (utf-8)
|
||||
FS.prototype.readAsString = function(filename) {
|
||||
return this.read(filename)
|
||||
.then(function(buf) {
|
||||
return buf.toString('utf-8');
|
||||
});
|
||||
};
|
||||
|
||||
// Find a file in a folder (case incensitive)
|
||||
// Return the real filename
|
||||
FS.prototype.findFile = function findFile(root, filename) {
|
||||
return this.listFiles(root)
|
||||
.then(function(files) {
|
||||
return _.find(files, function(file) {
|
||||
return (file.toLowerCase() == filename.toLowerCase());
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Load a JSON file
|
||||
// By default, fs only supports JSON
|
||||
FS.prototype.loadAsObject = function(filename) {
|
||||
return this.readAsString(filename)
|
||||
.then(function(str) {
|
||||
return JSON.parse(str);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = FS;
|
||||
@@ -0,0 +1,66 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
var path = require('path');
|
||||
|
||||
var fs = require('../utils/fs');
|
||||
var Promise = require('../utils/promise');
|
||||
var BaseFS = require('./');
|
||||
|
||||
function NodeFS() {
|
||||
BaseFS.call(this);
|
||||
}
|
||||
util.inherits(NodeFS, BaseFS);
|
||||
|
||||
// Check if a file exists, run a Promise(true) if that's the case, Promise(false) otherwise
|
||||
NodeFS.prototype.exists = function(filename) {
|
||||
return fs.exists(filename);
|
||||
};
|
||||
|
||||
// Read a file and returns a promise with the content as a buffer
|
||||
NodeFS.prototype.read = function(filename) {
|
||||
return fs.readFile(filename);
|
||||
};
|
||||
|
||||
// Read stat infos about a file
|
||||
NodeFS.prototype.stat = function(filename) {
|
||||
return fs.stat(filename);
|
||||
};
|
||||
|
||||
// List files in a directory
|
||||
NodeFS.prototype.readdir = function(folder) {
|
||||
return fs.readdir(folder)
|
||||
.then(function(files) {
|
||||
return _.chain(files)
|
||||
.map(function(file) {
|
||||
if (file == '.' || file == '..') return;
|
||||
|
||||
var stat = fs.statSync(path.join(folder, file));
|
||||
if (stat.isDirectory()) file = file + path.sep;
|
||||
return file;
|
||||
})
|
||||
.compact()
|
||||
.value();
|
||||
});
|
||||
};
|
||||
|
||||
// Load a JSON/JS file
|
||||
NodeFS.prototype.loadAsObject = function(filename) {
|
||||
return Promise()
|
||||
.then(function() {
|
||||
var jsFile;
|
||||
|
||||
try {
|
||||
jsFile = require.resolve(filename);
|
||||
|
||||
// Invalidate node.js cache for livreloading
|
||||
delete require.cache[jsFile];
|
||||
|
||||
return require(jsFile);
|
||||
}
|
||||
catch(err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = NodeFS;
|
||||
@@ -1,70 +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("fs");
|
||||
var parse = require("../../parse");
|
||||
var BaseGenerator = require("../page");
|
||||
|
||||
/*
|
||||
* This generator inherits from the single page generator
|
||||
* and convert the page output to ebook
|
||||
*/
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
|
||||
// Options for eBook generation
|
||||
this.options = _.defaults(this.options, {
|
||||
extension: "epub"
|
||||
});
|
||||
|
||||
if (!this.options.cover && fs.existsSync(path.join(this.options.input, "cover.png"))) {
|
||||
this.options.cover = path.join(this.options.input, "cover.png")
|
||||
}
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
var that = this;
|
||||
|
||||
return BaseGenerator.prototype.finish.apply(this)
|
||||
.then(function() {
|
||||
var d = Q.defer();
|
||||
|
||||
var _options = {
|
||||
"--cover": that.options.cover
|
||||
};
|
||||
|
||||
var command = [
|
||||
"ebook-convert",
|
||||
path.join(that.options.output, "index.html"),
|
||||
path.join(that.options.output, "index."+that.options.extension),
|
||||
_.chain(_options)
|
||||
.map(function(value, key) {
|
||||
if (value == null) return null;
|
||||
return key+"="+value;
|
||||
})
|
||||
.compact()
|
||||
.value()
|
||||
.join(" ")
|
||||
].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,59 +0,0 @@
|
||||
var Q = require("q");
|
||||
var fs = require("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([
|
||||
'.git/'
|
||||
], '__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;
|
||||
},
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
var path = require("path");
|
||||
var Q = require("q");
|
||||
var fs = require("./fs");
|
||||
|
||||
var BaseGenerator = function(options) {
|
||||
this.options = options;
|
||||
};
|
||||
|
||||
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.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,229 +0,0 @@
|
||||
var Q = require("q");
|
||||
var _ = require("lodash");
|
||||
var path = require("path");
|
||||
var swig = require('swig');
|
||||
var tmp = require('tmp');
|
||||
|
||||
var fs = require("./fs");
|
||||
var parse = require("../parse");
|
||||
|
||||
var generators = {
|
||||
"site": require("./site"),
|
||||
"page": require("./page"),
|
||||
"pdf": require("./pdf"),
|
||||
"ebook": require("./ebook"),
|
||||
"json": require("./json")
|
||||
};
|
||||
|
||||
var defaultDescription = "Book generated using GitBook";
|
||||
|
||||
/*
|
||||
* Use a specific generator to convert a gitbook to a site/pdf/ebook/
|
||||
* output is always a folder
|
||||
*/
|
||||
var generate = function(options) {
|
||||
var generator = null;
|
||||
var files;
|
||||
|
||||
options = _.defaults(options || {}, {
|
||||
// Folders to use
|
||||
input: null,
|
||||
output: null,
|
||||
|
||||
// Output generator
|
||||
generator: "site",
|
||||
|
||||
// Book title, keyword, description
|
||||
title: null,
|
||||
description: null,
|
||||
|
||||
// Origin github repository id
|
||||
github: null,
|
||||
githubHost: 'https://github.com/',
|
||||
|
||||
// Theming
|
||||
theme: path.resolve(__dirname, '../../theme')
|
||||
});
|
||||
|
||||
if (!options.input) {
|
||||
return Q.reject(new Error("Need option input (book input directory)"));
|
||||
}
|
||||
|
||||
if (!generators[options.generator]) {
|
||||
return Q.reject(new Error("Invalid generator (availables are: "+_.keys(generators).join(", ")));
|
||||
}
|
||||
|
||||
options.output = options.output || path.join(options.input, "_book");
|
||||
|
||||
// Clean output folder
|
||||
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;
|
||||
})
|
||||
})
|
||||
|
||||
// Create the generator
|
||||
.then(function() {
|
||||
generator = new generators[options.generator](options);
|
||||
})
|
||||
|
||||
// Detect multi-languages book
|
||||
.then(function() {
|
||||
if (_.contains(files, "LANGS.md")) {
|
||||
// Multi-languages book
|
||||
return fs.readFile(path.join(options.input, "LANGS.md"), "utf-8")
|
||||
|
||||
// Generate sub-books
|
||||
.then(function(_langsSummary) {
|
||||
options.langsSummary = parse.langs(_langsSummary);
|
||||
|
||||
// Generated a book for each valid entry
|
||||
return Q.all(
|
||||
_.map(options.langsSummary.list, function(entry) {
|
||||
return generate(_.extend({}, options, {
|
||||
input: path.join(options.input, entry.path),
|
||||
output: path.join(options.output, entry.path)
|
||||
}));
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
// Generate languages index
|
||||
.then(function() {
|
||||
return generator.langsIndex(options.langsSummary);
|
||||
});
|
||||
} else if (!_.contains(files, "SUMMARY.md") || !_.contains(files, "README.md")) {
|
||||
// Invalid book
|
||||
return Q.reject(new Error("Invalid gitbook repository, need SUMMARY.md and README.md"));
|
||||
} else {
|
||||
// Generate the book
|
||||
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;
|
||||
});
|
||||
})
|
||||
|
||||
// Get summary
|
||||
.then(function() {
|
||||
return fs.readFile(path.join(options.input, "SUMMARY.md"), "utf-8")
|
||||
.then(function(_summary) {
|
||||
options.summary = parse.summary(_summary);
|
||||
|
||||
// Parse navigation
|
||||
options.navigation = parse.navigation(options.summary);
|
||||
});
|
||||
})
|
||||
|
||||
// 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 gneration
|
||||
.then(function() {
|
||||
return generator.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
|
||||
};
|
||||
@@ -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,47 +0,0 @@
|
||||
var util = require("util");
|
||||
var path = require("path");
|
||||
var Q = require("q");
|
||||
|
||||
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, {
|
||||
repo: that.options.githubId,
|
||||
dir: path.dirname(input) || '/'
|
||||
});
|
||||
})
|
||||
.then(function(sections) {
|
||||
json.sections = sections;
|
||||
})
|
||||
.then(function() {
|
||||
return fs.writeFile(
|
||||
path.join(that.options.output, input.replace(".md", ".json")),
|
||||
JSON.stringify(json, null, 4)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
// ignore
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
@@ -1,84 +0,0 @@
|
||||
var _ = require("lodash");
|
||||
var util = require("util");
|
||||
var path = require("path");
|
||||
var Q = require("q");
|
||||
var swig = require('swig');
|
||||
var hljs = require('highlight.js');
|
||||
|
||||
var fs = require("../fs");
|
||||
var parse = require("../../parse");
|
||||
var BaseGenerator = require("../site");
|
||||
|
||||
// Swig filter: highlight coloration
|
||||
swig.setFilter('code', function(code, lang) {
|
||||
try {
|
||||
return hljs.highlight(lang, code).value;
|
||||
} catch(e) {
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* This generator will generate a simple index.html which can be converted as a PDF
|
||||
*/
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
|
||||
// Load base template
|
||||
this.template = swig.compileFile(path.resolve(this.options.theme, 'templates/page.html'));
|
||||
|
||||
// List of pages content
|
||||
this.pages = {};
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
Generator.prototype.convertFile = function(content, input) {
|
||||
var that = this;
|
||||
var json = {
|
||||
path: input,
|
||||
progress: parse.progress(this.options.navigation, input)
|
||||
};
|
||||
|
||||
return Q()
|
||||
.then(function() {
|
||||
return parse.page(content, {
|
||||
repo: that.options.githubId,
|
||||
dir: path.dirname(input) || '/',
|
||||
outdir: './',
|
||||
});
|
||||
})
|
||||
.then(function(sections) {
|
||||
json.content = sections;
|
||||
})
|
||||
.then(function() {
|
||||
that.pages[input] = json;
|
||||
});
|
||||
};
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
var that = this;
|
||||
var basePath = ".";
|
||||
var output = path.join(this.options.output, "index.html");
|
||||
|
||||
return Q()
|
||||
// Generate html
|
||||
.then(function(pages) {
|
||||
return that._writeTemplate(that.template, {
|
||||
pages: that.pages,
|
||||
|
||||
basePath: basePath,
|
||||
staticBase: path.join(basePath, "gitbook"),
|
||||
}, output);
|
||||
})
|
||||
|
||||
// Copy assets
|
||||
.then(function() {
|
||||
return fs.copy(
|
||||
path.join(that.options.theme, "assets"),
|
||||
path.join(that.options.output, "gitbook")
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
@@ -1,56 +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("../fs");
|
||||
var parse = require("../../parse");
|
||||
var BaseGenerator = require("../page");
|
||||
|
||||
/*
|
||||
* This generator inherits from the single page generator
|
||||
* and convert the page output to pdf using gitbook-pdf
|
||||
*/
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
|
||||
// Options for PDF generation
|
||||
this.options = _.defaults(this.options, {
|
||||
paperformat: "A4"
|
||||
});
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
var that = this;
|
||||
|
||||
return BaseGenerator.prototype.finish.apply(this)
|
||||
.then(function() {
|
||||
var d = Q.defer();
|
||||
|
||||
var command = [
|
||||
"gitbook-pdf",
|
||||
"generate",
|
||||
path.join(that.options.output, "index.html"),
|
||||
path.join(that.options.output, "index.pdf"),
|
||||
"--format="+that.options.paperformat
|
||||
].join(" ");
|
||||
|
||||
exec(command, function (error, stdout, stderr) {
|
||||
if (error) {
|
||||
if (error.code == 127) {
|
||||
error.message = "Need to install gitbook-pdf using: npm install gitbook-pdf -g";
|
||||
} else {
|
||||
error.message = error.message + " "+stdout;
|
||||
}
|
||||
return d.reject(error);
|
||||
}
|
||||
d.resolve();
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
@@ -1,156 +0,0 @@
|
||||
var util = require("util");
|
||||
var path = require("path");
|
||||
var Q = require("q");
|
||||
var _ = require("lodash");
|
||||
var swig = require('swig');
|
||||
|
||||
var fs = require("../fs");
|
||||
var parse = require("../../parse");
|
||||
var BaseGenerator = require("../generator");
|
||||
|
||||
var indexer = require('./search_indexer');
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
var Generator = function() {
|
||||
BaseGenerator.apply(this, arguments);
|
||||
|
||||
// Attach methods to instance
|
||||
_.bindAll(this);
|
||||
|
||||
this.revision = Date.now();
|
||||
this.indexer = indexer();
|
||||
|
||||
// Load base template
|
||||
this.template = swig.compileFile(path.resolve(this.options.theme, 'templates/site.html'));
|
||||
this.langsTemplate = swig.compileFile(path.resolve(this.options.theme, 'templates/langs.html'));
|
||||
};
|
||||
util.inherits(Generator, BaseGenerator);
|
||||
|
||||
|
||||
// Generate a template
|
||||
Generator.prototype._writeTemplate = function(tpl, options, output) {
|
||||
var that = this;
|
||||
return Q()
|
||||
.then(function(sections) {
|
||||
return tpl(_.extend({
|
||||
revision: that.revision,
|
||||
|
||||
title: that.options.title,
|
||||
description: that.options.description,
|
||||
|
||||
githubAuthor: that.options.github ? that.options.github.split("/")[0] : "",
|
||||
githubId: that.options.github,
|
||||
githubHost: that.options.githubHost,
|
||||
|
||||
summary: that.options.summary,
|
||||
allNavigation: that.options.navigation
|
||||
}, options));
|
||||
})
|
||||
.then(function(html) {
|
||||
return fs.writeFile(
|
||||
output,
|
||||
html
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Generator.prototype.indexPage = function(lexed, pagePath) {
|
||||
this.indexer.add(lexed, pagePath);
|
||||
return Q();
|
||||
};
|
||||
|
||||
// Convert a markdown file to html
|
||||
Generator.prototype.convertFile = function(content, _input) {
|
||||
var that = this;
|
||||
var progress = parse.progress(this.options.navigation, _input);
|
||||
|
||||
_output = _input.replace(".md", ".html");
|
||||
if (_output == "README.html") _output = "index.html";
|
||||
|
||||
var input = path.join(this.options.input, _input);
|
||||
var output = path.join(this.options.output, _output);
|
||||
var basePath = path.relative(path.dirname(output), this.options.output) || ".";
|
||||
|
||||
return Q()
|
||||
.then(function() {
|
||||
// Lex page
|
||||
return parse.lex(content);
|
||||
})
|
||||
.then(function(lexed) {
|
||||
// Index page in search
|
||||
return that.indexPage(lexed, _output)
|
||||
.then(_.constant(lexed));
|
||||
})
|
||||
.then(function(lexed) {
|
||||
// Get HTML generated sections
|
||||
return parse.page(lexed, {
|
||||
repo: that.options.githubId,
|
||||
dir: path.dirname(_input) || '/',
|
||||
outdir: path.dirname(_input) || '/',
|
||||
});
|
||||
})
|
||||
.then(function(sections) {
|
||||
return that._writeTemplate(that.template, {
|
||||
progress: progress,
|
||||
|
||||
_input: _input,
|
||||
content: sections,
|
||||
|
||||
basePath: basePath,
|
||||
staticBase: path.join(basePath, "gitbook"),
|
||||
}, output);
|
||||
});
|
||||
};
|
||||
|
||||
// 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();
|
||||
});
|
||||
};
|
||||
|
||||
// Copy assets
|
||||
Generator.prototype.copyAssets = function() {
|
||||
var that = this;
|
||||
|
||||
return fs.copy(
|
||||
path.join(that.options.theme, "assets"),
|
||||
path.join(that.options.output, "gitbook")
|
||||
);
|
||||
};
|
||||
|
||||
// Dump search index to disk
|
||||
Generator.prototype.writeSearchIndex = function() {
|
||||
return fs.writeFile(
|
||||
path.join(this.options.output, 'search_index.json'),
|
||||
this.indexer.dump()
|
||||
);
|
||||
};
|
||||
|
||||
Generator.prototype.finish = function() {
|
||||
return this.copyAssets()
|
||||
.then(this.writeSearchIndex);
|
||||
};
|
||||
|
||||
module.exports = Generator;
|
||||
@@ -1,71 +0,0 @@
|
||||
var Q = require("q");
|
||||
var _ = require("lodash");
|
||||
|
||||
var lunr = require('lunr');
|
||||
var marked = require('marked');
|
||||
var textRenderer = require('marked-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);
|
||||
|
||||
// marked's Render expects this, we don't use it yet
|
||||
section.links = {};
|
||||
|
||||
var options = _.extend({}, marked.defaults, {
|
||||
renderer: this.renderer
|
||||
});
|
||||
|
||||
return marked.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;
|
||||
@@ -0,0 +1,33 @@
|
||||
var semver = require('semver');
|
||||
var pkg = require('../package.json');
|
||||
|
||||
var VERSION = pkg.version;
|
||||
var VERSION_STABLE = VERSION.replace(/\-(\S+)/g, '');
|
||||
|
||||
var START_TIME = new Date();
|
||||
|
||||
// Verify that this gitbook version satisfies a requirement
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Return templating/json context for gitbook itself
|
||||
function getContext() {
|
||||
return {
|
||||
gitbook: {
|
||||
version: pkg.version,
|
||||
time: START_TIME
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
version: pkg.version,
|
||||
satisfies: satisfies,
|
||||
getContext: getContext
|
||||
};
|
||||
+6
-4
@@ -1,5 +1,7 @@
|
||||
module.exports = {
|
||||
parse: require('./parse/'),
|
||||
generate: require('./generate/')
|
||||
};
|
||||
var Book = require('./book');
|
||||
var cli = require('./cli');
|
||||
|
||||
module.exports = {
|
||||
Book: Book,
|
||||
commands: cli.commands
|
||||
};
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
|
||||
var fs = require('./utils/fs');
|
||||
var Promise = require('./utils/promise');
|
||||
|
||||
// Initialize folder structure for a book
|
||||
// Read SUMMARY to created the right chapter
|
||||
function initBook(book) {
|
||||
var extensionToUse = '.md';
|
||||
|
||||
book.log.info.ln('init book at', book.root);
|
||||
return fs.mkdirp(book.root)
|
||||
.then(function() {
|
||||
return book.config.load();
|
||||
})
|
||||
.then(function() {
|
||||
book.log.info.ln('detect structure from SUMMARY (if it exists)');
|
||||
return book.summary.load();
|
||||
})
|
||||
.then(function() {
|
||||
var summary = book.summary.path || 'SUMMARY.md';
|
||||
var articles = book.summary.flatten();
|
||||
|
||||
// Use extension of summary
|
||||
extensionToUse = path.extname(summary);
|
||||
|
||||
// Readme doesn't have a path
|
||||
if (!articles[0].path) {
|
||||
articles[0].path = 'README' + extensionToUse;
|
||||
}
|
||||
|
||||
// Summary doesn't exists? create one
|
||||
if (!book.summary.path) {
|
||||
articles.push({
|
||||
title: 'Summary',
|
||||
path: 'SUMMARY'+extensionToUse
|
||||
});
|
||||
}
|
||||
|
||||
// Create files that don't exist
|
||||
return Promise.serie(articles, function(article) {
|
||||
if (!article.path) return;
|
||||
|
||||
var absolutePath = book.resolve(article.path);
|
||||
|
||||
return fs.exists(absolutePath)
|
||||
.then(function(exists) {
|
||||
if(exists) {
|
||||
book.log.info.ln('found', article.path);
|
||||
return;
|
||||
} else {
|
||||
book.log.info.ln('create', article.path);
|
||||
}
|
||||
|
||||
return fs.mkdirp(path.dirname(absolutePath))
|
||||
.then(function() {
|
||||
return fs.writeFile(absolutePath, '# '+article.title+'\n\n');
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
book.log.info.ln('initialization is finished');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = initBook;
|
||||
@@ -0,0 +1,140 @@
|
||||
var util = require('util');
|
||||
var path = require('path');
|
||||
var crc = require('crc');
|
||||
|
||||
var FolderOutput = require('./folder')();
|
||||
var Promise = require('../utils/promise');
|
||||
var fs = require('../utils/fs');
|
||||
var imagesUtil = require('../utils/images');
|
||||
var location = require('../utils/location');
|
||||
|
||||
var DEFAULT_ASSETS_FOLDER = 'assets';
|
||||
|
||||
/*
|
||||
Mixin to inline all the assets in a book:
|
||||
- Outline <svg> tags
|
||||
- Download remote images
|
||||
- Convert .svg images as png
|
||||
*/
|
||||
|
||||
module.exports = function assetsInliner(Base) {
|
||||
Base = Base || FolderOutput;
|
||||
|
||||
function AssetsInliner() {
|
||||
Base.apply(this, arguments);
|
||||
|
||||
// Map of svg already converted
|
||||
this.svgs = {};
|
||||
this.inlineSvgs = {};
|
||||
|
||||
// Map of images already downloaded
|
||||
this.downloaded = {};
|
||||
}
|
||||
util.inherits(AssetsInliner, Base);
|
||||
|
||||
// Output a SVG buffer as a file
|
||||
AssetsInliner.prototype.onOutputSVG = function(page, svg) {
|
||||
this.log.debug.ln('output svg from', page.path);
|
||||
|
||||
// Convert svg buffer to a png file
|
||||
return this.convertSVGBuffer(svg)
|
||||
|
||||
// Return relative path from the page
|
||||
.then(function(filename) {
|
||||
return page.relative('/' + filename);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Output an image as a file
|
||||
AssetsInliner.prototype.onOutputImage = function(page, src) {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
|
||||
// Download file if external
|
||||
.then(function() {
|
||||
if (!location.isExternal(src)) return;
|
||||
|
||||
return that.downloadAsset(src)
|
||||
.then(function(_asset) {
|
||||
src = '/' + _asset;
|
||||
});
|
||||
|
||||
})
|
||||
.then(function() {
|
||||
// Resolve src to a relative filepath to the book's root
|
||||
src = page.resolveLocal(src);
|
||||
|
||||
// Already a PNG/JPG/.. ?
|
||||
if (path.extname(src).toLowerCase() != '.svg') {
|
||||
return src;
|
||||
}
|
||||
|
||||
// Convert SVG to PNG
|
||||
return that.convertSVGFile(that.resolve(src));
|
||||
})
|
||||
|
||||
// Return relative path from the page
|
||||
.then(function(filename) {
|
||||
return page.relative(filename);
|
||||
});
|
||||
};
|
||||
|
||||
// Download an asset if not already download; returns the output file
|
||||
AssetsInliner.prototype.downloadAsset = function(src) {
|
||||
if (this.downloaded[src]) return Promise(this.downloaded[src]);
|
||||
|
||||
var that = this;
|
||||
var ext = path.extname(src);
|
||||
var hash = crc.crc32(src).toString(16);
|
||||
|
||||
// Create new file
|
||||
return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + ext)
|
||||
.then(function(filename) {
|
||||
that.downloaded[src] = filename;
|
||||
|
||||
that.log.debug.ln('downloading asset', src);
|
||||
return fs.download(src, that.resolve(filename))
|
||||
.thenResolve(filename);
|
||||
});
|
||||
};
|
||||
|
||||
// Convert a .svg into an .png
|
||||
// Return the output filename for the .png
|
||||
AssetsInliner.prototype.convertSVGFile = function(src) {
|
||||
if (this.svgs[src]) return Promise(this.svgs[src]);
|
||||
|
||||
var that = this;
|
||||
var hash = crc.crc32(src).toString(16);
|
||||
|
||||
// Create new file
|
||||
return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png')
|
||||
.then(function(filename) {
|
||||
that.svgs[src] = filename;
|
||||
|
||||
return imagesUtil.convertSVGToPNG(src, that.resolve(filename))
|
||||
.thenResolve(filename);
|
||||
});
|
||||
};
|
||||
|
||||
// Convert an inline svg into an .png
|
||||
// Return the output filename for the .png
|
||||
AssetsInliner.prototype.convertSVGBuffer = function(buf) {
|
||||
var that = this;
|
||||
var hash = crc.crc32(buf).toString(16);
|
||||
|
||||
// Already converted?
|
||||
if (this.inlineSvgs[hash]) return Promise(this.inlineSvgs[hash]);
|
||||
|
||||
return this.createNewFile(DEFAULT_ASSETS_FOLDER, hash + '.png')
|
||||
.then(function(filename) {
|
||||
that.inlineSvgs[hash] = filename;
|
||||
|
||||
return imagesUtil.convertSVGBufferToPNG(buf, that.resolve(filename))
|
||||
.thenResolve(filename);
|
||||
});
|
||||
};
|
||||
|
||||
return AssetsInliner;
|
||||
};
|
||||
@@ -0,0 +1,275 @@
|
||||
var _ = require('lodash');
|
||||
var Ignore = require('ignore');
|
||||
var path = require('path');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var pathUtil = require('../utils/path');
|
||||
var location = require('../utils/location');
|
||||
var PluginsManager = require('../plugins');
|
||||
var TemplateEngine = require('../template');
|
||||
|
||||
/*
|
||||
Output is like a stream interface for a parsed book
|
||||
to output "something".
|
||||
|
||||
The process is mostly on the behavior of "onPage" and "onAsset"
|
||||
*/
|
||||
|
||||
function Output(book, opts, parent) {
|
||||
_.bindAll(this);
|
||||
this.parent = parent;
|
||||
|
||||
this.opts = _.defaults({}, opts || {}, {
|
||||
directoryIndex: true
|
||||
});
|
||||
|
||||
this.book = book;
|
||||
book.output = this;
|
||||
this.log = this.book.log;
|
||||
|
||||
// Create plugins manager
|
||||
this.plugins = new PluginsManager(this.book);
|
||||
|
||||
// Create template engine
|
||||
this.template = new TemplateEngine(this);
|
||||
|
||||
// Files to ignore in output
|
||||
this.ignore = Ignore();
|
||||
}
|
||||
|
||||
// Default extension for output
|
||||
Output.prototype.defaultExtension = '.html';
|
||||
|
||||
// Start the generation, for a parsed book
|
||||
Output.prototype.generate = function() {
|
||||
var that = this;
|
||||
var isMultilingual = this.book.isMultilingual();
|
||||
|
||||
return Promise()
|
||||
|
||||
// Load all plugins
|
||||
.then(function() {
|
||||
return that.plugins.loadAll()
|
||||
.then(function() {
|
||||
that.template.addFilters(that.plugins.getFilters());
|
||||
that.template.addBlocks(that.plugins.getBlocks());
|
||||
});
|
||||
})
|
||||
|
||||
// Transform the configuration
|
||||
.then(function() {
|
||||
return that.plugins.hook('config', that.book.config.dump())
|
||||
.then(function(cfg) {
|
||||
that.book.config.replace(cfg);
|
||||
});
|
||||
})
|
||||
|
||||
// Initialize the generation
|
||||
.then(function() {
|
||||
return that.plugins.hook('init');
|
||||
})
|
||||
.then(function() {
|
||||
that.log.info.ln('preparing the generation');
|
||||
return that.prepare();
|
||||
})
|
||||
|
||||
// Process all files
|
||||
.then(function() {
|
||||
that.log.debug.ln('listing files');
|
||||
return that.book.fs.listAllFiles(that.book.root);
|
||||
})
|
||||
|
||||
// We want to process assets first, then pages
|
||||
// Since pages can have logic based on existance of assets
|
||||
.then(function(files) {
|
||||
// Split into pages/assets
|
||||
var byTypes = _.chain(files)
|
||||
.filter(that.ignore.createFilter())
|
||||
|
||||
// Ignore file present in a language book
|
||||
.filter(function(filename) {
|
||||
return !(isMultilingual && that.book.isInLanguageBook(filename));
|
||||
})
|
||||
|
||||
.groupBy(function(filename) {
|
||||
return (that.book.hasPage(filename)? 'page' : 'asset');
|
||||
})
|
||||
|
||||
.value();
|
||||
|
||||
return Promise.serie(byTypes.asset, function(filename) {
|
||||
that.log.debug.ln('copy asset', filename);
|
||||
return that.onAsset(filename);
|
||||
})
|
||||
.then(function() {
|
||||
return Promise.serie(byTypes.page, function(filename) {
|
||||
that.log.debug.ln('process page', filename);
|
||||
return that.onPage(that.book.getPage(filename));
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
// Generate sub-books
|
||||
.then(function() {
|
||||
if (!that.book.isMultilingual()) return;
|
||||
|
||||
return Promise.serie(that.book.books, function(subbook) {
|
||||
that.log.info.ln('');
|
||||
that.log.info.ln('start generation of language "' + path.relative(that.book.root, subbook.root) + '"');
|
||||
|
||||
var out = that.onLanguageBook(subbook);
|
||||
return out.generate();
|
||||
});
|
||||
})
|
||||
|
||||
// Finish the generation
|
||||
.then(function() {
|
||||
return that.plugins.hook('finish:before');
|
||||
})
|
||||
.then(function() {
|
||||
that.log.debug.ln('finishing the generation');
|
||||
return that.finish();
|
||||
})
|
||||
.then(function() {
|
||||
return that.plugins.hook('finish');
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
if (!that.book.isLanguageBook()) that.log.info.ln('');
|
||||
that.log.info.ok('generation finished with success!');
|
||||
});
|
||||
};
|
||||
|
||||
// Prepare the generation
|
||||
Output.prototype.prepare = function() {
|
||||
this.ignore.addPattern(_.compact([
|
||||
'.gitignore',
|
||||
'.ignore',
|
||||
'.bookignore',
|
||||
'node_modules',
|
||||
|
||||
// The configuration file should not be copied in the output
|
||||
this.book.config.path,
|
||||
|
||||
// Structure file to ignore
|
||||
this.book.summary.path,
|
||||
this.book.langs.path
|
||||
]));
|
||||
};
|
||||
|
||||
// Write a page (parsable file), ex: markdown, etc
|
||||
Output.prototype.onPage = function(page) {
|
||||
return page.toHTML(this);
|
||||
};
|
||||
|
||||
// Copy an asset file (non-parsable), ex: images, etc
|
||||
Output.prototype.onAsset = function(filename) {
|
||||
|
||||
};
|
||||
|
||||
// Finish the generation
|
||||
Output.prototype.finish = function() {
|
||||
|
||||
};
|
||||
|
||||
// Resolve an HTML link
|
||||
Output.prototype.onRelativeLink = function(currentPage, href) {
|
||||
var to = currentPage.followPage(href);
|
||||
|
||||
// Replace by an .html link
|
||||
if (to) {
|
||||
href = to.path;
|
||||
|
||||
// Recalcul as relative link
|
||||
href = currentPage.relative(href);
|
||||
|
||||
// Replace .md by .html
|
||||
href = this.outputUrl(href);
|
||||
}
|
||||
|
||||
return href;
|
||||
};
|
||||
|
||||
// Output a SVG buffer as a file
|
||||
Output.prototype.onOutputSVG = function(page, svg) {
|
||||
return null;
|
||||
};
|
||||
|
||||
// Output an image as a file
|
||||
// Normalize the relative link
|
||||
Output.prototype.onOutputImage = function(page, imgFile) {
|
||||
imgFile = page.resolveLocal(imgFile);
|
||||
return page.relative(imgFile);
|
||||
};
|
||||
|
||||
// Read a template by its source URL
|
||||
Output.prototype.onGetTemplate = function(sourceUrl) {
|
||||
throw new Error('template not found '+sourceUrl);
|
||||
};
|
||||
|
||||
// Generate a source URL for a template
|
||||
Output.prototype.onResolveTemplate = function(from, to) {
|
||||
return path.resolve(path.dirname(from), to);
|
||||
};
|
||||
|
||||
// Prepare output for a language book
|
||||
Output.prototype.onLanguageBook = function(book) {
|
||||
return new this.constructor(book, this.opts, this);
|
||||
};
|
||||
|
||||
|
||||
// ---- Utilities ----
|
||||
|
||||
// Return a default context for templates
|
||||
Output.prototype.getContext = function() {
|
||||
return _.extend(
|
||||
{},
|
||||
this.book.getContext(),
|
||||
this.book.langs.getContext(),
|
||||
this.book.summary.getContext(),
|
||||
this.book.glossary.getContext(),
|
||||
this.book.config.getContext()
|
||||
);
|
||||
};
|
||||
|
||||
// Resolve a file path in the context of a specific page
|
||||
// Result is an "absolute path relative to the output folder"
|
||||
Output.prototype.resolveForPage = function(page, href) {
|
||||
if (_.isString(page)) page = this.book.getPage(page);
|
||||
|
||||
href = page.relative(href);
|
||||
return this.onRelativeLink(page, href);
|
||||
};
|
||||
|
||||
// Filename for output
|
||||
// READMEs are replaced by index.html
|
||||
// /test/README.md -> /test/index.html
|
||||
Output.prototype.outputPath = function(filename, ext) {
|
||||
ext = ext || this.defaultExtension;
|
||||
var output = filename;
|
||||
|
||||
if (
|
||||
path.basename(filename, path.extname(filename)) == 'README' ||
|
||||
output == this.book.readme.path
|
||||
) {
|
||||
output = path.join(path.dirname(output), 'index'+ext);
|
||||
} else {
|
||||
output = pathUtil.setExtension(output, ext);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
// Filename for output
|
||||
// /test/index.html -> /test/
|
||||
Output.prototype.outputUrl = function(filename, ext) {
|
||||
var href = this.outputPath(filename, ext);
|
||||
|
||||
if (path.basename(href) == 'index.html' && this.opts.directoryIndex) {
|
||||
href = path.dirname(href) + '/';
|
||||
}
|
||||
|
||||
return location.normalize(href);
|
||||
};
|
||||
|
||||
module.exports = Output;
|
||||
@@ -0,0 +1,67 @@
|
||||
var path = require('path');
|
||||
var util = require('util');
|
||||
|
||||
var folderOutput = require('./folder');
|
||||
var Git = require('../utils/git');
|
||||
var fs = require('../utils/fs');
|
||||
var pathUtil = require('../utils/path');
|
||||
var location = require('../utils/location');
|
||||
|
||||
/*
|
||||
Mixin for output to resolve git conrefs
|
||||
*/
|
||||
|
||||
module.exports = function conrefsLoader(Base) {
|
||||
Base = folderOutput(Base);
|
||||
|
||||
function ConrefsLoader() {
|
||||
Base.apply(this, arguments);
|
||||
|
||||
this.git = new Git();
|
||||
}
|
||||
util.inherits(ConrefsLoader, Base);
|
||||
|
||||
// Read a template by its source URL
|
||||
ConrefsLoader.prototype.onGetTemplate = function(sourceURL) {
|
||||
var that = this;
|
||||
|
||||
return this.git.resolve(sourceURL)
|
||||
.then(function(filepath) {
|
||||
// Is local file
|
||||
if (!filepath) {
|
||||
filepath = that.book.resolve(sourceURL);
|
||||
} else {
|
||||
that.book.log.debug.ln('resolve from git', sourceURL, 'to', filepath);
|
||||
}
|
||||
|
||||
// Read file from absolute path
|
||||
return fs.readFile(filepath)
|
||||
.then(function(source) {
|
||||
return {
|
||||
src: source.toString('utf8'),
|
||||
path: filepath
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Generate a source URL for a template
|
||||
ConrefsLoader.prototype.onResolveTemplate = function(from, to) {
|
||||
// If origin is in the book, we enforce result file to be in the book
|
||||
if (this.book.isInBook(from)) {
|
||||
var href = location.toAbsolute(to, path.dirname(from), '');
|
||||
return this.book.resolve(href);
|
||||
}
|
||||
|
||||
// If origin is in a git repository, we resolve file in the git repository
|
||||
var gitRoot = this.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);
|
||||
};
|
||||
|
||||
return ConrefsLoader;
|
||||
};
|
||||
@@ -0,0 +1,190 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
var juice = require('juice');
|
||||
|
||||
var command = require('../utils/command');
|
||||
var fs = require('../utils/fs');
|
||||
var Promise = require('../utils/promise');
|
||||
var error = require('../utils/error');
|
||||
var WebsiteOutput = require('./website');
|
||||
var assetsInliner = require('./assets-inliner');
|
||||
|
||||
function _EbookOutput() {
|
||||
WebsiteOutput.apply(this, arguments);
|
||||
|
||||
// ebook-convert does not support link like "./"
|
||||
this.opts.directoryIndex = false;
|
||||
}
|
||||
util.inherits(_EbookOutput, WebsiteOutput);
|
||||
|
||||
var EbookOutput = assetsInliner(_EbookOutput);
|
||||
|
||||
EbookOutput.prototype.name = 'ebook';
|
||||
|
||||
// Finish generation, create ebook using ebook-convert
|
||||
EbookOutput.prototype.finish = function() {
|
||||
var that = this;
|
||||
if (that.book.isMultilingual()) {
|
||||
return EbookOutput.super_.prototype.finish.apply(that);
|
||||
}
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
return EbookOutput.super_.prototype.finish.apply(that);
|
||||
})
|
||||
|
||||
// Generate SUMMARY.html
|
||||
.then(function() {
|
||||
return that.render('summary', that.getContext())
|
||||
.then(function(html) {
|
||||
return that.writeFile(
|
||||
'SUMMARY.html',
|
||||
html
|
||||
);
|
||||
});
|
||||
})
|
||||
|
||||
// Start ebook-convert
|
||||
.then(function() {
|
||||
return that.ebookConvertOption();
|
||||
})
|
||||
|
||||
.then(function(options) {
|
||||
if (!that.opts.format) return;
|
||||
|
||||
var cmd = [
|
||||
'ebook-convert',
|
||||
that.resolve('SUMMARY.html'),
|
||||
that.resolve('index.'+that.opts.format),
|
||||
command.optionsToShellArgs(options)
|
||||
].join(' ');
|
||||
|
||||
return command.exec(cmd)
|
||||
.progress(function(data) {
|
||||
that.book.log.debug(data);
|
||||
})
|
||||
.fail(function(err) {
|
||||
if (err.code == 127) {
|
||||
throw error.RequireInstallError({
|
||||
cmd: 'ebook-convert',
|
||||
install: 'Install it from Calibre: https://calibre-ebook.com'
|
||||
});
|
||||
}
|
||||
|
||||
throw error.EbookError(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Generate header/footer for PDF
|
||||
EbookOutput.prototype.getPDFTemplate = function(tpl) {
|
||||
var that = this;
|
||||
var context = _.extend(
|
||||
{
|
||||
// Nunjucks context mapping to ebook-convert templating
|
||||
page: {
|
||||
num: '_PAGENUM_',
|
||||
title: '_TITLE_',
|
||||
section: '_SECTION_'
|
||||
}
|
||||
},
|
||||
this.getContext()
|
||||
);
|
||||
|
||||
return this.render('pdf_'+tpl, context)
|
||||
|
||||
// Inline css, include css relative to the output folder
|
||||
.then(function(output) {
|
||||
return Promise.nfcall(juice.juiceResources, output, {
|
||||
webResources: {
|
||||
relativeTo: that.root()
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Locate the cover file to use
|
||||
// Use configuration or search a "cover.jpg" file
|
||||
// For multi-lingual book, it can use the one from the main book
|
||||
EbookOutput.prototype.locateCover = function() {
|
||||
var cover = this.book.config.get('cover', 'cover.jpg');
|
||||
|
||||
// Resolve to absolute
|
||||
cover = this.resolve(cover);
|
||||
|
||||
// Cover doesn't exist and multilingual?
|
||||
if (!fs.existsSync(cover)) {
|
||||
if (this.parent) return this.parent.locateCover()
|
||||
else return undefined;
|
||||
}
|
||||
|
||||
return cover;
|
||||
};
|
||||
|
||||
// Generate options for ebook-convert
|
||||
EbookOutput.prototype.ebookConvertOption = function() {
|
||||
var that = this;
|
||||
|
||||
var options = {
|
||||
'--cover': this.locateCover(),
|
||||
'--title': that.book.config.get('title'),
|
||||
'--comments': that.book.config.get('description'),
|
||||
'--isbn': that.book.config.get('isbn'),
|
||||
'--authors': that.book.config.get('author'),
|
||||
'--language': that.book.config.get('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.opts.format == 'epub') {
|
||||
options = _.extend(options, {
|
||||
'--dont-split-on-page-breaks': true
|
||||
});
|
||||
}
|
||||
|
||||
if (that.opts.format != 'pdf') return Promise(options);
|
||||
|
||||
var pdfOptions = that.book.config.get('pdf');
|
||||
|
||||
options = _.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)
|
||||
});
|
||||
|
||||
return that.getPDFTemplate('header')
|
||||
.then(function(tpl) {
|
||||
options['--pdf-header-template'] = tpl;
|
||||
|
||||
return that.getPDFTemplate('footer');
|
||||
})
|
||||
.then(function(tpl) {
|
||||
options['--pdf-footer-template'] = tpl;
|
||||
|
||||
return options;
|
||||
});
|
||||
};
|
||||
|
||||
// Don't write multi-lingual index for wbook
|
||||
EbookOutput.prototype.outputMultilingualIndex = function() {
|
||||
|
||||
};
|
||||
|
||||
module.exports = EbookOutput;
|
||||
@@ -0,0 +1,152 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
var path = require('path');
|
||||
|
||||
var Output = require('./base');
|
||||
var fs = require('../utils/fs');
|
||||
var pathUtil = require('../utils/path');
|
||||
var Promise = require('../utils/promise');
|
||||
|
||||
/*
|
||||
This output requires the native fs module to output
|
||||
book as a directory (mapping assets and pages)
|
||||
*/
|
||||
|
||||
module.exports = function folderOutput(Base) {
|
||||
Base = Base || Output;
|
||||
|
||||
function FolderOutput() {
|
||||
Base.apply(this, arguments);
|
||||
|
||||
this.opts.root = path.resolve(this.opts.root || this.book.resolve('_book'));
|
||||
}
|
||||
util.inherits(FolderOutput, Base);
|
||||
|
||||
// Copy an asset file (non-parsable), ex: images, etc
|
||||
FolderOutput.prototype.onAsset = function(filename) {
|
||||
return this.copyFile(
|
||||
this.book.resolve(filename),
|
||||
filename
|
||||
);
|
||||
};
|
||||
|
||||
// Prepare the generation by creating the output folder
|
||||
FolderOutput.prototype.prepare = function() {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
return FolderOutput.super_.prototype.prepare.apply(that);
|
||||
})
|
||||
|
||||
// Cleanup output folder
|
||||
.then(function() {
|
||||
that.log.debug.ln('removing previous output directory');
|
||||
return fs.rmDir(that.root())
|
||||
.fail(function() {
|
||||
return Promise();
|
||||
});
|
||||
})
|
||||
|
||||
// Create output folder
|
||||
.then(function() {
|
||||
that.log.debug.ln('creating output directory');
|
||||
return fs.mkdirp(that.root());
|
||||
})
|
||||
|
||||
// Add output folder to ignored files
|
||||
.then(function() {
|
||||
that.ignore.addPattern([
|
||||
path.relative(that.book.root, that.root())
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
// Prepare output for a language book
|
||||
FolderOutput.prototype.onLanguageBook = function(book) {
|
||||
return new this.constructor(book, _.extend({}, this.opts, {
|
||||
|
||||
// Language output should be output in sub-directory of output
|
||||
root: path.resolve(this.root(), book.language)
|
||||
}), this);
|
||||
};
|
||||
|
||||
// ----- Utility methods -----
|
||||
|
||||
// Return path to the root folder
|
||||
FolderOutput.prototype.root = function() {
|
||||
return this.opts.root;
|
||||
};
|
||||
|
||||
// Resolve a file in the output directory
|
||||
FolderOutput.prototype.resolve = function(filename) {
|
||||
return pathUtil.resolveInRoot.apply(null, [this.root()].concat(_.toArray(arguments)));
|
||||
};
|
||||
|
||||
// Copy a file to the output
|
||||
FolderOutput.prototype.copyFile = function(from, to) {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
to = that.resolve(to);
|
||||
var folder = path.dirname(to);
|
||||
|
||||
// Ensure folder exists
|
||||
return fs.mkdirp(folder);
|
||||
})
|
||||
.then(function() {
|
||||
return fs.copy(from, to);
|
||||
});
|
||||
};
|
||||
|
||||
// Write a file/buffer to the output folder
|
||||
FolderOutput.prototype.writeFile = function(filename, buf) {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
filename = that.resolve(filename);
|
||||
var folder = path.dirname(filename);
|
||||
|
||||
// Ensure folder exists
|
||||
return fs.mkdirp(folder);
|
||||
})
|
||||
|
||||
// Write the file
|
||||
.then(function() {
|
||||
return fs.writeFile(filename, buf);
|
||||
});
|
||||
};
|
||||
|
||||
// Return true if a file exists in the output folder
|
||||
FolderOutput.prototype.hasFile = function(filename) {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
return fs.exists(that.resolve(filename));
|
||||
});
|
||||
};
|
||||
|
||||
// Create a new unique file
|
||||
// Returns its filename
|
||||
FolderOutput.prototype.createNewFile = function(base, filename) {
|
||||
var that = this;
|
||||
|
||||
if (!filename) {
|
||||
filename = path.basename(filename);
|
||||
base = path.dirname(base);
|
||||
}
|
||||
|
||||
return fs.uniqueFilename(this.resolve(base), filename)
|
||||
.then(function(out) {
|
||||
out = path.join(base, out);
|
||||
|
||||
return fs.ensure(that.resolve(out))
|
||||
.thenResolve(out);
|
||||
});
|
||||
};
|
||||
|
||||
return FolderOutput;
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
var conrefsLoader = require('./conrefs');
|
||||
|
||||
var JSONOutput = conrefsLoader();
|
||||
|
||||
JSONOutput.prototype.name = 'json';
|
||||
|
||||
// Don't copy asset on JSON output
|
||||
JSONOutput.prototype.onAsset = function(filename) {};
|
||||
|
||||
// Write a page (parsable file)
|
||||
JSONOutput.prototype.onPage = function(page) {
|
||||
var that = this;
|
||||
|
||||
// Parse the page
|
||||
return page.toHTML(this)
|
||||
|
||||
// Write as json
|
||||
.then(function() {
|
||||
var json = page.getContext();
|
||||
|
||||
// Delete some private properties
|
||||
delete json.config;
|
||||
|
||||
// Specify JSON output version
|
||||
json.version = '2';
|
||||
|
||||
return that.writeFile(
|
||||
page.withExtension('.json'),
|
||||
JSON.stringify(json, null, 4)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// At the end of generation, generate README.json for multilingual books
|
||||
JSONOutput.prototype.finish = function() {
|
||||
if (!this.book.isMultilingual()) return;
|
||||
|
||||
// Copy README.json from main book
|
||||
var mainLanguage = this.book.langs.getDefault().id;
|
||||
return this.copyFile(
|
||||
this.resolve(mainLanguage, 'README.json'),
|
||||
'README.json'
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
module.exports = JSONOutput;
|
||||
@@ -0,0 +1,270 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var util = require('util');
|
||||
var nunjucks = require('nunjucks');
|
||||
var I18n = require('i18n-t');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var location = require('../utils/location');
|
||||
var fs = require('../utils/fs');
|
||||
var defaultFilters = require('../template/filters');
|
||||
var conrefsLoader = require('./conrefs');
|
||||
var Output = require('./base');
|
||||
|
||||
// Tranform a theme ID into a plugin
|
||||
function themeID(plugin) {
|
||||
return 'theme-' + plugin;
|
||||
}
|
||||
|
||||
// Directory for a theme with the templates
|
||||
function templatesPath(dir) {
|
||||
return path.join(dir, '_layouts');
|
||||
}
|
||||
|
||||
function _WebsiteOutput() {
|
||||
Output.apply(this, arguments);
|
||||
|
||||
// Nunjucks environment
|
||||
this.env;
|
||||
|
||||
// Plugin instance for the main theme
|
||||
this.theme;
|
||||
|
||||
// Plugin instance for the default theme
|
||||
this.defaultTheme;
|
||||
|
||||
// Resources loaded from plugins
|
||||
this.resources;
|
||||
|
||||
// i18n for themes
|
||||
this.i18n = new I18n();
|
||||
}
|
||||
util.inherits(_WebsiteOutput, Output);
|
||||
|
||||
var WebsiteOutput = conrefsLoader(_WebsiteOutput);
|
||||
|
||||
// Name of the generator
|
||||
// It's being used as a prefix for templates
|
||||
WebsiteOutput.prototype.name = 'website';
|
||||
|
||||
// Load and setup the theme
|
||||
WebsiteOutput.prototype.prepare = function() {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
return WebsiteOutput.super_.prototype.prepare.apply(that);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
var themeName = that.book.config.get('theme');
|
||||
that.theme = that.plugins.get(themeID(themeName));
|
||||
that.themeDefault = that.plugins.get(themeID('default'));
|
||||
|
||||
if (!that.theme) {
|
||||
throw new Error('Theme "' + themeName + '" is not installed, add "' + themeID(themeName) + '" to your "book.json"');
|
||||
}
|
||||
|
||||
if (that.themeDefault.root != that.theme.root) {
|
||||
that.log.info.ln('build using theme "' + themeName + '"');
|
||||
}
|
||||
|
||||
// This list is ordered to give priority to templates in the book
|
||||
var searchPaths = _.chain([
|
||||
// The book itself can contains a "_layouts" folder
|
||||
that.book.root,
|
||||
|
||||
// Installed plugin (it can be identical to themeDefault.root)
|
||||
that.theme.root,
|
||||
|
||||
// Is default theme still installed
|
||||
that.themeDefault? that.themeDefault.root : null
|
||||
])
|
||||
.compact()
|
||||
.uniq()
|
||||
.value();
|
||||
|
||||
// Load i18n
|
||||
_.each(searchPaths.concat().reverse(), function(searchPath) {
|
||||
var i18nRoot = path.resolve(searchPath, '_i18n');
|
||||
|
||||
if (!fs.existsSync(i18nRoot)) return;
|
||||
that.i18n.load(i18nRoot);
|
||||
});
|
||||
|
||||
that.env = new nunjucks.Environment(new nunjucks.FileSystemLoader(_.map(searchPaths, templatesPath)));
|
||||
|
||||
// Add GitBook default filters
|
||||
_.each(defaultFilters, function(fn, filter) {
|
||||
that.env.addFilter(filter, fn);
|
||||
});
|
||||
|
||||
// Translate using _i18n locales
|
||||
that.env.addFilter('t', function(s) {
|
||||
return that.i18n.t(that.book.config.get('language'), s);
|
||||
});
|
||||
|
||||
// Transform an absolute path into a relative path
|
||||
// using this.ctx.page.path
|
||||
that.env.addFilter('resolveFile', function(href) {
|
||||
return location.normalize(that.resolveForPage(this.ctx.file.path, href));
|
||||
});
|
||||
|
||||
// Test if a file exists
|
||||
that.env.addFilter('fileExists', function(href) {
|
||||
return fs.existsSync(that.resolve(href));
|
||||
});
|
||||
|
||||
// Transform a '.md' into a '.html' (README -> index)
|
||||
that.env.addFilter('contentURL', function(s) {
|
||||
return location.normalize(that.outputUrl(s));
|
||||
});
|
||||
|
||||
// Relase path to an asset
|
||||
that.env.addFilter('resolveAsset', function(href) {
|
||||
href = path.join('gitbook', href);
|
||||
|
||||
// Resolve for current file
|
||||
if (this.ctx.file) {
|
||||
href = that.resolveForPage(this.ctx.file.path, '/' + href);
|
||||
}
|
||||
|
||||
// Use assets from parent
|
||||
if (that.book.isLanguageBook()) {
|
||||
href = path.join('../', href);
|
||||
}
|
||||
|
||||
return location.normalize(href);
|
||||
});
|
||||
})
|
||||
|
||||
// Copy assets from themes before copying files from book
|
||||
.then(function() {
|
||||
if (that.book.isLanguageBook()) return;
|
||||
|
||||
return Promise.serie([
|
||||
// Assets from the book are already copied
|
||||
// The order is reversed from the template's one
|
||||
|
||||
// Is default theme still installed
|
||||
that.themeDefault && that.themeDefault.root != that.theme.root?
|
||||
that.themeDefault.root : null,
|
||||
|
||||
// Installed plugin (it can be identical to themeDefault.root)
|
||||
that.theme.root
|
||||
], function(folder) {
|
||||
if (!folder) return;
|
||||
|
||||
// Copy assets only if exists (don't fail otherwise)
|
||||
var assetFolder = path.join(folder, '_assets', that.name);
|
||||
if (!fs.existsSync(assetFolder)) return;
|
||||
|
||||
that.log.debug.ln('copy assets from theme', assetFolder);
|
||||
return fs.copyDir(
|
||||
assetFolder,
|
||||
that.resolve('gitbook'),
|
||||
{
|
||||
deleteFirst: false, // Delete "to" before
|
||||
overwrite: true,
|
||||
confirm: true
|
||||
}
|
||||
);
|
||||
});
|
||||
})
|
||||
|
||||
// Load resources for plugins
|
||||
.then(function() {
|
||||
return that.plugins.getResources(that.name)
|
||||
.then(function(resources) {
|
||||
that.resources = resources;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Write a page (parsable file)
|
||||
WebsiteOutput.prototype.onPage = function(page) {
|
||||
var that = this;
|
||||
|
||||
// Parse the page
|
||||
return page.toHTML(this)
|
||||
|
||||
// Render the page template with the same context as the json output
|
||||
.then(function() {
|
||||
return that.render('page', page.getContext());
|
||||
})
|
||||
|
||||
// Write the HTML file
|
||||
.then(function(html) {
|
||||
return that.writeFile(
|
||||
that.outputPath(page.path),
|
||||
html
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Finish generation, create ebook using ebook-convert
|
||||
WebsiteOutput.prototype.finish = function() {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
return WebsiteOutput.super_.prototype.finish.apply(that);
|
||||
})
|
||||
|
||||
// Copy assets from plugins
|
||||
.then(function() {
|
||||
if (that.book.isLanguageBook()) return;
|
||||
return that.plugins.copyResources(that.name, that.resolve('gitbook'));
|
||||
})
|
||||
|
||||
// Generate homepage to select languages
|
||||
.then(function() {
|
||||
if (!that.book.isMultilingual()) return;
|
||||
return that.outputMultilingualIndex();
|
||||
});
|
||||
};
|
||||
|
||||
// ----- Utilities ----
|
||||
|
||||
// Write multi-languages index
|
||||
WebsiteOutput.prototype.outputMultilingualIndex = function() {
|
||||
var that = this;
|
||||
|
||||
return that.render('languages', that.getContext())
|
||||
.then(function(html) {
|
||||
return that.writeFile(
|
||||
'index.html',
|
||||
html
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Render a template using nunjucks
|
||||
// Templates are stored in `_layouts` folders
|
||||
WebsiteOutput.prototype.render = function(tpl, context) {
|
||||
var filename = this.templateName(tpl);
|
||||
context = _.extend(context, {
|
||||
template: {
|
||||
// Same template but in the default theme
|
||||
default: this.themeDefault? path.resolve(templatesPath(this.themeDefault.root), filename) : null,
|
||||
|
||||
// Same template but in the theme
|
||||
theme: path.resolve(templatesPath(this.theme.root), filename)
|
||||
},
|
||||
|
||||
plugins: {
|
||||
resources: this.resources
|
||||
},
|
||||
|
||||
options: this.opts
|
||||
});
|
||||
|
||||
return Promise.nfcall(this.env.render.bind(this.env), filename, context);
|
||||
};
|
||||
|
||||
// Return a complete name for a template
|
||||
WebsiteOutput.prototype.templateName = function(name) {
|
||||
return path.join(this.name, name+'.html');
|
||||
};
|
||||
|
||||
module.exports = WebsiteOutput;
|
||||
@@ -0,0 +1,280 @@
|
||||
var _ = require('lodash');
|
||||
var url = require('url');
|
||||
var cheerio = require('cheerio');
|
||||
var domSerializer = require('dom-serializer');
|
||||
var slug = require('github-slugid');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var location = require('../utils/location');
|
||||
|
||||
// Selector to ignore
|
||||
var ANNOTATION_IGNORE = '.no-glossary,code,pre,a,script,h1,h2,h3,h4,h5,h6';
|
||||
|
||||
function HTMLPipeline(htmlString, opts) {
|
||||
_.bindAll(this);
|
||||
|
||||
this.opts = _.defaults(opts || {}, {
|
||||
// Called once the description has been found
|
||||
onDescription: function(description) { },
|
||||
|
||||
// Calcul new href for a relative link
|
||||
onRelativeLink: _.identity,
|
||||
|
||||
// Output an image
|
||||
onImage: _.identity,
|
||||
|
||||
// Syntax highlighting
|
||||
onCodeBlock: _.identity,
|
||||
|
||||
// Output a svg, if returns null the svg is kept inlined
|
||||
onOutputSVG: _.constant(null),
|
||||
|
||||
// Words to annotate
|
||||
annotations: [],
|
||||
|
||||
// When an annotation is applied
|
||||
onAnnotation: function () { }
|
||||
});
|
||||
|
||||
this.$ = cheerio.load(htmlString, {
|
||||
// We should parse html without trying to normalize too much
|
||||
xmlMode: false,
|
||||
|
||||
// SVG need some attributes to use uppercases
|
||||
lowerCaseAttributeNames: false,
|
||||
lowerCaseTags: false
|
||||
});
|
||||
}
|
||||
|
||||
// Transform a query of elements in the page
|
||||
HTMLPipeline.prototype._transform = function(query, fn) {
|
||||
var that = this;
|
||||
|
||||
var $elements = this.$(query);
|
||||
|
||||
return Promise.serie($elements, function(el) {
|
||||
var $el = that.$(el);
|
||||
return fn.call(that, $el);
|
||||
});
|
||||
};
|
||||
|
||||
// Normalize links
|
||||
HTMLPipeline.prototype.transformLinks = function() {
|
||||
return this._transform('a', function($a) {
|
||||
var href = $a.attr('href');
|
||||
if (!href) return;
|
||||
|
||||
if (location.isAnchor(href)) {
|
||||
// Don't "change" anchor links
|
||||
} else if (location.isRelative(href)) {
|
||||
// Preserve anchor
|
||||
var parsed = url.parse(href);
|
||||
var filename = this.opts.onRelativeLink(parsed.pathname);
|
||||
|
||||
$a.attr('href', filename + (parsed.hash || ''));
|
||||
} else {
|
||||
// External links
|
||||
$a.attr('target', '_blank');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Normalize images
|
||||
HTMLPipeline.prototype.transformImages = function() {
|
||||
return this._transform('img', function($img) {
|
||||
return Promise(this.opts.onImage($img.attr('src')))
|
||||
.then(function(filename) {
|
||||
$img.attr('src', filename);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Normalize code blocks
|
||||
HTMLPipeline.prototype.transformCodeBlocks = function() {
|
||||
return this._transform('code', function($code) {
|
||||
// Extract language
|
||||
var lang = _.chain(
|
||||
($code.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 = $code.text();
|
||||
|
||||
return Promise(this.opts.onCodeBlock(source, lang))
|
||||
.then(function(blk) {
|
||||
if (blk.html === false) {
|
||||
$code.text(blk.body);
|
||||
} else {
|
||||
$code.html(blk.body);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Add ID to headings
|
||||
HTMLPipeline.prototype.transformHeadings = function() {
|
||||
var that = this;
|
||||
|
||||
this.$('h1,h2,h3,h4,h5,h6').each(function() {
|
||||
var $h = that.$(this);
|
||||
|
||||
// Already has an ID?
|
||||
if ($h.attr('id')) return;
|
||||
$h.attr('id', slug($h.text()));
|
||||
});
|
||||
};
|
||||
|
||||
// Outline SVG from the HML
|
||||
HTMLPipeline.prototype.transformSvgs = function() {
|
||||
var that = this;
|
||||
|
||||
return this._transform('svg', function($svg) {
|
||||
var content = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
renderDOM(that.$, $svg)
|
||||
].join('\n');
|
||||
|
||||
return Promise(that.opts.onOutputSVG(content))
|
||||
.then(function(filename) {
|
||||
if (!filename) return;
|
||||
|
||||
$svg.replaceWith(that.$('<img>').attr('src', filename));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Annotate the content
|
||||
HTMLPipeline.prototype.applyAnnotations = function() {
|
||||
var that = this;
|
||||
|
||||
_.each(this.opts.annotations, function(annotation) {
|
||||
var searchRegex = new RegExp( '\\b(' + pregQuote(annotation.name.toLowerCase()) + ')\\b' , 'gi' );
|
||||
|
||||
that.$('*').each(function() {
|
||||
var $this = that.$(this);
|
||||
|
||||
if (
|
||||
$this.is(ANNOTATION_IGNORE) ||
|
||||
$this.parents(ANNOTATION_IGNORE).length > 0
|
||||
) return;
|
||||
|
||||
replaceText(that.$, this, searchRegex, function(match) {
|
||||
that.opts.onAnnotation(annotation);
|
||||
|
||||
return '<a href="' + that.opts.onRelativeLink(annotation.href) + '" '
|
||||
+ 'class="glossary-term" title="'+_.escape(annotation.description)+'">'
|
||||
+ match
|
||||
+ '</a>';
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Extract page description from html
|
||||
// This can totally be improved
|
||||
HTMLPipeline.prototype.extractDescription = function() {
|
||||
var $p = this.$('p').first();
|
||||
var description = $p.text().trim().slice(0, 155);
|
||||
|
||||
this.opts.onDescription(description);
|
||||
};
|
||||
|
||||
// Write content to the pipeline
|
||||
HTMLPipeline.prototype.output = function() {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(this.extractDescription)
|
||||
.then(this.transformImages)
|
||||
.then(this.transformHeadings)
|
||||
.then(this.transformCodeBlocks)
|
||||
.then(this.transformSvgs)
|
||||
.then(this.applyAnnotations)
|
||||
|
||||
// Transform of links should be applied after annotations
|
||||
// because annotations are created as links
|
||||
.then(this.transformLinks)
|
||||
|
||||
.then(function() {
|
||||
return renderDOM(that.$);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Replace text in an element
|
||||
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');
|
||||
}
|
||||
|
||||
module.exports = HTMLPipeline;
|
||||
@@ -0,0 +1,250 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var direction = require('direction');
|
||||
var fm = require('front-matter');
|
||||
|
||||
var error = require('../utils/error');
|
||||
var pathUtil = require('../utils/path');
|
||||
var location = require('../utils/location');
|
||||
var parsers = require('../parsers');
|
||||
var gitbook = require('../gitbook');
|
||||
var pluginCompatibility = require('../plugins/compatibility');
|
||||
var HTMLPipeline = require('./html');
|
||||
|
||||
/*
|
||||
A page represent a parsable file in the book (Markdown, Asciidoc, etc)
|
||||
*/
|
||||
|
||||
function Page(book, filename) {
|
||||
if (!(this instanceof Page)) return new Page(book, filename);
|
||||
var extension;
|
||||
_.bindAll(this);
|
||||
|
||||
this.book = book;
|
||||
this.log = this.book.log;
|
||||
|
||||
// Current content
|
||||
this.content = '';
|
||||
|
||||
// Short description for the page
|
||||
this.description = '';
|
||||
|
||||
// Relative path to the page
|
||||
this.path = location.normalize(filename);
|
||||
|
||||
// Absolute path to the page
|
||||
this.rawPath = this.book.resolve(filename);
|
||||
|
||||
// Last modification date
|
||||
this.mtime = 0;
|
||||
|
||||
// Can we parse it?
|
||||
extension = path.extname(this.path);
|
||||
this.parser = parsers.get(extension);
|
||||
if (!this.parser) throw error.ParsingError(new Error('Can\'t parse file "'+this.path+'"'));
|
||||
|
||||
this.type = this.parser.name;
|
||||
}
|
||||
|
||||
// Return the filename of the page with another extension
|
||||
// "README.md" -> "README.html"
|
||||
Page.prototype.withExtension = function(ext) {
|
||||
return pathUtil.setExtension(this.path, ext);
|
||||
};
|
||||
|
||||
// Resolve a filename relative to this page
|
||||
// It returns a path relative to the book root folder
|
||||
Page.prototype.resolveLocal = function() {
|
||||
var dir = path.dirname(this.path);
|
||||
var file = path.join.apply(path, _.toArray(arguments));
|
||||
|
||||
return location.toAbsolute(file, dir, '');
|
||||
};
|
||||
|
||||
// Resolve a filename relative to this page
|
||||
// It returns an absolute path for the FS
|
||||
Page.prototype.resolve = function() {
|
||||
return this.book.resolve(this.resolveLocal.apply(this, arguments));
|
||||
};
|
||||
|
||||
// Convert an absolute path (in the book) to a relative path from this page
|
||||
Page.prototype.relative = function(name) {
|
||||
// Convert /test.png -> test.png
|
||||
name = location.toAbsolute(name, '', '');
|
||||
|
||||
return location.relative(
|
||||
this.resolve('.') + '/',
|
||||
this.book.resolve(name)
|
||||
);
|
||||
};
|
||||
|
||||
// Return a page result of a relative page from this page
|
||||
Page.prototype.followPage = function(filename) {
|
||||
var absPath = this.resolveLocal(filename);
|
||||
return this.book.getPage(absPath);
|
||||
};
|
||||
|
||||
// Update content of the page
|
||||
Page.prototype.update = function(content) {
|
||||
this.content = content;
|
||||
};
|
||||
|
||||
// Read the page as a string
|
||||
Page.prototype.read = function() {
|
||||
var that = this;
|
||||
|
||||
return this.book.statFile(this.path)
|
||||
.then(function(stat) {
|
||||
that.mtime = stat.mtime;
|
||||
return that.book.readFile(that.path);
|
||||
})
|
||||
.then(this.update);
|
||||
};
|
||||
|
||||
// Return templating context for this page
|
||||
// This is used both for themes and page parsing
|
||||
Page.prototype.getContext = function() {
|
||||
var article = this.book.summary.getArticle(this);
|
||||
var next = article? article.next() : null;
|
||||
var prev = article? article.prev() : null;
|
||||
|
||||
// Detect text direction in this page
|
||||
var dir = this.book.config.get('direction');
|
||||
if (!dir) {
|
||||
dir = direction(this.content);
|
||||
if (dir == 'neutral') dir = null;
|
||||
}
|
||||
|
||||
return _.extend(
|
||||
{
|
||||
file: {
|
||||
path: this.path,
|
||||
mtime: this.mtime,
|
||||
type: this.type
|
||||
},
|
||||
page: {
|
||||
title: article? article.title : null,
|
||||
description: this.description,
|
||||
next: next? next.getContext() : null,
|
||||
previous: prev? prev.getContext() : null,
|
||||
level: article? article.level : null,
|
||||
depth: article? article.depth : 0,
|
||||
content: this.content,
|
||||
dir: dir
|
||||
}
|
||||
},
|
||||
gitbook.getContext(),
|
||||
this.book.getContext(),
|
||||
this.book.langs.getContext(),
|
||||
this.book.summary.getContext(),
|
||||
this.book.glossary.getContext(),
|
||||
this.book.config.getContext()
|
||||
);
|
||||
};
|
||||
|
||||
// Parse the page and return its content
|
||||
Page.prototype.toHTML = function(output) {
|
||||
var that = this;
|
||||
|
||||
this.log.debug.ln('start parsing file', this.path);
|
||||
|
||||
// Call a hook in the output
|
||||
// using an utility to "keep" compatibility with gitbook 2
|
||||
function hook(name) {
|
||||
return pluginCompatibility.pageHook(that, function(ctx) {
|
||||
return output.plugins.hook(name, ctx);
|
||||
})
|
||||
.then(function(result) {
|
||||
if(_.isString(result)) that.update(result);
|
||||
});
|
||||
}
|
||||
|
||||
return this.read()
|
||||
|
||||
// Parse yaml front matter
|
||||
.then(function() {
|
||||
var parsed = fm(that.content);
|
||||
|
||||
// Extend page with the fontmatter attribute
|
||||
that.description = parsed.attributes.description || '';
|
||||
|
||||
// Keep only the body
|
||||
that.update(parsed.body);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return hook('page:before');
|
||||
})
|
||||
|
||||
// Pre-process page with parser
|
||||
.then(function() {
|
||||
return that.parser.page.prepare(that.content)
|
||||
.then(that.update);
|
||||
})
|
||||
|
||||
// Render template
|
||||
.then(function() {
|
||||
return output.template.render(that.content, that.getContext(), {
|
||||
path: that.path
|
||||
})
|
||||
.then(that.update);
|
||||
})
|
||||
|
||||
// Render markup using the parser
|
||||
.then(function() {
|
||||
return that.parser.page(that.content)
|
||||
.then(function(out) {
|
||||
that.update(out.content);
|
||||
});
|
||||
})
|
||||
|
||||
// Post process templating
|
||||
.then(function() {
|
||||
return output.template.postProcess(that.content)
|
||||
.then(that.update);
|
||||
})
|
||||
|
||||
// Normalize HTML output
|
||||
.then(function() {
|
||||
var pipelineOpts = {
|
||||
onRelativeLink: _.partial(output.onRelativeLink, that),
|
||||
onImage: _.partial(output.onOutputImage, that),
|
||||
onOutputSVG: _.partial(output.onOutputSVG, that),
|
||||
|
||||
// Use 'code' template block
|
||||
onCodeBlock: function(source, lang) {
|
||||
return output.template.applyBlock('code', {
|
||||
body: source,
|
||||
kwargs: {
|
||||
language: lang
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Extract description from page's content if no frontmatter
|
||||
onDescription: function(description) {
|
||||
if (that.description) return;
|
||||
that.description = description;
|
||||
},
|
||||
|
||||
// Convert glossary entries to annotations
|
||||
annotations: that.book.glossary.annotations()
|
||||
};
|
||||
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
|
||||
|
||||
return pipeline.output()
|
||||
.then(that.update);
|
||||
})
|
||||
|
||||
.then(function() {
|
||||
return hook('page');
|
||||
})
|
||||
|
||||
// Return content itself
|
||||
.then(function() {
|
||||
return that.content;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports = Page;
|
||||
@@ -1,9 +0,0 @@
|
||||
module.exports = {
|
||||
summary: require('./summary'),
|
||||
langs: require('./langs'),
|
||||
page: require('./page'),
|
||||
lex: require('./lex'),
|
||||
progress: require('./progress'),
|
||||
navigation: require('./navigation'),
|
||||
readme: require('./readme')
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
var _ = require("lodash")
|
||||
var parseSummary = require("./summary");
|
||||
|
||||
var parseLangs = function(content) {
|
||||
var summary = parseSummary(content);
|
||||
|
||||
return {
|
||||
list: _.chain(summary.chapters)
|
||||
.filter(function(entry) {
|
||||
return entry.path != null;
|
||||
})
|
||||
.map(function(entry) {
|
||||
return {
|
||||
title: entry.title,
|
||||
path: entry.path,
|
||||
lang: entry.path.replace("/", "")
|
||||
};
|
||||
})
|
||||
.value()
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = parseLangs;
|
||||
@@ -1,125 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var marked = require('marked');
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
function isQuizNode(node) {
|
||||
return (/^[(\[][ x][)\]]/).test(node.text || node);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
function isQuiz(nodes) {
|
||||
if (nodes.length < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Support having a first paragraph block
|
||||
// before our series of questions
|
||||
var quizNodes = nodes.slice(nodes[0].type === 'paragraph' ? 1 : 0);
|
||||
|
||||
// No questions
|
||||
if (!_.some(quizNodes, { type: 'blockquote_start' })) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if section has list of questions
|
||||
// or table of questions
|
||||
var listIdx = _.findIndex(quizNodes, { type: 'list_item_start' });
|
||||
var tableIdx = _.findIndex(quizNodes, { type: 'table' });
|
||||
|
||||
if(
|
||||
// List of questions
|
||||
listIdx !== -1 && isQuizNode(quizNodes[listIdx + 1]) &&
|
||||
|
||||
// Table of questions
|
||||
(
|
||||
tableIdx !== -1 &&
|
||||
_.every(quizNodes[tableIdx].cells[0].slice(1), isQuizNode)
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 = marked.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;
|
||||
}, [])
|
||||
.value();
|
||||
}
|
||||
|
||||
// Exports
|
||||
module.exports = lexPage;
|
||||
@@ -1,89 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
// Cleans up an article/chapter object
|
||||
// remove 'articles' attributes
|
||||
function clean(obj) {
|
||||
return obj && _.omit(obj, ['articles']);
|
||||
}
|
||||
|
||||
// Returns from a summary a map of
|
||||
/*
|
||||
{
|
||||
"file/path.md": {
|
||||
prev: ...,
|
||||
next: ...,
|
||||
},
|
||||
...
|
||||
}
|
||||
*/
|
||||
function navigation(summary, files) {
|
||||
// Support single files as well as list
|
||||
files = _.isArray(files) ? files : (_.isString(files) ? [files] : null);
|
||||
|
||||
// Mapping of prev/next for a give path
|
||||
var mapping = {};
|
||||
|
||||
// Special README nav
|
||||
var README_NAV = {
|
||||
path: 'README.md',
|
||||
title: 'Introduction',
|
||||
};
|
||||
|
||||
// Walk the chapter/article tree and create navigation entires
|
||||
_.each(summary.chapters, function(chapter, idx, chapters) {
|
||||
// Skip if no path
|
||||
if(!chapter.path) return;
|
||||
|
||||
var currentChapter = clean(chapter);
|
||||
var prevChapter = (idx-1 < 0) ? README_NAV : chapters[idx-1];
|
||||
var nextChapter = (idx+1 >= chapters.length) ? null : chapters[idx+1];
|
||||
|
||||
var prev = (!prevChapter || _.isEmpty(prevChapter.articles)) ?
|
||||
prevChapter : _.last(prevChapter.articles);
|
||||
var next = (!chapter || _.isEmpty(chapter.articles)) ?
|
||||
nextChapter : _.first(chapter.articles);
|
||||
|
||||
// Add chapter mapping
|
||||
mapping[chapter.path] = {
|
||||
title: chapter.title,
|
||||
prev: clean(prev),
|
||||
next: clean(next),
|
||||
level: (idx+1).toString(),
|
||||
};
|
||||
|
||||
// Check a chapter's articles
|
||||
_.each(chapter.articles, function(article, _idx, articles) {
|
||||
// Skip if no path
|
||||
if(!article.path) return;
|
||||
|
||||
var prev = (_idx-1 < 0) ? currentChapter : clean(articles[_idx-1]);
|
||||
var next = (_idx+1 >= articles.length) ? nextChapter : clean(articles[_idx+1]);
|
||||
|
||||
mapping[article.path] = {
|
||||
title: article.title,
|
||||
prev: clean(prev),
|
||||
next: clean(next),
|
||||
level: [idx+1, _idx+1].join('.'),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Hack for README.html
|
||||
mapping['README.md'] = {
|
||||
title: README_NAV.title,
|
||||
prev: null,
|
||||
next: clean(summary.chapters[0]),
|
||||
level: '0',
|
||||
};
|
||||
|
||||
// Filter for only files we want
|
||||
if(files) {
|
||||
return _.pick(mapping, files);
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
|
||||
// Exports
|
||||
module.exports = navigation;
|
||||
@@ -1,153 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var marked = require('marked');
|
||||
var hljs = require('highlight.js');
|
||||
|
||||
var lex = require('./lex');
|
||||
var renderer = require('./renderer');
|
||||
|
||||
var lnormalize = require('../utils/lang').normalize;
|
||||
|
||||
|
||||
// Synchronous highlighting with highlight.js
|
||||
marked.setOptions({
|
||||
highlight: function (code, lang) {
|
||||
try {
|
||||
return hljs.highlight(lang, code).value;
|
||||
} catch(e) {
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Render a section using our custom renderer
|
||||
function render(section, _options) {
|
||||
// Copy section
|
||||
section = _.toArray(section);
|
||||
|
||||
// marked's Render expects this, we don't use it yet
|
||||
section.links = {};
|
||||
|
||||
// Build options using defaults and our custom renderer
|
||||
var options = _.extend({}, marked.defaults, {
|
||||
renderer: renderer(null, _options)
|
||||
});
|
||||
|
||||
return marked.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
|
||||
return (_.isArray(src) ? src : lex(src))
|
||||
.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),
|
||||
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),
|
||||
solution: render(question.solutionNodes),
|
||||
feedback: render(question.feedbackNodes)
|
||||
});
|
||||
}
|
||||
|
||||
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),
|
||||
quiz: quiz
|
||||
};
|
||||
}
|
||||
|
||||
// Render normal pages
|
||||
return {
|
||||
id: section.id,
|
||||
type: section.type,
|
||||
content: render(section, options)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Exports
|
||||
module.exports = parsePage;
|
||||
@@ -1,50 +0,0 @@
|
||||
var _ = require("lodash");
|
||||
|
||||
// Returns from a navigation and a current file, a snapshot of current detailed state
|
||||
var calculProgress = function(navigation, current) {
|
||||
var n = _.size(navigation);
|
||||
var percent = 0, prevPercent = 0, currentChapter = null;
|
||||
var done = true;
|
||||
|
||||
var chapters = _.chain(navigation)
|
||||
.map(function(nav, path) {
|
||||
nav.path = path;
|
||||
return nav;
|
||||
})
|
||||
.sortBy(function(nav) {
|
||||
return nav.level;
|
||||
})
|
||||
.map(function(nav, i) {
|
||||
// Calcul percent
|
||||
nav.percent = (i * 100) / Math.max((n - 1), 1);
|
||||
|
||||
// Is it done
|
||||
nav.done = done;
|
||||
if (nav.path == current) {
|
||||
currentChapter = nav;
|
||||
percent = nav.percent;
|
||||
done = false;
|
||||
} else if (done) {
|
||||
prevPercent = nav.percent;
|
||||
}
|
||||
|
||||
return nav;
|
||||
})
|
||||
.value();
|
||||
|
||||
return {
|
||||
// Previous percent
|
||||
prevPercent: prevPercent,
|
||||
|
||||
// Current percent
|
||||
percent: percent,
|
||||
|
||||
// List of chapter with progress
|
||||
chapters: chapters,
|
||||
|
||||
// Current chapter
|
||||
current: currentChapter
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = calculProgress;
|
||||
@@ -1,32 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var marked = require('marked');
|
||||
|
||||
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;
|
||||
|
||||
// Parse content
|
||||
nodes = marked.lexer(src);
|
||||
|
||||
var title = extractFirstNode(nodes, "heading");
|
||||
var description = extractFirstNode(nodes, "paragraph");
|
||||
|
||||
return {
|
||||
title: title,
|
||||
description: description
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Exports
|
||||
module.exports = parseReadme;
|
||||
@@ -1,115 +0,0 @@
|
||||
var url = require('url');
|
||||
var inherits = require('util').inherits;
|
||||
|
||||
var path = require('path');
|
||||
|
||||
var marked = require('marked');
|
||||
|
||||
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++;
|
||||
}
|
||||
inherits(GitBookRenderer, marked.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) {
|
||||
// Don't build if it looks malicious
|
||||
if (this.options.sanitize && this._unsanitized(href)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Parsed version of the url
|
||||
var parsed = url.parse(href);
|
||||
|
||||
var o = this._extra_options;
|
||||
// Relative link, rewrite it to point to github repo
|
||||
if(!parsed.protocol && parsed.path && parsed.path[0] != '/' && o && o.repo && o.dir) {
|
||||
href = url.resolve('https://github.com/' + o.repo + '/blob/', [o.dir, href].join("/"));
|
||||
parsed = url.parse(href);
|
||||
}
|
||||
|
||||
// Generate HTML for link
|
||||
var out = '<a href="' + parsed.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(!parsed.protocol && parsed.path && parsed.path[0] != '/' && o && o.dir && o.outdir) {
|
||||
var outdir = o.outdir.charAt(o.outdir.length - 1) === '/' ? o.outdir : o.outdir + '/';
|
||||
_href = url.resolve(outdir, [o.dir, href].join('/'));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
var field = "<input name='quiz-row-" + this.id + "-" + this.quizRowId + "' type='";
|
||||
field += match[1] === '(' ? "radio" : "checkbox";
|
||||
field += match[2] === 'x' ? "' checked/>" : "'/>";
|
||||
return text.replace(fieldRegex, field);
|
||||
}
|
||||
|
||||
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));
|
||||
};
|
||||
|
||||
// Exports
|
||||
module.exports = GitBookRenderer;
|
||||
@@ -1,117 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var marked = require('marked');
|
||||
|
||||
|
||||
// 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 marked 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 = marked.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,
|
||||
|
||||
// Replace .md references with .html
|
||||
path: matches[2],
|
||||
};
|
||||
}
|
||||
|
||||
function parseArticle(chapterNum, nodes, idx) {
|
||||
return parseTitle(_.first(nodes).text, [chapterNum, idx+1]);
|
||||
}
|
||||
|
||||
function parseChapter(nodes, idx) {
|
||||
return _.extend(parseTitle(_.first(nodes).text, [idx+1]), {
|
||||
articles: _.map(listSplit(filterList(nodes), 'list_item_start', 'list_item_end'), parseArticle.bind(null, idx+1))
|
||||
});
|
||||
}
|
||||
|
||||
function parseSummary(src) {
|
||||
var nodes = marked.lexer(src);
|
||||
|
||||
// Get out list of chapters
|
||||
var chapterList = filterList(nodes);
|
||||
|
||||
// Split out chapter sections
|
||||
var chapters = _.chain(listSplit(chapterList, 'list_item_start', 'list_item_end'))
|
||||
.map(parseChapter)
|
||||
.value();
|
||||
|
||||
return {
|
||||
chapters: chapters
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Exports
|
||||
module.exports = parseSummary;
|
||||
@@ -0,0 +1,60 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
|
||||
var markdownParser = require('gitbook-markdown');
|
||||
var asciidocParser = require('gitbook-asciidoc');
|
||||
|
||||
var Promise = require('./utils/promise');
|
||||
|
||||
// This list is ordered by priority of parsers to use
|
||||
var PARSERS = [
|
||||
createParser(markdownParser, {
|
||||
name: 'markdown',
|
||||
extensions: ['.md', '.markdown', '.mdown']
|
||||
}),
|
||||
createParser(asciidocParser, {
|
||||
name: 'asciidoc',
|
||||
extensions: ['.adoc', '.asciidoc']
|
||||
})
|
||||
];
|
||||
|
||||
|
||||
// Prepare and compose a parser
|
||||
function createParser(parser, base) {
|
||||
var nparser = base;
|
||||
|
||||
nparser.glossary = Promise.wrapfn(parser.glossary);
|
||||
nparser.glossary.toText = Promise.wrapfn(parser.glossary.toText);
|
||||
|
||||
nparser.summary = Promise.wrapfn(parser.summary);
|
||||
nparser.summary.toText = Promise.wrapfn(parser.summary.toText);
|
||||
|
||||
nparser.langs = Promise.wrapfn(parser.langs);
|
||||
nparser.langs.toText = Promise.wrapfn(parser.langs.toText);
|
||||
|
||||
nparser.readme = Promise.wrapfn(parser.readme);
|
||||
|
||||
nparser.page = Promise.wrapfn(parser.page);
|
||||
nparser.page.prepare = Promise.wrapfn(parser.page.prepare || _.identity);
|
||||
|
||||
return nparser;
|
||||
}
|
||||
|
||||
// Return a specific parser according to an extension
|
||||
function getParser(ext) {
|
||||
return _.find(PARSERS, function(input) {
|
||||
return input.name == ext || _.contains(input.extensions, ext);
|
||||
});
|
||||
}
|
||||
|
||||
// Return parser for a file
|
||||
function getParserForFile(filename) {
|
||||
return getParser(path.extname(filename));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
all: PARSERS,
|
||||
extensions: _.flatten(_.pluck(PARSERS, 'extensions')),
|
||||
get: getParser,
|
||||
getForFile: getParserForFile
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
var _ = require('lodash');
|
||||
var error = require('../utils/error');
|
||||
|
||||
/*
|
||||
Return the context for a plugin.
|
||||
It tries to keep compatibilities with GitBook v2
|
||||
*/
|
||||
function pluginCtx(plugin) {
|
||||
var book = plugin.book;
|
||||
var ctx = book;
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// Call a function "fn" with a context of page similar to the one in GitBook v2
|
||||
function pageHook(page, fn) {
|
||||
var ctx = {
|
||||
type: page.type,
|
||||
content: page.content,
|
||||
path: page.path,
|
||||
rawPath: page.rawPath
|
||||
};
|
||||
|
||||
// Deprecate sections
|
||||
error.deprecateField(ctx, 'sections', [
|
||||
{ content: ctx.content }
|
||||
], '"sections" property is deprecated, use page.content instead');
|
||||
|
||||
return fn(ctx)
|
||||
.then(function(result) {
|
||||
if (!result) return undefined;
|
||||
if (result.content) {
|
||||
return result.content;
|
||||
}
|
||||
|
||||
if (result.sections) {
|
||||
return _.pluck(result.sections, 'content').join('\n');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pluginCtx: pluginCtx,
|
||||
pageHook: pageHook
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var fs = require('../utils/fs');
|
||||
var BookPlugin = require('./plugin');
|
||||
var registry = require('./registry');
|
||||
var pluginsConfig = require('../config/plugins');
|
||||
|
||||
/*
|
||||
PluginsManager is an interface to work with multiple plugins at once:
|
||||
- Extract assets from plugins
|
||||
- Call hooks for all plugins, etc
|
||||
*/
|
||||
|
||||
function PluginsManager(book) {
|
||||
this.book = book;
|
||||
this.log = this.book.log;
|
||||
this.plugins = [];
|
||||
|
||||
_.bindAll(this);
|
||||
}
|
||||
|
||||
// Return count of plugins loaded
|
||||
PluginsManager.prototype.count = function() {
|
||||
return _.size(this.plugins);
|
||||
};
|
||||
|
||||
// Returns a plugin by its name
|
||||
PluginsManager.prototype.get = function(name) {
|
||||
return _.find(this.plugins, {
|
||||
id: name
|
||||
});
|
||||
};
|
||||
|
||||
// Load a plugin, or a list of plugins
|
||||
PluginsManager.prototype.load = function(name) {
|
||||
var that = this;
|
||||
|
||||
if (_.isArray(name)) {
|
||||
return Promise.serie(name, function(_name) {
|
||||
return that.load(_name);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise()
|
||||
|
||||
// Initiate and load the plugin
|
||||
.then(function() {
|
||||
var plugin;
|
||||
|
||||
if (!_.isString(name)) plugin = name;
|
||||
else plugin = new BookPlugin(that.book, name);
|
||||
|
||||
if (that.get(plugin.id)) {
|
||||
throw new Error('Plugin "'+plugin.id+'" is already loaded');
|
||||
}
|
||||
|
||||
|
||||
if (plugin.isLoaded()) return plugin;
|
||||
else return plugin.load()
|
||||
.thenResolve(plugin);
|
||||
})
|
||||
|
||||
// Setup the plugin
|
||||
.then(this._setup);
|
||||
};
|
||||
|
||||
// Load all plugins from the book's configuration
|
||||
PluginsManager.prototype.loadAll = function() {
|
||||
var plugins = _.pluck(this.book.config.get('plugins'), 'name');
|
||||
|
||||
this.log.info.ln('loading', plugins.length, 'plugins');
|
||||
return this.load(plugins);
|
||||
};
|
||||
|
||||
// Setup a plugin
|
||||
// Register its filter, blocks, etc
|
||||
PluginsManager.prototype._setup = function(plugin) {
|
||||
this.plugins.push(plugin);
|
||||
};
|
||||
|
||||
// Install all plugins for the book
|
||||
PluginsManager.prototype.install = function() {
|
||||
var that = this;
|
||||
var plugins = _.filter(this.book.config.get('plugins'), function(plugin) {
|
||||
return !pluginsConfig.isDefaultPlugin(plugin.name);
|
||||
});
|
||||
|
||||
if (plugins.length == 0) {
|
||||
this.log.info.ln('nothing to install!');
|
||||
return Promise(0);
|
||||
}
|
||||
|
||||
this.log.info.ln('installing', plugins.length, 'plugins');
|
||||
|
||||
return Promise.serie(plugins, function(plugin) {
|
||||
return registry.install(that.book, plugin.name, plugin.version);
|
||||
})
|
||||
.thenResolve(plugins.length);
|
||||
};
|
||||
|
||||
// Call a hook on all plugins to transform an input
|
||||
PluginsManager.prototype.hook = function(name, input) {
|
||||
return Promise.reduce(this.plugins, function(current, plugin) {
|
||||
return plugin.hook(name, current);
|
||||
}, input);
|
||||
};
|
||||
|
||||
// Extract all resources for a namespace
|
||||
PluginsManager.prototype.getResources = function(namespace) {
|
||||
return Promise.reduce(this.plugins, function(out, plugin) {
|
||||
return plugin.getResources(namespace)
|
||||
.then(function(pluginResources) {
|
||||
_.each(BookPlugin.RESOURCES, function(resourceType) {
|
||||
out[resourceType] = (out[resourceType] || []).concat(pluginResources[resourceType] || []);
|
||||
});
|
||||
|
||||
return out;
|
||||
});
|
||||
}, {});
|
||||
};
|
||||
|
||||
// Copy all resources for a plugin
|
||||
PluginsManager.prototype.copyResources = function(namespace, outputRoot) {
|
||||
return Promise.serie(this.plugins, function(plugin) {
|
||||
return plugin.getResources(namespace)
|
||||
.then(function(resources) {
|
||||
if (!resources.assets) return;
|
||||
|
||||
var input = path.resolve(plugin.root, resources.assets);
|
||||
var output = path.resolve(outputRoot, plugin.npmId);
|
||||
|
||||
return fs.copyDir(input, output);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Get all filters and blocks
|
||||
PluginsManager.prototype.getFilters = function() {
|
||||
return _.reduce(this.plugins, function(out, plugin) {
|
||||
var filters = plugin.getFilters();
|
||||
|
||||
return _.extend(out, filters);
|
||||
}, {});
|
||||
};
|
||||
PluginsManager.prototype.getBlocks = function() {
|
||||
return _.reduce(this.plugins, function(out, plugin) {
|
||||
var blocks = plugin.getBlocks();
|
||||
|
||||
return _.extend(out, blocks);
|
||||
}, {});
|
||||
};
|
||||
|
||||
module.exports = PluginsManager;
|
||||
@@ -0,0 +1,300 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var url = require('url');
|
||||
var resolve = require('resolve');
|
||||
var mergeDefaults = require('merge-defaults');
|
||||
var jsonschema = require('jsonschema');
|
||||
var jsonSchemaDefaults = require('json-schema-defaults');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var error = require('../utils/error');
|
||||
var gitbook = require('../gitbook');
|
||||
var registry = require('./registry');
|
||||
var compatibility = require('./compatibility');
|
||||
|
||||
var HOOKS = [
|
||||
'init', 'finish', 'finish:before', 'config', 'page', 'page:before'
|
||||
];
|
||||
|
||||
var RESOURCES = ['js', 'css'];
|
||||
|
||||
// Return true if an error is a "module not found"
|
||||
// Wait on https://github.com/substack/node-resolve/pull/81 to be merged
|
||||
function isModuleNotFound(err) {
|
||||
return err.message.indexOf('Cannot find module') >= 0;
|
||||
}
|
||||
|
||||
function BookPlugin(book, pluginId) {
|
||||
this.book = book;
|
||||
this.log = this.book.log.prefix(pluginId);
|
||||
|
||||
this.id = pluginId;
|
||||
this.npmId = registry.npmId(pluginId);
|
||||
this.root;
|
||||
|
||||
this.packageInfos = undefined;
|
||||
this.content = undefined;
|
||||
|
||||
// Cache for resources
|
||||
this._resources = {};
|
||||
|
||||
_.bindAll(this);
|
||||
}
|
||||
|
||||
// Return true if plugin has been loaded correctly
|
||||
BookPlugin.prototype.isLoaded = function() {
|
||||
return Boolean(this.packageInfos && this.content);
|
||||
};
|
||||
|
||||
// Bind a function to the plugin's context
|
||||
BookPlugin.prototype.bind = function(fn) {
|
||||
return fn.bind(compatibility.pluginCtx(this));
|
||||
};
|
||||
|
||||
// Load this plugin
|
||||
// An optional folder to search in can be passed
|
||||
BookPlugin.prototype.load = function(folder) {
|
||||
var that = this;
|
||||
|
||||
if (this.isLoaded()) {
|
||||
return Promise.reject(new Error('Plugin "' + this.id + '" is already loaded'));
|
||||
}
|
||||
|
||||
// Fodlers to search plugins in
|
||||
var searchPaths = _.compact([
|
||||
folder,
|
||||
this.book.resolve('node_modules'),
|
||||
__dirname
|
||||
]);
|
||||
|
||||
// Try loading plugins from different location
|
||||
var p = Promise.some(searchPaths, function(baseDir) {
|
||||
// Locate plugin and load pacjage.json
|
||||
try {
|
||||
var res = resolve.sync(that.npmId + '/package.json', { basedir: baseDir });
|
||||
|
||||
that.root = path.dirname(res);
|
||||
that.packageInfos = require(res);
|
||||
} catch (err) {
|
||||
if (!isModuleNotFound(err)) throw err;
|
||||
|
||||
that.packageInfos = undefined;
|
||||
that.content = undefined;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load plugin JS content
|
||||
try {
|
||||
that.content = require(resolve.sync(that.npmId, { basedir: baseDir }));
|
||||
} catch(err) {
|
||||
// It's no big deal if the plugin doesn't have an "index.js"
|
||||
// (For example: themes)
|
||||
if (isModuleNotFound(err)) {
|
||||
that.content = {};
|
||||
} else {
|
||||
throw new error.PluginError(err, {
|
||||
plugin: that.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
|
||||
.then(that.validate)
|
||||
|
||||
// Validate the configuration and update it
|
||||
.then(function() {
|
||||
var config = that.book.config.get(that.getConfigKey(), {});
|
||||
return that.validateConfig(config);
|
||||
})
|
||||
.then(function(config) {
|
||||
that.book.config.set(that.getConfigKey(), config);
|
||||
});
|
||||
|
||||
this.log.info('loading plugin "' + this.id + '"... ');
|
||||
return this.log.info.promise(p);
|
||||
};
|
||||
|
||||
// Verify the definition of a plugin
|
||||
// Also verify that the plugin accepts the current gitbook version
|
||||
// This method throws erros if plugin is invalid
|
||||
BookPlugin.prototype.validate = function() {
|
||||
var isValid = (
|
||||
this.packageInfos &&
|
||||
this.packageInfos.name &&
|
||||
this.packageInfos.engines &&
|
||||
this.packageInfos.engines.gitbook
|
||||
);
|
||||
|
||||
if (!this.isLoaded()) {
|
||||
throw new Error('Couldn\'t locate plugin "' + this.id + '", Run \'gitbook install\' to install plugins from registry.');
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid plugin "' + this.id + '"');
|
||||
}
|
||||
|
||||
if (!gitbook.satisfies(this.packageInfos.engines.gitbook)) {
|
||||
throw new Error('GitBook doesn\'t satisfy the requirements of this plugin: '+this.packageInfos.engines.gitbook);
|
||||
}
|
||||
};
|
||||
|
||||
// Normalize, validate configuration for this plugin using its schema
|
||||
// Throw an error when shcema is not respected
|
||||
BookPlugin.prototype.validateConfig = function(config) {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
var schema = that.packageInfos.gitbook || {};
|
||||
if (!schema) return config;
|
||||
|
||||
// Normalize schema
|
||||
schema.id = '/'+that.getConfigKey();
|
||||
schema.type = 'object';
|
||||
|
||||
// Validate and throw if invalid
|
||||
var v = new jsonschema.Validator();
|
||||
var result = v.validate(config, schema, {
|
||||
propertyName: that.getConfigKey()
|
||||
});
|
||||
|
||||
// Throw error
|
||||
if (result.errors.length > 0) {
|
||||
throw new error.ConfigurationError(new Error(result.errors[0].stack));
|
||||
}
|
||||
|
||||
// Insert default values
|
||||
var defaults = jsonSchemaDefaults(schema);
|
||||
return mergeDefaults(config, defaults);
|
||||
});
|
||||
};
|
||||
|
||||
// Return key for configuration
|
||||
BookPlugin.prototype.getConfigKey = function() {
|
||||
return 'pluginsConfig.'+this.id;
|
||||
};
|
||||
|
||||
// Call a hook and returns its result
|
||||
BookPlugin.prototype.hook = function(name, input) {
|
||||
var that = this;
|
||||
var hookFunc = this.content.hooks? this.content.hooks[name] : null;
|
||||
input = input || {};
|
||||
|
||||
if (!hookFunc) return Promise(input);
|
||||
|
||||
this.book.log.debug.ln('call hook "' + name + '" for plugin "' + this.id + '"');
|
||||
if (!_.contains(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 Promise()
|
||||
.then(function() {
|
||||
return that.bind(hookFunc)(input);
|
||||
});
|
||||
};
|
||||
|
||||
// Return resources without normalization
|
||||
BookPlugin.prototype._getResources = function(base) {
|
||||
var that = this;
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
if (that._resources[base]) return that._resources[base];
|
||||
|
||||
base = base;
|
||||
var book = that.content[base];
|
||||
|
||||
// Compatibility with version 1.x.x
|
||||
if (base == 'website') book = book || that.content.book;
|
||||
|
||||
// Nothing specified, fallback to default
|
||||
if (!book) {
|
||||
return Promise({});
|
||||
}
|
||||
|
||||
// Dynamic function
|
||||
if(typeof book === 'function') {
|
||||
// Call giving it the context of our book
|
||||
return that.bind(book)();
|
||||
}
|
||||
|
||||
// Plain data object
|
||||
return book;
|
||||
})
|
||||
|
||||
.then(function(resources) {
|
||||
that._resources[base] = resources;
|
||||
return _.cloneDeep(resources);
|
||||
});
|
||||
};
|
||||
|
||||
// Normalize a specific resource
|
||||
BookPlugin.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.npmId+'/'+resource };
|
||||
};
|
||||
|
||||
|
||||
// Normalize resources and return them
|
||||
BookPlugin.prototype.getResources = function(base) {
|
||||
var that = this;
|
||||
|
||||
return this._getResources(base)
|
||||
.then(function(resources) {
|
||||
_.each(RESOURCES, function(resourceType) {
|
||||
resources[resourceType] = _.map(resources[resourceType] || [], that.normalizeResource);
|
||||
});
|
||||
|
||||
return resources;
|
||||
});
|
||||
};
|
||||
|
||||
// Normalize filters and return them
|
||||
BookPlugin.prototype.getFilters = function() {
|
||||
var that = this;
|
||||
|
||||
return _.mapValues(this.content.filters || {}, function(fn, filter) {
|
||||
return function() {
|
||||
var ctx = _.extend(compatibility.pluginCtx(that), this);
|
||||
|
||||
return fn.apply(ctx, arguments);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Normalize blocks and return them
|
||||
BookPlugin.prototype.getBlocks = function() {
|
||||
var that = this;
|
||||
|
||||
return _.mapValues(this.content.blocks || {}, function(block, blockName) {
|
||||
block = _.isFunction(block)? { process: block } : block;
|
||||
|
||||
var fn = block.process;
|
||||
block.process = function() {
|
||||
var ctx = _.extend(compatibility.pluginCtx(that), this);
|
||||
|
||||
return fn.apply(ctx, arguments);
|
||||
};
|
||||
|
||||
return block;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = BookPlugin;
|
||||
module.exports.RESOURCES = RESOURCES;
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
var npm = require('npm');
|
||||
var npmi = require('npmi');
|
||||
var semver = require('semver');
|
||||
var _ = require('lodash');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var gitbook = require('../gitbook');
|
||||
|
||||
var PLUGIN_PREFIX = 'gitbook-plugin-';
|
||||
|
||||
// Return an absolute name for the plugin (the one on NPM)
|
||||
function npmId(name) {
|
||||
if (name.indexOf(PLUGIN_PREFIX) === 0) return name;
|
||||
return [PLUGIN_PREFIX, name].join('');
|
||||
}
|
||||
|
||||
// Return a plugin ID 9the one on GitBook
|
||||
function pluginId(name) {
|
||||
return name.replace(PLUGIN_PREFIX, '');
|
||||
}
|
||||
|
||||
// Validate an NPM plugin ID
|
||||
function validateId(name) {
|
||||
return name.indexOf(PLUGIN_PREFIX) === 0;
|
||||
}
|
||||
|
||||
// Initialize NPM for operations
|
||||
var initNPM = _.memoize(function() {
|
||||
return Promise.nfcall(npm.load, {
|
||||
silent: true,
|
||||
loglevel: 'silent'
|
||||
});
|
||||
});
|
||||
|
||||
// Link a plugin for use in a specific book
|
||||
function linkPlugin(book, pluginPath) {
|
||||
book.log('linking', pluginPath);
|
||||
}
|
||||
|
||||
// Resolve the latest version for a plugin
|
||||
function resolveVersion(plugin) {
|
||||
var npnName = npmId(plugin);
|
||||
|
||||
return initNPM()
|
||||
.then(function() {
|
||||
return Promise.nfcall(npm.commands.view, [npnName+'@*', '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 && gitbook.satisfies(v.gitbook);
|
||||
})
|
||||
.sort(function(v1, v2) {
|
||||
return semver.lt(v1.version, v2.version)? 1 : -1;
|
||||
})
|
||||
.pluck('version')
|
||||
.first()
|
||||
.value();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Install a plugin in a book
|
||||
function installPlugin(book, plugin, version) {
|
||||
book.log.info.ln('installing plugin', plugin);
|
||||
|
||||
var npnName = npmId(plugin);
|
||||
|
||||
return Promise()
|
||||
.then(function() {
|
||||
if (version) return version;
|
||||
|
||||
book.log.info.ln('No version specified, resolve plugin "' + plugin + '"');
|
||||
return resolveVersion(plugin);
|
||||
})
|
||||
|
||||
// Install the plugin with the resolved version
|
||||
.then(function(version) {
|
||||
if (!version) {
|
||||
throw new Error('Found no satisfactory version for plugin "' + plugin + '"');
|
||||
}
|
||||
|
||||
book.log.info.ln('install plugin' + plugin +'" from npm ('+npnName+') with version', version);
|
||||
return Promise.nfcall(npmi, {
|
||||
'name': npnName,
|
||||
'version': version,
|
||||
'path': book.root,
|
||||
'npmLoad': {
|
||||
'loglevel': 'silent',
|
||||
'loaded': true,
|
||||
'prefix': book.root
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function() {
|
||||
book.log.info.ok('plugin "' + plugin + '" installed with success');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
npmId: npmId,
|
||||
pluginId: pluginId,
|
||||
validateId: validateId,
|
||||
|
||||
resolve: resolveVersion,
|
||||
link: linkPlugin,
|
||||
install: installPlugin
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
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 replaced by plugins
|
||||
code: function(blk) {
|
||||
return {
|
||||
html: false,
|
||||
body: blk.body
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
var moment = require('moment');
|
||||
|
||||
|
||||
module.exports = {
|
||||
// Format a date
|
||||
// ex: 'MMMM Do YYYY, h:mm:ss a
|
||||
date: function(time, format) {
|
||||
return moment(time).format(format);
|
||||
},
|
||||
|
||||
// Relative Time
|
||||
dateFromNow: function(time) {
|
||||
return moment(time).fromNow();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,431 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var nunjucks = require('nunjucks');
|
||||
var escapeStringRegexp = require('escape-string-regexp');
|
||||
|
||||
var Promise = require('../utils/promise');
|
||||
var error = require('../utils/error');
|
||||
var parsers = require('../parsers');
|
||||
var defaultBlocks = require('./blocks');
|
||||
var defaultFilters = require('./filters');
|
||||
var Loader = require('./loader');
|
||||
|
||||
// Return extension name for a specific block
|
||||
function blockExtName(name) {
|
||||
return 'Block'+name+'Extension';
|
||||
}
|
||||
|
||||
// Normalize the result of block process function
|
||||
function normBlockResult(blk) {
|
||||
if (_.isString(blk)) blk = { body: blk };
|
||||
return blk;
|
||||
}
|
||||
|
||||
function TemplateEngine(output) {
|
||||
this.output = output;
|
||||
this.book = output.book;
|
||||
this.log = this.book.log;
|
||||
|
||||
// Create file loader
|
||||
this.loader = new Loader(this);
|
||||
|
||||
// Create nunjucks instance
|
||||
this.env = new nunjucks.Environment(
|
||||
this.loader,
|
||||
{
|
||||
// Escaping is done after by the asciidoc/markdown parser
|
||||
autoescape: false,
|
||||
|
||||
// Syntax
|
||||
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 and filters
|
||||
this.addBlocks(defaultBlocks);
|
||||
this.addFilters(defaultFilters);
|
||||
}
|
||||
|
||||
// Bind a function to a context
|
||||
// Filters and blocks are binded to this context
|
||||
TemplateEngine.prototype.bindContext = function(func) {
|
||||
var ctx = {
|
||||
ctx: this.ctx,
|
||||
output: this.output,
|
||||
generator: this.output.name
|
||||
};
|
||||
|
||||
return _.bind(func, ctx);
|
||||
};
|
||||
|
||||
// Interpolate a string content to replace shortcuts according to the filetype
|
||||
TemplateEngine.prototype.interpolate = function(filepath, source) {
|
||||
var parser = parsers.get(path.extname(filepath));
|
||||
var type = parser? parser.name : null;
|
||||
|
||||
return this.applyShortcuts(type, source);
|
||||
};
|
||||
|
||||
// Add a new custom filter
|
||||
TemplateEngine.prototype.addFilter = function(filterName, func) {
|
||||
try {
|
||||
this.env.getFilter(filterName);
|
||||
this.log.error.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);
|
||||
|
||||
Promise()
|
||||
.then(function() {
|
||||
return func.apply(ctx, args.slice(0, -1));
|
||||
})
|
||||
.nodeify(callback);
|
||||
}), true);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Add multiple filters at once
|
||||
TemplateEngine.prototype.addFilters = function(filters) {
|
||||
_.each(filters, function(filter, name) {
|
||||
this.addFilter(name, filter);
|
||||
}, this);
|
||||
};
|
||||
|
||||
// Return true if a block is defined
|
||||
TemplateEngine.prototype.hasBlock = function(name) {
|
||||
return this.env.hasExtension(blockExtName(name));
|
||||
};
|
||||
|
||||
// Remove/Disable a block
|
||||
TemplateEngine.prototype.removeBlock = function(name) {
|
||||
if (!this.hasBlock(name)) return;
|
||||
|
||||
// Remove nunjucks extension
|
||||
this.env.removeExtension(blockExtName(name));
|
||||
|
||||
// Cleanup shortcuts
|
||||
this.shortcuts = _.reject(this.shortcuts, {
|
||||
block: name
|
||||
});
|
||||
};
|
||||
|
||||
// Add a block
|
||||
// Using the extensions of nunjucks: https://mozilla.github.io/nunjucks/api.html#addextension
|
||||
TemplateEngine.prototype.addBlock = function(name, block) {
|
||||
var that = this, Ext, extName;
|
||||
|
||||
// Block can be a simple function
|
||||
if (_.isFunction(block)) block = { process: block };
|
||||
|
||||
block = _.defaults(block || {}, {
|
||||
shortcuts: [],
|
||||
end: 'end'+name,
|
||||
blocks: []
|
||||
});
|
||||
|
||||
extName = blockExtName(name);
|
||||
|
||||
if (!block.process) {
|
||||
throw new Error('Invalid block "' + name + '", it should have a "process" method');
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
do {
|
||||
// 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;
|
||||
|
||||
// Parse signature and move to the end of the block
|
||||
if (lastBlockName != block.end) {
|
||||
lastBlockArgs = parser.parseSignature(null, true);
|
||||
parser.advanceAfterBlockEnd(lastBlockName);
|
||||
}
|
||||
} while (lastBlockName != block.end)
|
||||
|
||||
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]()
|
||||
};
|
||||
});
|
||||
|
||||
Promise()
|
||||
.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 any
|
||||
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 at once
|
||||
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 (Promise.isPromise(r)) return r.then(normBlockResult);
|
||||
else return normBlockResult(r);
|
||||
};
|
||||
|
||||
// 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+'@%@';
|
||||
};
|
||||
|
||||
// Render a string (without post processing)
|
||||
TemplateEngine.prototype.render = function(content, context, options) {
|
||||
options = _.defaults(options || {}, {
|
||||
path: null
|
||||
});
|
||||
var filename = options.path;
|
||||
|
||||
// Setup path and type
|
||||
if (options.path) {
|
||||
options.path = this.book.resolve(options.path);
|
||||
}
|
||||
|
||||
// Replace shortcuts
|
||||
content = this.applyShortcuts(options.type, content);
|
||||
|
||||
return Promise.nfcall(this.env.renderString.bind(this.env), content, context, options)
|
||||
.fail(function(err) {
|
||||
throw error.TemplateError(err, {
|
||||
filename: filename || '<inline>'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Render a string with post-processing
|
||||
TemplateEngine.prototype.renderString = function(content, context, options) {
|
||||
return this.render(content, context, options)
|
||||
.then(this.postProcess);
|
||||
};
|
||||
|
||||
// Apply a shortcut to a string
|
||||
TemplateEngine.prototype.applyShortcut = function(content, shortcut) {
|
||||
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+' %}';
|
||||
});
|
||||
};
|
||||
|
||||
// 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;
|
||||
});
|
||||
};
|
||||
|
||||
// Apply all shortcuts to a template
|
||||
TemplateEngine.prototype.applyShortcuts = function(type, content) {
|
||||
return _.chain(this.shortcuts)
|
||||
.filter(function(shortcut) {
|
||||
return _.contains(shortcut.parsers, type);
|
||||
})
|
||||
.reduce(this.applyShortcut, content)
|
||||
.value();
|
||||
};
|
||||
|
||||
|
||||
// Post process content
|
||||
TemplateEngine.prototype.postProcess = function(content) {
|
||||
var that = this;
|
||||
|
||||
return Promise(content)
|
||||
.then(that.replaceBlocks)
|
||||
.then(function(_content) {
|
||||
return Promise.serie(that.blockBodies, function(blk, blkId) {
|
||||
return Promise()
|
||||
.then(function() {
|
||||
if (!blk.post) return;
|
||||
return blk.post();
|
||||
})
|
||||
.then(function() {
|
||||
delete that.blockBodies[blkId];
|
||||
});
|
||||
})
|
||||
.thenResolve(_content);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = TemplateEngine;
|
||||
@@ -0,0 +1,42 @@
|
||||
var nunjucks = require('nunjucks');
|
||||
var location = require('../utils/location');
|
||||
|
||||
/*
|
||||
Simple nunjucks loader which is passing the reponsability to the Output
|
||||
*/
|
||||
|
||||
var Loader = nunjucks.Loader.extend({
|
||||
async: true,
|
||||
|
||||
init: function(engine, opts) {
|
||||
this.engine = engine;
|
||||
this.output = engine.output;
|
||||
},
|
||||
|
||||
getSource: function(sourceURL, callback) {
|
||||
var that = this;
|
||||
|
||||
this.output.onGetTemplate(sourceURL)
|
||||
.then(function(out) {
|
||||
// We disable cache since content is modified (shortcuts, ...)
|
||||
out.noCache = true;
|
||||
|
||||
// Transform template before runnign it
|
||||
out.source = that.engine.interpolate(out.path, out.source);
|
||||
|
||||
return out;
|
||||
})
|
||||
.nodeify(callback);
|
||||
},
|
||||
|
||||
resolve: function(from, to) {
|
||||
return this.output.onResolveTemplate(from, to);
|
||||
},
|
||||
|
||||
// Handle all files as relative, so that nunjucks pass responsability to 'resolve'
|
||||
isRelative: function(filename) {
|
||||
return location.isRelative(filename);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Loader;
|
||||
@@ -0,0 +1,80 @@
|
||||
var _ = require('lodash');
|
||||
var childProcess = require('child_process');
|
||||
var spawn = require("spawn-cmd").spawn;
|
||||
var Promise = require('./promise');
|
||||
|
||||
// Execute a command
|
||||
function exec(command, options) {
|
||||
var d = Promise.defer();
|
||||
|
||||
var child = childProcess.exec(command, options, function(err, stdout, stderr) {
|
||||
if (!err) {
|
||||
return d.resolve();
|
||||
}
|
||||
|
||||
err.message = stdout.toString('utf8') + stderr.toString('utf8');
|
||||
d.reject(err);
|
||||
});
|
||||
|
||||
child.stdout.on('data', function (data) {
|
||||
d.notify(data);
|
||||
});
|
||||
|
||||
child.stderr.on('data', function (data) {
|
||||
d.notify(data);
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
// Spawn an executable
|
||||
function spawnCmd(command, args, options) {
|
||||
var d = Promise.defer();
|
||||
var child = spawn(command, args, options);
|
||||
|
||||
child.on('error', function(error) {
|
||||
return d.reject(error);
|
||||
});
|
||||
|
||||
child.stdout.on('data', function (data) {
|
||||
d.notify(data);
|
||||
});
|
||||
|
||||
child.stderr.on('data', function (data) {
|
||||
d.notify(data);
|
||||
});
|
||||
|
||||
child.on('close', function(code) {
|
||||
if (code === 0) {
|
||||
d.resolve();
|
||||
} else {
|
||||
d.reject(new Error('Error with command "'+command+'"'));
|
||||
}
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
// Transform an option object to a command line string
|
||||
function escapeShellArg(s) {
|
||||
s = s.replace(/"/g, '\\"');
|
||||
return '"' + s + '"';
|
||||
}
|
||||
|
||||
function optionsToShellArgs(options) {
|
||||
return _.chain(options)
|
||||
.map(function(value, key) {
|
||||
if (value === null || value === undefined || value === false) return null;
|
||||
if (value === true) return key;
|
||||
return key + '=' + escapeShellArg(value);
|
||||
})
|
||||
.compact()
|
||||
.value()
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
exec: exec,
|
||||
spawn: spawnCmd,
|
||||
optionsToShellArgs: optionsToShellArgs
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
var _ = require('lodash');
|
||||
var TypedError = require('error/typed');
|
||||
var WrappedError = require('error/wrapped');
|
||||
var deprecated = require('deprecated');
|
||||
|
||||
var Logger = require('./logger');
|
||||
|
||||
var log = new Logger();
|
||||
|
||||
// Enforce as an Error object, and cleanup message
|
||||
function enforce(err) {
|
||||
if (_.isString(err)) err = new Error(err);
|
||||
err.message = err.message.replace(/^Error: /, '');
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
// Random error wrappers during parsing/generation
|
||||
var ParsingError = WrappedError({
|
||||
message: 'Parsing Error: {origMessage}',
|
||||
type: 'parse'
|
||||
});
|
||||
var OutputError = WrappedError({
|
||||
message: 'Output Error: {origMessage}',
|
||||
type: 'generate'
|
||||
});
|
||||
|
||||
// A file does not exists
|
||||
var FileNotFoundError = TypedError({
|
||||
type: 'file.not-found',
|
||||
message: 'No "{filename}" file (or is ignored)',
|
||||
filename: null
|
||||
});
|
||||
|
||||
// A file is outside the scope
|
||||
var FileOutOfScopeError = TypedError({
|
||||
type: 'file.out-of-scope',
|
||||
message: '"{filename}" not in "{root}"',
|
||||
filename: null,
|
||||
root: null,
|
||||
code: 'EACCESS'
|
||||
});
|
||||
|
||||
// A file is outside the scope
|
||||
var RequireInstallError = TypedError({
|
||||
type: 'install.required',
|
||||
message: '"{cmd}" is not installed.\n{install}',
|
||||
cmd: null,
|
||||
code: 'ENOENT',
|
||||
install: ''
|
||||
});
|
||||
|
||||
// Error for nunjucks templates
|
||||
var TemplateError = WrappedError({
|
||||
message: 'Error compiling template "{filename}": {origMessage}',
|
||||
type: 'template',
|
||||
filename: null
|
||||
});
|
||||
|
||||
// Error for nunjucks templates
|
||||
var PluginError = WrappedError({
|
||||
message: 'Error with plugin "{plugin}": {origMessage}',
|
||||
type: 'plugin',
|
||||
plugin: null
|
||||
});
|
||||
|
||||
// Error with the book's configuration
|
||||
var ConfigurationError = WrappedError({
|
||||
message: 'Error with book\'s configuration: {origMessage}',
|
||||
type: 'configuration'
|
||||
});
|
||||
|
||||
// Error during ebook generation
|
||||
var EbookError = WrappedError({
|
||||
message: 'Error during ebook generation: {origMessage}\n{stdout}',
|
||||
type: 'ebook',
|
||||
stdout: ''
|
||||
});
|
||||
|
||||
// Deprecate methods/fields
|
||||
function deprecateMethod(fn, msg) {
|
||||
return deprecated.method(msg, log.warn.ln, fn);
|
||||
}
|
||||
function deprecateField(obj, prop, value, msg) {
|
||||
return deprecated.field(msg, log.warn.ln, obj, prop, value);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
enforce: enforce,
|
||||
|
||||
ParsingError: ParsingError,
|
||||
OutputError: OutputError,
|
||||
RequireInstallError: RequireInstallError,
|
||||
|
||||
FileNotFoundError: FileNotFoundError,
|
||||
FileOutOfScopeError: FileOutOfScopeError,
|
||||
|
||||
TemplateError: TemplateError,
|
||||
PluginError: PluginError,
|
||||
ConfigurationError: ConfigurationError,
|
||||
EbookError: EbookError,
|
||||
|
||||
deprecateMethod: deprecateMethod,
|
||||
deprecateField: deprecateField
|
||||
};
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
var fs = require('graceful-fs');
|
||||
var mkdirp = require('mkdirp');
|
||||
var destroy = require('destroy');
|
||||
var rmdir = require('rmdir');
|
||||
var tmp = require('tmp');
|
||||
var request = require('request');
|
||||
var path = require('path');
|
||||
var cp = require('cp');
|
||||
var cpr = require('cpr');
|
||||
|
||||
var Promise = require('./promise');
|
||||
|
||||
// Write a stream to a file
|
||||
function writeStream(filename, st) {
|
||||
var d = Promise.defer();
|
||||
|
||||
var wstream = fs.createWriteStream(filename);
|
||||
var cleanup = function() {
|
||||
destroy(wstream);
|
||||
wstream.removeAllListeners();
|
||||
};
|
||||
|
||||
wstream.on('finish', function () {
|
||||
cleanup();
|
||||
d.resolve();
|
||||
});
|
||||
wstream.on('error', function (err) {
|
||||
cleanup();
|
||||
d.reject(err);
|
||||
});
|
||||
|
||||
st.on('error', function(err) {
|
||||
cleanup();
|
||||
d.reject(err);
|
||||
});
|
||||
|
||||
st.pipe(wstream);
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
// Return a promise resolved with a boolean
|
||||
function fileExists(filename) {
|
||||
var d = Promise.defer();
|
||||
|
||||
fs.exists(filename, function(exists) {
|
||||
d.resolve(exists);
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
}
|
||||
|
||||
// Generate temporary file
|
||||
function genTmpFile(opts) {
|
||||
return Promise.nfcall(tmp.file, opts)
|
||||
.get(0);
|
||||
}
|
||||
|
||||
// Generate temporary dir
|
||||
function genTmpDir(opts) {
|
||||
return Promise.nfcall(tmp.dir, opts)
|
||||
.get(0);
|
||||
}
|
||||
|
||||
// Download an image
|
||||
function download(uri, dest) {
|
||||
return writeStream(dest, request(uri));
|
||||
}
|
||||
|
||||
// Find a filename available in a folder
|
||||
function uniqueFilename(base, filename) {
|
||||
var ext = path.extname(filename);
|
||||
filename = path.resolve(base, 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 Promise(path.relative(base, _filename));
|
||||
}
|
||||
|
||||
// Create all required folder to create a file
|
||||
function ensureFile(filename) {
|
||||
var base = path.dirname(filename);
|
||||
return Promise.nfcall(mkdirp, base);
|
||||
}
|
||||
|
||||
// Remove a folder
|
||||
function rmDir(base) {
|
||||
return Promise.nfcall(rmdir, base, {
|
||||
fs: fs
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
exists: fileExists,
|
||||
existsSync: fs.existsSync,
|
||||
mkdirp: Promise.nfbind(mkdirp),
|
||||
readFile: Promise.nfbind(fs.readFile),
|
||||
writeFile: Promise.nfbind(fs.writeFile),
|
||||
stat: Promise.nfbind(fs.stat),
|
||||
statSync: fs.statSync,
|
||||
readdir: Promise.nfbind(fs.readdir),
|
||||
writeStream: writeStream,
|
||||
copy: Promise.nfbind(cp),
|
||||
copyDir: Promise.nfbind(cpr),
|
||||
tmpFile: genTmpFile,
|
||||
tmpDir: genTmpDir,
|
||||
download: download,
|
||||
uniqueFilename: uniqueFilename,
|
||||
ensure: ensureFile,
|
||||
rmDir: rmDir
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
var crc = require('crc');
|
||||
var URI = require('urijs');
|
||||
|
||||
var pathUtil = require('./path');
|
||||
var Promise = require('./promise');
|
||||
var command = require('./command');
|
||||
var fs = require('./fs');
|
||||
|
||||
var GIT_PREFIX = 'git+';
|
||||
|
||||
function Git() {
|
||||
this.tmpDir;
|
||||
this.cloned = {};
|
||||
}
|
||||
|
||||
// Return an unique ID for a combinaison host/ref
|
||||
Git.prototype.repoID = function(host, ref) {
|
||||
return crc.crc32(host+'#'+(ref || '')).toString(16);
|
||||
};
|
||||
|
||||
// Allocate a temporary folder for cloning repos in it
|
||||
Git.prototype.allocateDir = function() {
|
||||
var that = this;
|
||||
|
||||
if (this.tmpDir) return Promise();
|
||||
|
||||
return fs.tmpDir()
|
||||
.then(function(dir) {
|
||||
that.tmpDir = dir;
|
||||
});
|
||||
};
|
||||
|
||||
// Clone a git repository if non existant
|
||||
Git.prototype.clone = function(host, ref) {
|
||||
var that = this;
|
||||
|
||||
return this.allocateDir()
|
||||
|
||||
// Return or clone the git repo
|
||||
.then(function() {
|
||||
// Unique ID for repo/ref combinaison
|
||||
var repoId = that.repoID(host, ref);
|
||||
|
||||
// Absolute path to the folder
|
||||
var repoPath = path.join(that.tmpDir, repoId);
|
||||
|
||||
if (that.cloned[repoId]) return repoPath;
|
||||
|
||||
// Clone repo
|
||||
return command.exec('git clone '+host+' '+repoPath)
|
||||
|
||||
// Checkout reference if specified
|
||||
.then(function() {
|
||||
that.cloned[repoId] = true;
|
||||
|
||||
if (!ref) return;
|
||||
return command.exec('git checkout '+ref, { cwd: repoPath });
|
||||
})
|
||||
.thenResolve(repoPath);
|
||||
});
|
||||
};
|
||||
|
||||
// Get file from a git repo
|
||||
Git.prototype.resolve = function(giturl) {
|
||||
// Path to a file in a git repo?
|
||||
if (!Git.isUrl(giturl)) {
|
||||
if (this.resolveRoot(giturl)) return Promise(giturl);
|
||||
return Promise(null);
|
||||
}
|
||||
if (_.isString(giturl)) giturl = Git.parseUrl(giturl);
|
||||
if (!giturl) return Promise(null);
|
||||
|
||||
// Clone or get from cache
|
||||
return this.clone(giturl.host, giturl.ref)
|
||||
.then(function(repo) {
|
||||
return path.resolve(repo, giturl.filepath);
|
||||
});
|
||||
};
|
||||
|
||||
// Return root of git repo from a filepath
|
||||
Git.prototype.resolveRoot = function(filepath) {
|
||||
var relativeToGit, repoId;
|
||||
|
||||
// No git repo cloned, or file is not in a git repository
|
||||
if (!this.tmpDir || !pathUtil.isInRoot(this.tmpDir, filepath)) return null;
|
||||
|
||||
// Extract first directory (is the repo id)
|
||||
relativeToGit = path.relative(this.tmpDir, filepath);
|
||||
repoId = _.first(relativeToGit.split(path.sep));
|
||||
if (!repoId) return;
|
||||
|
||||
// Return an absolute file
|
||||
return path.resolve(this.tmpDir, repoId);
|
||||
};
|
||||
|
||||
// Check if an url is a git dependency url
|
||||
Git.isUrl = function(giturl) {
|
||||
return (giturl.indexOf(GIT_PREFIX) === 0);
|
||||
};
|
||||
|
||||
// Parse and extract infos
|
||||
Git.parseUrl = function(giturl) {
|
||||
var ref, uri, fileParts, filepath;
|
||||
|
||||
if (!Git.isUrl(giturl)) return null;
|
||||
giturl = giturl.slice(GIT_PREFIX.length);
|
||||
|
||||
uri = new URI(giturl);
|
||||
ref = uri.fragment() || null;
|
||||
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,
|
||||
filepath: filepath
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Git;
|
||||
@@ -0,0 +1,44 @@
|
||||
var Promise = require('./promise');
|
||||
var command = require('./command');
|
||||
var fs = require('./fs');
|
||||
var error = require('./error');
|
||||
|
||||
// Convert a svg file to a pmg
|
||||
function convertSVGToPNG(source, dest, options) {
|
||||
if (!fs.existsSync(source)) return Promise.reject(new error.FileNotFoundError({ filename: source }));
|
||||
|
||||
return command.spawn('svgexport', [source, dest])
|
||||
.fail(function(err) {
|
||||
if (err.code == 'ENOENT') {
|
||||
err = error.RequireInstallError({
|
||||
cmd: 'svgexport',
|
||||
install: 'Install it using: "npm install svgexport -g"'
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
})
|
||||
.then(function() {
|
||||
if (fs.existsSync(dest)) return;
|
||||
|
||||
throw new Error('Error converting '+source+' into '+dest);
|
||||
});
|
||||
}
|
||||
|
||||
// Convert a svg buffer to a png file
|
||||
function convertSVGBufferToPNG(buf, dest) {
|
||||
// Create a temporary SVG file to convert
|
||||
return fs.tmpFile({
|
||||
postfix: '.svg'
|
||||
})
|
||||
.then(function(tmpSvg) {
|
||||
return fs.writeFile(tmpSvg, buf)
|
||||
.then(function() {
|
||||
return convertSVGToPNG(tmpSvg, dest);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
convertSVGToPNG: convertSVGToPNG,
|
||||
convertSVGBufferToPNG: convertSVGBufferToPNG
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
lang: require('./lang'),
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
var MAP = {
|
||||
'py': 'python',
|
||||
'js': 'javascript',
|
||||
'rb': 'ruby',
|
||||
};
|
||||
|
||||
function normalize(lang) {
|
||||
if(!lang) { return null; }
|
||||
|
||||
var lower = lang.toLowerCase();
|
||||
return MAP[lower] || lower;
|
||||
}
|
||||
|
||||
// Exports
|
||||
module.exports = {
|
||||
normalize: normalize,
|
||||
MAP: MAP
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
var url = require('url');
|
||||
var path = require('path');
|
||||
|
||||
// Is the url an external url
|
||||
function isExternal(href) {
|
||||
try {
|
||||
return Boolean(url.parse(href).protocol);
|
||||
} catch(err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Inverse of isExternal
|
||||
function isRelative(href) {
|
||||
return !isExternal(href);
|
||||
}
|
||||
|
||||
// Return true if the link is an achor
|
||||
function isAnchor(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
|
||||
function normalize(s) {
|
||||
return path.normalize(s).replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
// Convert relative to absolute path
|
||||
// dir: directory parent of the file currently in rendering process
|
||||
// outdir: directory parent from the html output
|
||||
function toAbsolute(_href, dir, outdir) {
|
||||
if (isExternal(_href)) return _href;
|
||||
outdir = outdir == undefined? dir : outdir;
|
||||
|
||||
_href = normalize(_href);
|
||||
dir = normalize(dir);
|
||||
outdir = normalize(outdir);
|
||||
|
||||
// Path "_href" inside the base folder
|
||||
var hrefInRoot = path.normalize(path.join(dir, _href));
|
||||
if (_href[0] == '/') hrefInRoot = path.normalize(_href.slice(1));
|
||||
|
||||
// Make it relative to output
|
||||
_href = path.relative(outdir, hrefInRoot);
|
||||
|
||||
// Normalize windows paths
|
||||
_href = normalize(_href);
|
||||
|
||||
return _href;
|
||||
}
|
||||
|
||||
// Convert an absolute path to a relative path for a specific folder (dir)
|
||||
// ('test/', 'hello.md') -> '../hello.md'
|
||||
function relative(dir, file) {
|
||||
return normalize(path.relative(dir, file));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isExternal: isExternal,
|
||||
isRelative: isRelative,
|
||||
isAnchor: isAnchor,
|
||||
normalize: normalize,
|
||||
toAbsolute: toAbsolute,
|
||||
relative: relative
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
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
|
||||
};
|
||||
|
||||
function Logger(write, logLevel, prefix) {
|
||||
if (!(this instanceof Logger)) return new Logger(write, logLevel);
|
||||
|
||||
this._write = write || function(msg) { process.stdout.write(msg); };
|
||||
this.lastChar = '\n';
|
||||
|
||||
// Define log level
|
||||
this.setLevel(logLevel);
|
||||
|
||||
_.bindAll(this);
|
||||
|
||||
// Create easy-to-use method like "logger.debug.ln('....')"
|
||||
_.each(_.omit(LEVELS, 'DISABLED'), function(level, levelKey) {
|
||||
levelKey = levelKey.toLowerCase();
|
||||
|
||||
this[levelKey] = _.partial(this.log, level);
|
||||
this[levelKey].ln = _.partial(this.logLn, level);
|
||||
this[levelKey].ok = _.partial(this.ok, level);
|
||||
this[levelKey].fail = _.partial(this.fail, level);
|
||||
this[levelKey].promise = _.partial(this.promise, level);
|
||||
}, this);
|
||||
}
|
||||
|
||||
// Create a new logger prefixed from this logger
|
||||
Logger.prototype.prefix = function(prefix) {
|
||||
return (new Logger(this._write, this.logLevel, prefix));
|
||||
};
|
||||
|
||||
// Change minimum level
|
||||
Logger.prototype.setLevel = function(logLevel) {
|
||||
if (_.isString(logLevel)) logLevel = LEVELS[logLevel.toUpperCase()];
|
||||
this.logLevel = logLevel;
|
||||
};
|
||||
|
||||
// Print a simple string
|
||||
Logger.prototype.write = function(msg) {
|
||||
msg = msg.toString();
|
||||
this.lastChar = _.last(msg);
|
||||
return this._write(msg);
|
||||
};
|
||||
|
||||
// Format a string using the first argument as a printf-like format.
|
||||
Logger.prototype.format = function() {
|
||||
return util.format.apply(util, arguments);
|
||||
};
|
||||
|
||||
// Print a line
|
||||
Logger.prototype.writeLn = function(msg) {
|
||||
return this.write((msg || '')+'\n');
|
||||
};
|
||||
|
||||
// Log/Print a message if level is allowed
|
||||
Logger.prototype.log = function(level) {
|
||||
if (level < this.logLevel) return;
|
||||
|
||||
var levelKey = _.findKey(LEVELS, function(v) { return v == level; });
|
||||
var args = Array.prototype.slice.apply(arguments, [1]);
|
||||
var msg = this.format.apply(this, args);
|
||||
|
||||
if (this.lastChar == '\n') {
|
||||
msg = COLORS[levelKey](levelKey.toLowerCase()+':')+' '+msg;
|
||||
}
|
||||
|
||||
return this.write(msg);
|
||||
};
|
||||
|
||||
// Log/Print a line if level is allowed
|
||||
Logger.prototype.logLn = function() {
|
||||
if (this.lastChar != '\n') this.write('\n');
|
||||
|
||||
var args = Array.prototype.slice.apply(arguments);
|
||||
args.push('\n');
|
||||
return this.log.apply(this, args);
|
||||
};
|
||||
|
||||
// Log a confirmation [OK]
|
||||
Logger.prototype.ok = function(level) {
|
||||
var args = Array.prototype.slice.apply(arguments, [1]);
|
||||
var msg = this.format.apply(this, args);
|
||||
if (arguments.length > 1) {
|
||||
this.logLn(level, color.green('>> ') + msg.trim().replace(/\n/g, color.green('\n>> ')));
|
||||
} else {
|
||||
this.log(level, color.green('OK'), '\n');
|
||||
}
|
||||
};
|
||||
|
||||
// Log a "FAIL"
|
||||
Logger.prototype.fail = function(level) {
|
||||
return this.log(level, color.red('ERROR') + '\n');
|
||||
};
|
||||
|
||||
// Log state of a promise
|
||||
Logger.prototype.promise = function(level, p) {
|
||||
var that = this;
|
||||
|
||||
return p.
|
||||
then(function(st) {
|
||||
that.ok(level);
|
||||
return st;
|
||||
}, function(err) {
|
||||
that.fail(level);
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
Logger.LEVELS = LEVELS;
|
||||
Logger.COLORS = COLORS;
|
||||
|
||||
module.exports = Logger;
|
||||
@@ -0,0 +1,58 @@
|
||||
var _ = require('lodash');
|
||||
var path = require('path');
|
||||
|
||||
var error = require('./error');
|
||||
|
||||
// Normalize a filename
|
||||
function normalizePath(filename) {
|
||||
return path.normalize(filename);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
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)) {
|
||||
throw new error.FileOutOfScopeError({
|
||||
filename: result,
|
||||
root: root
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Chnage extension
|
||||
function setExtension(filename, ext) {
|
||||
return path.join(
|
||||
path.dirname(filename),
|
||||
path.basename(filename, path.extname(filename)) + ext
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isInRoot: isInRoot,
|
||||
resolveInRoot: resolveInRoot,
|
||||
normalize: normalizePath,
|
||||
setExtension: setExtension
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user