mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-26 20:27:00 +00:00
Compare commits
670 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da2ee396cf | |||
| 2e66333aa6 | |||
| a2ad586a93 | |||
| 856b222a61 | |||
| fade3740c1 | |||
| 803d6218a7 | |||
| 7128db842a | |||
| 0342a11da0 | |||
| af80cbcd7c | |||
| a84d02ece0 | |||
| 45b9fc2094 | |||
| a96ef569bd | |||
| 4b3ac4e827 | |||
| 7029c01806 | |||
| 11893255d1 | |||
| 53f94b317e | |||
| 4717b0a17f | |||
| 43ce7badb7 | |||
| 9e399da06a | |||
| 026b33227d | |||
| e94634bda5 | |||
| 194ebc3da9 | |||
| 16af264360 | |||
| 64ccb6b00b | |||
| 8bf3241b63 | |||
| 6a4f6eadd2 | |||
| 532df30db7 | |||
| fd1f7a5609 | |||
| febdd8a8bd | |||
| b04724ae0b | |||
| 5dbd8f5740 | |||
| f3af207e6e | |||
| 7d05f8cb89 | |||
| 9d3c397446 | |||
| 0681a51c2f | |||
| 36e548fd3c | |||
| ec149e6112 | |||
| e1bcb4ed7d | |||
| 4aae18617b | |||
| 6d00944576 | |||
| be8b11dcb4 | |||
| 1780435c4f | |||
| ebf7a5cf50 | |||
| 5f0a81d18b | |||
| b564ef6580 | |||
| 1d877db5f0 | |||
| 92678bb93f | |||
| 4f2c729d60 | |||
| 59079bc18d | |||
| 66c2a6c076 | |||
| 182bb9eb86 | |||
| 9ab738e478 | |||
| d0555e2bd0 | |||
| cdf9842d68 | |||
| a1ae5c2295 | |||
| 373a486a14 | |||
| f262a7cd89 | |||
| b63b0cc50d | |||
| 5839d4d8a8 | |||
| d9c3cb418a | |||
| 12c7ac1498 | |||
| 2c3072fd8a | |||
| bb14f48c39 | |||
| d56019dc18 | |||
| 0a6216a093 | |||
| 4c174f916e | |||
| 4db57fe5c5 | |||
| 2a78bcebab | |||
| be14a8ea4d | |||
| 37920c758b | |||
| 52f9409e24 | |||
| 05646996c0 | |||
| c439cf8513 | |||
| 1a3e4e8337 | |||
| 631cebfd2f | |||
| f1f2024489 | |||
| 227b329e78 | |||
| da8c78b3c8 | |||
| c42c5897fe | |||
| 3aea3e5d88 | |||
| 95b3b4ebb7 | |||
| ba56801352 | |||
| 48ee0e35b0 | |||
| 2fff291cad | |||
| baea45d924 | |||
| 52d0384605 | |||
| 961b9d7031 | |||
| 5b0622014a | |||
| 7253dbc476 | |||
| b4dd3e1ddc | |||
| 4b6fc0ae2f | |||
| 5d4dc499a5 | |||
| 0e7aece96d | |||
| b7c245d75a | |||
| 5fed4e1ced | |||
| c71da9955e | |||
| 3a2abb870b | |||
| e8947a2c11 | |||
| 885a161d42 | |||
| fe0d4bf024 | |||
| aa39b8e71b | |||
| 3000dcefe1 | |||
| f1405d3958 | |||
| caeadd431b | |||
| d5796ccf04 | |||
| 71cce33836 | |||
| 9eeb7304c7 | |||
| 3b5bea0eb5 | |||
| 17d921cb11 | |||
| 10ab06a32c | |||
| 1bc802e4c8 | |||
| 9a9b5a2621 | |||
| 66231f8b7f | |||
| 7037ac55bd | |||
| cf35fef3b3 | |||
| 16800cc70a | |||
| 417013a230 | |||
| 5a57a2c62a | |||
| 4cfffb26f2 | |||
| 9d4221936e | |||
| 34baae1a5d | |||
| cd0ed86589 | |||
| 2d429b731b | |||
| 03b89194d7 | |||
| 68ec88c620 | |||
| 47a27fc12d | |||
| 9cedcc6b6e | |||
| d6de8f4173 | |||
| 1d291ff2f0 | |||
| 58ae96b7f6 | |||
| 1354582c4e | |||
| 4119917555 | |||
| 9d20a9afa5 | |||
| 45683185a2 | |||
| 6ce4949df3 | |||
| 0614613b84 | |||
| 6faf6a53f0 | |||
| bced415a50 | |||
| 805886e4a9 | |||
| 83c98cf4d8 | |||
| 86848e7acb | |||
| 9ce3646d6e | |||
| 45752fc79c | |||
| a863b55c79 | |||
| 16ca48b2c0 | |||
| 87ae2481e4 | |||
| ccb5dcf559 | |||
| 84315a99f1 | |||
| 026cc8304d | |||
| cefbfdc7b8 | |||
| 4049279269 | |||
| b04befd67c | |||
| d82032298d | |||
| 34eb35699d | |||
| 3ae72bb47c | |||
| cd2d5e5101 | |||
| 5abac3a79a | |||
| d22d090b05 | |||
| 24b657f790 | |||
| 649a6c980c | |||
| 24624f0d0e | |||
| 9aaf561c41 | |||
| 8be4cbc887 | |||
| 32556e6724 | |||
| ad425b9772 | |||
| 5238fa352c | |||
| 32183b6650 | |||
| cb3d6a7f01 | |||
| c978f0ab21 | |||
| 8753897edc | |||
| da83d9e91d | |||
| 2cf507a807 | |||
| 1489129407 | |||
| b5c407cf7b | |||
| 9d8dffb6f0 | |||
| 6015defe11 | |||
| 7f28379154 | |||
| 6f98ee1846 | |||
| 42d53ec032 | |||
| 287c80b2e1 | |||
| 33ff60cee7 | |||
| 26fc021b90 | |||
| 33d7e022e6 | |||
| 09782b964b | |||
| 7938ee8ec6 | |||
| c314281da0 | |||
| 2e8e4f63d7 | |||
| bc5df83846 | |||
| f1ef1902f3 | |||
| 748c46172b | |||
| 51f4cd6dae | |||
| 64ce8bcf42 | |||
| ff42b58f77 | |||
| 7fa3e34f9f | |||
| e46cd78f80 | |||
| fab88bb47e | |||
| fa7cf3a65f | |||
| d0885491b7 | |||
| 45c54d0cd7 | |||
| f49089a7d3 | |||
| 0b1888e184 | |||
| 2257e42299 | |||
| 0e0c1a473b | |||
| a18370551a | |||
| a8aeab17d6 | |||
| 2adca5deb9 | |||
| 3b3f43a35d | |||
| ced0be930d | |||
| 6d58a90613 | |||
| 2796065886 | |||
| b9fa6c3051 | |||
| 82c2c214eb | |||
| ee35da4be5 | |||
| 24c4f41945 | |||
| fe298f2a3f | |||
| ab3d94a55d | |||
| ff31514c66 | |||
| 3f1ec310ca | |||
| b796069ade | |||
| 839904a704 | |||
| 0e74c9bad5 | |||
| d7e415b0b3 | |||
| b170b4d197 | |||
| a14ca3e268 | |||
| 9c071dade5 | |||
| 56e44651fa | |||
| 42fac98992 | |||
| 4f5513add0 | |||
| 90cbfc0596 | |||
| 38ab9fa05a | |||
| 12e20c5fcc | |||
| 6b103f4532 | |||
| d4fa5c18a8 | |||
| 2a116dfdb0 | |||
| 0349f54f3c | |||
| 695bda312c | |||
| 144bd4c97a | |||
| e105983cf8 | |||
| ea3afbed75 | |||
| d563abe3df | |||
| 70fd4ded29 | |||
| 9ce9388dfc | |||
| c575ad83ae | |||
| 936fa1545b | |||
| c9c2dde2b6 | |||
| d9a1d387c7 | |||
| bf29d187c8 | |||
| d86f9b2667 | |||
| aca4313fc3 | |||
| 4cb5bb06fb | |||
| 24988b75ea | |||
| 775e858d05 | |||
| 1e3d871c1e | |||
| 94a4310c1e | |||
| e8ee57be83 | |||
| 50a132cb2c | |||
| 59be32a54f | |||
| 4ae7cdb5d1 | |||
| 990a4efeb5 | |||
| 9c927dcf13 | |||
| 7a08b3f109 | |||
| 94d4234cb8 | |||
| fbc851bc4e | |||
| e92b661a2a | |||
| b9fe60915f | |||
| 1bf7d092f0 | |||
| 826d472511 | |||
| a6b1f4bd87 | |||
| 004f91f02c | |||
| e6c6ec4293 | |||
| a1d3acf065 | |||
| a732660f59 | |||
| e1bb0638f8 | |||
| 788c0c7e7e | |||
| 762fe16a17 | |||
| 15123805d1 | |||
| 23aecb4f2b | |||
| f1d200b81b | |||
| b566711ee1 | |||
| 22da5aac11 | |||
| f7b95102c5 | |||
| ca70c934d6 | |||
| 1cc48fa314 | |||
| b63b5574d3 | |||
| 67106e8698 | |||
| dc2ab51a88 | |||
| cedfa186f2 | |||
| 80f0d1bfed | |||
| 7178ab1f82 | |||
| bf895b707e | |||
| 996170a4ad | |||
| aa66a4f928 | |||
| 23010fa54a | |||
| 0baebc7979 | |||
| 257158321c | |||
| ddf0a41e29 | |||
| 357e1e8e96 | |||
| 7a89f9869a | |||
| 30fbd22efe | |||
| 29df73ba72 | |||
| f8676483c1 | |||
| c79a6f2fba | |||
| c38e67ea61 | |||
| 97a5f8805e | |||
| 421fef6ebf | |||
| 33406c3082 | |||
| c3851889b0 | |||
| 4a5de2da6b | |||
| 1afb23cada | |||
| a3b0f3f868 | |||
| 0aa32916d6 | |||
| ebe845d46c | |||
| edee8d2fc5 | |||
| 5f3de0876f | |||
| 2ceab04d01 | |||
| de37de44e8 | |||
| ccb87d5a13 | |||
| e2e0ca3f05 | |||
| b33a79704b | |||
| 6def174b84 | |||
| 346a18b444 | |||
| b32685c698 | |||
| 09b3618da8 | |||
| 901328f247 | |||
| b47ebaa9d9 | |||
| 465e8d6c0a | |||
| ecd86ec877 | |||
| 5d7864154c | |||
| 7b915428f7 | |||
| 0bce3517fd | |||
| 089815a8fd | |||
| 7e8f1a9632 | |||
| 8469648241 | |||
| e7a44358a5 | |||
| fb930d889f | |||
| 721a75309b | |||
| b856bff91b | |||
| 0744f000f9 | |||
| d1e226dba5 | |||
| 1923d28d71 | |||
| 484a306adb | |||
| 5bfc9f2f56 | |||
| 86a65e5287 | |||
| 956d301dd7 | |||
| 00d1a7b5a6 | |||
| 2718c9c017 | |||
| 3234c1e03f | |||
| c8f8eece92 | |||
| 7394efa9dc | |||
| 3e4241fc88 | |||
| a5ae79604c | |||
| fc4bdd68a1 | |||
| f83975b022 | |||
| 167d3957bf | |||
| a9571fd67d | |||
| ac87f48ba5 | |||
| 05403f527d | |||
| 53c72f0806 | |||
| c8c2a61f96 | |||
| 08d780d1f6 | |||
| 2e74fafa36 | |||
| 28ac9ab88a | |||
| 26acab4dfb | |||
| d4b562e001 | |||
| 61956e0646 | |||
| 7892259fe6 | |||
| b97d1c7796 | |||
| a091c5b331 | |||
| 784b3379b5 | |||
| 62446e75d2 | |||
| 8aa2f3075c | |||
| 839ee0385a | |||
| d6b60c6116 | |||
| 9013f02850 | |||
| 961d43f085 | |||
| 7bd49606e3 | |||
| d7c8635350 | |||
| 19e9ff81d2 | |||
| e137bf632a | |||
| 2fef898a1e | |||
| 0945807184 | |||
| d85663b153 | |||
| 481a7d790c | |||
| ef589a6b13 | |||
| d5c4af3377 | |||
| 0b4df4cdf2 | |||
| d6814dc48e | |||
| ce50a1d50f | |||
| a76e018d36 | |||
| b06f1fd677 | |||
| 3d8b1880dc | |||
| 1a13c8c9bb | |||
| 852fae3a09 | |||
| d342d31c8e | |||
| 5b3f54fbc8 | |||
| 308ece784c | |||
| 0758ddf04e | |||
| f24d9aef73 | |||
| f8bb55f258 | |||
| 76daebeb84 | |||
| b0d366b7e8 | |||
| 3d0054dd64 | |||
| 13bc89ff44 | |||
| bb910f984e | |||
| e576fdea14 | |||
| c7864927ec | |||
| 54956de59f | |||
| 2cbd30d1f2 | |||
| 55614f1675 | |||
| 4f7421993f | |||
| 32ad7b530e | |||
| ee5d5010f6 | |||
| cf7e5884cd | |||
| 11a0a64d9e | |||
| b6051babac | |||
| 34947b5e20 | |||
| c621380b66 | |||
| c4b54033ce | |||
| 4d85d6eb6e | |||
| cc9274c9af | |||
| bbdf7efc34 | |||
| 38a986995a | |||
| ee8d35df7a | |||
| be411cd5ff | |||
| 2104019171 | |||
| 4e564707df | |||
| da8611e0c3 | |||
| 5d11641a5f | |||
| 24d38e4fcf | |||
| efb65a8413 | |||
| 3fc90554f5 | |||
| 4ebe28faae | |||
| 693171cb05 | |||
| 42a2d090aa | |||
| c84eaa83dd | |||
| 74c3ff80cb | |||
| 5893d3ee64 | |||
| 7f435c56a0 | |||
| 59b7100cf6 | |||
| 0923d1aa24 | |||
| ffe6c36f2f | |||
| 8c4752c5d8 | |||
| d6202e522b | |||
| 50a24c62c5 | |||
| 6b17d08892 | |||
| 79c91fd8c5 | |||
| 95764258ab | |||
| 41e6872557 | |||
| 30c96c37c2 | |||
| 20f1cf3dff | |||
| 8de0d6186e | |||
| edd7d8aefe | |||
| a8da4903f4 | |||
| c8df5033f7 | |||
| 412f418da3 | |||
| 3782d21069 | |||
| 82a29f4470 | |||
| 4a65326b24 | |||
| d75d8729be | |||
| c87060454e | |||
| bbba52c7ff | |||
| 33418bdb33 | |||
| de47db2b66 | |||
| fa6fcffbe1 | |||
| 93d3f11891 | |||
| 59479f10fc | |||
| 11e72253d4 | |||
| 37fca9c88a | |||
| 6d67048e98 | |||
| c516a5c1bd | |||
| 429431c13c | |||
| b80dfd7b9a | |||
| 4c144b6ceb | |||
| 718e32773a | |||
| fb75d5d3ee | |||
| 99bbcbebfc | |||
| 289e4b5dc1 | |||
| a974dcdab6 | |||
| 9a48620152 | |||
| e444b3a402 | |||
| fabf4f0c0a | |||
| d467888d6a | |||
| eb46311561 | |||
| 63839bb0a7 | |||
| 793fada0fb | |||
| 494ba78156 | |||
| 9816b27ebf | |||
| 1c43b7891b | |||
| 9d7739213e | |||
| 2ab5f6a305 | |||
| c73103969f | |||
| 6cbbdf5d5b | |||
| 3a48435497 | |||
| 084925de8b | |||
| 5bac2fe695 | |||
| 2c29fb241f | |||
| 77dc11bcc2 | |||
| 5154622566 | |||
| c681cd286a | |||
| c1d53ec11f | |||
| 36b49c66c6 | |||
| 80b8e340da | |||
| b51470db39 | |||
| a5f4596f8d | |||
| 5f3c3760c0 | |||
| 8b05885d57 | |||
| d9d7ab4ee5 | |||
| 30bce5f9bb | |||
| fbe0282738 | |||
| 5bad08b9a0 | |||
| 310691bca2 | |||
| 8f7967acd0 | |||
| faf18476b1 | |||
| 9acccad23a | |||
| db1a0aedf8 | |||
| 01a26fda25 | |||
| 0229272b0a | |||
| c817acff09 | |||
| a5eec81819 | |||
| 98f70879bd | |||
| ceb4627f4e | |||
| 8d9d4fe23f | |||
| 730298110c | |||
| 8578f7d0d1 | |||
| 07a5daefb0 | |||
| 0ed524bce5 | |||
| 94199af7b2 | |||
| b8d7a00f58 | |||
| aca963befc | |||
| 3ea4e334bf | |||
| 9955fa8c29 | |||
| fb37a6bcab | |||
| 4f8bdbb518 | |||
| 73c190891d | |||
| 93e701f471 | |||
| 68b9bf7e38 | |||
| 826f90505c | |||
| 7ae36f16c8 | |||
| 281f6d5535 | |||
| b4072a06c4 | |||
| cb3ee423c9 | |||
| 1360a744a7 | |||
| f3058598dc | |||
| 94db0f6ec9 | |||
| f08732095d | |||
| f946650ddb | |||
| 14fd6bbdce | |||
| 5059e8df34 | |||
| 3d405a1ac5 | |||
| 78e00db018 | |||
| e62bc31e63 | |||
| 4763f1342c | |||
| 9413e53b00 | |||
| 35d352bbeb | |||
| 8dd5e0c250 | |||
| 139e487998 | |||
| 9bb0a3fbd3 | |||
| 9a1a6f2625 | |||
| e781d6d2b1 | |||
| 5b342b50ce | |||
| 869822d180 | |||
| e50422bce5 | |||
| 999882e723 | |||
| d68dc82159 | |||
| cc9fdd0032 | |||
| 503cc4a729 | |||
| d585269228 | |||
| 50f535b910 | |||
| 978bdd149e | |||
| a9d558e891 | |||
| 8291eb58df | |||
| 7298dec353 | |||
| 7574b01863 | |||
| 98e739a12b | |||
| 262ce03e46 | |||
| 1906691a35 | |||
| 4ca23b84a4 | |||
| b05eb8440b | |||
| f4de7748f5 | |||
| 93c26a839b | |||
| 878e7441f2 | |||
| 27d387e3e1 | |||
| 7b7e4f60bc | |||
| 8bac111acf | |||
| 7a46d3b497 | |||
| 357aa92e31 | |||
| fa17f16716 | |||
| ce99c8c706 | |||
| be828cf004 | |||
| 1057bb15d3 | |||
| ea5e8891a8 | |||
| 57d83f082a | |||
| 8a3f80ba82 | |||
| 374ebd6f7a | |||
| ab2ebefe2b | |||
| c34a9f6213 | |||
| f5df41c75f | |||
| 0c2dc06cb7 | |||
| 4552ccd7d7 | |||
| 525bcd8295 | |||
| ac2ad8d8b5 | |||
| 14ce495e9e | |||
| 4aed2cf652 | |||
| 244fb0ca28 | |||
| f6e123f1ed | |||
| e1e4e7f011 | |||
| 814b6ff489 | |||
| 3aa17ab1f4 | |||
| 4646ed8f4a | |||
| 3f5c8acb3c | |||
| 93b257314a | |||
| 19a1db8cb4 | |||
| c3275a4aa9 | |||
| 927185d95c | |||
| a3df6c6f0c | |||
| e1fa977b5b | |||
| ce95f316b9 | |||
| a162af5a75 | |||
| fb0ea4610d | |||
| 4f86a9978b | |||
| cc78b880ce | |||
| 4336fdb241 | |||
| 87db7cf1d4 | |||
| 09ff802c7e | |||
| 71b59b1950 | |||
| a3300ad9c6 | |||
| 8f40d4affa | |||
| 560f3fcccb | |||
| ecaadf63c5 | |||
| d490a6e0b4 | |||
| 53ced205b8 | |||
| 61c4983949 | |||
| fb862f466f | |||
| 8d51300386 | |||
| c4b413563d | |||
| 8f852971f9 | |||
| 3c54c89464 | |||
| d07aa13822 | |||
| c218f7d0e3 | |||
| 72ad872e90 | |||
| 854f244d22 | |||
| 9acbb223db | |||
| c59fc687db | |||
| 0791b917f1 | |||
| ba182ce8e4 | |||
| 663291c515 | |||
| 99253eecea | |||
| 916a02cb36 | |||
| 114c4abcd1 | |||
| e1e3d11932 | |||
| df46217c78 | |||
| d18cc92396 | |||
| 8f63b4b790 | |||
| ce7de93936 | |||
| ef2a24fe6f | |||
| 038cd0155b | |||
| fb430ac1ed | |||
| 53cfa3703b | |||
| 7ed9e8d145 | |||
| 93e0b0cbea | |||
| 0118bd3168 | |||
| 07d5245a5f | |||
| 6b844213ec | |||
| dabdeb8a80 | |||
| ecd56315e6 | |||
| 15b9e839e9 | |||
| d7f7dcd47b | |||
| b1981e09b8 | |||
| c9edd0a795 | |||
| d83e0716bb |
+3
-1
@@ -1,2 +1,4 @@
|
||||
docs/**/*
|
||||
test/node_modules/**/*
|
||||
_book/**/*
|
||||
node_modules/**/*
|
||||
test/**/*
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
{
|
||||
"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
|
||||
"browser": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "eslint:recommended"
|
||||
}
|
||||
"globals": {
|
||||
"expect": true
|
||||
},
|
||||
"extends": "gitbook"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
IMPORTANT NOTICE
|
||||
Only post things related to the gitbook CLI
|
||||
For GitBook.com, the Editor, or anything else
|
||||
please go to
|
||||
https://github.com/GitbookIO/feedback/issues
|
||||
|
||||
Please open separate issues for each bug report.
|
||||
-->
|
||||
|
||||
|
||||
### What is the current behavior?
|
||||
|
||||
<!--
|
||||
- For bugs, provide the steps to reproduce
|
||||
- Provide the GitBook version being used (`gitbook -V`)
|
||||
- Provide system informations (OS and version)
|
||||
- Provide your username and the URL of the book
|
||||
-->
|
||||
|
||||
|
||||
|
||||
### What is the expected behavior?
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
Thank you for giving us your feedback!
|
||||
-- The GitBook Team
|
||||
-->
|
||||
+5
-2
@@ -22,14 +22,17 @@ 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
|
||||
|
||||
# GitBook with babel
|
||||
/packages/gitbook-core/lib
|
||||
/packages/gitbook-plugin/lib
|
||||
|
||||
# Output of documentation
|
||||
_book
|
||||
|
||||
book.pdf
|
||||
book.epub
|
||||
book.mobi
|
||||
|
||||
+5
-1
@@ -1,10 +1,14 @@
|
||||
sudo: false
|
||||
language: node_js
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- "stable"
|
||||
- "4.1"
|
||||
- "0.12"
|
||||
before_install:
|
||||
- npm install svgexport -g
|
||||
before_script:
|
||||
- npm run bootstrap
|
||||
after_success:
|
||||
- npm run lint
|
||||
|
||||
+4
-3
@@ -7,9 +7,10 @@ Names below are ordered by first contribution.
|
||||
Author
|
||||
------
|
||||
|
||||
- Samy Pessé <samypesse@gmail.com> (@SamyPesse)
|
||||
- Aaron O'Mullan <aaron.omullan@gmail.com> (@AaronO)
|
||||
|
||||
- Samy Pessé <samy@gitbook.com> (@SamyPesse)
|
||||
- Aaron O'Mullan <aaron@gitbook.com> (@AaronO)
|
||||
- Johan Preynat <johan@gitbook.com> (@jpreynat)
|
||||
- Nicolas Gaborit <nicolas@gitbook.com> (@soreine)
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
+61
-2
@@ -2,7 +2,63 @@
|
||||
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)
|
||||
## 4.0.0
|
||||
- **Breaking Changes:** Most plugins and themes should be incompatible with this version
|
||||
- GitBook rendering is now done using React, which brings better APIs for plugins and a more interactive website.
|
||||
- *Performances*: Installation of plugins is much faster thanks to `ied`
|
||||
- *Performances*: Git conrefs are faster over large books
|
||||
- *Website Feature:* new default plugin `copy-code` to copy code blocks content in one click
|
||||
- *Website Feature:* new default plugin `heading-anchors` to have clickable headings
|
||||
- *Website Feature:* `fontsettings` is now only controlling the font size, and can work on multiple themes
|
||||
- *Plugin API*: hook `page` can no longer modify the content, only the page's attributes
|
||||
- *Plugin API*: plugins can no longer export resources, instead use the `_assets` folder
|
||||
- [ ] *Plugin API*: testing framework
|
||||
- [ ] `gitbook init` creates `.gitignore` if not found
|
||||
- [ ] Ignore files are no longer copied to the output
|
||||
|
||||
## 3.2.2
|
||||
- Fix catching parsing errors leading to possible missing pages in generated book
|
||||
- Rollback markdown parser to `kramed` until `markup-it` is stable enough
|
||||
|
||||
## 3.2.1
|
||||
- Fix bug on Firefox when navigating to an url containing an hash
|
||||
- Update nunjucks to fix JS error when page contains a lot of templating blocks
|
||||
|
||||
## 3.2.0
|
||||
- Switch markdown parser from `kramed` to `markup-it`
|
||||
- Fix support of `|` in tables
|
||||
- Fix access to `output.name` in templating
|
||||
- Fix i18n for website and PDF
|
||||
- Fix minor scrolling issues on website
|
||||
- Improve options of default search indexer (keywords, disabling per pages)
|
||||
- Improve pertinence of search results
|
||||
|
||||
## 3.1.1
|
||||
- Fix order of plugins during loading
|
||||
- Fix error when using math and conrefs
|
||||
- Fix target attribute for external links
|
||||
- Fix serve command
|
||||
|
||||
## 3.1.0
|
||||
- Always load themes after plugins
|
||||
- README/Introduction doesn't require to be the first entry in the summary
|
||||
- Highlight active entry in summary when scrolling a page with anchors
|
||||
- Support empty parts in summary
|
||||
|
||||
## 3.0.3
|
||||
- Fix redirection in `gitbook serve` when accessing a folder without trailing slash
|
||||
- Fix links/annotations for glossary not being correctly resolved
|
||||
- Fix regression for supporting uppercase filenames in structure files
|
||||
- Fix `gitbook install` when using Git URLs for plugins
|
||||
|
||||
## 3.0.2
|
||||
- Fix crash for anchor links
|
||||
|
||||
## 3.0.1
|
||||
- Fix regression in link resolution when contain anchor
|
||||
- Fix `structure.<name>` configuration not supporting filenames with dots
|
||||
|
||||
## 3.0.0
|
||||
- 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
|
||||
@@ -11,14 +67,17 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- `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
|
||||
- Root folder for the book can be specified in the `"root"` property of the `book.json` file
|
||||
- Multi-lingual books share assets folder
|
||||
- YAML front matter is parsed and can extend page's properties
|
||||
- Fix `uk` translation
|
||||
- Fix heading ID including dashes
|
||||
- Fix error in code highlighting for unknown languages
|
||||
- Fix data-uri images being handled as external images
|
||||
- Accept SSH url as plugin version
|
||||
- Add templating blocks `markdown`, `asciidoc` and `markup`
|
||||
- Better search experience
|
||||
- Better default theme, more responsive and cleaner
|
||||
|
||||
## 2.6.7
|
||||
- Fix bug with filenames including spaces
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
# Contributing
|
||||
|
||||
Want to contribute to GitBook? That would be awesome!
|
||||
|
||||
- [Send feedback](#send-feedback)
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [Asking Questions](#asking-questions)
|
||||
- [Submitting Pull Requests](#submitting-pull-requests)
|
||||
- [Running Tests](#running-tests)
|
||||
|
||||
## Send feedback
|
||||
|
||||
We’ve done our best to test but your feedback is really important. If you encounter problems or have feedback about GitBook products (GitBook.com, GitBook Editor, or the Toolchain), please log an issue in the [GitbookIO/feedback](https://github.com/GitbookIO/feedback/issues) repository.
|
||||
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
If you run into any weird behavior while using GitBook, feel free to open a new issue in this repository! To be most helpful, please include the steps to reproduce the bug as best you can, including the output of command `gitbook -V`.
|
||||
|
||||
## Asking Questions
|
||||
|
||||
Questions are very welcome :smile:! Previous questions that folks have asked are tagged with a [`question`](https://github.com/GitBookIO/gitbook/issues?q=is%3Aissue+is%3Aclosed+label%3Aquestion) label, so before opening a new issue double-check that someone hasn't asked it before. But if you don't see anything, or if you're not sure if it's the same, err on the side of asking!
|
||||
|
||||
We've also got a [GitBook Community Slack](https://slack.gitbook.com/) where you can ask questions and get answers from other people using GitBook.
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
All pull requests are super welcomed and greatly appreciated! Easy issues are marked with an [`easy-one`](https://github.com/GitBookIO/gitbook/issues?q=is%3Aopen+is%3Aissue+label%3Aeasy-one) label if you're looking for a simple place to get familiar with the code base.
|
||||
|
||||
Please include tests and docs with every pull request!
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run the examples, you need to have the GitBook repository cloned to your computer. After that, you need to `cd` into the directory where you cloned it, and install the dependencies from `npm` using `yarn`.
|
||||
|
||||
```
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
Then you'll need to bootstrap it:
|
||||
|
||||
```
|
||||
$ npm run bootstrap
|
||||
```
|
||||
|
||||
Which will also compile the source files. Then run the tests with:
|
||||
|
||||
```
|
||||
$ npm test
|
||||
```
|
||||
@@ -1,12 +1,22 @@
|
||||
GitBook
|
||||
=======
|
||||
[](https://www.gitbook.com)
|
||||
|
||||
[](http://badge.fury.io/js/gitbook)
|
||||
[](https://travis-ci.org/GitbookIO/gitbook)
|
||||
[](https://ci.appveyor.com/project/GitBook/gitbook)
|
||||
[](https://slack.gitbook.com)
|
||||
<p align="center">A <em>modern</em> customizable documentation format and toolchain</p>
|
||||
|
||||
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).
|
||||
|
||||
<p align="center"><a href="#features"><strong>Features</strong></a> · <a href="https://www.gitbook.com"><strong>Hosting</strong></a> · <a href="http://redux.js.org/"><strong>Example</strong></a> · <a href="https://www.gitbook.com/editor"><strong>Editor</strong></a> · <a href="https://toolchain.gitbook.com/"><strong>Documentation</strong></a> · <a href="./CONTRIBUTING.md"><strong>Contributing!</strong></a> · <a href="https://twitter.com/GitBookIO"><strong>Twitter</strong></a></p>
|
||||
<br/>
|
||||
|
||||

|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="https://badge.fury.io/js/gitbook.svg" />
|
||||
<a href="https://travis-ci.org/GitbookIO/gitbook"><img src="https://travis-ci.org/GitbookIO/gitbook.png?branch=master" /></a>
|
||||
<a href="https://ci.appveyor.com/project/GitBook/gitbook"><img src="https://ci.appveyor.com/api/projects/status/63nlflxcwmb2pue6?svg=true" /></a>
|
||||
<a href="https://ci.appveyor.com/project/GitBook/gitbook"><img src="https://slack.gitbook.com/badge.svg" /></a>
|
||||
</p>
|
||||
|
||||
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: [ReduxJS documentation](http://redux.js.org/).
|
||||
|
||||
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).
|
||||
|
||||
@@ -14,8 +24,6 @@ Check out the [GitBook Community Slack Channel](https://slack.gitbook.com), Stay
|
||||
|
||||
Complete documentation is available at [toolchain.gitbook.com](http://toolchain.gitbook.com/).
|
||||
|
||||

|
||||
|
||||
## Getting started
|
||||
|
||||
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).
|
||||
|
||||
+11
-2
@@ -5,13 +5,18 @@ init:
|
||||
# Test against these versions of Node.js.
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "0.12"
|
||||
- nodejs_version: "4.1"
|
||||
- nodejs_version: "6"
|
||||
- nodejs_version: "5"
|
||||
- nodejs_version: "4"
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
# 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
|
||||
- set CI=true
|
||||
# install svgexport
|
||||
- npm install svgexport -g
|
||||
# install modules
|
||||
@@ -27,3 +32,7 @@ test_script:
|
||||
|
||||
# Don't actually build.
|
||||
build: off
|
||||
|
||||
# Clone only last commit
|
||||
shallow_clone: true
|
||||
clone_depth: 1
|
||||
|
||||
@@ -5,12 +5,8 @@ module.exports = {
|
||||
root: './docs',
|
||||
title: 'GitBook Toolchain Documentation',
|
||||
|
||||
// Enforce use of GitBook v3
|
||||
gitbook: pkg.version,
|
||||
|
||||
// Use the "official" theme
|
||||
plugins: ['theme-official', 'sitemap'],
|
||||
theme: 'official',
|
||||
plugins: ['sitemap'],
|
||||
|
||||
variables: {
|
||||
version: pkg.version
|
||||
@@ -18,7 +14,7 @@ module.exports = {
|
||||
|
||||
pluginsConfig: {
|
||||
sitemap: {
|
||||
hostname: 'https://docs.gitbook.com'
|
||||
hostname: 'https://toolchain.gitbook.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+2
-3
@@ -4,7 +4,7 @@ This document aims to be a comprehensive guide to GitBook. It contains the full
|
||||
|
||||
### What is GitBook?
|
||||
|
||||
GitBook is a command line tool (and Node.js library) for building beautiful books using GitHub/Git and Markdown (or AsciiDoc). . This documentation has been generated using GitBook.
|
||||
GitBook is a command line tool (and Node.js library) for building beautiful books using GitHub/Git and Markdown (or AsciiDoc). This documentation has been generated using GitBook.
|
||||
|
||||
GitBook can output your content as a website ([customizable](themes/README.md) and [extensibles](plugins/README.md)) or as an ebook (PDF, ePub or Mobi).
|
||||
|
||||
@@ -18,9 +18,8 @@ Check out the [GitBook Community Slack Channel](https://slack.gitbook.com), Stay
|
||||
|
||||
### FAQ
|
||||
|
||||
Some questions are frequently asked. If you have a problem you should [check this out](faq.md) first.
|
||||
Some questions are frequently asked. If you have a problem you should [check this out](faq.md) first.
|
||||
|
||||
### Contribute to this documentation
|
||||
|
||||
You can contribute to improve this documentation on [GitHub](https://github.com/GitbookIO/gitbook) by signaling issues or proposing changes.
|
||||
|
||||
|
||||
+8
-2
@@ -15,6 +15,7 @@
|
||||
* [Markdown](syntax/markdown.md)
|
||||
* [Headings](syntax/markdown.md#headings)
|
||||
* [Paragraphs](syntax/markdown.md#paragraphs)
|
||||
* [Emphasis](syntax/markdown.md#emphasis)
|
||||
* [Lists](syntax/markdown.md#lists)
|
||||
* [Links](syntax/markdown.md#links)
|
||||
* [Images](syntax/markdown.md#images)
|
||||
@@ -41,10 +42,15 @@
|
||||
* [Test your plugin](plugins/testing.md)
|
||||
* [Theming](themes/README.md)
|
||||
|
||||
### Plugin Development
|
||||
|
||||
* [Introduction](./api/README.md)
|
||||
* [Node](./api/node.md)
|
||||
* [Connect to the context](./api/connect.md)
|
||||
* [Components](./api/components.md)
|
||||
|
||||
--
|
||||
|
||||
* [FAQ](faq.md)
|
||||
* [Examples](examples.md)
|
||||
* [Release notes](https://github.com/GitbookIO/gitbook/blob/master/CHANGES.md)
|
||||
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{% extends template.self %}
|
||||
|
||||
{% 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="{{ "faq.md"|resolveFile }}" class="btn btn-link pull-right hidden-xs">
|
||||
F.A.Q
|
||||
</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,24 @@
|
||||
# Plugin Architecture
|
||||
|
||||
A GitBook plugin is a NPM package that follows a defined convention.
|
||||
|
||||
`gitbook-plugin` is a command line utility to help you create, test and release plugins.
|
||||
|
||||
### Bootstrap your first plugin
|
||||
|
||||
Install `gitbook-plugin` from NPM:
|
||||
|
||||
```
|
||||
$ npm install gitbook-plugin -g
|
||||
```
|
||||
|
||||
Then create your plugin:
|
||||
|
||||
```
|
||||
$ gitbook-plugin create
|
||||
```
|
||||
|
||||
You will be asked for a plugin name, and a few other things to complete the creation process.
|
||||
|
||||
|
||||
### Publish your plugin
|
||||
@@ -0,0 +1,86 @@
|
||||
# Components
|
||||
|
||||
|
||||
## Injection
|
||||
|
||||
Plugins can inject components by registering React components to some roles.
|
||||
|
||||
#### Register a components
|
||||
|
||||
During the initialization phase of your plugin, dispatch the `GitBook.registerComponent` action:
|
||||
|
||||
```js
|
||||
dispatch(GitBook.registerComponent(MyCustomButton, { role: 'toolbar:buttons:left' }));
|
||||
```
|
||||
|
||||
#### Roles
|
||||
|
||||
Custom roles can be use for interopability with other plugins, but GitBook and the default theme set a convention for common roles:
|
||||
|
||||
| Role | Description | Props |
|
||||
| ---- | ----------- | ----- |
|
||||
| `page:container` | DIV container for the page's content | `{ page: Page }` |
|
||||
| `summary:container` | DIV container for the whole summary | `{ summary: Summary }` |
|
||||
| `summary:parts` | DIV container for summary's parts | `{ parts: List<SummaryPart> }` |
|
||||
| `summary:part` | DIV for a specific part | `{ part: SummaryPart }` |
|
||||
| `summary:articles` | UL container for a part's articles | `{ articles: List<SummaryArticle> }` |
|
||||
| `summary:article` | LI for a specific article | `{ article: SummaryArticle }` |
|
||||
|
||||
## Default Components
|
||||
|
||||
#### `GitBook.Head`
|
||||
|
||||
Extends the meta tags of the page. This is an alias for [react-helmet](https://github.com/nfl/react-helmet).
|
||||
|
||||
```js
|
||||
<GitBook.Head
|
||||
title="My page"
|
||||
/>
|
||||
```
|
||||
|
||||
#### `GitBook.ImportCSS`
|
||||
|
||||
Import a CSS file by resolving the path correctly according to the current page:
|
||||
|
||||
```js
|
||||
<GitBook.ImportCSS href="myfile.css" />
|
||||
```
|
||||
|
||||
#### `GitBook.ImportJS`
|
||||
|
||||
Import a JS file by resolving the path correctly according to the current page:
|
||||
|
||||
```js
|
||||
<GitBook.ImportJS src="mylib.js" />
|
||||
```
|
||||
|
||||
#### `GitBook.InjectedComponent`
|
||||
|
||||
Inject a component matching a specific role:
|
||||
|
||||
```js
|
||||
<GitBook.InjectedComponent matching={{ role: 'mycustomrole' }} props={{ someProp: 1 }}>
|
||||
<b>Inner content</b>
|
||||
</GitBook.InjectedComponent>
|
||||
```
|
||||
|
||||
#### `GitBook.InjectedComponentSet`
|
||||
|
||||
Same API as `InjectedComponentSet` but render the matching components in chain instead of composed:
|
||||
|
||||
```js
|
||||
<GitBook.InjectedComponentSet matching={{ role: 'mytoolbar' }} />
|
||||
```
|
||||
|
||||
**Warning:** Children are discarded.
|
||||
|
||||
#### `GitBook.FlexLayout` and `GitBook.FlexBox`
|
||||
|
||||
A simple wrapper that provides a Flexbox layout with the given direction and style. Any additional props you set on the Flexbox are rendered.
|
||||
|
||||
```js
|
||||
<GitBook.FlexLayout column>
|
||||
<GitBook.FlexBox>First column</GitBook.FlexBox>
|
||||
<GitBook.FlexBox>Second column</GitBook.FlexBox>
|
||||
</GitBook.FlexLayout>
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
# Connect to the context
|
||||
|
||||
`GitBook.connect(Component, [mapStateToProps], [mapActionsToProps])` connects a react component to the GitBook context.
|
||||
|
||||
It does not modify the component class passed to it.
|
||||
Instead, it returns a new, connected component class, for you to use.
|
||||
|
||||
### `mapStateToProps(state, [ownProps]): stateProps`
|
||||
|
||||
If specified, the component will subscribe to GitBook store updates. Any time it updates, `mapStateToProps` will be called. Its result must be a plain object, and it will be merged into the component’s props.
|
||||
|
||||
If you omit it, the component will not be subscribed to the GitBook store. If `ownProps` is specified as a second argument, its value will be the props passed to your component, and `mapStateToProps` will be additionally re-invoked whenever the component receives new props (e.g. if props received from a parent component have shallowly changed, and you use the `ownProps` argument, `mapStateToProps` is re-evaluated).
|
||||
|
||||
For example to render the title of the current page:
|
||||
|
||||
```js
|
||||
const GitBook = require('gitbook-core');
|
||||
|
||||
let PageTitle = React.createClass({
|
||||
render() {
|
||||
const { page } = this.props;
|
||||
return <h1>{page.title}</h1>;
|
||||
}
|
||||
});
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return { page: state.page };
|
||||
}
|
||||
|
||||
PageTitle = GitBook.connect(PageTitle, mapStateToProps);
|
||||
```
|
||||
|
||||
### `mapActionsToProps(actions, [dispatch])`
|
||||
@@ -0,0 +1,44 @@
|
||||
# Internationalize your plugin
|
||||
|
||||
GitBook has built-in support for internationalization. Plugins can register new languages and provide the right messages for different languages.
|
||||
|
||||
### Register locale and messages
|
||||
|
||||
The first step is to register messages for a language:
|
||||
|
||||
```js
|
||||
module.exports = GitBook.createPlugin({
|
||||
init: (dispatch, getState, { I18n }) => {
|
||||
dispatch(I18n.registerLocale('en-US', {
|
||||
MY_PLUGIN_MESSAGE: 'Hello World'
|
||||
}));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Render a message in a component
|
||||
|
||||
`GitBook.connect` adds a `i18n` prop to access the internationalization:
|
||||
|
||||
```js
|
||||
const GitBook = require('gitbook-core');
|
||||
const { React } = GitBook;
|
||||
|
||||
const MyButton = React.createClass({
|
||||
propTypes: {
|
||||
i18n: GitBook.Shapes.i18n
|
||||
},
|
||||
|
||||
render() {
|
||||
const { i18n } = this.props;
|
||||
|
||||
return (
|
||||
<GitBook.Button>
|
||||
{i18.t('MY_PLUGIN_MESSAGE')}
|
||||
</GitBook.Button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = GitBook.connect(MyButton);
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
# Navigation
|
||||
|
||||
### Listen to url change
|
||||
|
||||
Listen for changes to the current location:
|
||||
|
||||
```js
|
||||
const onLocationChanged = (location) => {
|
||||
console.log(location.pathname);
|
||||
console.log(location.query);
|
||||
console.log(location.hash);
|
||||
};
|
||||
|
||||
module.exports = GitBook.createPlugin({
|
||||
init: (dispatch, getState, { Navigation }) => {
|
||||
dispatch(Navigation.listen(onLocationChanged));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The `onLocationChanged` will be triggered for initial state.
|
||||
|
||||
### Changing the url
|
||||
@@ -0,0 +1,97 @@
|
||||
# Node APIs
|
||||
|
||||
GitBooks provides different Node APIs and contexts to plugins. These APIs can vary according to the GitBook version being used, your plugin should specify the `engines.gitbook` field in `package.json` accordingly.
|
||||
|
||||
#### Book instance
|
||||
|
||||
The `Book` interface is the central point of GitBook, it centralize all access read methods.
|
||||
|
||||
```js
|
||||
// Read configuration from book.json
|
||||
var value = book.config.get('title', 'Default Value');
|
||||
|
||||
// Resolve a filename to an absolute path
|
||||
var filepath = book.resolve('README.md');
|
||||
|
||||
// Render an inline markup string
|
||||
book.renderInline('markdown', 'This is **Markdown**')
|
||||
.then(function(str) { ... })
|
||||
|
||||
// Render a markup string (block mode)
|
||||
book.renderBlock('markdown', '* This is **Markdown**')
|
||||
.then(function(str) { ... })
|
||||
```
|
||||
|
||||
#### Output instance
|
||||
|
||||
The `Output` class represent the output/write process.
|
||||
|
||||
```js
|
||||
// Return root folder for the output
|
||||
var root = output.root();
|
||||
|
||||
// Resolve a file in the output folder
|
||||
var filepath = output.resolve('myimage.png');
|
||||
|
||||
// Convert a filename to an URL (returns a path to an html file)
|
||||
var fileurl = output.toURL('mychapter/README.md');
|
||||
|
||||
// Write a file in the output folder
|
||||
output.writeFile('hello.txt', 'Hello World')
|
||||
.then(function() { ... });
|
||||
|
||||
// Copy a file to the output folder
|
||||
output.copyFile('./myfile.jpg', 'cover.jpg')
|
||||
.then(function() { ... });
|
||||
|
||||
// Verify that a file exists
|
||||
output.hasFile('hello.txt')
|
||||
.then(function(exists) { ... });
|
||||
```
|
||||
|
||||
#### Page instance
|
||||
|
||||
A page instance represent the current parsed page.
|
||||
|
||||
```js
|
||||
// Title of the page (from SUMMARY)
|
||||
page.title
|
||||
|
||||
// Content of the page (Markdown/Asciidoc/HTML according to the stage)
|
||||
page.content
|
||||
|
||||
// Relative path in the book
|
||||
page.path
|
||||
|
||||
// Absolute path to the file
|
||||
page.rawPath
|
||||
|
||||
// Type of parser used for this file
|
||||
page.type ('markdown' or 'asciidoc')
|
||||
```
|
||||
|
||||
#### Context for Blocks and Filters
|
||||
|
||||
Blocks and filters have access to the same context, this context is bind to the template engine execution:
|
||||
|
||||
```js
|
||||
{
|
||||
// Current templating syntax
|
||||
"ctx": {
|
||||
// For example, after a {% set message = "hello" %}
|
||||
"message": "hello"
|
||||
},
|
||||
|
||||
// Book instance
|
||||
"book" <Book>,
|
||||
|
||||
// Output instance
|
||||
"output": <Output>
|
||||
}
|
||||
```
|
||||
|
||||
For example a filter or block function can access the current book using: `this.book`.
|
||||
|
||||
#### Context for Hooks
|
||||
|
||||
Hooks only have access to the `<Book>` instance using `this.book`.
|
||||
+10
-3
@@ -7,6 +7,7 @@ GitBook allows you to customize your book using a flexible configuration. These
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `root` | Path to the root folder containing all the book's files, except `book.json`|
|
||||
| `structure` | To specify paths for Readme, Summary, Glossary etc. See [Structure paragraph](#structure). |
|
||||
| `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. |
|
||||
@@ -19,18 +20,24 @@ GitBook allows you to customize your book using a flexible configuration. These
|
||||
|
||||
Plugins and their configurations are specified in the `book.json`. See [the plugins section](plugins/README.md) for more details.
|
||||
|
||||
Since version 3.0.0, GitBook can use themes. See [the theming section](themes/README.md) for more details.
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `plugins` | List of plugins to load |
|
||||
| `pluginsConfig` |Configuration for plugins |
|
||||
|
||||
### Theme
|
||||
### Structure
|
||||
|
||||
Since version 3.0.0, GitBook can use themes. See [the theming section](themes/README.md) for more details.
|
||||
In addition to the `root` variable, you can tell Gitbook the name of the files for Readme, Summary, Glossary, Languages (instead of using the default names such as `README.md`).
|
||||
These files must be at the root of your book (or the root of every language book). Paths such as `dir/MY_README.md` are not accepted.
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `theme` | The theme to use for the book |
|
||||
| `structure.readme` | Readme file name (defaults to `README.md`) |
|
||||
| `structure.summary` | Summary file name (defaults to `SUMMARY.md`) |
|
||||
| `structure.glossary` | Glossary file name (defaults to `GLOSSARY.md`) |
|
||||
| `structure.languages` | Languages file name (defaults to `LANGS.md`) |
|
||||
|
||||
### PDF Options
|
||||
|
||||
|
||||
+29
-3
@@ -2,10 +2,35 @@
|
||||
|
||||
GitBook can generates a website, but can also output content as ebook (ePub, Mobi, PDF).
|
||||
|
||||
```
|
||||
# Generate a PDF file
|
||||
$ gitbook pdf ./ ./mybook.pdf
|
||||
|
||||
# Generate an ePub file
|
||||
$ gitbook epub ./ ./mybook.epub
|
||||
|
||||
# Generate a Mobi file
|
||||
$ gitbook mobi ./ ./mybook.mobi
|
||||
```
|
||||
|
||||
### Installing ebook-convert
|
||||
|
||||
`ebook-convert` is required to generate ebooks (epub, mobi, pdf).
|
||||
|
||||
##### GNU/Linux
|
||||
|
||||
Install the [Calibre application](https://calibre-ebook.com/download).
|
||||
|
||||
```
|
||||
$ sudo aptitude install calibre
|
||||
```
|
||||
|
||||
In some GNU/Linux distributions node is installed as nodejs, you need to manually create a symlink:
|
||||
|
||||
```
|
||||
$sudo ln -s /usr/bin/nodejs /usr/bin/node
|
||||
```
|
||||
|
||||
##### OS X
|
||||
|
||||
Download the [Calibre application](https://calibre-ebook.com/download). After moving the `calibre.app` to your Applications folder create a symbolic link to the ebook-convert tool:
|
||||
@@ -18,12 +43,13 @@ You can replace `/usr/bin` with any directory that is in your $PATH.
|
||||
|
||||
### Cover
|
||||
|
||||
Covers are used for all the ebook formats. It's an important part of an ebook brandline.
|
||||
Covers are used for all the ebook formats. You can either provide one yourself, or generate one using the [autocover plugin](https://plugins.gitbook.com/plugin/autocover).
|
||||
|
||||
To provide a cover, place a **`cover.jpg`** file at the root directory of your book. Adding a **`cover_small.jpg`** will specify a smaller version of the cover. The cover should be a **JPEG** file.
|
||||
|
||||
A good cover should respect the following guidelines:
|
||||
|
||||
* Size of 1800x2360 (pixels)
|
||||
* Size of 1800x2360 pixels for `cover.jpg`, 200x262 for `cover_small.jpg`
|
||||
* No border
|
||||
* Clearly visible book title
|
||||
* Any important text should be visible in the small version
|
||||
|
||||
|
||||
+2
-1
@@ -23,5 +23,6 @@ More than 50,000 books have been published on [GitBook.com](https://www.gitbook.
|
||||
### Documentation
|
||||
|
||||
- [DuckDuckHack Documentation](http://docs.duckduckhack.com) by [DuckDuckGo](https://duckduckgo.com/about)
|
||||
- [Loomio Handbook](http://loomio.coop/) and [guide to using Loomio](https://loomio.gitbooks.io/manual/content/en/index.html) both by [Loomio](https://www.loomio.org/)
|
||||
- [Enspiral Handbook](http://handbook.enspiral.com/) by [Enspiral](http://enspiral.com/)
|
||||
- This documentation
|
||||
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ Any text editor should work! But we advise using the [GitBook Editor](https://ww
|
||||
|
||||
The GitBook format supports right to left, and bi-directional writing. To enable it, you either need to specify a language (ex: `ar`), or force GitBook to use RTL in your `book.json`:
|
||||
|
||||
``` json
|
||||
```json
|
||||
{
|
||||
"language": "ar",
|
||||
"direction": "rtl"
|
||||
@@ -34,7 +34,7 @@ You should always use paths and the `.md` extensions when linking to your files,
|
||||
|
||||
#### 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).
|
||||
Yes, GitBooks can be created in [sub-directories](structure.md#subdirectory). GitBook.com and the CLI also looks by default in a series of [folders](structure.md).
|
||||
|
||||
#### Does GitBook supports RTL languages?
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 227 KiB |
+3
-3
@@ -2,13 +2,13 @@
|
||||
|
||||
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 :
|
||||
The `GLOSSARY.md` format is a list of `h2` headings, along with a description paragraph:
|
||||
|
||||
```markdown
|
||||
# Term
|
||||
## Term
|
||||
Definition for this term
|
||||
|
||||
# Another term
|
||||
## Another term
|
||||
With it's definition, this can contain bold text
|
||||
and all other kinds of inline markup ...
|
||||
```
|
||||
|
||||
+2
-3
@@ -25,7 +25,7 @@ Each chapter has a dedicated page (`part#/README.md`) and is split into subchapt
|
||||
|
||||
##### Anchors
|
||||
|
||||
Chaptersi n the Table of Contents can be pointing to specific part of a file using anchor.
|
||||
Chapters in the Table of Contents can be pointing to specific part of a file using anchor.
|
||||
|
||||
```markdown
|
||||
# Summary
|
||||
@@ -40,7 +40,6 @@ Chaptersi n the Table of Contents can be pointing to specific part of a file usi
|
||||
* [Better tools for authors](part2/README.md#tools)
|
||||
```
|
||||
|
||||
|
||||
##### Parts
|
||||
|
||||
The Table of Contents can be divided into parts separated by headings or horizontal lines:
|
||||
@@ -73,7 +72,7 @@ Most of the files for GitBook use the Markdown syntax by default. GitBook infers
|
||||
|
||||
##### Example of a chapter file
|
||||
|
||||
``` markdown
|
||||
```markdown
|
||||
# Title of the chapter
|
||||
|
||||
This is a great introduction.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
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 Analytics, etc.
|
||||
|
||||
### How to find plugins?
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ GitBooks provides different APIs and contexts to plugins. These APIs can vary ac
|
||||
|
||||
#### Book instance
|
||||
|
||||
The `Book` class is the central point of GitBook, it centralize all access read methods. This class is defined in [book.js](https://github.com/GitbookIO/gitbook/blob/master/lib/book.js).
|
||||
The `Book` class is the central point of GitBook, it centralize all access read methods. This class is defined in [book.js](https://github.com/GitbookIO/gitbook/blob/master/book.js).
|
||||
|
||||
```js
|
||||
// Read configuration from book.json
|
||||
@@ -37,7 +37,7 @@ var filepath = output.resolve('myimage.png');
|
||||
var fileurl = output.toURL('mychapter/README.md');
|
||||
|
||||
// Write a file in the output folder
|
||||
output.write('hello.txt', 'Hello World')
|
||||
output.writeFile('hello.txt', 'Hello World')
|
||||
.then(function() { ... });
|
||||
|
||||
// Copy a file to the output folder
|
||||
|
||||
@@ -54,4 +54,4 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
```
|
||||
|
||||
@@ -71,7 +71,6 @@ In the `page` hook, `page.content` is the HTML generated from the markdown/ascii
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Asynchronous Operations
|
||||
|
||||
Hooks callbacks can be asynchronous and return promises.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
### Testing your plugin locally
|
||||
|
||||
Testing your plugin on your book before plushing it is possible using [npm link](https://docs.npmjs.com/cli/link).
|
||||
Testing your plugin on your book before publishing it is possible using [npm link](https://docs.npmjs.com/cli/link).
|
||||
|
||||
In the plugin's folder, run:
|
||||
|
||||
@@ -10,7 +10,7 @@ In the plugin's folder, run:
|
||||
$ npm link
|
||||
```
|
||||
|
||||
The nin your book's folder:
|
||||
Then in your book's folder:
|
||||
|
||||
```
|
||||
$ npm link gitbook-plugin-<plugin's name>
|
||||
@@ -19,4 +19,3 @@ $ npm link gitbook-plugin-<plugin's name>
|
||||
### Unit testing on Travis
|
||||
|
||||
[gitbook-tester](https://github.com/todvora/gitbook-tester) makes it easy to write **Node.js/Mocha** unit tests for your plugins. Using [Travis.org](https://travis.org), tests can be run on each commits/tags.
|
||||
|
||||
|
||||
+1
-2
@@ -14,7 +14,7 @@ It integrates well with the [GitBook Editor](https://www.gitbook.com/editor).
|
||||
|
||||
Installing GitBook is easy and straightforward. Your system just needs to meet these two requirements:
|
||||
|
||||
* NodeJS (v4.0.0 and above are adviced)
|
||||
* NodeJS (v4.0.0 and above is recommended)
|
||||
* Windows, Linux, Unix, or Mac OS X
|
||||
|
||||
##### Install with NPM
|
||||
@@ -66,4 +66,3 @@ You can use the options `--log=debug` and `--debug` to get better error messages
|
||||
```
|
||||
$ gitbook build ./ --log=debug --debug
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/* Reduce size of logo to allow long versions like 3.0.0-pre.1 */
|
||||
.gb-page-header .container .logo h1 {
|
||||
font-size: 24px ;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ This is the main entry of your book: the introduction. This file is **required**
|
||||
|
||||
### SUMMARY.adoc
|
||||
|
||||
This file defines the list of chapters and subchapters. Just like in Markdown, 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.
|
||||
This file defines the list of chapters and subchapters. Just like in Markdown, 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.
|
||||
|
||||
@@ -60,6 +60,5 @@ observer producing a sense of wonder.
|
||||
A popular web programming language, used by many large websites such
|
||||
as Facebook. Rasmus Lerdorf originally created PHP in 1994 to power
|
||||
his personal homepage (PHP originally stood for "Personal Home Page"
|
||||
but now stands for "PHP: Hypertext Preprocessor"). ```
|
||||
|
||||
|
||||
but now stands for "PHP: Hypertext Preprocessor").
|
||||
```
|
||||
|
||||
@@ -21,7 +21,7 @@ This looks up username from the context and displays it. Variable names can have
|
||||
|
||||
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 [predefined variables](variables.md) from the context.
|
||||
GitBook provides a set of [predefined variables](variables.md) from the context.
|
||||
|
||||
### Filters
|
||||
|
||||
@@ -92,7 +92,7 @@ Inclusion and inheritance is detailled in the [Content References](conrefs.md) s
|
||||
|
||||
If you want GitBook to ignore any of the special templating tags, you can use raw and anything inside of it will be output as plain text.
|
||||
|
||||
``` twig
|
||||
```twig
|
||||
{% raw %}
|
||||
this will {{ not be processed }}
|
||||
{% endraw %}
|
||||
|
||||
@@ -4,7 +4,8 @@ GitBook provides a serie of builtin filters and blocks to help you write templat
|
||||
|
||||
### Filters
|
||||
|
||||
`value|default(default, [boolean])`If value is strictly undefined, return default, otherwise value. If boolean is true, any JavaScript falsy value will return default (false, "", etc)
|
||||
`value|default(default, [boolean])`
|
||||
If value is strictly undefined, return default, otherwise value. If boolean is true, any JavaScript falsy value will return default (false, "", etc)
|
||||
|
||||
`arr|sort(reverse, caseSens, attr)`
|
||||
Sort arr with JavaScript's arr.sort function. If reverse is true, result will be reversed. Sort is case-insensitive by default, but setting caseSens to true makes it case-sensitive. If attr is passed, will compare attr from each item.
|
||||
|
||||
@@ -51,5 +51,5 @@ This is the default content
|
||||
|
||||
# License
|
||||
|
||||
{% import "./LICENSE" %}
|
||||
{% include "./LICENSE" %}
|
||||
```
|
||||
|
||||
@@ -10,6 +10,8 @@ The following is a reference of the available data during book's parsing and the
|
||||
| `gitbook` | GitBook specific information |
|
||||
| `page` | Current page specific information |
|
||||
| `file` | File associated with the current page specific information |
|
||||
| `readme` | Information about the Readme |
|
||||
| `glossary` | Information about the Glossary |
|
||||
| `summary` | Information about the table of contents |
|
||||
| `languages` | List of languages for multi-lingual books |
|
||||
| `output` | Information about the output generator |
|
||||
@@ -19,14 +21,26 @@ The following is a reference of the available data during book's parsing and the
|
||||
|
||||
| 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 |
|
||||
| `book.[value]` | All other values under `variables` in the `book.json` are accessible here |
|
||||
|
||||
For example, with this `book.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
variables: {
|
||||
hello: "everyone"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
... the following text `{{ book.hello }}` will expand to `everyone`.
|
||||
|
||||
### GitBook Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `gitbook.time` | The current time (when you run the `gitbook` command) . |
|
||||
| `gitbook.time` | The current time (when you run the `gitbook` command). |
|
||||
| `gitbook.version` | Version of GitBook used to generate the book |
|
||||
|
||||
### File Variables
|
||||
@@ -71,3 +85,14 @@ Languages are defined by `{ id: 'en', title: 'English' }`.
|
||||
| `output.name` | Name of the output generator, possible values are `website`, `json`, `ebook` |
|
||||
| `output.format` | When `output.name == "ebook"`, `format` defines the ebook format that will be generated, possible values are `pdf`, `epub` or `mobi` |
|
||||
|
||||
### Readme Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `readme.path` | Path to the Readme in the book |
|
||||
|
||||
### Glossary Variables
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ----------- |
|
||||
| `glossary.path` | Path to the Glossary in the book |
|
||||
|
||||
Vendored
+17
-2
@@ -12,12 +12,27 @@ A theme is a plugin containing templates and assets. Overriding any individual t
|
||||
| -------- | ----------- |
|
||||
| `_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) |
|
||||
| `_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 their book's source (without creating an external theme). Templates will be resolved in the `_layouts` folder of the book first, then in
|
||||
Authors can extend the templates of a theme directly from their book's source (without creating an external theme). Templates will be resolved in the `_layouts` folder of the book first, then in the installed plugins/themes.
|
||||
|
||||
### Extend instead of Forking
|
||||
|
||||
When you want to make your theme changes available to multiple books, instead of forking the default theme, you can extend it using the [templating syntax](../templating/README.md):
|
||||
|
||||
```html
|
||||
{% extends template.self %}
|
||||
|
||||
{% block body %}
|
||||
{{ super() }}
|
||||
... This will be added to the "body" block
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
Take a look at the [API](https://github.com/GitbookIO/theme-api) theme for a more complete example.
|
||||
|
||||
### Publish a theme
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"lerna": "2.0.0-beta.31",
|
||||
"version": "4.0.0-alpha.4"
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
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;
|
||||
@@ -1,99 +0,0 @@
|
||||
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;
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
Readme: require('./readme'),
|
||||
Summary: require('./summary'),
|
||||
Glossary: require('./glossary'),
|
||||
Langs: require('./langs')
|
||||
};
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
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;
|
||||
@@ -1,26 +0,0 @@
|
||||
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;
|
||||
@@ -1,348 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var util = require('util');
|
||||
|
||||
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 && !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;
|
||||
-405
@@ -1,405 +0,0 @@
|
||||
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');
|
||||
|
||||
// options.generator
|
||||
cfg.generator = this.output? this.output.name : null;
|
||||
|
||||
// options.output
|
||||
cfg.output = this.output? this.output.root() : null;
|
||||
|
||||
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() {
|
||||
var that = this;
|
||||
|
||||
return this.config.load()
|
||||
.then(function() {
|
||||
var rootFolder = that.config.get('root');
|
||||
if (!rootFolder) return;
|
||||
|
||||
that.originalRoot = that.root;
|
||||
that.root = path.resolve(that.root, rootFolder);
|
||||
});
|
||||
};
|
||||
|
||||
// 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.getByExt(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)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// ----- Parser Methods
|
||||
|
||||
// Render a markup string in inline mode
|
||||
Book.prototype.renderInline = function(type, src) {
|
||||
var parser = parsers.get(type);
|
||||
return parser.inline(src)
|
||||
.get('content');
|
||||
};
|
||||
|
||||
// Render a markup string in block mode
|
||||
Book.prototype.renderBlock = function(type, src) {
|
||||
var parser = parsers.get(type);
|
||||
return parser.page(src)
|
||||
.get('content');
|
||||
};
|
||||
|
||||
|
||||
// ----- DEPRECATED METHODS
|
||||
|
||||
Book.prototype.contentLink = error.deprecateMethod(function(s) {
|
||||
return this.output.toURL(s);
|
||||
}, '.contentLink() is deprecated, use ".output.toURL()" instead');
|
||||
|
||||
Book.prototype.contentPath = error.deprecateMethod(function(s) {
|
||||
return this.output.toURL(s);
|
||||
}, '.contentPath() is deprecated, use ".output.toURL()" instead');
|
||||
|
||||
Book.prototype.isSubBook = error.deprecateMethod(function() {
|
||||
return this.isLanguageBook();
|
||||
}, '.isSubBook() is deprecated, use ".isLanguageBook()" instead');
|
||||
|
||||
|
||||
// Initialize a book
|
||||
Book.init = function(fs, root, opts) {
|
||||
var book = new Book(_.extend(opts || {}, {
|
||||
root: root,
|
||||
fs: fs
|
||||
}));
|
||||
|
||||
return initBook(book);
|
||||
};
|
||||
|
||||
|
||||
module.exports = Book;
|
||||
@@ -1,140 +0,0 @@
|
||||
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());
|
||||
var book = new Book({
|
||||
fs: nodeFS,
|
||||
root: input,
|
||||
logLevel: kwargs.log
|
||||
});
|
||||
|
||||
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[0] || ('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
|
||||
};
|
||||
@@ -1,199 +0,0 @@
|
||||
/* 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 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() {
|
||||
var book = new Book({
|
||||
fs: helper.nodeFS,
|
||||
root: input,
|
||||
logLevel: kwargs.log
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
};
|
||||
@@ -1,96 +0,0 @@
|
||||
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;
|
||||
@@ -1,42 +0,0 @@
|
||||
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;
|
||||
@@ -1,132 +0,0 @@
|
||||
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;
|
||||
@@ -1,67 +0,0 @@
|
||||
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.slice(1).join('@');
|
||||
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
|
||||
};
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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
@@ -1,106 +0,0 @@
|
||||
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;
|
||||
@@ -1,66 +0,0 @@
|
||||
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,33 +0,0 @@
|
||||
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
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
var Book = require('./book');
|
||||
var cli = require('./cli');
|
||||
|
||||
module.exports = {
|
||||
Book: Book,
|
||||
commands: cli.commands
|
||||
};
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
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;
|
||||
@@ -1,140 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,301 +0,0 @@
|
||||
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 error = require('../utils/error');
|
||||
var PluginsManager = require('../plugins');
|
||||
var TemplateEngine = require('../template');
|
||||
var gitbook = require('../gitbook');
|
||||
|
||||
/*
|
||||
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();
|
||||
|
||||
// Hack to inherits from rules of the book:
|
||||
// Waiting for https://github.com/kaelzhang/node-ignore/pull/19 to be merged
|
||||
this.ignore._rules = this.ignore._rules.concat(this.book.ignore._rules);
|
||||
this.ignore._cache = {};
|
||||
}
|
||||
|
||||
// Default name for generator
|
||||
Output.prototype.name = 'base';
|
||||
|
||||
// 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',
|
||||
'_layouts',
|
||||
|
||||
// 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.toURL(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 conetxt for the output itself
|
||||
Output.prototype.getSelfContext = function() {
|
||||
return {
|
||||
name: this.name
|
||||
};
|
||||
};
|
||||
|
||||
// Return a default context for templates
|
||||
Output.prototype.getContext = function() {
|
||||
var ctx = _.extend(
|
||||
{
|
||||
output: this.getSelfContext()
|
||||
},
|
||||
this.book.getContext(),
|
||||
(this.book.isLanguageBook()? this.book.parent: this.book).langs.getContext(),
|
||||
this.book.summary.getContext(),
|
||||
this.book.glossary.getContext(),
|
||||
this.book.config.getContext(),
|
||||
gitbook.getContext()
|
||||
);
|
||||
|
||||
// Deprecated fields
|
||||
error.deprecateField(ctx.gitbook, 'generator', this.name, '"gitbook.generator" property is deprecated, use "output.name" instead');
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
// 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.toURL = 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;
|
||||
@@ -1,67 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,199 +0,0 @@
|
||||
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';
|
||||
|
||||
// Return context for templating
|
||||
// Incldue type of ebbook generated
|
||||
EbookOutput.prototype.getSelfContext = function() {
|
||||
var ctx = EbookOutput.super_.prototype.getSelfContext.apply(this);
|
||||
ctx.format = this.opts.format;
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
// 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;
|
||||
@@ -1,152 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
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.getOutputContext(that);
|
||||
|
||||
// Delete some private properties
|
||||
delete json.config;
|
||||
|
||||
// Specify JSON output version
|
||||
json.version = '3';
|
||||
|
||||
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;
|
||||
@@ -1,233 +0,0 @@
|
||||
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 FSLoader = require('../template/fs-loader');
|
||||
var conrefsLoader = require('./conrefs');
|
||||
var Output = require('./base');
|
||||
|
||||
|
||||
// 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() {
|
||||
// This list is ordered to give priority to templates in the book
|
||||
var searchPaths = _.pluck(that.plugins.list(), 'root');
|
||||
|
||||
// The book itself can contains a "_layouts" folder
|
||||
searchPaths.unshift(that.book.root);
|
||||
|
||||
// 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 FSLoader(_.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 that.toURL(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;
|
||||
|
||||
// Assets from the book are already copied
|
||||
// Copy assets from plugins (start with default plugins)
|
||||
return Promise.serie(that.plugins.list().reverse(), function(plugin) {
|
||||
// Copy assets only if exists (don't fail otherwise)
|
||||
var assetFolder = path.join(plugin.root, '_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,
|
||||
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.getOutputContext(that));
|
||||
})
|
||||
|
||||
// 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: {
|
||||
self: 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;
|
||||
@@ -1,280 +0,0 @@
|
||||
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;
|
||||
@@ -1,247 +0,0 @@
|
||||
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;
|
||||
|
||||
// Map of attributes from YAML frontmatter
|
||||
// Description is also extracted by default from content
|
||||
this.attributes = {};
|
||||
|
||||
// Current content
|
||||
this.content = '';
|
||||
|
||||
// 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.getByExt(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 {
|
||||
file: {
|
||||
path: this.path,
|
||||
mtime: this.mtime,
|
||||
type: this.type
|
||||
},
|
||||
page: _.extend({}, this.attributes, {
|
||||
title: article? article.title : null,
|
||||
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
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
// Return complete context for templating (page + book + summary + ...)
|
||||
Page.prototype.getOutputContext = function(output) {
|
||||
return _.extend({}, this.getContext(), output.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);
|
||||
|
||||
// Extract attributes
|
||||
that.attributes = parsed.attributes;
|
||||
|
||||
// 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.getOutputContext(output), {
|
||||
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.attributes.description) return;
|
||||
that.attributes.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,70 +0,0 @@
|
||||
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);
|
||||
|
||||
nparser.inline = Promise.wrapfn(parser.inline);
|
||||
|
||||
return nparser;
|
||||
}
|
||||
|
||||
// Return a specific parser
|
||||
function getParser(name) {
|
||||
return _.find(PARSERS, {
|
||||
name: name
|
||||
});
|
||||
}
|
||||
|
||||
// Return a specific parser according to an extension
|
||||
function getParserByExt(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,
|
||||
getByExt: getParserByExt,
|
||||
getForFile: getParserForFile
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
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
|
||||
};
|
||||
@@ -1,188 +0,0 @@
|
||||
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);
|
||||
}
|
||||
|
||||
// Returns the list of plugins
|
||||
PluginsManager.prototype.list = function() {
|
||||
return this.plugins;
|
||||
};
|
||||
|
||||
// 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 (could be a BookPlugin or {name,path})
|
||||
PluginsManager.prototype.load = function(plugin) {
|
||||
var that = this;
|
||||
|
||||
if (_.isArray(plugin)) {
|
||||
return Promise.serie(plugin, that.load);
|
||||
}
|
||||
|
||||
return Promise()
|
||||
|
||||
// Initiate and load the plugin
|
||||
.then(function() {
|
||||
if (!(plugin instanceof BookPlugin)) {
|
||||
plugin = new BookPlugin(that.book, plugin.name, plugin.path);
|
||||
}
|
||||
|
||||
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 that = this;
|
||||
var pluginNames = _.pluck(this.book.config.get('plugins'), 'name');
|
||||
|
||||
return registry.list(this.book)
|
||||
.then(function(plugins) {
|
||||
// Filter out plugins not listed of first level
|
||||
// (aka pre-installed plugins)
|
||||
plugins = _.filter(plugins, function(plugin) {
|
||||
return (
|
||||
plugin.depth > 1 ||
|
||||
_.contains(pluginNames, plugin.name)
|
||||
);
|
||||
});
|
||||
|
||||
// Sort plugins to match list in book.json
|
||||
plugins.sort(function(a, b){
|
||||
return pluginNames.indexOf(a.name) < pluginNames.indexOf(b.name) ? -1 : 1;
|
||||
});
|
||||
|
||||
// Log state
|
||||
that.log.info.ln(_.size(plugins) + ' are installed');
|
||||
if (_.size(pluginNames) != _.size(plugins)) that.log.info.ln(_.size(pluginNames) + ' explicitly listed');
|
||||
|
||||
// Verify that all plugins are present
|
||||
var notInstalled = _.filter(pluginNames, function(name) {
|
||||
return !_.find(plugins, { name: name });
|
||||
});
|
||||
|
||||
if (_.size(notInstalled) > 0) {
|
||||
throw new Error('Couldn\'t locate plugins "' + notInstalled.join(', ') + '", Run \'gitbook install\' to install plugins from registry.');
|
||||
}
|
||||
|
||||
// Load plugins
|
||||
return that.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;
|
||||
@@ -1,288 +0,0 @@
|
||||
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, pluginFolder) {
|
||||
this.book = book;
|
||||
this.log = this.book.log.prefix(pluginId);
|
||||
|
||||
|
||||
this.id = pluginId;
|
||||
this.npmId = registry.npmId(pluginId);
|
||||
this.root = pluginFolder;
|
||||
|
||||
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 from its root folder
|
||||
BookPlugin.prototype.load = function(folder) {
|
||||
var that = this;
|
||||
|
||||
if (this.isLoaded()) {
|
||||
return Promise.reject(new Error('Plugin "' + this.id + '" is already loaded'));
|
||||
}
|
||||
|
||||
// Try loading plugins from different location
|
||||
var p = Promise()
|
||||
.then(function() {
|
||||
// Locate plugin and load pacjage.json
|
||||
try {
|
||||
var res = resolve.sync('./package.json', { basedir: that.root });
|
||||
|
||||
that.root = path.dirname(res);
|
||||
that.packageInfos = require(res);
|
||||
} catch (err) {
|
||||
if (!isModuleNotFound(err)) throw err;
|
||||
|
||||
that.packageInfos = undefined;
|
||||
that.content = undefined;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Load plugin JS content
|
||||
try {
|
||||
that.content = require(that.root);
|
||||
} 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
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.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.isLoaded() &&
|
||||
this.packageInfos &&
|
||||
this.packageInfos.name &&
|
||||
this.packageInfos.engines &&
|
||||
this.packageInfos.engines.gitbook
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Error loading plugin "' + this.id + '" at "' + this.root + '"');
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
var npm = require('npm');
|
||||
var npmi = require('npmi');
|
||||
var path = require('path');
|
||||
var semver = require('semver');
|
||||
var _ = require('lodash');
|
||||
var readInstalled = require('read-installed');
|
||||
|
||||
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 && 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');
|
||||
});
|
||||
}
|
||||
|
||||
// List all packages installed inside a folder
|
||||
// Returns an ordered list of plugins
|
||||
function listInstalled(folder) {
|
||||
var options = {
|
||||
dev: false,
|
||||
log: function() {},
|
||||
depth: 4
|
||||
};
|
||||
var results = [];
|
||||
|
||||
function onPackage(pkg, isRoot) {
|
||||
if (!validateId(pkg.name)){
|
||||
if (!isRoot) return;
|
||||
} else {
|
||||
results.push({
|
||||
name: pluginId(pkg.name),
|
||||
version: pkg.version,
|
||||
path: pkg.realPath,
|
||||
depth: pkg.depth
|
||||
});
|
||||
}
|
||||
|
||||
_.each(pkg.dependencies, function(dep) {
|
||||
onPackage(dep);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.nfcall(readInstalled, folder, options)
|
||||
.then(function(data) {
|
||||
onPackage(data, true);
|
||||
return _.uniq(results, 'name');
|
||||
});
|
||||
}
|
||||
|
||||
// List installed plugins for a book (defaults and installed)
|
||||
function listPlugins(book) {
|
||||
return Promise.all([
|
||||
listInstalled(path.resolve(__dirname, '../..')),
|
||||
listInstalled(book.root)
|
||||
])
|
||||
.spread(function(defaultPlugins, plugins) {
|
||||
var results = plugins.concat(defaultPlugins);
|
||||
return _.uniq(results, 'name');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
npmId: npmId,
|
||||
pluginId: pluginId,
|
||||
validateId: validateId,
|
||||
|
||||
resolve: resolveVersion,
|
||||
link: linkPlugin,
|
||||
install: installPlugin,
|
||||
list: listPlugins,
|
||||
listInstalled: listInstalled
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
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
|
||||
};
|
||||
},
|
||||
|
||||
// Render some markdown to HTML
|
||||
markdown: function(blk) {
|
||||
return this.book.renderInline('markdown', blk.body)
|
||||
.then(function(out) {
|
||||
return { body: out };
|
||||
});
|
||||
},
|
||||
asciidoc: function(blk) {
|
||||
return this.book.renderInline('asciidoc', blk.body)
|
||||
.then(function(out) {
|
||||
return { body: out };
|
||||
});
|
||||
},
|
||||
markup: function(blk) {
|
||||
return this.book.renderInline(this.ctx.file.type, blk.body)
|
||||
.then(function(out) {
|
||||
return { body: out };
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,80 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var nunjucks = require('nunjucks');
|
||||
|
||||
/*
|
||||
Nunjucks loader similar to FileSystemLoader, but avoid infinite looping
|
||||
*/
|
||||
|
||||
function isRelative(filename) {
|
||||
return (filename.indexOf('./') === 0 || filename.indexOf('../') === 0);
|
||||
}
|
||||
|
||||
var Loader = nunjucks.Loader.extend({
|
||||
init: function(searchPaths) {
|
||||
this.searchPaths = _.map(searchPaths, path.normalize);
|
||||
},
|
||||
|
||||
getSource: function(fullpath) {
|
||||
if (!fullpath) return null;
|
||||
|
||||
fullpath = this.resolve(null, fullpath);
|
||||
|
||||
if(!fullpath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
src: fs.readFileSync(fullpath, 'utf-8'),
|
||||
path: fullpath,
|
||||
noCache: true
|
||||
};
|
||||
},
|
||||
|
||||
// We handle absolute paths ourselves in ".resolve"
|
||||
isRelative: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
resolve: function(from, to) {
|
||||
var searchPaths = this.searchPaths;
|
||||
|
||||
// Relative template like "./test.html"
|
||||
if (isRelative(to) && from) {
|
||||
return path.resolve(path.dirname(from), to);
|
||||
}
|
||||
|
||||
// Determine in which search folder we currently are
|
||||
var originalSearchPath = _.chain(this.searchPaths)
|
||||
.sortBy(function(s) {
|
||||
return -s.length;
|
||||
})
|
||||
.find(function(basePath) {
|
||||
return (from && from.indexOf(basePath) === 0);
|
||||
})
|
||||
.value();
|
||||
var originalFilename = originalSearchPath? path.relative(originalSearchPath, from) : null;
|
||||
|
||||
// If we are including same file from a different search path
|
||||
// Slice the search paths to avoid including from previous ones
|
||||
if (originalFilename == to) {
|
||||
var currentIndex = searchPaths.indexOf(originalSearchPath);
|
||||
searchPaths = searchPaths.slice(currentIndex + 1);
|
||||
}
|
||||
|
||||
// Absolute template to resolve in root folder
|
||||
var resultFolder = _.find(searchPaths, function(basePath) {
|
||||
var p = path.resolve(basePath, to);
|
||||
|
||||
return (
|
||||
p.indexOf(basePath) === 0
|
||||
&& fs.existsSync(p)
|
||||
);
|
||||
});
|
||||
if (!resultFolder) return null;
|
||||
return path.resolve(resultFolder, to);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Loader;
|
||||
@@ -1,436 +0,0 @@
|
||||
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 that = this;
|
||||
|
||||
return function() {
|
||||
var ctx = {
|
||||
ctx: this.ctx,
|
||||
book: that.book,
|
||||
output: that.output
|
||||
};
|
||||
error.deprecateField(ctx, 'generator', that.output.name, '"generator" property is deprecated, use "output.generator" instead');
|
||||
|
||||
return func.apply(ctx, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
// Interpolate a string content to replace shortcuts according to the filetype
|
||||
TemplateEngine.prototype.interpolate = function(filepath, source) {
|
||||
var parser = parsers.getByExt(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;
|
||||
@@ -1,42 +0,0 @@
|
||||
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;
|
||||
@@ -1,80 +0,0 @@
|
||||
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
|
||||
};
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
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
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
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;
|
||||
@@ -1,44 +0,0 @@
|
||||
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,70 +0,0 @@
|
||||
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
|
||||
};
|
||||
@@ -1,128 +0,0 @@
|
||||
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;
|
||||
@@ -1,62 +0,0 @@
|
||||
var Q = require('q');
|
||||
var _ = require('lodash');
|
||||
|
||||
// Reduce an array to a promise
|
||||
function reduce(arr, iter, base) {
|
||||
return _.reduce(arr, function(prev, elem, i) {
|
||||
return prev.then(function(val) {
|
||||
return iter(val, elem, i);
|
||||
});
|
||||
}, Q(base));
|
||||
}
|
||||
|
||||
// Transform an array
|
||||
function serie(arr, iter, base) {
|
||||
return reduce(arr, function(before, item, i) {
|
||||
return Q(iter(item, i))
|
||||
.then(function(r) {
|
||||
before.push(r);
|
||||
return before;
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
// Iter over an array and return first result (not null)
|
||||
function some(arr, iter) {
|
||||
return _.reduce(arr, function(prev, elem, i) {
|
||||
return prev.then(function(val) {
|
||||
if (val) return val;
|
||||
|
||||
return iter(elem, i);
|
||||
});
|
||||
}, Q());
|
||||
}
|
||||
|
||||
// Map an array using an async (promised) iterator
|
||||
function map(arr, iter) {
|
||||
return reduce(arr, function(prev, entry, i) {
|
||||
return Q(iter(entry, i))
|
||||
.then(function(out) {
|
||||
prev.push(out);
|
||||
return prev;
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
// Wrap a fucntion in a promise
|
||||
function wrap(func) {
|
||||
return _.wrap(func, function(_func) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
return Q()
|
||||
.then(function() {
|
||||
return _func.apply(null, args);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Q;
|
||||
module.exports.reduce = reduce;
|
||||
module.exports.map = map;
|
||||
module.exports.serie = serie;
|
||||
module.exports.some = some;
|
||||
module.exports.wrapfn = wrap;
|
||||
+13
-80
@@ -1,93 +1,26 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "3.0.0-pre.6",
|
||||
"homepage": "https://www.gitbook.com",
|
||||
"description": "Library and cmd utility to generate GitBooks",
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"bash-color": "0.0.3",
|
||||
"cheerio": "0.20.0",
|
||||
"chokidar": "1.4.3",
|
||||
"cp": "0.2.0",
|
||||
"cpr": "1.1.1",
|
||||
"crc": "3.4.0",
|
||||
"deprecated": "0.0.1",
|
||||
"destroy": "1.0.4",
|
||||
"direction": "0.1.5",
|
||||
"dom-serializer": "0.1.0",
|
||||
"error": "7.0.2",
|
||||
"escape-string-regexp": "1.0.5",
|
||||
"eslint": "^2.2.0",
|
||||
"front-matter": "2.0.7",
|
||||
"gitbook-asciidoc": "1.1.0",
|
||||
"gitbook-markdown": "1.2.0",
|
||||
"gitbook-plugin-fontsettings": "1.0.2",
|
||||
"gitbook-plugin-highlight": "2.0.2",
|
||||
"gitbook-plugin-livereload": "0.0.1",
|
||||
"gitbook-plugin-search": "1.2.0",
|
||||
"gitbook-plugin-sharing": "1.0.1",
|
||||
"gitbook-plugin-theme-default": "1.0.0-pre.5",
|
||||
"github-slugid": "1.0.1",
|
||||
"graceful-fs": "4.1.3",
|
||||
"i18n-t": "1.0.0",
|
||||
"ignore": "3.0.14",
|
||||
"json-schema-defaults": "0.1.1",
|
||||
"jsonschema": "1.1.0",
|
||||
"juice": "1.10.0",
|
||||
"lodash": "3.10.1",
|
||||
"merge-defaults": "0.2.1",
|
||||
"mkdirp": "0.5.1",
|
||||
"moment": "2.12.0",
|
||||
"npm": "3.8.6",
|
||||
"npmi": "1.0.1",
|
||||
"nunjucks": "2.4.1",
|
||||
"nunjucks-autoescape": "1.0.1",
|
||||
"q": "1.4.1",
|
||||
"read-installed": "^4.0.3",
|
||||
"request": "2.70.0",
|
||||
"resolve": "1.1.7",
|
||||
"rmdir": "1.2.0",
|
||||
"semver": "5.1.0",
|
||||
"send": "0.13.2",
|
||||
"spawn-cmd": "0.0.2",
|
||||
"tiny-lr": "0.2.1",
|
||||
"tmp": "0.0.28",
|
||||
"urijs": "1.17.1"
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"eslint": "2.7.0",
|
||||
"mocha": "2.4.5",
|
||||
"should": "8.3.0"
|
||||
"eslint": "3.12.2",
|
||||
"eslint-config-gitbook": "1.5.0",
|
||||
"lerna": "2.0.0-beta.31"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node_modules/.bin/mocha --reporter spec --bail --timeout 15000 ./test/all.js",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint .",
|
||||
"test": "lerna run --concurrency 1 test",
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"clean": "lerna clean",
|
||||
"dist": "lerna run --concurrency 1 prepublish",
|
||||
"publish-version": "./scripts/publish.sh"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/GitbookIO/gitbook.git"
|
||||
"url": "git+https://github.com/GitbookIO/gitbook.git"
|
||||
},
|
||||
"bin": {
|
||||
"gitbook": "./bin/gitbook.js"
|
||||
},
|
||||
"keywords": [
|
||||
"git",
|
||||
"book",
|
||||
"gitbook"
|
||||
],
|
||||
"author": "FriendCode Inc. <contact@gitbook.com>",
|
||||
"author": "GitBook Inc. <contact@gitbook.com>",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/GitbookIO/gitbook/issues"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Aaron O'Mullan",
|
||||
"email": "aaron@gitbook.com"
|
||||
},
|
||||
{
|
||||
"name": "Samy Pessé",
|
||||
"email": "samy@gitbook.com"
|
||||
}
|
||||
]
|
||||
"homepage": "https://github.com/GitbookIO/gitbook#readme"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["es2015", "react", "stage-2"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
gitbook.core.min.js
|
||||
@@ -0,0 +1,3 @@
|
||||
src
|
||||
!lib
|
||||
!dist
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "gitbook-core",
|
||||
"version": "4.0.0-alpha.4",
|
||||
"description": "Core for GitBook plugins API",
|
||||
"main": "./lib/index.js",
|
||||
"dependencies": {
|
||||
"bluebird": "^3.4.6",
|
||||
"classnames": "^2.2.5",
|
||||
"entities": "^1.1.1",
|
||||
"history": "^4.3.0",
|
||||
"html-tags": "^1.1.1",
|
||||
"immutable": "^3.8.1",
|
||||
"mousetrap": "1.6.0",
|
||||
"react": "15.4.1",
|
||||
"react-addons-css-transition-group": "15.4.1",
|
||||
"react-dom": "15.4.1",
|
||||
"react-helmet": "^3.1.0",
|
||||
"react-immutable-proptypes": "^2.1.0",
|
||||
"react-intl": "^2.1.5",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-safe-html": "0.4.0",
|
||||
"redux": "^3.5.2",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"reflexbox": "^2.2.2",
|
||||
"whatwg-fetch": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.14.0",
|
||||
"babel-preset-es2015": "^6.14.0",
|
||||
"babel-preset-react": "^6.11.1",
|
||||
"babel-preset-stage-2": "^6.13.0",
|
||||
"browserify": "^13.1.0",
|
||||
"envify": "^3.4.1",
|
||||
"uglify-js": "^2.7.3"
|
||||
},
|
||||
"scripts": {
|
||||
"dist-lib": "rm -rf lib/ && babel -d lib/ src/",
|
||||
"dist-standalone": "mkdir -p dist && browserify -r ./lib/index.js:gitbook-core -r react -r react-dom ./lib/index.js | uglifyjs -c > ./dist/gitbook.core.min.js",
|
||||
"dist": "npm run dist-lib && npm run dist-standalone",
|
||||
"prepublish": "npm run dist"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/GitbookIO/gitbook.git"
|
||||
},
|
||||
"author": "GitBook Inc. <contact@gitbook.com>",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/GitbookIO/gitbook/issues"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
module.exports = {
|
||||
// Components
|
||||
REGISTER_COMPONENT: 'components/register',
|
||||
UNREGISTER_COMPONENT: 'components/unregister',
|
||||
// Navigation
|
||||
HISTORY_ACTIVATE: 'history/activate',
|
||||
HISTORY_DEACTIVATE: 'history/deactivate',
|
||||
HISTORY_LISTEN: 'history/listen',
|
||||
HISTORY_UPDATE: 'history/update',
|
||||
PAGE_FETCH_START: 'history/fetch:start',
|
||||
PAGE_FETCH_END: 'history/fetch:end',
|
||||
PAGE_FETCH_ERROR: 'history/fetch:error',
|
||||
// i18n
|
||||
I18N_REGISTER_LOCALE: 'i18n/register:locale'
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
const ACTION_TYPES = require('./TYPES');
|
||||
|
||||
/**
|
||||
* Find all components matching a descriptor
|
||||
* @param {List<ComponentDescriptor>} state
|
||||
* @param {String} matching.role
|
||||
*/
|
||||
function findMatchingComponents(state, matching) {
|
||||
return state
|
||||
.filter(({descriptor}) => {
|
||||
if (matching.role && matching.role !== descriptor.role) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map(component => component.Component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new component
|
||||
* @param {React.Class} Component
|
||||
* @param {Descriptor} descriptor
|
||||
* @return {Action}
|
||||
*/
|
||||
function registerComponent(Component, descriptor) {
|
||||
return {
|
||||
type: ACTION_TYPES.REGISTER_COMPONENT,
|
||||
Component,
|
||||
descriptor
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findMatchingComponents,
|
||||
registerComponent
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
const ACTION_TYPES = require('./TYPES');
|
||||
const getPayload = require('../lib/getPayload');
|
||||
const Location = require('../models/Location');
|
||||
|
||||
const SUPPORTED = (
|
||||
typeof window !== 'undefined' &&
|
||||
window.history && window.history.pushState && window.history.replaceState &&
|
||||
// pushState isn't reliable on iOS until 5.
|
||||
!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/)
|
||||
);
|
||||
|
||||
/**
|
||||
* Initialize the history
|
||||
*/
|
||||
function activate() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ACTION_TYPES.HISTORY_ACTIVATE,
|
||||
listener: (location) => {
|
||||
location = Location.fromNative(location);
|
||||
const prevLocation = getState().history.location;
|
||||
|
||||
// Fetch page if required
|
||||
if (!prevLocation || location.pathname !== prevLocation.pathname) {
|
||||
dispatch(fetchPage(location.pathname));
|
||||
}
|
||||
|
||||
// Signal location to listener
|
||||
dispatch(emit());
|
||||
|
||||
// Update the location
|
||||
dispatch({
|
||||
type: ACTION_TYPES.HISTORY_UPDATE,
|
||||
location
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger for existing listeners
|
||||
dispatch(emit());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit current location
|
||||
* @param {List|Array<Function>} to?
|
||||
*/
|
||||
function emit(to) {
|
||||
return (dispatch, getState) => {
|
||||
const { listeners, client } = getState().history;
|
||||
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = Location.fromNative(client.location);
|
||||
|
||||
to = to || listeners;
|
||||
|
||||
to.forEach(handler => {
|
||||
handler(location, dispatch, getState);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup the history
|
||||
*/
|
||||
function deactivate() {
|
||||
return { type: ACTION_TYPES.HISTORY_DEACTIVATE };
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new url into the history
|
||||
* @param {String|Location} location
|
||||
* @return {Action} action
|
||||
*/
|
||||
function push(location) {
|
||||
return (dispatch, getState) => {
|
||||
const { client } = getState().history;
|
||||
location = Location.fromNative(location);
|
||||
|
||||
if (SUPPORTED) {
|
||||
client.push(location.toNative());
|
||||
} else {
|
||||
redirect(location.toString());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace current state in history
|
||||
* @param {String|Location} location
|
||||
* @return {Action} action
|
||||
*/
|
||||
function replace(location) {
|
||||
return (dispatch, getState) => {
|
||||
const { client } = getState().history;
|
||||
location = Location.fromNative(location);
|
||||
|
||||
if (SUPPORTED) {
|
||||
client.replace(location.toNative());
|
||||
} else {
|
||||
redirect(location.toString());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard redirection
|
||||
* @param {String} uri
|
||||
* @return {Action} action
|
||||
*/
|
||||
function redirect(uri) {
|
||||
return () => {
|
||||
window.location.href = uri;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to url change
|
||||
* @param {Function} listener
|
||||
* @return {Action} action
|
||||
*/
|
||||
function listen(listener) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: ACTION_TYPES.HISTORY_LISTEN, listener });
|
||||
|
||||
// Trigger for existing listeners
|
||||
dispatch(emit([ listener ]));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a new page and update the store accordingly
|
||||
* @param {String} pathname
|
||||
* @return {Action} action
|
||||
*/
|
||||
function fetchPage(pathname) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: ACTION_TYPES.PAGE_FETCH_START });
|
||||
|
||||
window.fetch(pathname, {
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(
|
||||
response => {
|
||||
return response.text();
|
||||
}
|
||||
)
|
||||
.then(
|
||||
html => {
|
||||
const payload = getPayload(html);
|
||||
|
||||
if (!payload) {
|
||||
throw new Error('No payload found in page');
|
||||
}
|
||||
|
||||
dispatch({ type: ACTION_TYPES.PAGE_FETCH_END, payload });
|
||||
}
|
||||
)
|
||||
.catch(
|
||||
error => {
|
||||
// dispatch(redirect(pathname));
|
||||
dispatch({ type: ACTION_TYPES.PAGE_FETCH_ERROR, error });
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a new article
|
||||
* @param {SummaryArticle} article
|
||||
* @return {Action} action
|
||||
*/
|
||||
function fetchArticle(article) {
|
||||
return fetchPage(article.path);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate,
|
||||
listen,
|
||||
push,
|
||||
replace,
|
||||
fetchPage,
|
||||
fetchArticle
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
const ACTION_TYPES = require('./TYPES');
|
||||
|
||||
/**
|
||||
* Register messages for a locale
|
||||
* @param {String} locale
|
||||
* @param {Map<String:String>} messages
|
||||
* @return {Action}
|
||||
*/
|
||||
function registerLocale(locale, messages) {
|
||||
return { type: ACTION_TYPES.I18N_REGISTER_LOCALE, locale, messages };
|
||||
}
|
||||
|
||||
/**
|
||||
* Register multiple locales
|
||||
* @param {Map<String:Object>} locales
|
||||
* @return {Action}
|
||||
*/
|
||||
function registerLocales(locales) {
|
||||
return (dispatch) => {
|
||||
for (const locale in locales) {
|
||||
if (!locales.hasOwnProperty(locale)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dispatch(registerLocale(locale, locales[locale]));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
registerLocale,
|
||||
registerLocales
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
const React = require('react');
|
||||
const HotKeys = require('./HotKeys');
|
||||
|
||||
/**
|
||||
* Backdrop for modals, dropdown, etc. that covers the whole screen
|
||||
* and handles click and pressing escape.
|
||||
*
|
||||
* <Backdrop onClose={onCloseModal} />
|
||||
*/
|
||||
const Backdrop = React.createClass({
|
||||
propTypes: {
|
||||
// Callback when backdrop is closed
|
||||
onClose: React.PropTypes.func.isRequired,
|
||||
// Z-index for the backdrop
|
||||
zIndex: React.PropTypes.number,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
zIndex: 200
|
||||
};
|
||||
},
|
||||
|
||||
onClick(event) {
|
||||
const { onClose } = this.props;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClose();
|
||||
},
|
||||
|
||||
render() {
|
||||
const { zIndex, children, onClose } = this.props;
|
||||
const style = {
|
||||
zIndex,
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
};
|
||||
const keyMap = {
|
||||
'escape': onClose
|
||||
};
|
||||
|
||||
return (
|
||||
<HotKeys keyMap={keyMap}>
|
||||
<div style={style} onClick={this.onClick}>{children}</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Backdrop;
|
||||
@@ -0,0 +1,22 @@
|
||||
const React = require('react');
|
||||
const classNames = require('classnames');
|
||||
|
||||
const Button = React.createClass({
|
||||
propTypes: {
|
||||
active: React.PropTypes.bool,
|
||||
className: React.PropTypes.string,
|
||||
children: React.PropTypes.node,
|
||||
onClick: React.PropTypes.func
|
||||
},
|
||||
|
||||
render() {
|
||||
const { children, active, onClick } = this.props;
|
||||
const className = classNames('GitBook-Button', this.props.className, {
|
||||
active
|
||||
});
|
||||
|
||||
return <button className={className} onClick={onClick}>{children}</button>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Button;
|
||||
@@ -0,0 +1,23 @@
|
||||
const React = require('react');
|
||||
const classNames = require('classnames');
|
||||
|
||||
const ButtonGroup = React.createClass({
|
||||
propTypes: {
|
||||
className: React.PropTypes.string,
|
||||
children: React.PropTypes.node,
|
||||
onClick: React.PropTypes.func
|
||||
},
|
||||
|
||||
render() {
|
||||
let { className, children } = this.props;
|
||||
|
||||
className = classNames(
|
||||
'GitBook-ButtonGroup',
|
||||
className
|
||||
);
|
||||
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ButtonGroup;
|
||||
@@ -0,0 +1,34 @@
|
||||
const React = require('react');
|
||||
const { Provider } = require('react-redux');
|
||||
|
||||
const ContextShape = require('../propTypes/Context');
|
||||
|
||||
/**
|
||||
* React component to provide a GitBook context to children components.
|
||||
*/
|
||||
|
||||
const ContextProvider = React.createClass({
|
||||
propTypes: {
|
||||
context: ContextShape.isRequired,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
gitbook: ContextShape
|
||||
},
|
||||
|
||||
getChildContext() {
|
||||
const { context } = this.props;
|
||||
|
||||
return {
|
||||
gitbook: context
|
||||
};
|
||||
},
|
||||
|
||||
render() {
|
||||
const { context, children } = this.props;
|
||||
return <Provider store={context.store}>{children}</Provider>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ContextProvider;
|
||||
@@ -0,0 +1,126 @@
|
||||
const React = require('react');
|
||||
const classNames = require('classnames');
|
||||
|
||||
/**
|
||||
* Dropdown to display a menu
|
||||
*
|
||||
* <Dropdown.Container>
|
||||
*
|
||||
* <Button />
|
||||
*
|
||||
* <Dropdown.Menu>
|
||||
* <Dropdown.Item href={...}> ... </Dropdown.Item>
|
||||
* <Dropdown.Item onClick={...}> ... </Dropdown.Item>
|
||||
* </Dropdown.Menu>
|
||||
* </Dropdown.Container>
|
||||
*/
|
||||
|
||||
const DropdownContainer = React.createClass({
|
||||
propTypes: {
|
||||
className: React.PropTypes.string,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
render() {
|
||||
let { className, children } = this.props;
|
||||
|
||||
className = classNames(
|
||||
'GitBook-Dropdown',
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A dropdown item which can contains informations.
|
||||
*/
|
||||
const DropdownItem = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
|
||||
return (
|
||||
<div className="GitBook-DropdownItem">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* A dropdown item, which is always a link.
|
||||
*/
|
||||
const DropdownItemLink = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.node,
|
||||
onClick: React.PropTypes.func,
|
||||
href: React.PropTypes.string
|
||||
},
|
||||
|
||||
onClick(event) {
|
||||
const { onClick, href } = this.props;
|
||||
|
||||
if (href) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (onClick) {
|
||||
onClick();
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const { children, href, ...otherProps } = this.props;
|
||||
|
||||
return (
|
||||
<a {...otherProps} className="GitBook-DropdownItemLink" href={href || '#'} onClick={this.onClick} >
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* A DropdownMenu to display DropdownItems. Must be inside a
|
||||
* DropdownContainer.
|
||||
*/
|
||||
const DropdownMenu = React.createClass({
|
||||
propTypes: {
|
||||
className: React.PropTypes.string,
|
||||
children: React.PropTypes.node
|
||||
},
|
||||
|
||||
render() {
|
||||
let { className, children } = this.props;
|
||||
className = classNames('GitBook-DropdownMenu', className);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const Dropdown = {
|
||||
Item: DropdownItem,
|
||||
ItemLink: DropdownItemLink,
|
||||
Menu: DropdownMenu,
|
||||
Container: DropdownContainer
|
||||
};
|
||||
|
||||
module.exports = Dropdown;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user