Compare commits
908 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5ff1f3d22 | |||
| b22a150a37 | |||
| 6da3c31494 | |||
| c72a8d5e84 | |||
| d00d8b6776 | |||
| db651e7ff4 | |||
| 16f135835e | |||
| fcec69ae2c | |||
| 306feb497c | |||
| a9f76e1475 | |||
| fe3b4cc98e | |||
| 14b7ef9066 | |||
| 9b14dc40a5 | |||
| 954253305a | |||
| 7a7ad74ff1 | |||
| d6e86af41a | |||
| 71ed8b5e93 | |||
| 7a6f706dc4 | |||
| dd365eaede | |||
| bc8f6792e8 | |||
| 2812fe2775 | |||
| b0e7298523 | |||
| 6ca06d4115 | |||
| 8f07bbabea | |||
| 4679b1d931 | |||
| cea57c05e2 | |||
| 311be67bde | |||
| 88c52ded2e | |||
| 5a0bcb932d | |||
| 49fcf10de2 | |||
| ace2896ec8 | |||
| 9df919083d | |||
| 103e5f8409 | |||
| a14daca1c6 | |||
| 33921beaa0 | |||
| d4cfe90959 | |||
| 051618c024 | |||
| fe4880feaa | |||
| 363cb4b0fb | |||
| e290ec24f1 | |||
| 45ae86a248 | |||
| f90981adf6 | |||
| 93a3725480 | |||
| 1b8c0e3c76 | |||
| 1b0b44a5b7 | |||
| f9e0272513 | |||
| 13633e7896 | |||
| 5659db4877 | |||
| cb80f46843 | |||
| f650b0d69e | |||
| d841ad5962 | |||
| fe9e93fc4d | |||
| 1a91fbda40 | |||
| 91aa96da7d | |||
| db7c9f3565 | |||
| 76b47e3393 | |||
| d385e2cab6 | |||
| 0f328f375e | |||
| c73aceb3d3 | |||
| daeecf825c | |||
| 607748af3b | |||
| 7197289364 | |||
| fc60f16475 | |||
| 08fab253ef | |||
| 3d9685c6a2 | |||
| 6d996ef270 | |||
| 55c7cbbbfa | |||
| e9f3a169c9 | |||
| 70b27bcef6 | |||
| a1df862f06 | |||
| 0c6829f8af | |||
| 2b9f3c195f | |||
| f445b96cb7 | |||
| e0d0ad5027 | |||
| 3f71fde155 | |||
| 6e4e1f8404 | |||
| 3c85720137 | |||
| 4713abc8f1 | |||
| e7e94dc697 | |||
| c1a93d33e7 | |||
| cd885bf855 | |||
| ddfe457abf | |||
| f4d337d252 | |||
| cbfec5b9e9 | |||
| af3358d195 | |||
| 641358a905 | |||
| f2df220a1f | |||
| a2ca31bb19 | |||
| b09333db79 | |||
| ac98e7b177 | |||
| 14d1d4270f | |||
| 150e55aa00 | |||
| 2f0c96b618 | |||
| 6c2137a420 | |||
| e1878050b5 | |||
| 1c0614bd88 | |||
| ba7a37a3b8 | |||
| 77fd7dcc1d | |||
| 7311ab0b3f | |||
| 4160ff3ee2 | |||
| 7151d15abb | |||
| 19f443f68e | |||
| 9f1b3bcea0 | |||
| 2dae244185 | |||
| c62ce32cc9 | |||
| f3a90812a6 | |||
| 096b8ccd2e | |||
| 923993dfb1 | |||
| fcf4e4ef0e | |||
| ae1795f82c | |||
| 2666553383 | |||
| 15af03c8bf | |||
| 97c7d65931 | |||
| bbc7c7d080 | |||
| 1a751b2f6e | |||
| abe9c82e75 | |||
| 956ac95e7a | |||
| 40e1837b14 | |||
| ddea5be2a4 | |||
| b4968bb8d4 | |||
| ca8e34c94d | |||
| 4a94340f42 | |||
| 7d69becd2e | |||
| 1a9b5d4ac6 | |||
| bdc0e6507e | |||
| 93fb25c702 | |||
| 23d41e5617 | |||
| d62620a282 | |||
| 62fdd541f5 | |||
| 7f53c917d9 | |||
| 015eba6687 | |||
| 5b8b54b774 | |||
| efcedf2f24 | |||
| 512daa3189 | |||
| ad3727b3fc | |||
| e2a81fdd60 | |||
| fe7bc0d4bd | |||
| 9ad8b86ece | |||
| 96b42dedea | |||
| 4942324250 | |||
| 0cf5657c43 | |||
| 3daef9d308 | |||
| 7f0ded4926 | |||
| 1041c1d533 | |||
| f06a9dc1f0 | |||
| 45ab2bb218 | |||
| f17cb3ecac | |||
| 86974b2660 | |||
| 57b4ef08b6 | |||
| cc16bc47dd | |||
| 078c1eecba | |||
| eb53f2e844 | |||
| e8954b3488 | |||
| 20b9c3829e | |||
| 709153f7f5 | |||
| d5cc3fbcf1 | |||
| d3cff5c1cd | |||
| 83106e7a5e | |||
| 525e58d318 | |||
| 9d353bd3dd | |||
| 47d13cb23c | |||
| 7c13753bb5 | |||
| affbcbb79e | |||
| 96a45a4a96 | |||
| 6f6eb89bd8 | |||
| eea708a18d | |||
| 508bb2d55d | |||
| 09cdc19a21 | |||
| 2cb61c9d5f | |||
| 986d6f4096 | |||
| 1e20373428 | |||
| 17ba258355 | |||
| c53e95414f | |||
| 41e2b7658b | |||
| 79fe7afb65 | |||
| 29e3858d21 | |||
| b5969394f1 | |||
| 904daba10b | |||
| 7cef79288a | |||
| 6ab579bb2a | |||
| 60f8aad5c9 | |||
| a2bb02638b | |||
| 479acfbe9e | |||
| 6b62a2e58e | |||
| a6781265a7 | |||
| 23ff9829d5 | |||
| c70e897f1c | |||
| ded0a1617f | |||
| 88fefc1521 | |||
| 4c056a91ba | |||
| 9f138c007a | |||
| 320c0a6fb5 | |||
| 31f7c25a8f | |||
| c776f171ab | |||
| 6b0b3fa2ca | |||
| 1de6cee1b2 | |||
| 151e6d49c0 | |||
| 4e4920cc78 | |||
| d5544d8969 | |||
| 14ef86f9ca | |||
| a1edab6ef5 | |||
| 5b483ff78d | |||
| 30384789d2 | |||
| 4cf49732a2 | |||
| 0cc6e270f9 | |||
| 06d94fed56 | |||
| 6f94fc0a3b | |||
| 84049bf0c5 | |||
| 2cd0995f21 | |||
| 4fda71364e | |||
| 3702efee5b | |||
| 9218f2e1ef | |||
| 97d5efd94a | |||
| 15e1eb36ca | |||
| 64de5c6812 | |||
| 15500aef70 | |||
| 3f52109c1c | |||
| 84c837869e | |||
| dd777c9872 | |||
| 468fa8b6dd | |||
| e9350f2932 | |||
| 0dbeaf973d | |||
| 3150ef15da | |||
| e20591b49d | |||
| 583141de7c | |||
| 3f96319f10 | |||
| 83b5e10d70 | |||
| 52dea1e70e | |||
| abf37ad739 | |||
| 6136e7f346 | |||
| 4d31df2f30 | |||
| 95d5768e0e | |||
| 7ee0c10c10 | |||
| e1f7f00cd4 | |||
| e5a4889b36 | |||
| 9769045e04 | |||
| b47f2cfcf5 | |||
| 096ac2d552 | |||
| 4e4c016be5 | |||
| 2abe4d404d | |||
| 3b0a3ca1b2 | |||
| 4d7124f6bf | |||
| b6c9f85d22 | |||
| 0966f5172b | |||
| 855eae37d0 | |||
| 820cbb006e | |||
| 63bb459ac6 | |||
| a8cd3201b5 | |||
| 19f1a1f81d | |||
| 782aae95d0 | |||
| 5fd0d4a790 | |||
| 5a306ac745 | |||
| 386c20cc63 | |||
| a3b22ac6bf | |||
| 117357b74e | |||
| db0f03d1b5 | |||
| fd09ff0e1b | |||
| e676e43dc5 | |||
| e75839b185 | |||
| 6a23391a9a | |||
| 80b748ad3e | |||
| fbf8f134fa | |||
| 27d9010974 | |||
| 1173e5a502 | |||
| 1e8409efa6 | |||
| a2687fdf50 | |||
| cb0c45b29f | |||
| 0fe78f379a | |||
| 192887cedc | |||
| 06e2a441db | |||
| 530f1903aa | |||
| 035449fe02 | |||
| 03ef3d4258 | |||
| 7ec25f364a | |||
| 28c4f4b9dd | |||
| 97fe2decc9 | |||
| 54cda3b3ae | |||
| dac055f01e | |||
| 1b4b8ed1da | |||
| 115db76f66 | |||
| a28be7c3c0 | |||
| 84b852c137 | |||
| 2dac9c978a | |||
| 1df1756c71 | |||
| 0d1b0c0493 | |||
| 709d57b3c1 | |||
| 496ebaa1a8 | |||
| 9689ebaa1c | |||
| 3c66068279 | |||
| bf99516e55 | |||
| c03b2f4dfa | |||
| b488af39f1 | |||
| ab64fec1a8 | |||
| 672aeda17f | |||
| 74369f724c | |||
| 57a0ee3d59 | |||
| 86694fd122 | |||
| df0734ae5e | |||
| e880bd464e | |||
| 9e09fe1826 | |||
| b773a7608d | |||
| da3788384b | |||
| 5aacdadff8 | |||
| b7f0d2e1fa | |||
| c207c4e048 | |||
| 623fdad59f | |||
| 6e6acaac0d | |||
| 4c02d3027c | |||
| a532d28d94 | |||
| 84420c1eb4 | |||
| f7b2bd8039 | |||
| c40a9eacb2 | |||
| a74aed15e9 | |||
| 94ba2b95d0 | |||
| b700d91a70 | |||
| e77c961d40 | |||
| 7efa75684e | |||
| 08989cfcdd | |||
| 7e16d6cb0d | |||
| 0af54b1142 | |||
| eb1cda6fdc | |||
| 94e5ab8593 | |||
| 3b8a061200 | |||
| 2f1a454467 | |||
| b7c62d8cc5 | |||
| 6492fe8c23 | |||
| 3d77d172ec | |||
| 87b5d4ee61 | |||
| 182ba044bb | |||
| 0b92db7b0c | |||
| 534d0ebcc3 | |||
| 564afab278 | |||
| 18506e00c9 | |||
| d02c90c0fb | |||
| db47593e34 | |||
| 860853ddb4 | |||
| 827a09114e | |||
| 7c1ce99eba | |||
| e62999f9ec | |||
| 4f5c05ffe9 | |||
| 9ca4f9c606 | |||
| 03310df95e | |||
| 3bc7d7b998 | |||
| 6f580c9beb | |||
| d33408fcbd | |||
| 5f664dd78b | |||
| dcbc3a55a4 | |||
| 497515c02f | |||
| 117239ca7c | |||
| c373d1c6d4 | |||
| df7a3fb8c0 | |||
| 199f3984c6 | |||
| f9ed0d3085 | |||
| 163b3f9e51 | |||
| f5c8484ea0 | |||
| da130f7745 | |||
| b5c4a33ab1 | |||
| a27e366f28 | |||
| 12f8299eb2 | |||
| fa881d4ce8 | |||
| ad615d9061 | |||
| 88ee4757e7 | |||
| ca5c3995fe | |||
| e4929df454 | |||
| 96718441ec | |||
| 8e2416dabb | |||
| 68e647a17e | |||
| 982cb2d188 | |||
| 7538b8556c | |||
| 0f278e1dfc | |||
| c3ff573184 | |||
| f11aaa7b7c | |||
| 6174c3b6f8 | |||
| 7def10195b | |||
| 0f4ef161d1 | |||
| bf4ec5eef7 | |||
| 4a545cc686 | |||
| e2a8bdab3c | |||
| 5de919f2f7 | |||
| cfd51f6579 | |||
| 2022c94a30 | |||
| bb8bb048aa | |||
| b3005f8f9f | |||
| 8f24a2e384 | |||
| e624fb50b7 | |||
| 80078861f2 | |||
| 26fcebe0c7 | |||
| 5ce7232dc3 | |||
| 9a334400f0 | |||
| bcc8d00e89 | |||
| 9d4a94616a | |||
| 7b1ad8aa38 | |||
| adda0569e0 | |||
| d13ae310d3 | |||
| 7f82f27c5f | |||
| 720b133839 | |||
| caded1d06b | |||
| 8275242960 | |||
| 265d6249f7 | |||
| e6b6ae8fdb | |||
| 2a073e0993 | |||
| f63b226b76 | |||
| 16fd601a5c | |||
| ef062adcc2 | |||
| 2024209519 | |||
| d7b345b2da | |||
| 17659b9720 | |||
| 58a0b4f17d | |||
| 61f64e9f04 | |||
| 66f6f94867 | |||
| c540d7186a | |||
| 293672432b | |||
| da2aabe7bd | |||
| 3ddbc5237f | |||
| 503e32208b | |||
| 15186dc9dd | |||
| 2c8dd74997 | |||
| 3d51088d4d | |||
| 822a8911ec | |||
| efca3c8ef5 | |||
| 5f349913aa | |||
| c1020a0130 | |||
| bff71d79d3 | |||
| 4d95d58e15 | |||
| 47e0aa795a | |||
| 6bf9c6fc13 | |||
| 533be694b5 | |||
| a97820f54b | |||
| f6b6361ad7 | |||
| 10669e0e36 | |||
| 69bcdf36c7 | |||
| 5ba17c9642 | |||
| 7fb9750717 | |||
| c95d9d68ea | |||
| 3aef7f604a | |||
| f5fe6d0bf6 | |||
| 94cc41900b | |||
| d649b3581e | |||
| 229b82bef5 | |||
| 531be77138 | |||
| 4cf0410b7c | |||
| 2ec5a97f4a | |||
| ddc4d31861 | |||
| 85dd39427b | |||
| 8b65fa771e | |||
| fa6cec26bf | |||
| 436973f470 | |||
| a2796784ab | |||
| d355d1f6aa | |||
| 71c5e36268 | |||
| 44ae572e2f | |||
| 7cd9cdb0eb | |||
| 090ee7c59a | |||
| 8a5705a253 | |||
| 344bb9092d | |||
| ef325733bb | |||
| 18798a7546 | |||
| bbce33778e | |||
| fc52a65c70 | |||
| 06e593d32e | |||
| 16a6fe15f2 | |||
| d8d2a6a1d2 | |||
| 4c35f89c2a | |||
| 5c9aa9e4d6 | |||
| ca576c27fc | |||
| 85a21f2359 | |||
| 075fdb8ac4 | |||
| 4429ffa9f3 | |||
| 73f2016a6a | |||
| 381c113d66 | |||
| 0329c852bd | |||
| f6ff5c648b | |||
| 12d68caa58 | |||
| ff32b4e2ad | |||
| c59882cce1 | |||
| 4d66d0c0a6 | |||
| bee0facaca | |||
| af3501636c | |||
| e33d9b4dcf | |||
| 31aaa30fcd | |||
| ec5882a31a | |||
| 671c07e699 | |||
| f4d0092bb8 | |||
| fe74d65f2b | |||
| 9e4b59e815 | |||
| 53de75927c | |||
| bcc15ea5e7 | |||
| 21977633a0 | |||
| e4a0ac4b04 | |||
| 681f1a6396 | |||
| bf30bf9115 | |||
| 948231a31d | |||
| 3657457d81 | |||
| 4bfb137e81 | |||
| 835496f0b6 | |||
| 519156138d | |||
| ac59d6d060 | |||
| baccdb4cfc | |||
| c95b3d6fc5 | |||
| d6f146f8a5 | |||
| 1f00504d25 | |||
| 8b2c1e5d23 | |||
| fa400d9dcb | |||
| 10453d1ef8 | |||
| 30623ce903 | |||
| da5cde5d1f | |||
| 3e86bb0f64 | |||
| 16bfebbf49 | |||
| 1f76b065fc | |||
| e2481cd66e | |||
| 8a05dcb70e | |||
| 261a2deb51 | |||
| 75a4f0d931 | |||
| b74711e63b | |||
| e3454a0187 | |||
| f72cbed479 | |||
| bc8214cb7c | |||
| 7f9e3fea81 | |||
| 1ca7462497 | |||
| 514c9391e0 | |||
| 7c633b604d | |||
| 51e9259fac | |||
| 0844d74fbe | |||
| a48e32bbf6 | |||
| 448e43c41e | |||
| 6fe7f17c89 | |||
| edb6f11b42 | |||
| d7758fe1a9 | |||
| a3376e24b1 | |||
| 6a51198a9b | |||
| a68a5ad5ba | |||
| 6ab92eee6d | |||
| b0c9f3d83b | |||
| b661977966 | |||
| 969b418291 | |||
| 8e9688792b | |||
| 9ac47df7dd | |||
| 6210d0b040 | |||
| 69d46e008b | |||
| f8a346b3ce | |||
| 0f6651bad9 | |||
| 231543feaf | |||
| b3a39ccefd | |||
| 956b0a62a6 | |||
| 94e70043f9 | |||
| bf201a66c2 | |||
| 39cc7c6816 | |||
| e9ac494ffd | |||
| cb50caaf53 | |||
| ae95407775 | |||
| 7277284132 | |||
| d486e7fa47 | |||
| 3c20641a2e | |||
| 5e20c809dc | |||
| 2b875cfb33 | |||
| 228c75669f | |||
| d1fdb82291 | |||
| 247babbe1a | |||
| b1ea697b7d | |||
| 822d98a1ef | |||
| 5ac29dcb50 | |||
| 9453c6d9d2 | |||
| 0c4333ae81 | |||
| ce82bef6f1 | |||
| cd9895061b | |||
| ccd6a22ea5 | |||
| 5e5f0dce9c | |||
| f5079765ff | |||
| 24fc3a2288 | |||
| 8961298859 | |||
| 4f54f62abc | |||
| eaa7e97461 | |||
| 59e30ecf37 | |||
| 3396814797 | |||
| fb9a7aa533 | |||
| 72f33c4615 | |||
| 16f2bf8f9b | |||
| 5f56d6582f | |||
| 76882ff75a | |||
| 3cac2672d0 | |||
| 7a3641df5a | |||
| b3f954b0af | |||
| f671c2e3d7 | |||
| 118594f663 | |||
| f4cad086bb | |||
| 51fd64c334 | |||
| 0ffb2a9681 | |||
| ca22ca57e1 | |||
| e9fe73d7ab | |||
| 28f377d169 | |||
| 02d35262f9 | |||
| 8df8488947 | |||
| 17ee7ffaca | |||
| bf4c813db0 | |||
| 070e12f76f | |||
| 9385666e08 | |||
| dfaf698b44 | |||
| 12f160fb31 | |||
| b58c2dd0ad | |||
| 4bb794d814 | |||
| 7ce13a3c2c | |||
| 19bfbe07f8 | |||
| fd0560b951 | |||
| 4cfda479fa | |||
| eabcc8a002 | |||
| cbfe84f1a9 | |||
| db975b42a0 | |||
| 7963218495 | |||
| bd04901454 | |||
| c3f92c402e | |||
| 53d6b4ca04 | |||
| 39832c0633 | |||
| 2676efb222 | |||
| a927a9b0ad | |||
| 8ed7a1e4e6 | |||
| d89e4b3d33 | |||
| 9589ab8949 | |||
| 3212325173 | |||
| 850304f43b | |||
| ed245c6c64 | |||
| 1018d9bfd1 | |||
| a1bfc80577 | |||
| 1b197be7fc | |||
| 9e582fa0eb | |||
| a1c9acb8b1 | |||
| 42120d1177 | |||
| b5074a88ec | |||
| ce74c36118 | |||
| 26b1438f20 | |||
| b83e5e0ea6 | |||
| f6aea819f3 | |||
| 647ee28ca1 | |||
| d117b50fd3 | |||
| ed8c2b95b8 | |||
| aba2269ddb | |||
| 967c69b2da | |||
| 5a6e4f1ddd | |||
| f55ca6c489 | |||
| 4b73b51e12 | |||
| eccc7c35ca | |||
| 7c4813b417 | |||
| acc04c2826 | |||
| 8e9bd6742f | |||
| 89f5ab8aaf | |||
| dc0426d836 | |||
| 6761d4c6d1 | |||
| 3426969221 | |||
| 9ce3032678 | |||
| 4cf0869330 | |||
| 201917a1be | |||
| 08e580a23b | |||
| 2b3e777340 | |||
| cfc1d534cb | |||
| 4ffe1b8e5a | |||
| d8825b1961 | |||
| f5289a35ec | |||
| fc99d90337 | |||
| eb509a4b8b | |||
| 09ac8ac24b | |||
| adb8486e5f | |||
| 1e151e2030 | |||
| fe7d16de91 | |||
| 075076975d | |||
| 8be16c64f9 | |||
| f79af6bfa3 | |||
| efb1e25690 | |||
| 36b242a710 | |||
| 659be5a690 | |||
| 46c04fa71e | |||
| 5e8be14d78 | |||
| 72e05d623c | |||
| ff1547ea04 | |||
| 7a393a972b | |||
| 92b6c1401a | |||
| 0afc8b58d2 | |||
| 1082ec2414 | |||
| 727eb26cb6 | |||
| 175c6fae0f | |||
| 3486b530be | |||
| bc5efc1767 | |||
| 90adaa3cc8 | |||
| 8786a6c6e2 | |||
| 246325085f | |||
| 68c76c9b2e | |||
| facefe8fed | |||
| 8e70e3b58b | |||
| 730c412925 | |||
| 2418feaf19 | |||
| c2591f428b | |||
| 0872fb6699 | |||
| 98b3327c75 | |||
| 992fdf2071 | |||
| bd9a84a0b7 | |||
| 24e04bad94 | |||
| e6079ca5e2 | |||
| bef909f5cb | |||
| 0b97fea56f | |||
| 2ea95ca08d | |||
| cb9db98642 | |||
| 58e2c2410f | |||
| 6040149cf8 | |||
| 17cdb0091b | |||
| 730c2617af | |||
| 757d5250dc | |||
| 163a92ebe0 | |||
| c79c761460 | |||
| e474e56b80 | |||
| 69307d0e2b | |||
| 9852502636 | |||
| 7a0bf9d332 | |||
| 2e7ea96818 | |||
| 932e2970ad | |||
| 373d10edb1 | |||
| bd0f74fbd9 | |||
| e57e626480 | |||
| cb04822812 | |||
| 647d5adf85 | |||
| 6df51733d3 | |||
| 9c9d92413e | |||
| c6eec286a8 | |||
| f6070f0084 | |||
| 87e6f21765 | |||
| 3afc572f6b | |||
| fea981a3f6 | |||
| 799c7ed3ce | |||
| b2a27347e8 | |||
| 215472dda2 | |||
| 8338bc888a | |||
| 5d15ef9625 | |||
| 42e31ef6db | |||
| deac4f5141 | |||
| 79b730ccf3 | |||
| ec3e5e3914 | |||
| 5f9919dcf7 | |||
| 608aa94fc3 | |||
| 9998d9d9bc | |||
| 924cc3e86c | |||
| 25b8e314af | |||
| 43431d2a78 | |||
| b36776f4c6 | |||
| f23fbb84e2 | |||
| 87599daef1 | |||
| 374a38ed36 | |||
| b501c380af | |||
| 7dd11fa3f2 | |||
| defc2f3403 | |||
| 9a27aa98b7 | |||
| 4458b5fa13 | |||
| 7cd34edcc2 | |||
| 7b18e51bf8 | |||
| cd927e9cda | |||
| b832c85923 | |||
| 33bb89beda | |||
| 33bde69750 | |||
| 2422a6518f | |||
| 0768f07b17 | |||
| 2f3d5977d1 | |||
| 8d38ebd251 | |||
| d630894d9b | |||
| 8f617ca624 | |||
| 57e3bd72a8 | |||
| 6b18873d23 | |||
| c1c455acc8 | |||
| f2f1f5f9bd | |||
| 7212e2b7d7 | |||
| fc84308cc9 | |||
| 91bbc4fd2c | |||
| 696dc2dc36 | |||
| 96571b93c1 | |||
| 3db5fb742e | |||
| c561fdf8b1 | |||
| f7480d5b42 | |||
| 93ebee8faf | |||
| 20350a3c07 | |||
| 8cd4d939f3 | |||
| 98e66f898e | |||
| 85a32a18e2 | |||
| 90648c0e2d | |||
| 6ef3d1316f | |||
| f3a2374dda | |||
| cbadb33d51 | |||
| 17e1b1fab7 | |||
| 6eea3e4d44 | |||
| 48726c7fed | |||
| 47c5cd71ea | |||
| a9bcd369f3 | |||
| b3e70414d8 | |||
| c9d851e9a4 | |||
| 86d71e6fc8 | |||
| 542f98190b | |||
| 16c0907607 | |||
| 292e32292b | |||
| 3c5cac3f3f | |||
| 2f7b724d5a | |||
| 318c08a97e | |||
| d6b97cf34d | |||
| 82e3344aed | |||
| 6349c1a0c7 | |||
| f53737ef23 | |||
| 1a88356dc3 | |||
| 5d4b549dc3 | |||
| d044ada7aa | |||
| 77a20a3a9f | |||
| 9821da8df7 | |||
| 2d538baf72 | |||
| c7e8ddd6b5 | |||
| 6fcebec713 | |||
| 993c793565 | |||
| f8edc59025 | |||
| 3af9de7407 | |||
| 72ea816cf7 | |||
| d641dae5f1 | |||
| ccf32821df | |||
| 8e23ce87d3 | |||
| 5a3522cf2e | |||
| aaec001931 | |||
| 3d9e5070f5 | |||
| 82a1b0e85c | |||
| 600a669d12 | |||
| 5aa12b5e09 | |||
| 6a6edf5ae6 | |||
| aeda7e389f | |||
| 09e79f0587 | |||
| 4a9270aa38 | |||
| 59f5331db4 | |||
| 74bd8cac60 | |||
| 542d64f0be | |||
| 5e611205ac | |||
| 2e77f1c777 | |||
| 8c49721c70 | |||
| 2fc01ff73a | |||
| bba08ad95f | |||
| 034dafddac | |||
| 6aa9a30b02 | |||
| f2dccb753f | |||
| 471f3b9748 | |||
| a927125909 | |||
| 9062cac3f0 | |||
| 059a80661d | |||
| 37e36bf137 | |||
| 3c2086eb2a | |||
| 1ff0b657be | |||
| 17dbfa7004 | |||
| 52900c89f7 | |||
| be8f4f6fb9 | |||
| 1482f1ae72 | |||
| 0f78345d0c | |||
| 4eeda34ad2 | |||
| ed1be562e2 | |||
| 04b359d15b | |||
| f07f70ca42 | |||
| 28d35180bd | |||
| 0e22301e8f | |||
| 4e5cf17311 | |||
| a4ca50533d | |||
| 1aa512d227 | |||
| 2f4644529c | |||
| d109e31f62 | |||
| 5168037b5a | |||
| 92535a9158 | |||
| 4f309bb3aa | |||
| b4c949cc07 | |||
| 90bb568faf | |||
| 60040e60fb | |||
| a00054e26b | |||
| 81071dd22c | |||
| c29223a98b | |||
| 5d99572006 | |||
| d83e1c7484 | |||
| 201ecbd7bc | |||
| 9333d1210d | |||
| 2615fa4bd1 | |||
| 3e5bc84058 | |||
| 5d8480fa8a | |||
| 56b0ce56f8 | |||
| 9dc4bd47e6 | |||
| 4fb42d7682 | |||
| b0df69ddec | |||
| b324d002ca | |||
| b291a1ad8c | |||
| dec196b4d9 | |||
| aba636ec38 | |||
| 038d9b3fee | |||
| 6a80531513 | |||
| a1b1b160cf | |||
| f368f9177c | |||
| a7bb51d8a5 | |||
| 357710f136 | |||
| ca9b9671a5 | |||
| e0aa705f71 | |||
| 51d2543ba1 | |||
| e5899cad1e | |||
| 0397f875c5 | |||
| 2cf88d4e77 | |||
| 32d7e52182 | |||
| 6fbdf4b445 | |||
| 0d4d8813ac | |||
| 935053cc33 | |||
| 2b319bef2f | |||
| 76e1bb4c06 | |||
| 7f8a323efe | |||
| ee388dc149 | |||
| 54948aad6f | |||
| 5e18cb6298 | |||
| ad50cb7efe | |||
| ed6b999623 | |||
| e7d5e72a87 | |||
| 57e990e6e0 |
+9
-2
@@ -1,4 +1,11 @@
|
||||
dist
|
||||
lib/handlebars/parser.js
|
||||
vendor
|
||||
.rvmrc
|
||||
.DS_Store
|
||||
lib/handlebars/compiler/parser.js
|
||||
/dist/
|
||||
/tmp/
|
||||
node_modules
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
npm-debug.log
|
||||
sauce_connect.log*
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"predef": [
|
||||
"console",
|
||||
"require",
|
||||
"suite",
|
||||
"equal",
|
||||
"equals",
|
||||
"test",
|
||||
"testBoth",
|
||||
"raises",
|
||||
"deepEqual",
|
||||
"start",
|
||||
"stop",
|
||||
"ok",
|
||||
"strictEqual",
|
||||
"module",
|
||||
"define",
|
||||
|
||||
"describe",
|
||||
"it",
|
||||
"afterEach",
|
||||
"beforeEach"
|
||||
],
|
||||
|
||||
"node" : true,
|
||||
"browser" : true,
|
||||
"esnext": true,
|
||||
|
||||
"boss" : true,
|
||||
"curly": false,
|
||||
"debug": false,
|
||||
"devel": false,
|
||||
"eqeqeq": false,
|
||||
"eqnull": true,
|
||||
"evil": true,
|
||||
"forin": false,
|
||||
"immed": false,
|
||||
"laxbreak": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"noempty": false,
|
||||
"nonew": false,
|
||||
"nomen": false,
|
||||
"onevar": false,
|
||||
"plusplus": false,
|
||||
"regexp": false,
|
||||
"undef": true,
|
||||
"sub": true,
|
||||
"strict": false,
|
||||
"white": false
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
.gitignore
|
||||
.rvmrc
|
||||
.jshintrc
|
||||
.travis.yml
|
||||
.rspec
|
||||
Gemfile
|
||||
Gemfile.lock
|
||||
Rakefile
|
||||
Gruntfile.js
|
||||
*.gemspec
|
||||
*.nuspec
|
||||
bench/*
|
||||
configurations/*
|
||||
components/*
|
||||
dist/cdnjs/*
|
||||
dist/components/*
|
||||
spec/*
|
||||
src/*
|
||||
tasks/*
|
||||
tmp/*
|
||||
publish/*
|
||||
vendor/*
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.8"
|
||||
|
||||
before_install:
|
||||
- npm install -g grunt-cli
|
||||
|
||||
script:
|
||||
- grunt --stack travis
|
||||
|
||||
email:
|
||||
on_failure: change
|
||||
on_success: never
|
||||
env:
|
||||
global:
|
||||
- S3_BUCKET_NAME=builds.handlebarsjs.com
|
||||
- secure: PJaukuvkBBsSDOqbIcNSSMgb96VVEaIt/eq9GPjXPeFbSd3hXgwhwVE62LrqtJO8BaUfX+PzpiQjEl4D5/KBmvlFZ057Hqmy0zmPOT5mDZfJe8Ja5zyvTMb+KkCWN/tjAp8kawHojE04pn6jIpPdwXFnAYwPhaHbATFrmdt9fdg=
|
||||
- secure: mBcGL2tnmiRujJdV/4fxrVd8E8wn6AW9IQKVcMv8tvOc7i5dOzZ39rpBKLuTMRXDtMV1LyLiuKYb1pHj1IyeadEahcLYFfGygF4LG7Yzp4NWHtRzQ7Q8LXaJV7dXDboYCFkn2a8/Rtx1YSVh/sCONf5UoRC+MUIqrj4UiHN9r3s=
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- node_js: "0.10"
|
||||
env:
|
||||
- PUBLISH=true
|
||||
- secure: pLTzghtVll9yGKJI0AaB0uI8GypfWxLTaIB0ZL8//yN3nAEIKMhf/RRilYTsn/rKj2NUa7vt2edYILi3lttOUlCBOwTc9amiRms1W8Lwr/3IdWPeBLvLuH1zNJRm2lBAwU4LBSqaOwhGaxOQr6KHTnWudhNhgOucxpZfvfI/dFw=
|
||||
- secure: yERYCf7AwL11D9uMtacly/THGV8BlzsMmrt+iQVvGA3GaY6QMmfYqf6P6cCH98sH5etd1Y+1e6YrPeMjqI6lyRllT7FptoyOdHulazQe86VQN4sc0EpqMlH088kB7gGjTut9Z+X9ViooT5XEh9WA5jXEI9pXhQJNoIHkWPuwGuY=
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
@@ -1,4 +0,0 @@
|
||||
source "http://rubygems.org"
|
||||
|
||||
gem "therubyracer", ">= 0.8.0"
|
||||
gem "rspec"
|
||||
@@ -1,20 +0,0 @@
|
||||
GEM
|
||||
remote: http://rubygems.org/
|
||||
specs:
|
||||
diff-lcs (1.1.2)
|
||||
rspec (2.1.0)
|
||||
rspec-core (~> 2.1.0)
|
||||
rspec-expectations (~> 2.1.0)
|
||||
rspec-mocks (~> 2.1.0)
|
||||
rspec-core (2.1.0)
|
||||
rspec-expectations (2.1.0)
|
||||
diff-lcs (~> 1.1.2)
|
||||
rspec-mocks (2.1.0)
|
||||
therubyracer (0.8.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
rspec
|
||||
therubyracer (>= 0.8.0)
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
var childProcess = require('child_process');
|
||||
|
||||
module.exports = function(grunt) {
|
||||
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
|
||||
jshint: {
|
||||
options: {
|
||||
jshintrc: '.jshintrc'
|
||||
},
|
||||
files: [
|
||||
'dist/cjs/**/!(*.min|parser).js'
|
||||
]
|
||||
},
|
||||
|
||||
clean: ['tmp', 'dist', 'lib/handlebars/compiler/parser.js'],
|
||||
|
||||
copy: {
|
||||
dist: {
|
||||
options: {
|
||||
processContent: function(content, path) {
|
||||
return grunt.template.process('/*!\n\n <%= pkg.name %> v<%= pkg.version %>\n\n<%= grunt.file.read("LICENSE") %>\n@license\n*/\n')
|
||||
+ content;
|
||||
}
|
||||
},
|
||||
files: [
|
||||
{expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/'}
|
||||
]
|
||||
},
|
||||
cdnjs: {
|
||||
files: [
|
||||
{expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/cdnjs'}
|
||||
]
|
||||
},
|
||||
components: {
|
||||
files: [
|
||||
{expand: true, cwd: 'components/', src: ['**'], dest: 'dist/components'},
|
||||
{expand: true, cwd: 'dist/', src: ['*.js'], dest: 'dist/components'}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
packager: {
|
||||
global: {
|
||||
type: 'global',
|
||||
export: 'Handlebars',
|
||||
files: [{
|
||||
cwd: 'lib/',
|
||||
expand: true,
|
||||
src: ['handlebars*.js'],
|
||||
dest: 'dist/'
|
||||
}]
|
||||
},
|
||||
|
||||
amd: {
|
||||
type: 'amd',
|
||||
anonymous: true,
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: 'lib/',
|
||||
src: '**/!(index).js',
|
||||
dest: 'dist/amd/'
|
||||
}]
|
||||
},
|
||||
|
||||
cjs: {
|
||||
type: 'cjs',
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: 'lib/',
|
||||
src: '**/!(index).js',
|
||||
dest: 'dist/cjs/'
|
||||
}]
|
||||
}
|
||||
},
|
||||
requirejs: {
|
||||
options: {
|
||||
optimize: "none",
|
||||
baseUrl: "dist/amd/"
|
||||
},
|
||||
dist: {
|
||||
options: {
|
||||
name: "handlebars",
|
||||
out: "dist/handlebars.amd.js"
|
||||
}
|
||||
},
|
||||
runtime: {
|
||||
options: {
|
||||
name: "handlebars.runtime",
|
||||
out: "dist/handlebars.runtime.amd.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
uglify: {
|
||||
options: {
|
||||
mangle: true,
|
||||
compress: true,
|
||||
preserveComments: 'some'
|
||||
},
|
||||
dist: {
|
||||
files: [{
|
||||
cwd: 'dist/',
|
||||
expand: true,
|
||||
src: ['handlebars*.js', '!*.min.js'],
|
||||
dest: 'dist/',
|
||||
rename: function(dest, src) {
|
||||
return dest + src.replace(/\.js$/, '.min.js');
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
|
||||
concat: {
|
||||
tests: {
|
||||
src: ['spec/!(require).js'],
|
||||
dest: 'tmp/tests.js'
|
||||
}
|
||||
},
|
||||
|
||||
connect: {
|
||||
server: {
|
||||
options: {
|
||||
base: '.',
|
||||
hostname: '*',
|
||||
port: 9999
|
||||
}
|
||||
}
|
||||
},
|
||||
'saucelabs-mocha': {
|
||||
all: {
|
||||
options: {
|
||||
build: process.env.TRAVIS_JOB_ID,
|
||||
urls: ['http://localhost:9999/spec/?headless=true', 'http://localhost:9999/spec/amd.html?headless=true'],
|
||||
detailedError: true,
|
||||
concurrency: 2,
|
||||
browsers: [
|
||||
{browserName: 'chrome'},
|
||||
{browserName: 'firefox'},
|
||||
{browserName: 'firefox', version: '3.6'},
|
||||
{browserName: 'safari', version: 7, platform: 'OS X 10.9'},
|
||||
{browserName: 'safari', version: 6, platform: 'OS X 10.8'},
|
||||
{browserName: 'opera', version: 12},
|
||||
{browserName: 'opera', version: 11},
|
||||
{browserName: 'internet explorer', version: 11, platform: 'Windows 8.1'},
|
||||
{browserName: 'internet explorer', version: 10, platform: 'Windows 8'},
|
||||
{browserName: 'internet explorer', version: 9, platform: 'Windows 7'},
|
||||
{browserName: 'internet explorer', version: 6, platform: 'XP'}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
scripts: {
|
||||
options: {
|
||||
atBegin: true
|
||||
},
|
||||
|
||||
files: ['src/*', 'lib/**/*.js', 'spec/**/*.js'],
|
||||
tasks: ['build', 'amd', 'tests', 'test']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Build a new version of the library
|
||||
this.registerTask('build', "Builds a distributable version of the current project", [
|
||||
'parser',
|
||||
'node',
|
||||
'globals',
|
||||
'jshint']);
|
||||
|
||||
this.registerTask('amd', ['packager:amd', 'requirejs']);
|
||||
this.registerTask('node', ['packager:cjs']);
|
||||
this.registerTask('globals', ['packager:global']);
|
||||
this.registerTask('tests', ['concat:tests']);
|
||||
|
||||
this.registerTask('release', 'Build final packages', ['amd', 'jshint', 'uglify', 'copy:dist', 'copy:components', 'copy:cdnjs']);
|
||||
|
||||
// Load tasks from npm
|
||||
grunt.loadNpmTasks('grunt-contrib-clean');
|
||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||
grunt.loadNpmTasks('grunt-contrib-connect');
|
||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||
grunt.loadNpmTasks('grunt-contrib-requirejs');
|
||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-saucelabs');
|
||||
grunt.loadNpmTasks('es6-module-packager');
|
||||
|
||||
grunt.task.loadTasks('tasks');
|
||||
|
||||
grunt.registerTask('bench', ['metrics']);
|
||||
grunt.registerTask('sauce', process.env.SAUCE_USERNAME ? ['tests', 'connect', 'saucelabs-mocha'] : []);
|
||||
|
||||
grunt.registerTask('travis', process.env.PUBLISH ? ['default', 'sauce', 'metrics', 'publish:latest'] : ['default']);
|
||||
|
||||
grunt.registerTask('dev', ['clean', 'connect', 'watch']);
|
||||
grunt.registerTask('default', ['clean', 'build', 'test', 'release']);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (C) 2011-2014 by Yehuda Katz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+357
-93
@@ -1,33 +1,59 @@
|
||||
[](https://travis-ci.org/wycats/handlebars.js)
|
||||
[](https://saucelabs.com/u/handlebars)
|
||||
|
||||
Handlebars.js
|
||||
=============
|
||||
|
||||
Handlebars.js is an extension to the [Mustache templating language](http://mustache.github.com/) created by Chris Wanstrath. Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.
|
||||
Handlebars.js is an extension to the [Mustache templating
|
||||
language](http://mustache.github.com/) created by Chris Wanstrath.
|
||||
Handlebars.js and Mustache are both logicless templating languages that
|
||||
keep the view and the code separated like we all know they should be.
|
||||
|
||||
Checkout the official Handlebars docs site at
|
||||
[http://www.handlebarsjs.com](http://www.handlebarsjs.com).
|
||||
|
||||
Installing
|
||||
----------
|
||||
Installing Handlebars is easy. Simply [download the package from GitHub](https://github.com/wycats/handlebars.js/archives/master) and add it to your web pages (you should usually use the most recent version).
|
||||
Installing Handlebars is easy. Simply download the package [from the official site](http://handlebarsjs.com/) or the [bower repository][bower-repo] and add it to your web pages (you should usually use the most recent version).
|
||||
|
||||
Alternatively, if you prefer having the latest version of handlebars from
|
||||
the 'master' branch, passing builds of the 'master' branch are automatically
|
||||
published to S3. You may download the latest passing master build by grabbing
|
||||
a `handlebars-latest.js` file from the [builds page][builds-page]. When the
|
||||
build is published, it is also available as a `handlebars-gitSHA.js` file on
|
||||
the builds page if you need a version to refer to others.
|
||||
`handlebars-runtime.js` builds are also available.
|
||||
|
||||
**Note**: The S3 builds page is provided as a convenience for the community,
|
||||
but you should not use it for hosting Handlebars in production.
|
||||
|
||||
Usage
|
||||
-----
|
||||
In general, the syntax of Handlebars.js templates is a superset of Mustache templates. For basic syntax, check out the [Mustache manpage](http://mustache.github.com/mustache.5.html).
|
||||
In general, the syntax of Handlebars.js templates is a superset
|
||||
of Mustache templates. For basic syntax, check out the [Mustache
|
||||
manpage](http://mustache.github.com/mustache.5.html).
|
||||
|
||||
Once you have a template, use the Handlebars.compile method to compile the template into a function. The generated function takes a context argument, which will be used to render the template.
|
||||
Once you have a template, use the `Handlebars.compile` method to compile
|
||||
the template into a function. The generated function takes a context
|
||||
argument, which will be used to render the template.
|
||||
|
||||
var source = "<p>Hello, my name is {{name}}. I am from {{hometown}}. I have " +
|
||||
"{{kids/length}} kids:</p>" +
|
||||
"<ul>{{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}</ul>";
|
||||
var template = Handlebars.compile(source);
|
||||
```js
|
||||
var source = "<p>Hello, my name is {{name}}. I am from {{hometown}}. I have " +
|
||||
"{{kids.length}} kids:</p>" +
|
||||
"<ul>{{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}</ul>";
|
||||
var template = Handlebars.compile(source);
|
||||
|
||||
var data = { "name": "Alan", "hometown": "Somewhere, TX",
|
||||
"kids": [{"name": "Jimmy", "age": "12"}, {"name": "Sally", "age": "4"}]};
|
||||
var result = template(data);
|
||||
var data = { "name": "Alan", "hometown": "Somewhere, TX",
|
||||
"kids": [{"name": "Jimmy", "age": "12"}, {"name": "Sally", "age": "4"}]};
|
||||
var result = template(data);
|
||||
|
||||
// Would render:
|
||||
// <p>Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:</p>
|
||||
// <ul>
|
||||
// <li>Jimmy is 12</li>
|
||||
// <li>Sally is 4</li>
|
||||
// </ul>
|
||||
// Would render:
|
||||
// <p>Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:</p>
|
||||
// <ul>
|
||||
// <li>Jimmy is 12</li>
|
||||
// <li>Sally is 4</li>
|
||||
// </ul>
|
||||
```
|
||||
|
||||
|
||||
Registering Helpers
|
||||
@@ -37,22 +63,31 @@ You can register helpers that Handlebars will use when evaluating your
|
||||
template. Here's an example, which assumes that your objects have a URL
|
||||
embedded in them, as well as the text for a link:
|
||||
|
||||
Handlebars.registerHelper('link_to', function(context) {
|
||||
return "<a href='" + context.url + "'>" + context.body + "</a>";
|
||||
});
|
||||
```js
|
||||
Handlebars.registerHelper('link_to', function() {
|
||||
return "<a href='" + this.url + "'>" + this.body + "</a>";
|
||||
});
|
||||
|
||||
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
|
||||
var source = "<ul>{{#posts}}<li>{{{link_to this}}}</li></ul>"
|
||||
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
|
||||
var source = "<ul>{{#posts}}<li>{{{link_to}}}</li>{{/posts}}</ul>"
|
||||
|
||||
var template = Handlebars.compile(source);
|
||||
template(context);
|
||||
var template = Handlebars.compile(source);
|
||||
template(context);
|
||||
|
||||
// Would render:
|
||||
//
|
||||
// <ul>
|
||||
// <li><a href='/hello-world'>Hello World!</a></li>
|
||||
// </ul>
|
||||
// Would render:
|
||||
//
|
||||
// <ul>
|
||||
// <li><a href='/hello-world'>Hello World!</a></li>
|
||||
// </ul>
|
||||
```
|
||||
|
||||
Helpers take precedence over fields defined on the context. To access a field
|
||||
that is masked by a helper, a path reference may be used. In the example above
|
||||
a field named `link_to` on the `context` object would be referenced using:
|
||||
|
||||
```
|
||||
{{./link_to}}
|
||||
```
|
||||
|
||||
Escaping
|
||||
--------
|
||||
@@ -67,48 +102,67 @@ To explicitly *not* escape the contents, use the triple-mustache
|
||||
|
||||
Differences Between Handlebars.js and Mustache
|
||||
----------------------------------------------
|
||||
Handlebars.js adds a couple of additional features to make writing templates easier and also changes a tiny detail of how partials work.
|
||||
Handlebars.js adds a couple of additional features to make writing
|
||||
templates easier and also changes a tiny detail of how partials work.
|
||||
|
||||
### Paths
|
||||
|
||||
Handlebars.js supports an extended expression syntax that we call paths. Paths are made up of typical expressions and / characters. Expressions allow you to not only display data from the current context, but to display data from contexts that are descendents and ancestors of the current context.
|
||||
Handlebars.js supports an extended expression syntax that we call paths.
|
||||
Paths are made up of typical expressions and . characters. Expressions
|
||||
allow you to not only display data from the current context, but to
|
||||
display data from contexts that are descendants and ancestors of the
|
||||
current context.
|
||||
|
||||
To display data from descendent contexts, use the `/` character. So, for example, if your data were structured like:
|
||||
To display data from descendant contexts, use the `.` character. So, for
|
||||
example, if your data were structured like:
|
||||
|
||||
var data = {"person": { "name": "Alan" }, company: {"name": "Rad, Inc." } };
|
||||
```js
|
||||
var data = {"person": { "name": "Alan" }, "company": {"name": "Rad, Inc." } };
|
||||
```
|
||||
|
||||
you could display the person's name from the top-level context with the following expression:
|
||||
You could display the person's name from the top-level context with the
|
||||
following expression:
|
||||
|
||||
{{person/name}}
|
||||
```
|
||||
{{person.name}}
|
||||
```
|
||||
|
||||
Similarly, if already traversed into the person object you could still display the company's name with an expression like `{{../company/name}}`, so:
|
||||
You can backtrack using `../`. For example, if you've already traversed
|
||||
into the person object you could still display the company's name with
|
||||
an expression like `{{../company.name}}`, so:
|
||||
|
||||
{{#person}}{{name}} - {{../company/name}}{{/person}}
|
||||
```
|
||||
{{#with person}}{{name}} - {{../company.name}}{{/person}}
|
||||
```
|
||||
|
||||
would render:
|
||||
|
||||
Alan - Rad, Inc.
|
||||
```
|
||||
Alan - Rad, Inc.
|
||||
```
|
||||
|
||||
### Strings
|
||||
|
||||
When calling a helper, you can pass paths or Strings as parameters. For
|
||||
instance:
|
||||
|
||||
Handlebars.registerHelper('link_to', function(title, context) {
|
||||
return "<a href='/posts" + context.id + "'>" + title + "</a>"
|
||||
});
|
||||
```js
|
||||
Handlebars.registerHelper('link_to', function(title, options) {
|
||||
return "<a href='/posts" + this.url + "'>" + title + "!</a>"
|
||||
});
|
||||
|
||||
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
|
||||
var source = '<ul>{{#posts}}<li>{{{link_to "Post" this}}}</li></ul>'
|
||||
var context = { posts: [{url: "/hello-world", body: "Hello World!"}] };
|
||||
var source = '<ul>{{#posts}}<li>{{{link_to "Post"}}}</li>{{/posts}}</ul>'
|
||||
|
||||
var template = Handlebars.compile(source);
|
||||
template(context);
|
||||
var template = Handlebars.compile(source);
|
||||
template(context);
|
||||
|
||||
// Would render:
|
||||
//
|
||||
// <ul>
|
||||
// <li><a href='/hello-world'>Post!</a></li>
|
||||
// </ul>
|
||||
// Would render:
|
||||
//
|
||||
// <ul>
|
||||
// <li><a href='/posts/hello-world'>Post!</a></li>
|
||||
// </ul>
|
||||
```
|
||||
|
||||
When you pass a String as a parameter to a helper, the literal String
|
||||
gets passed to the helper function.
|
||||
@@ -116,27 +170,42 @@ gets passed to the helper function.
|
||||
|
||||
### Block Helpers
|
||||
|
||||
Handlebars.js also adds the ability to define block helpers. Block helpers are functions that can be called from anywhere in the template. Here's an example:
|
||||
Handlebars.js also adds the ability to define block helpers. Block
|
||||
helpers are functions that can be called from anywhere in the template.
|
||||
Here's an example:
|
||||
|
||||
var source = "<ul>{{#people}}<li>{{{#link}}}{{name}}{{/link}}</li>{{/people}}</ul>";
|
||||
Handlebars.registerHelper('link', function(context, fn) {
|
||||
return '<a href="/people/' + this.__get__("id") + '">' + fn(this) + '</a>';
|
||||
});
|
||||
var template = Handlebars.compile(source);
|
||||
```js
|
||||
var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>";
|
||||
Handlebars.registerHelper('link', function(options) {
|
||||
return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
|
||||
});
|
||||
var template = Handlebars.compile(source);
|
||||
|
||||
var data = { "people": [
|
||||
{ "name": "Alan", "id": 1 },
|
||||
{ "name": "Yehuda", "id": 2 }
|
||||
]};
|
||||
template(data);
|
||||
var data = { "people": [
|
||||
{ "name": "Alan", "id": 1 },
|
||||
{ "name": "Yehuda", "id": 2 }
|
||||
]};
|
||||
template(data);
|
||||
|
||||
// Should render:
|
||||
// <ul>
|
||||
// <li><a href="/people/1">Alan</a></li>
|
||||
// <li><a href="/people/2">Yehuda</a></li>
|
||||
// </ul>
|
||||
// Should render:
|
||||
// <ul>
|
||||
// <li><a href="/people/1">Alan</a></li>
|
||||
// <li><a href="/people/2">Yehuda</a></li>
|
||||
// </ul>
|
||||
```
|
||||
|
||||
Whenever the block helper is called it is given two parameters, the argument that is passed to the helper, or the current context if no argument is passed and the compiled contents of the block. Inside of the block helper the value of `this` is the current context, wrapped to include a method named `__get__` that helps translate paths into values within the helpers.
|
||||
Whenever the block helper is called it is given one or more parameters,
|
||||
any arguments that are passed in the helper in the call and an `options`
|
||||
object containing the `fn` function which executes the block's child.
|
||||
The block's current context may be accessed through `this`.
|
||||
|
||||
Block helpers have the same syntax as mustache sections but should not be
|
||||
confused with one another. Sections are akin to an implicit `each` or
|
||||
`with` statement depending on the input data and helpers are explicit
|
||||
pieces of code that are free to implement whatever behavior they like.
|
||||
The [mustache spec](http://mustache.github.io/mustache.5.html)
|
||||
defines the exact behavior of sections. In the case of name conflicts,
|
||||
helpers are given priority.
|
||||
|
||||
### Partials
|
||||
|
||||
@@ -145,61 +214,256 @@ Handlebars when it encounters a partial (`{{> partialName}}`). Partials
|
||||
can either be String templates or compiled template functions. Here's an
|
||||
example:
|
||||
|
||||
var source = "<ul>{{#people}}<li>{{> link}}</li>{{/people}}</ul>";
|
||||
```js
|
||||
var source = "<ul>{{#people}}<li>{{> link}}</li>{{/people}}</ul>";
|
||||
|
||||
Handlebars.registerPartial('link', '<a href="/people/{{id}}">{{name}}</a>')
|
||||
var template = Handlebars.compile(source);
|
||||
Handlebars.registerPartial('link', '<a href="/people/{{id}}">{{name}}</a>')
|
||||
var template = Handlebars.compile(source);
|
||||
|
||||
var data = { "people": [
|
||||
{ "name": "Alan", "id": 1 },
|
||||
{ "name": "Yehuda", "id": 2 }
|
||||
]};
|
||||
var data = { "people": [
|
||||
{ "name": "Alan", "id": 1 },
|
||||
{ "name": "Yehuda", "id": 2 }
|
||||
]};
|
||||
|
||||
template(data);
|
||||
template(data);
|
||||
|
||||
// Should render:
|
||||
// <ul>
|
||||
// <li><a href="/people/1">Alan</a></li>
|
||||
// <li><a href="/people/2">Yehuda</a></li>
|
||||
// </ul>
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
You can add comments to your templates with the following syntax:
|
||||
|
||||
```js
|
||||
{{! This is a comment }}
|
||||
```
|
||||
|
||||
You can also use real html comments if you want them to end up in the output.
|
||||
|
||||
```html
|
||||
<div>
|
||||
{{! This comment will not end up in the output }}
|
||||
<!-- This comment will show up in the output -->
|
||||
</div>
|
||||
```
|
||||
|
||||
// Should render:
|
||||
// <ul>
|
||||
// <li><a href="/people/1">Alan</a></li>
|
||||
// <li><a href="/people/2">Yehuda</a></li>
|
||||
// </ul>
|
||||
|
||||
Precompiling Templates
|
||||
----------------------
|
||||
|
||||
TODO in the rewrite. This will use RubyRacer and not node.
|
||||
Handlebars allows templates to be precompiled and included as javascript
|
||||
code rather than the handlebars template allowing for faster startup time.
|
||||
|
||||
### Installation
|
||||
The precompiler script may be installed via npm using the `npm install -g handlebars`
|
||||
command.
|
||||
|
||||
### Usage
|
||||
|
||||
<pre>
|
||||
Precompile handlebar templates.
|
||||
Usage: handlebars template...
|
||||
|
||||
Options:
|
||||
-a, --amd Create an AMD format function (allows loading with RequireJS) [boolean]
|
||||
-f, --output Output File [string]
|
||||
-k, --known Known helpers [string]
|
||||
-o, --knownOnly Known helpers only [boolean]
|
||||
-m, --min Minimize output [boolean]
|
||||
-s, --simple Output template function only. [boolean]
|
||||
-r, --root Template root. Base value that will be stripped from template names. [string]
|
||||
-c, --commonjs Exports CommonJS style, path to Handlebars module [string]
|
||||
-h, --handlebarPath Path to handlebar.js (only valid for amd-style) [string]
|
||||
-n, --namespace Template namespace [string]
|
||||
-p, --partial Compiling a partial template [boolean]
|
||||
-d, --data Include data when compiling [boolean]
|
||||
-e, --extension Template extension. [string]
|
||||
-b, --bom Removes the BOM (Byte Order Mark) from the beginning of the templates. [boolean]
|
||||
</pre>
|
||||
|
||||
If using the precompiler's normal mode, the resulting templates will be
|
||||
stored to the `Handlebars.templates` object using the relative template
|
||||
name sans the extension. These templates may be executed in the same
|
||||
manner as templates.
|
||||
|
||||
If using the simple mode the precompiler will generate a single
|
||||
javascript method. To execute this method it must be passed to the using
|
||||
the `Handlebars.template` method and the resulting object may be as
|
||||
normal.
|
||||
|
||||
### Optimizations
|
||||
|
||||
- Rather than using the full _handlebars.js_ library, implementations that
|
||||
do not need to compile templates at runtime may include _handlebars.runtime.js_
|
||||
whose min+gzip size is approximately 1k.
|
||||
- If a helper is known to exist in the target environment they may be defined
|
||||
using the `--known name` argument may be used to optimize accesses to these
|
||||
helpers for size and speed.
|
||||
- When all helpers are known in advance the `--knownOnly` argument may be used
|
||||
to optimize all block helper references.
|
||||
- Implementations that do not use `@data` variables can improve performance of
|
||||
iteration centric templates by specifying `{data: false}` in the compiler options.
|
||||
|
||||
Supported Environments
|
||||
----------------------
|
||||
|
||||
Handlebars has been designed to work in any ECMAScript 3 environment. This includes
|
||||
|
||||
- Node.js
|
||||
- Chrome
|
||||
- Firefox
|
||||
- Safari 5+
|
||||
- Opera 11+
|
||||
- IE 6+
|
||||
|
||||
Older versions and other runtimes are likely to work but have not been formally
|
||||
tested.
|
||||
|
||||
[](https://saucelabs.com/u/handlebars)
|
||||
|
||||
Performance
|
||||
-----------
|
||||
|
||||
In a rough performance test, precompiled Handlebars.js templates (in the original version of Handlebars.js) rendered in about half the time of Mustache templates. It would be a shame if it were any other way, since they were precompiled, but the difference in architecture does have some big performance advantages. Justin Marney, a.k.a. [gotascii](http://github.com/gotascii), confirmed that with an [independent test](http://sorescode.com/2010/09/12/benchmarks.html). The rewritten Handlebars (current version) is faster than the old version, and we will have some benchmarks in the near future.
|
||||
In a rough performance test, precompiled Handlebars.js templates (in
|
||||
the original version of Handlebars.js) rendered in about half the
|
||||
time of Mustache templates. It would be a shame if it were any other
|
||||
way, since they were precompiled, but the difference in architecture
|
||||
does have some big performance advantages. Justin Marney, a.k.a.
|
||||
[gotascii](http://github.com/gotascii), confirmed that with an
|
||||
[independent test](http://sorescode.com/2010/09/12/benchmarks.html). The
|
||||
rewritten Handlebars (current version) is faster than the old version,
|
||||
and we will have some benchmarks in the near future.
|
||||
|
||||
|
||||
Building
|
||||
--------
|
||||
|
||||
To build handlebars, just run `rake release`, and you will get two files
|
||||
in the `dist` directory. The debug version comes with stack trace
|
||||
annotations for webkit browsers, but will slightly increase startup
|
||||
time.
|
||||
To build handlebars, just run `grunt build`, and the build will output to the `dist` directory.
|
||||
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
See [release-notes.md](https://github.com/wycats/handlebars.js/blob/master/release-notes.md) for upgrade notes.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
* Runtime/precompiler mismatches: Often result in errors like "can not find method match of object" or similar. Please verify the version of the runtime and the version used to precompile templates if odd issues occur after upgrading one component or another.
|
||||
* Handlebars.js can be cryptic when there's an error while rendering.
|
||||
* Using a variable, helper, or partial named `class` causes errors in IE browsers. (Instead, use `className`)
|
||||
|
||||
Handlebars Contrib
|
||||
------------------
|
||||
Alan Johnson, a.k.a. commondream, keeps a repository of useful helpers in the [handlebars-contrib](http://github.com/commondream/handlebars-contrib) repository. Feel free to copy those, or add some of your own.
|
||||
Reporting Issues
|
||||
----------------
|
||||
|
||||
Should you run into other issues with the project, please file an [issue][issue]. When filing issues a repo case running against the latest version of the code is appreciated. A [jsfiddle template][jsfiddle] is available for this purpose. As new versions are released the bitly link will be updated to point to a fiddle template with the latest version.
|
||||
|
||||
We also accept [pull requests][pull-request]!
|
||||
|
||||
Handlebars in the Wild
|
||||
-----------------
|
||||
* Don Park wrote an Express.js view engine adapter for Handlebars.js called [hbs](http://github.com/donpark/hbs)
|
||||
* [sammy.js](http://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey, supports Handlebars.js as one of its template plugins.
|
||||
----------------------
|
||||
|
||||
* [Assemble](http://assemble.io), by [@jonschlinkert](https://github.com/jonschlinkert)
|
||||
and [@doowb](https://github.com/doowb), is a static site generator that uses Handlebars.js
|
||||
as its template engine.
|
||||
* [CoSchedule](http://coschedule.com) An editorial calendar for WordPress that uses Handlebars.js
|
||||
* [Ember.js](http://www.emberjs.com) makes Handlebars.js the primary way to
|
||||
structure your views, also with automatic data binding support.
|
||||
* [Ghost](https://ghost.org/) Just a blogging platform.
|
||||
* [handlebars_assets](http://github.com/leshill/handlebars_assets): A Rails Asset Pipeline gem
|
||||
from Les Hill (@leshill).
|
||||
* [handlebars-helpers](https://github.com/assemble/handlebars-helpers) is an extensive library
|
||||
with 100+ handlebars helpers.
|
||||
* [hbs](http://github.com/donpark/hbs): An Express.js view engine adapter for Handlebars.js,
|
||||
from Don Park.
|
||||
* [koa-hbs](https://github.com/jwilm/koa-hbs): [koa](https://github.com/koajs/koa) generator based
|
||||
renderer for Handlebars.js.
|
||||
* [jblotus](http://github.com/jblotus) created [http://tryhandlebarsjs.com](http://tryhandlebarsjs.com)
|
||||
for anyone who would like to try out Handlebars.js in their browser.
|
||||
* [jQuery plugin](http://71104.github.io/jquery-handlebars/): allows you to use
|
||||
Handlebars.js with [jQuery](http://jquery.com/).
|
||||
* [Lumbar](http://walmartlabs.github.io/lumbar) provides easy module-based template management for
|
||||
handlebars projects.
|
||||
* [sammy.js](http://github.com/quirkey/sammy) by Aaron Quint, a.k.a. quirkey,
|
||||
supports Handlebars.js as one of its template plugins.
|
||||
* [SproutCore](http://www.sproutcore.com) uses Handlebars.js as its main
|
||||
templating engine, extending it with automatic data binding support.
|
||||
* [YUI](http://yuilibrary.com/yui/docs/handlebars/) implements a port of handlebars
|
||||
* [Swag](https://github.com/elving/swag) by [@elving](https://github.com/elving) is a growing collection of helpers for handlebars.js. Give your handlebars.js templates some swag son!
|
||||
* [DOMBars](https://github.com/blakeembrey/dombars) is a DOM-based templating engine built on the Handlebars parser and runtime
|
||||
|
||||
External Resources
|
||||
------------------
|
||||
|
||||
* [Gist about Synchronous and asynchronous loading of external handlebars templates](https://gist.github.com/2287070)
|
||||
|
||||
Have a project using Handlebars? Send us a [pull request][pull-request]!
|
||||
|
||||
Helping Out
|
||||
-----------
|
||||
If you notice any problems, please report them to the GitHub issue tracker at [http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues). Feel free to contact commondream or wycats through GitHub with any other questions or feature requests. To submit changes fork the project and send a pull request.
|
||||
|
||||
To build Handlebars.js you'll need a few things installed.
|
||||
|
||||
* Node.js
|
||||
* [Grunt](http://gruntjs.com/getting-started)
|
||||
|
||||
Project dependencies may be installed via `npm install`.
|
||||
|
||||
To build Handlebars.js from scratch, you'll want to run `grunt`
|
||||
in the root of the project. That will build Handlebars and output the
|
||||
results to the dist/ folder. To re-run tests, run `grunt test` or `npm test`.
|
||||
You can also run our set of benchmarks with `grunt bench`.
|
||||
|
||||
The `grunt dev` implements watching for tests and allows for in browser testing at `http://localhost:9999/spec/`.
|
||||
|
||||
If you notice any problems, please report them to the GitHub issue tracker at
|
||||
[http://github.com/wycats/handlebars.js/issues](http://github.com/wycats/handlebars.js/issues).
|
||||
Feel free to contact commondream or wycats through GitHub with any other
|
||||
questions or feature requests. To submit changes fork the project and
|
||||
send a pull request.
|
||||
|
||||
### Ember testing
|
||||
|
||||
The current ember distribution should be tested as part of the handlebars release process. This requires building the `handlebars-source` gem locally and then executing the ember test script.
|
||||
|
||||
```sh
|
||||
grunt build release
|
||||
export HANDLEBARS_PATH=`pwd`
|
||||
|
||||
cd $emberRepoDir
|
||||
bundle exec rake clean
|
||||
bundle exec rake test
|
||||
```
|
||||
|
||||
### Releasing
|
||||
|
||||
Handlebars utilizes the [release yeoman generator][generator-release] to perform most release tasks.
|
||||
|
||||
A full release may be completed with the following:
|
||||
|
||||
```
|
||||
yo release
|
||||
npm publish
|
||||
yo release:publish cdnjs handlebars.js dist/cdnjs/
|
||||
yo release:publish components handlebars.js dist/components/
|
||||
|
||||
cd dist/components/
|
||||
gem build handlebars-source.gemspec
|
||||
gem push handlebars-source-*.gem
|
||||
```
|
||||
|
||||
After this point the handlebars site needs to be updated to point to the new version numbers. The jsfiddle bitly link should be updated to point to the most recent distribution.
|
||||
|
||||
License
|
||||
-------
|
||||
Handlebars.js is released under the MIT license.
|
||||
|
||||
[bower-repo]: https://github.com/components/handlebars.js
|
||||
[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/bucket-listing.html?sort=lastmod&sortdir=desc
|
||||
[generator-release]: https://github.com/walmartlabs/generator-release
|
||||
[pull-request]: https://github.com/wycats/handlebars.js/pull/new/master
|
||||
[issue]: https://github.com/wycats/handlebars.js/issues/new
|
||||
[jsfiddle]: http://l.kde.cc/hbs-bug
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
require "rubygems"
|
||||
require "bundler/setup"
|
||||
|
||||
file "lib/handlebars/parser.js" => ["src/handlebars.yy","src/handlebars.l"] do
|
||||
system "jison src/handlebars.yy src/handlebars.l"
|
||||
sh "mv handlebars.js lib/handlebars/parser.js"
|
||||
end
|
||||
|
||||
task :compile => "lib/handlebars/parser.js"
|
||||
|
||||
desc "run the spec suite"
|
||||
task :spec => [:release] do
|
||||
system "rspec -cfs spec"
|
||||
end
|
||||
|
||||
task :default => [:compile, :spec]
|
||||
|
||||
def remove_exports(string)
|
||||
match = string.match(%r{^// BEGIN\(BROWSER\)\n(.*)\n^// END\(BROWSER\)}m)
|
||||
match ? match[1] : string
|
||||
end
|
||||
|
||||
minimal_deps = %w(parser base ast visitor utils vm).map do |file|
|
||||
"lib/handlebars/#{file}.js"
|
||||
end
|
||||
|
||||
base_deps = %w(parser base ast visitor runtime utils vm).map do |file|
|
||||
"lib/handlebars/#{file}.js"
|
||||
end
|
||||
|
||||
debug_deps = %w(parser base ast visitor printer runtime utils vm debug).map do |file|
|
||||
"lib/handlebars/#{file}.js"
|
||||
end
|
||||
|
||||
directory "dist"
|
||||
|
||||
minimal_deps.unshift "dist"
|
||||
base_deps.unshift "dist"
|
||||
debug_deps.unshift "dist"
|
||||
|
||||
def build_for_task(task)
|
||||
FileUtils.rm_rf("dist/*") if File.directory?("dist")
|
||||
FileUtils.mkdir_p("dist")
|
||||
|
||||
contents = []
|
||||
task.prerequisites.each do |filename|
|
||||
next if filename == "dist"
|
||||
|
||||
contents << "// #{filename}\n" + remove_exports(File.read(filename)) + ";"
|
||||
end
|
||||
|
||||
File.open(task.name, "w") do |file|
|
||||
file.puts contents.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
file "dist/handlebars.js" => minimal_deps do |task|
|
||||
build_for_task(task)
|
||||
end
|
||||
|
||||
file "dist/handlebars.base.js" => base_deps do |task|
|
||||
build_for_task(task)
|
||||
end
|
||||
|
||||
file "dist/handlebars.debug.js" => debug_deps do |task|
|
||||
build_for_task(task)
|
||||
end
|
||||
|
||||
task :build => [:compile, "dist/handlebars.js"]
|
||||
task :base => [:compile, "dist/handlebars.base.js"]
|
||||
task :debug => [:compile, "dist/handlebars.debug.js"]
|
||||
|
||||
desc "build the build, debug and base versions of handlebars"
|
||||
task :release => [:build, :debug, :base]
|
||||
|
||||
directory "vendor"
|
||||
|
||||
desc "benchmark against dust.js and mustache.js"
|
||||
task :bench => "vendor" do
|
||||
require "open-uri"
|
||||
File.open("vendor/mustache.js", "w") do |file|
|
||||
file.puts open("https://github.com/janl/mustache.js/raw/master/mustache.js").read
|
||||
file.puts "module.exports = Mustache;"
|
||||
end
|
||||
|
||||
File.open("vendor/benchmark.js", "w") do |file|
|
||||
file.puts open("https://github.com/mathiasbynens/benchmark.js/raw/master/benchmark.js").read
|
||||
end
|
||||
|
||||
if File.directory?("vendor/dustjs")
|
||||
system "cd vendor/dustjs && git pull"
|
||||
else
|
||||
system "git clone git://github.com/akdubya/dustjs.git vendor/dustjs"
|
||||
end
|
||||
|
||||
if File.directory?("vendor/coffee")
|
||||
system "cd vendor/coffee && git pull"
|
||||
else
|
||||
system "git clone git://github.com/jashkenas/coffee-script.git vendor/coffee"
|
||||
end
|
||||
|
||||
if File.directory?("vendor/eco")
|
||||
system "cd vendor/eco && git pull"
|
||||
else
|
||||
system "git clone git://github.com/sstephenson/eco.git vendor/eco"
|
||||
end
|
||||
|
||||
system "node bench/handlebars.js"
|
||||
end
|
||||
@@ -1,147 +0,0 @@
|
||||
|
||||
var Benchmark = require("benchmark");
|
||||
|
||||
var BenchWarmer = function(names) {
|
||||
this.benchmarks = [];
|
||||
this.currentBenches = [];
|
||||
this.names = [];
|
||||
this.errors = {};
|
||||
};
|
||||
|
||||
var print = require("sys").print;
|
||||
|
||||
BenchWarmer.prototype = {
|
||||
winners: function(benches) {
|
||||
var result = Benchmark.filter(benches, function(bench) { return bench.cycles; });
|
||||
|
||||
if (result.length > 1) {
|
||||
result.sort(function(a, b) { return b.compare(a); });
|
||||
first = result[0];
|
||||
last = result[result.length - 1];
|
||||
|
||||
var winners = [];
|
||||
|
||||
Benchmark.each(result, function(bench) {
|
||||
if (bench.compare(first) === 0) {
|
||||
winners.push(bench);
|
||||
}
|
||||
});
|
||||
|
||||
return winners;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
},
|
||||
suite: function(suite, fn) {
|
||||
this.suiteName = suite;
|
||||
this.first = true;
|
||||
|
||||
var self = this;
|
||||
|
||||
fn(function(name, benchFn) {
|
||||
self.push(name, benchFn);
|
||||
});
|
||||
},
|
||||
push: function(name, fn) {
|
||||
if(this.names.indexOf(name) == -1) {
|
||||
this.names.push(name);
|
||||
}
|
||||
|
||||
var first = this.first, suiteName = this.suiteName, self = this;
|
||||
this.first = false;
|
||||
|
||||
var bench = new Benchmark(function() {
|
||||
fn();
|
||||
}, {
|
||||
name: this.suiteName + ": " + name,
|
||||
onComplete: function() {
|
||||
if(first) { self.startLine(suiteName); }
|
||||
self.writeBench(bench);
|
||||
self.currentBenches.push(bench);
|
||||
}, onError: function() {
|
||||
self.errors[this.name] = this;
|
||||
}
|
||||
});
|
||||
|
||||
this.benchmarks.push(bench);
|
||||
},
|
||||
bench: function() {
|
||||
var benchSize = 0, names = this.names, self = this, i, l;
|
||||
|
||||
for(i=0, l=names.length; i<l; i++) {
|
||||
var name = names[i];
|
||||
|
||||
if(benchSize < name.length) { benchSize = name.length; }
|
||||
}
|
||||
|
||||
this.nameSize = benchSize + 2;
|
||||
this.benchSize = 20;
|
||||
var horSize = 0;
|
||||
|
||||
this.startLine("ops/msec");
|
||||
horSize = horSize + "ops/msec ".length;
|
||||
for(i=0, l=names.length; i<l; i++) {
|
||||
print(names[i] + new Array(this.benchSize - names[i].length + 1).join(" "));
|
||||
horSize = horSize + this.benchSize;
|
||||
}
|
||||
|
||||
print("WINNER(S)");
|
||||
horSize = horSize + "WINNER(S)".length;
|
||||
|
||||
print("\n" + new Array(horSize + 1).join("-"));
|
||||
|
||||
Benchmark.invoke(this.benchmarks, {
|
||||
name: "run",
|
||||
onComplete: function() {
|
||||
var errors = false, prop, bench;
|
||||
for(prop in self.errors) { if(self.errors.hasOwnProperty(prop)) { errors = true; break; } }
|
||||
|
||||
if(errors) {
|
||||
print("\n\nErrors:\n");
|
||||
for(prop in self.errors) {
|
||||
if(self.errors.hasOwnProperty(prop)) {
|
||||
bench = self.errors[prop];
|
||||
print("\n" + bench.name + ":\n");
|
||||
print(bench.error.message);
|
||||
if(bench.error.stack) {
|
||||
print(bench.error.stack.join("\n"));
|
||||
}
|
||||
print("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
startLine: function(name) {
|
||||
var winners = Benchmark.map(this.winners(this.currentBenches), function(bench) {
|
||||
return bench.name.split(": ")[1];
|
||||
});
|
||||
|
||||
this.currentBenches = [];
|
||||
|
||||
print(winners.join(", "));
|
||||
print("\n");
|
||||
var padding = this.nameSize - name.length + 1;
|
||||
name = name + new Array(padding).join(" ");
|
||||
print(name);
|
||||
},
|
||||
writeBench: function(bench) {
|
||||
var out;
|
||||
|
||||
if(!bench.error) {
|
||||
var count = bench.hz,
|
||||
moe = count * bench.stats.RME / 100;
|
||||
|
||||
out = Math.round(count / 1000) + " ±" + Math.round(moe / 1000) + " (" + bench.cycles + ")";
|
||||
} else {
|
||||
out = "E";
|
||||
}
|
||||
|
||||
var padding = this.benchSize - out.length + 1;
|
||||
out = out + new Array(padding).join(" ");
|
||||
print(out);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = BenchWarmer;
|
||||
@@ -0,0 +1,39 @@
|
||||
var _ = require('underscore'),
|
||||
async = require('async'),
|
||||
fs = require('fs'),
|
||||
zlib = require('zlib');
|
||||
|
||||
module.exports = function(grunt, callback) {
|
||||
var distFiles = fs.readdirSync('dist'),
|
||||
distSizes = {};
|
||||
|
||||
async.each(distFiles, function(file, callback) {
|
||||
var content;
|
||||
try {
|
||||
content = fs.readFileSync('dist/' + file);
|
||||
} catch (err) {
|
||||
if (err.code === 'EISDIR') {
|
||||
callback();
|
||||
return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
file = file.replace(/\.js/, '').replace(/\./g, '_');
|
||||
distSizes[file] = content.length;
|
||||
|
||||
zlib.gzip(content, function(err, data) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
distSizes[file + '_gz'] = data.length;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function() {
|
||||
grunt.log.writeln('Distribution sizes: ' + JSON.stringify(distSizes, undefined, 2));
|
||||
callback([distSizes]);
|
||||
});
|
||||
};
|
||||
@@ -1,166 +0,0 @@
|
||||
require.paths.push("lib");
|
||||
require.paths.push("vendor");
|
||||
require.paths.push("vendor/dustjs/lib");
|
||||
require.paths.push("vendor/coffee/lib");
|
||||
require.paths.push("vendor/eco/lib");
|
||||
|
||||
|
||||
var BenchWarmer = require("./benchwarmer");
|
||||
Handlebars = require("handlebars");
|
||||
|
||||
var dust = require("dust");
|
||||
var Mustache = require("mustache");
|
||||
var ecoExports = require("eco");
|
||||
|
||||
eco = function(str) {
|
||||
var module = {};
|
||||
var template = new Function("module", ecoExports.compile(str));
|
||||
template(module);
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
var benchDetails = {
|
||||
string: {
|
||||
context: {},
|
||||
handlebars: "Hello world",
|
||||
dust: "Hello world",
|
||||
mustache: "Hello world",
|
||||
eco: "Hello world"
|
||||
},
|
||||
variables: {
|
||||
context: {name: "Mick", count: 30},
|
||||
handlebars: "Hello {{name}}! You have {{count}} new messages.",
|
||||
dust: "Hello {name}! You have {count} new messages.",
|
||||
mustache: "Hello {{name}}! You have {{count}} new messages.",
|
||||
eco: "Hello <%= @name %>! You have <%= @count %> new messages."
|
||||
},
|
||||
object: {
|
||||
context: { person: { name: "Larry", age: 45 } },
|
||||
handlebars: "{{#with person}}{{name}}{{age}}{{/with}}",
|
||||
dust: "{#person}{name}{age}{/person}",
|
||||
mustache: "{{#person}}{{name}}{{age}}{{/person}}"
|
||||
},
|
||||
array: {
|
||||
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
|
||||
handlebars: "{{#each names}}{{name}}{{/each}}",
|
||||
dust: "{#names}{name}{/names}",
|
||||
mustache: "{{#names}}{{name}}{{/names}}",
|
||||
eco: "<% for item in @names: %><%= item.name %><% end %>"
|
||||
},
|
||||
partial: {
|
||||
context: { peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 1}] },
|
||||
partials: {
|
||||
mustache: { variables: "Hello {{name}}! You have {{count}} new messages." },
|
||||
handlebars: { variables: "Hello {{name}}! You have {{count}} new messages." }
|
||||
},
|
||||
handlebars: "{{#each peeps}}{{>variables}}{{/each}}",
|
||||
dust: "{#peeps}{>variables/}{/peeps}",
|
||||
mustache: "{{#peeps}}{{>variables}}{{/peeps}}"
|
||||
},
|
||||
recursion: {
|
||||
context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] },
|
||||
partials: {
|
||||
mustache: { recursion: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" },
|
||||
handlebars: { recursion: "{{name}}{{#each kids}}{{>recursion}}{{/each}}" }
|
||||
},
|
||||
handlebars: "{{name}}{{#each kids}}{{>recursion}}{{/each}}",
|
||||
dust: "{name}{#kids}{>recursion:./}{/kids}",
|
||||
mustache: "{{name}}{{#kids}}{{>recursion}}{{/kids}}"
|
||||
},
|
||||
complex: {
|
||||
handlebars: "<h1>{{header}}</h1>{{#if items}}<ul>{{#each items}}{{#if current}}" +
|
||||
"<li><strong>{{name}}</strong></li>{{^}}" +
|
||||
"<li><a href=\"{{url}}\">{{name}}</a></li>{{/if}}" +
|
||||
"{{/each}}</ul>{{^}}<p>The list is empty.</p>{{/if}}",
|
||||
|
||||
dust: "<h1>{header}</h1>\n" +
|
||||
"{?items}\n" +
|
||||
" <ul>\n" +
|
||||
" {#items}\n" +
|
||||
" {#current}\n" +
|
||||
" <li><strong>{name}</strong></li>\n" +
|
||||
" {:else}\n" +
|
||||
" <li><a href=\"{url}\">{name}</a></li>\n" +
|
||||
" {/current}\n" +
|
||||
" {/items}\n" +
|
||||
" </ul>\n" +
|
||||
"{:else}\n" +
|
||||
" <p>The list is empty.</p>\n" +
|
||||
"{/items}",
|
||||
context: {
|
||||
header: function() {
|
||||
return "Colors";
|
||||
},
|
||||
items: [
|
||||
{name: "red", current: true, url: "#Red"},
|
||||
{name: "green", current: false, url: "#Green"},
|
||||
{name: "blue", current: false, url: "#Blue"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
handlebarsTemplates = {};
|
||||
ecoTemplates = {};
|
||||
|
||||
var warmer = new BenchWarmer();
|
||||
|
||||
var makeSuite = function(name) {
|
||||
warmer.suite(name, function(bench) {
|
||||
var templateName = name;
|
||||
var details = benchDetails[templateName];
|
||||
var mustachePartials = details.partials && details.partials.mustache;
|
||||
var mustacheSource = details.mustache;
|
||||
var context = details.context;
|
||||
|
||||
var error = function() { throw new Error("EWOT"); };
|
||||
|
||||
|
||||
//bench("dust", function() {
|
||||
//dust.render(templateName, context, function(err, out) { });
|
||||
//});
|
||||
|
||||
bench("handlebars", function() {
|
||||
handlebarsTemplates[templateName](context);
|
||||
});
|
||||
|
||||
//if(ecoTemplates[templateName]) {
|
||||
//bench("eco", function() {
|
||||
//ecoTemplates[templateName](context);
|
||||
//});
|
||||
//} else {
|
||||
//bench("eco", error);
|
||||
//}
|
||||
|
||||
//if(mustacheSource) {
|
||||
//bench("mustache", function() {
|
||||
//Mustache.to_html(mustacheSource, context, mustachePartials);
|
||||
//});
|
||||
//} else {
|
||||
//bench("mustache", error);
|
||||
//}
|
||||
});
|
||||
}
|
||||
|
||||
for(var name in benchDetails) {
|
||||
if(benchDetails.hasOwnProperty(name)) {
|
||||
dust.loadSource(dust.compile(benchDetails[name].dust, name));
|
||||
handlebarsTemplates[name] = Handlebars.compile(benchDetails[name].handlebars);
|
||||
|
||||
if(benchDetails[name].eco) { ecoTemplates[name] = eco(benchDetails[name].eco); }
|
||||
|
||||
var partials = benchDetails[name].partials;
|
||||
if(partials) {
|
||||
for(var partialName in partials.handlebars) {
|
||||
if(partials.handlebars.hasOwnProperty(partialName)) {
|
||||
Handlebars.registerPartial(partialName, partials.handlebars[partialName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
makeSuite(name);
|
||||
}
|
||||
}
|
||||
|
||||
warmer.bench();
|
||||
@@ -0,0 +1,14 @@
|
||||
var fs = require('fs');
|
||||
|
||||
var metrics = fs.readdirSync(__dirname);
|
||||
metrics.forEach(function(metric) {
|
||||
if (metric === 'index.js' || !/(.*)\.js$/.test(metric)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var name = RegExp.$1;
|
||||
metric = require('./' + name);
|
||||
if (metric instanceof Function) {
|
||||
module.exports[name] = metric;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
var _ = require('underscore'),
|
||||
templates = require('./templates');
|
||||
|
||||
module.exports = function(grunt, callback) {
|
||||
// Deferring to here in case we have a build for parser, etc as part of this grunt exec
|
||||
var Handlebars = require('../lib');
|
||||
|
||||
var templateSizes = {};
|
||||
_.each(templates, function(info, template) {
|
||||
var src = info.handlebars,
|
||||
compiled = Handlebars.precompile(src, {}),
|
||||
knownHelpers = Handlebars.precompile(src, {knownHelpersOnly: true, knownHelpers: info.helpers});
|
||||
|
||||
templateSizes[template] = compiled.length;
|
||||
templateSizes['knownOnly_' + template] = knownHelpers.length;
|
||||
});
|
||||
grunt.log.writeln('Precompiled sizes: ' + JSON.stringify(templateSizes, undefined, 2));
|
||||
callback([templateSizes]);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
helpers: {
|
||||
foo: function(options) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
context: {
|
||||
bar: true
|
||||
},
|
||||
|
||||
handlebars: '{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}'
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
|
||||
handlebars: "{{#each names}}{{name}}{{/each}}",
|
||||
dust: "{#names}{name}{/names}",
|
||||
mustache: "{{#names}}{{name}}{{/names}}",
|
||||
eco: "<% for item in @names: %><%= item.name %><% end %>"
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
|
||||
handlebars: "{{#names}}{{name}}{{/names}}"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<h1>{header}</h1>
|
||||
{?items}
|
||||
<ul>
|
||||
{#items}
|
||||
{#current}
|
||||
<li><strong>{name}</strong></li>
|
||||
{:else}
|
||||
<li><a href="{url}">{name}</a></li>
|
||||
{/current}
|
||||
{/items}
|
||||
</ul>
|
||||
{:else}
|
||||
<p>The list is empty.</p>
|
||||
{/items}
|
||||
@@ -0,0 +1,14 @@
|
||||
<h1><%= @header() %></h1>
|
||||
<% if @items.length: %>
|
||||
<ul>
|
||||
<% for item in @items: %>
|
||||
<% if item.current: %>
|
||||
<li><strong><%= item.name %></strong></li>
|
||||
<% else: %>
|
||||
<li><a href="<%= item.url %>"><%= item.name %></a></li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else: %>
|
||||
<p>The list is empty.</p>
|
||||
<% end %>
|
||||
@@ -0,0 +1,14 @@
|
||||
<h1>{{header}}</h1>
|
||||
{{#if items}}
|
||||
<ul>
|
||||
{{#each items}}
|
||||
{{#if current}}
|
||||
<li><strong>{{name}}</strong></li>
|
||||
{{^}}
|
||||
<li><a href="{{url}}">{{name}}</a></li>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{^}}
|
||||
<p>The list is empty.</p>
|
||||
{{/if}}
|
||||
@@ -0,0 +1,20 @@
|
||||
var fs = require('fs');
|
||||
|
||||
module.exports = {
|
||||
context: {
|
||||
header: function() {
|
||||
return "Colors";
|
||||
},
|
||||
hasItems: true, // To make things fairer in mustache land due to no `{{if}}` construct on arrays
|
||||
items: [
|
||||
{name: "red", current: true, url: "#Red"},
|
||||
{name: "green", current: false, url: "#Green"},
|
||||
{name: "blue", current: false, url: "#Blue"}
|
||||
]
|
||||
},
|
||||
|
||||
handlebars: fs.readFileSync(__dirname + '/complex.handlebars').toString(),
|
||||
dust: fs.readFileSync(__dirname + '/complex.dust').toString(),
|
||||
eco: fs.readFileSync(__dirname + '/complex.eco').toString(),
|
||||
mustache: fs.readFileSync(__dirname + '/complex.mustache').toString()
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
<h1>{{header}}</h1>
|
||||
{{#hasItems}}
|
||||
<ul>
|
||||
{{#items}}
|
||||
{{#current}}
|
||||
<li><strong>{{name}}</strong></li>
|
||||
{{/current}}
|
||||
{{^current}}
|
||||
<li><a href="{{url}}">{{name}}</a></li>
|
||||
{{/current}}
|
||||
{{/items}}
|
||||
</ul>
|
||||
{{/hasItems}}
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}] },
|
||||
handlebars: "{{#each names}}{{@index}}{{name}}{{/each}}"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
context: { names: [{name: "Moe"}, {name: "Larry"}, {name: "Curly"}, {name: "Shemp"}], foo: 'bar' },
|
||||
handlebars: "{{#each names}}{{../foo}}{{/each}}",
|
||||
mustache: "{{#names}}{{foo}}{{/names}}",
|
||||
eco: "<% for item in @names: %><%= @foo %><% end %>"
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
context: { names: [{bat: 'foo', name: ["Moe"]}, {bat: 'foo', name: ["Larry"]}, {bat: 'foo', name: ["Curly"]}, {bat: 'foo', name: ["Shemp"]}], foo: 'bar' },
|
||||
handlebars: "{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}",
|
||||
mustache: "{{#names}}{{#name}}{{bat}}{{foo}}{{/name}}{{/names}}",
|
||||
eco: "<% for item in @names: %><% for child in item.name: %><%= item.bat %><%= @foo %><% end %><% end %>"
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
var fs = require('fs');
|
||||
|
||||
var templates = fs.readdirSync(__dirname);
|
||||
templates.forEach(function(template) {
|
||||
if (template === 'index.js' || !/(.*)\.js$/.test(template)) {
|
||||
return;
|
||||
}
|
||||
module.exports[RegExp.$1] = require('./' + RegExp.$1);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
context: { person: { name: "Larry", age: 45 } },
|
||||
handlebars: "{{#person}}{{name}}{{age}}{{/person}}"
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
context: { person: { name: "Larry", age: 45 } },
|
||||
handlebars: "{{#with person}}{{name}}{{age}}{{/with}}",
|
||||
dust: "{#person}{name}{age}{/person}",
|
||||
eco: "<%= @person.name %><%= @person.age %>",
|
||||
mustache: "{{#person}}{{name}}{{age}}{{/person}}"
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
context: { name: '1', kids: [{ name: '1.1', kids: [{name: '1.1.1', kids: []}] }] },
|
||||
partials: {
|
||||
mustache: { recursion: "{{name}}{{#kids}}{{>recursion}}{{/kids}}" },
|
||||
handlebars: { recursion: "{{name}}{{#each kids}}{{>recursion}}{{/each}}" }
|
||||
},
|
||||
handlebars: "{{name}}{{#each kids}}{{>recursion}}{{/each}}",
|
||||
dust: "{name}{#kids}{>recursion:./}{/kids}",
|
||||
mustache: "{{name}}{{#kids}}{{>recursion}}{{/kids}}"
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
context: { peeps: [{name: "Moe", count: 15}, {name: "Larry", count: 5}, {name: "Curly", count: 1}] },
|
||||
partials: {
|
||||
mustache: { variables: "Hello {{name}}! You have {{count}} new messages." },
|
||||
handlebars: { variables: "Hello {{name}}! You have {{count}} new messages." }
|
||||
},
|
||||
|
||||
handlebars: "{{#each peeps}}{{>variables}}{{/each}}",
|
||||
dust: "{#peeps}{>variables/}{/peeps}",
|
||||
mustache: "{{#peeps}}{{>variables}}{{/peeps}}"
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
context: { person: { name: "Larry", age: 45 } },
|
||||
handlebars: "{{person.name}}{{person.age}}{{person.foo}}{{animal.age}}",
|
||||
dust: "{person.name}{person.age}{person.foo}{animal.age}",
|
||||
eco: "<%= @person.name %><%= @person.age %><%= @person.foo %><% if @animal: %><%= @animal.age %><% end %>",
|
||||
mustache: "{{person.name}}{{person.age}}{{person.foo}}{{animal.age}}"
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
context: {},
|
||||
handlebars: "Hello world",
|
||||
dust: "Hello world",
|
||||
mustache: "Hello world",
|
||||
eco: "Hello world"
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
helpers: {
|
||||
echo: function(value) {
|
||||
return 'foo ' + value;
|
||||
},
|
||||
header: function() {
|
||||
return "Colors";
|
||||
}
|
||||
},
|
||||
handlebars: "{{echo (header)}}",
|
||||
eco: "<%= @echo(@header()) %>"
|
||||
};
|
||||
|
||||
module.exports.context = module.exports.helpers;
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
context: {name: "Mick", count: 30},
|
||||
handlebars: "Hello {{name}}! You have {{count}} new messages.",
|
||||
dust: "Hello {name}! You have {count} new messages.",
|
||||
mustache: "Hello {{name}}! You have {{count}} new messages.",
|
||||
eco: "Hello <%= @name %>! You have <%= @count %> new messages."
|
||||
};
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
var _ = require('underscore'),
|
||||
runner = require('./util/template-runner'),
|
||||
templates = require('./templates'),
|
||||
|
||||
eco, dust, Handlebars, Mustache, eco;
|
||||
|
||||
try {
|
||||
dust = require("dustjs-linkedin");
|
||||
} catch (err) { /* NOP */ }
|
||||
|
||||
try {
|
||||
Mustache = require("mustache");
|
||||
} catch (err) { /* NOP */ }
|
||||
|
||||
try {
|
||||
eco = require("eco");
|
||||
} catch (err) { /* NOP */ }
|
||||
|
||||
function error() {
|
||||
throw new Error("EWOT");
|
||||
}
|
||||
|
||||
function makeSuite(bench, name, template, handlebarsOnly) {
|
||||
// Create aliases to minimize any impact from having to walk up the closure tree.
|
||||
var templateName = name,
|
||||
|
||||
context = template.context,
|
||||
partials = template.partials,
|
||||
|
||||
handlebarsOut,
|
||||
dustOut,
|
||||
ecoOut,
|
||||
mustacheOut;
|
||||
|
||||
var handlebar = Handlebars.compile(template.handlebars, {data: false}),
|
||||
options = {helpers: template.helpers};
|
||||
_.each(template.partials && template.partials.handlebars, function(partial, name) {
|
||||
Handlebars.registerPartial(name, Handlebars.compile(partial, {data: false}));
|
||||
});
|
||||
|
||||
handlebarsOut = handlebar(context, options);
|
||||
bench("handlebars", function() {
|
||||
handlebar(context, options);
|
||||
});
|
||||
|
||||
if (handlebarsOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dust) {
|
||||
if (template.dust) {
|
||||
dustOut = false;
|
||||
dust.loadSource(dust.compile(template.dust, templateName));
|
||||
|
||||
dust.render(templateName, context, function(err, out) { dustOut = out; });
|
||||
|
||||
bench("dust", function() {
|
||||
dust.render(templateName, context, function(err, out) { });
|
||||
});
|
||||
} else {
|
||||
bench('dust', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (eco) {
|
||||
if (template.eco) {
|
||||
var ecoTemplate = eco.compile(template.eco);
|
||||
|
||||
ecoOut = ecoTemplate(context);
|
||||
|
||||
bench("eco", function() {
|
||||
ecoTemplate(context);
|
||||
});
|
||||
} else {
|
||||
bench("eco", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (Mustache) {
|
||||
var mustacheSource = template.mustache,
|
||||
mustachePartials = partials && partials.mustache;
|
||||
|
||||
if (mustacheSource) {
|
||||
mustacheOut = Mustache.to_html(mustacheSource, context, mustachePartials);
|
||||
|
||||
bench("mustache", function() {
|
||||
Mustache.to_html(mustacheSource, context, mustachePartials);
|
||||
});
|
||||
} else {
|
||||
bench("mustache", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Hack around whitespace until we have whitespace control
|
||||
handlebarsOut = handlebarsOut.replace(/\s/g, '');
|
||||
function compare(b, lang) {
|
||||
if (b == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
b = b.replace(/\s/g, '');
|
||||
|
||||
if (handlebarsOut !== b) {
|
||||
throw new Error('Template output mismatch: ' + name
|
||||
+ '\n\nHandlebars: ' + handlebarsOut
|
||||
+ '\n\n' + lang + ': ' + b);
|
||||
}
|
||||
}
|
||||
|
||||
compare(dustOut, 'dust');
|
||||
compare(ecoOut, 'eco');
|
||||
compare(mustacheOut, 'mustache');
|
||||
}
|
||||
|
||||
module.exports = function(grunt, callback) {
|
||||
// Deferring load incase we are being run inline with the grunt build
|
||||
Handlebars = require('../lib');
|
||||
|
||||
console.log('Execution Throughput');
|
||||
runner(grunt, makeSuite, function(times, scaled) {
|
||||
callback(scaled);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,199 @@
|
||||
var _ = require('underscore'),
|
||||
Benchmark = require("benchmark");
|
||||
|
||||
var BenchWarmer = function(names) {
|
||||
this.benchmarks = [];
|
||||
this.currentBenches = [];
|
||||
this.names = [];
|
||||
this.times = {};
|
||||
this.minimum = Infinity;
|
||||
this.maximum = -Infinity;
|
||||
this.errors = {};
|
||||
};
|
||||
|
||||
var print = require("sys").print;
|
||||
|
||||
BenchWarmer.prototype = {
|
||||
winners: function(benches) {
|
||||
return Benchmark.filter(benches, 'fastest');
|
||||
},
|
||||
suite: function(suite, fn) {
|
||||
this.suiteName = suite;
|
||||
this.times[suite] = {};
|
||||
this.first = true;
|
||||
|
||||
var self = this;
|
||||
|
||||
fn(function(name, benchFn) {
|
||||
self.push(name, benchFn);
|
||||
});
|
||||
},
|
||||
push: function(name, fn) {
|
||||
if(this.names.indexOf(name) == -1) {
|
||||
this.names.push(name);
|
||||
}
|
||||
|
||||
var first = this.first, suiteName = this.suiteName, self = this;
|
||||
this.first = false;
|
||||
|
||||
var bench = new Benchmark(fn, {
|
||||
name: this.suiteName + ": " + name,
|
||||
onComplete: function() {
|
||||
if(first) { self.startLine(suiteName); }
|
||||
self.writeBench(bench);
|
||||
self.currentBenches.push(bench);
|
||||
}, onError: function() {
|
||||
self.errors[this.name] = this;
|
||||
}
|
||||
});
|
||||
bench.suiteName = this.suiteName;
|
||||
bench.benchName = name;
|
||||
|
||||
this.benchmarks.push(bench);
|
||||
},
|
||||
|
||||
bench: function(callback) {
|
||||
var self = this;
|
||||
|
||||
this.printHeader('ops/msec', true);
|
||||
|
||||
Benchmark.invoke(this.benchmarks, {
|
||||
name: "run",
|
||||
onComplete: function() {
|
||||
self.scaleTimes();
|
||||
|
||||
self.startLine('');
|
||||
|
||||
print('\n');
|
||||
self.printHeader('scaled');
|
||||
_.each(self.scaled, function(value, name) {
|
||||
self.startLine(name);
|
||||
|
||||
_.each(self.names, function(lang) {
|
||||
self.writeValue(value[lang] || '');
|
||||
});
|
||||
});
|
||||
print('\n');
|
||||
|
||||
var errors = false, prop, bench;
|
||||
for(prop in self.errors) {
|
||||
if (self.errors.hasOwnProperty(prop)
|
||||
&& self.errors[prop].error.message !== 'EWOT') {
|
||||
errors = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(errors) {
|
||||
print("\n\nErrors:\n");
|
||||
for(prop in self.errors) {
|
||||
if (self.errors.hasOwnProperty(prop)
|
||||
&& self.errors[prop].error.message !== 'EWOT') {
|
||||
bench = self.errors[prop];
|
||||
print("\n" + bench.name + ":\n");
|
||||
print(bench.error.message);
|
||||
if(bench.error.stack) {
|
||||
print(bench.error.stack.join("\n"));
|
||||
}
|
||||
print("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
print("\n");
|
||||
},
|
||||
|
||||
scaleTimes: function() {
|
||||
var scaled = this.scaled = {};
|
||||
_.each(this.times, function(times, name) {
|
||||
var output = scaled[name] = {};
|
||||
|
||||
_.each(times, function(time, lang) {
|
||||
output[lang] = ((time - this.minimum) / (this.maximum - this.minimum) * 100).toFixed(2);
|
||||
}, this);
|
||||
}, this);
|
||||
},
|
||||
|
||||
printHeader: function(title, winners) {
|
||||
var benchSize = 0, names = this.names, i, l;
|
||||
|
||||
for(i=0, l=names.length; i<l; i++) {
|
||||
var name = names[i];
|
||||
|
||||
if(benchSize < name.length) { benchSize = name.length; }
|
||||
}
|
||||
|
||||
this.nameSize = benchSize + 2;
|
||||
this.benchSize = 20;
|
||||
var horSize = 0;
|
||||
|
||||
this.startLine(title);
|
||||
horSize = horSize + this.benchSize;
|
||||
for(i=0, l=names.length; i<l; i++) {
|
||||
this.writeValue(names[i]);
|
||||
horSize = horSize + this.benchSize;
|
||||
}
|
||||
|
||||
if (winners) {
|
||||
print("WINNER(S)");
|
||||
horSize = horSize + "WINNER(S)".length;
|
||||
}
|
||||
|
||||
print("\n" + new Array(horSize + 1).join("-"));
|
||||
},
|
||||
|
||||
startLine: function(name) {
|
||||
var winners = Benchmark.map(this.winners(this.currentBenches), function(bench) {
|
||||
return bench.name.split(": ")[1];
|
||||
});
|
||||
|
||||
this.currentBenches = [];
|
||||
|
||||
print(winners.join(", "));
|
||||
print("\n");
|
||||
|
||||
if (name) {
|
||||
this.writeValue(name);
|
||||
}
|
||||
},
|
||||
writeBench: function(bench) {
|
||||
var out;
|
||||
|
||||
if(!bench.error) {
|
||||
var count = bench.hz,
|
||||
moe = count * bench.stats.rme / 100,
|
||||
minimum,
|
||||
maximum;
|
||||
|
||||
count = Math.round(count / 1000);
|
||||
moe = Math.round(moe / 1000);
|
||||
minimum = count - moe;
|
||||
maximum = count + moe;
|
||||
|
||||
out = count + " ±" + moe + " (" + bench.cycles + ")";
|
||||
|
||||
this.times[bench.suiteName][bench.benchName] = count;
|
||||
this.minimum = Math.min(this.minimum, minimum);
|
||||
this.maximum = Math.max(this.maximum, maximum);
|
||||
} else {
|
||||
if (bench.error.message === 'EWOT') {
|
||||
out = 'NA';
|
||||
} else {
|
||||
out = 'E';
|
||||
}
|
||||
}
|
||||
this.writeValue(out);
|
||||
},
|
||||
|
||||
writeValue: function(out) {
|
||||
var padding = this.benchSize - out.length + 1;
|
||||
out = out + new Array(padding).join(" ");
|
||||
print(out);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = BenchWarmer;
|
||||
@@ -0,0 +1,27 @@
|
||||
var _ = require('underscore'),
|
||||
BenchWarmer = require('./benchwarmer'),
|
||||
templates = require('../templates');
|
||||
|
||||
module.exports = function(grunt, makeSuite, callback) {
|
||||
var warmer = new BenchWarmer();
|
||||
|
||||
var handlebarsOnly = grunt.option('handlebars-only'),
|
||||
grep = grunt.option('grep');
|
||||
if (grep) {
|
||||
grep = new RegExp(grep);
|
||||
}
|
||||
|
||||
_.each(templates, function(template, name) {
|
||||
if (!template.handlebars || (grep && !grep.test(name))) {
|
||||
return;
|
||||
}
|
||||
|
||||
warmer.suite(name, function(bench) {
|
||||
makeSuite(bench, name, template, handlebarsOnly);
|
||||
});
|
||||
});
|
||||
|
||||
warmer.bench(function() {
|
||||
callback && callback(warmer.times, warmer.scaled);
|
||||
});
|
||||
};
|
||||
Executable
+222
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var optimist = require('optimist')
|
||||
.usage('Precompile handlebar templates.\nUsage: $0 template...', {
|
||||
'f': {
|
||||
'type': 'string',
|
||||
'description': 'Output File',
|
||||
'alias': 'output'
|
||||
},
|
||||
'a': {
|
||||
'type': 'boolean',
|
||||
'description': 'Exports amd style (require.js)',
|
||||
'alias': 'amd'
|
||||
},
|
||||
'c': {
|
||||
'type': 'string',
|
||||
'description': 'Exports CommonJS style, path to Handlebars module',
|
||||
'alias': 'commonjs',
|
||||
'default': null
|
||||
},
|
||||
'h': {
|
||||
'type': 'string',
|
||||
'description': 'Path to handlebar.js (only valid for amd-style)',
|
||||
'alias': 'handlebarPath',
|
||||
'default': ''
|
||||
},
|
||||
'k': {
|
||||
'type': 'string',
|
||||
'description': 'Known helpers',
|
||||
'alias': 'known'
|
||||
},
|
||||
'o': {
|
||||
'type': 'boolean',
|
||||
'description': 'Known helpers only',
|
||||
'alias': 'knownOnly'
|
||||
},
|
||||
'm': {
|
||||
'type': 'boolean',
|
||||
'description': 'Minimize output',
|
||||
'alias': 'min'
|
||||
},
|
||||
'n': {
|
||||
'type': 'string',
|
||||
'description': 'Template namespace',
|
||||
'alias': 'namespace',
|
||||
'default': 'Handlebars.templates'
|
||||
},
|
||||
's': {
|
||||
'type': 'boolean',
|
||||
'description': 'Output template function only.',
|
||||
'alias': 'simple'
|
||||
},
|
||||
'r': {
|
||||
'type': 'string',
|
||||
'description': 'Template root. Base value that will be stripped from template names.',
|
||||
'alias': 'root'
|
||||
},
|
||||
'p' : {
|
||||
'type': 'boolean',
|
||||
'description': 'Compiling a partial template',
|
||||
'alias': 'partial'
|
||||
},
|
||||
'd' : {
|
||||
'type': 'boolean',
|
||||
'description': 'Include data when compiling',
|
||||
'alias': 'data'
|
||||
},
|
||||
'e': {
|
||||
'type': 'string',
|
||||
'description': 'Template extension.',
|
||||
'alias': 'extension',
|
||||
'default': 'handlebars'
|
||||
},
|
||||
'b': {
|
||||
'type': 'boolean',
|
||||
'description': 'Removes the BOM (Byte Order Mark) from the beginning of the templates.',
|
||||
'alias': 'bom'
|
||||
}
|
||||
})
|
||||
|
||||
.check(function(argv) {
|
||||
var template = [0];
|
||||
if (!argv._.length) {
|
||||
throw 'Must define at least one template or directory.';
|
||||
}
|
||||
|
||||
argv._.forEach(function(template) {
|
||||
try {
|
||||
fs.statSync(template);
|
||||
} catch (err) {
|
||||
throw 'Unable to open template file "' + template + '"';
|
||||
}
|
||||
});
|
||||
})
|
||||
.check(function(argv) {
|
||||
if (argv.simple && argv.min) {
|
||||
throw 'Unable to minimze simple output';
|
||||
}
|
||||
if (argv.simple && (argv._.length !== 1 || fs.statSync(argv._[0]).isDirectory())) {
|
||||
throw 'Unable to output multiple templates in simple mode';
|
||||
}
|
||||
});
|
||||
|
||||
var fs = require('fs'),
|
||||
handlebars = require('../lib'),
|
||||
basename = require('path').basename,
|
||||
uglify = require('uglify-js');
|
||||
|
||||
var argv = optimist.argv,
|
||||
template = argv._[0];
|
||||
|
||||
// Convert the known list into a hash
|
||||
var known = {};
|
||||
if (argv.known && !Array.isArray(argv.known)) {
|
||||
argv.known = [argv.known];
|
||||
}
|
||||
if (argv.known) {
|
||||
for (var i = 0, len = argv.known.length; i < len; i++) {
|
||||
known[argv.known[i]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Build file extension pattern
|
||||
var extension = argv.extension.replace(/[\\^$*+?.():=!|{}\-\[\]]/g, function(arg) { return '\\' + arg; });
|
||||
extension = new RegExp('\\.' + extension + '$');
|
||||
|
||||
var output = [];
|
||||
if (!argv.simple) {
|
||||
if (argv.amd) {
|
||||
output.push('define([\'' + argv.handlebarPath + 'handlebars.runtime\'], function(Handlebars) {\n Handlebars = Handlebars["default"];');
|
||||
} else if (argv.commonjs) {
|
||||
output.push('var Handlebars = require("' + argv.commonjs + '");');
|
||||
} else {
|
||||
output.push('(function() {\n');
|
||||
}
|
||||
output.push(' var template = Handlebars.template, templates = ');
|
||||
output.push(argv.namespace);
|
||||
output.push(' = ');
|
||||
output.push(argv.namespace);
|
||||
output.push(' || {};\n');
|
||||
}
|
||||
function processTemplate(template, root, explicit) {
|
||||
var path = template,
|
||||
stat = fs.statSync(path);
|
||||
if (stat.isDirectory()) {
|
||||
fs.readdirSync(template).map(function(file) {
|
||||
var path = template + '/' + file;
|
||||
|
||||
if (extension.test(path) || fs.statSync(path).isDirectory()) {
|
||||
processTemplate(path, root || template);
|
||||
}
|
||||
});
|
||||
} else if (explicit || extension.test(path)) {
|
||||
var data = fs.readFileSync(path, 'utf8');
|
||||
|
||||
if (argv.bom && data.indexOf('\uFEFF') === 0) {
|
||||
data = data.substring(1);
|
||||
}
|
||||
|
||||
var options = {
|
||||
knownHelpers: known,
|
||||
knownHelpersOnly: argv.o
|
||||
};
|
||||
|
||||
if (argv.data) {
|
||||
options.data = true;
|
||||
}
|
||||
|
||||
// Clean the template name
|
||||
if (!root) {
|
||||
template = basename(template);
|
||||
} else if (template.indexOf(root) === 0) {
|
||||
template = template.substring(root.length+1);
|
||||
}
|
||||
template = template.replace(extension, '');
|
||||
|
||||
if (argv.simple) {
|
||||
output.push(handlebars.precompile(data, options) + '\n');
|
||||
} else if (argv.partial) {
|
||||
if(argv.amd && (argv._.length == 1 && !fs.statSync(argv._[0]).isDirectory())) {
|
||||
output.push('return ');
|
||||
}
|
||||
output.push('Handlebars.partials[\'' + template + '\'] = template(' + handlebars.precompile(data, options) + ');\n');
|
||||
} else {
|
||||
if(argv.amd && (argv._.length == 1 && !fs.statSync(argv._[0]).isDirectory())) {
|
||||
output.push('return ');
|
||||
}
|
||||
output.push('templates[\'' + template + '\'] = template(' + handlebars.precompile(data, options) + ');\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
argv._.forEach(function(template) {
|
||||
processTemplate(template, argv.root, true);
|
||||
});
|
||||
|
||||
// Output the content
|
||||
if (!argv.simple) {
|
||||
if (argv.amd) {
|
||||
if(argv._.length > 1 || (argv._.length == 1 && fs.statSync(argv._[0]).isDirectory())) {
|
||||
if(argv.partial){
|
||||
output.push('return Handlebars.partials;\n');
|
||||
} else {
|
||||
output.push('return templates;\n');
|
||||
}
|
||||
}
|
||||
output.push('});');
|
||||
} else if (!argv.commonjs) {
|
||||
output.push('})();');
|
||||
}
|
||||
}
|
||||
output = output.join('');
|
||||
|
||||
if (argv.min) {
|
||||
output = uglify.minify(output, {fromString: true}).code;
|
||||
}
|
||||
|
||||
if (argv.output) {
|
||||
fs.writeFileSync(argv.output, output, 'utf8');
|
||||
} else {
|
||||
console.log(output);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "handlebars",
|
||||
"version": "v2.0.0-alpha.1",
|
||||
"main": "handlebars.js",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "handlebars",
|
||||
"repo": "components/handlebars.js",
|
||||
"version": "1.0.0",
|
||||
"main": "handlebars.js",
|
||||
"scripts": [
|
||||
"handlebars.js"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "components/handlebars.js",
|
||||
"description": "Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.",
|
||||
"homepage": "http://handlebarsjs.com",
|
||||
"license": "MIT",
|
||||
"type": "component",
|
||||
"keywords": [
|
||||
"handlebars",
|
||||
"mustache",
|
||||
"html"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Chris Wanstrath",
|
||||
"homepage": "http://chriswanstrath.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"robloach/component-installer": "*"
|
||||
},
|
||||
"extra": {
|
||||
"component": {
|
||||
"name": "handlebars",
|
||||
"scripts": [
|
||||
"handlebars.js"
|
||||
],
|
||||
"files": [
|
||||
"handlebars.runtime.js"
|
||||
],
|
||||
"shim": {
|
||||
"exports": "Handlebars"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
require 'json'
|
||||
|
||||
package = JSON.parse(File.read('bower.json'))
|
||||
|
||||
Gem::Specification.new do |gem|
|
||||
gem.name = "handlebars-source"
|
||||
gem.authors = ["Yehuda Katz"]
|
||||
gem.email = ["wycats@gmail.com"]
|
||||
gem.date = Time.now.strftime("%Y-%m-%d")
|
||||
gem.description = %q{Handlebars.js source code wrapper for (pre)compilation gems.}
|
||||
gem.summary = %q{Handlebars.js source code wrapper}
|
||||
gem.homepage = "https://github.com/wycats/handlebars.js/"
|
||||
gem.version = package["version"]
|
||||
gem.license = "MIT"
|
||||
|
||||
gem.files = [
|
||||
'handlebars.js',
|
||||
'handlebars.runtime.js',
|
||||
'lib/handlebars/source.rb'
|
||||
]
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0"?>
|
||||
<package>
|
||||
<metadata>
|
||||
<id>handlebars.js</id>
|
||||
<version>v2.0.0-alpha.1</version>
|
||||
<authors>handlebars.js Authors</authors>
|
||||
<licenseUrl>https://github.com/wycats/handlebars.js/blob/master/LICENSE</licenseUrl>
|
||||
<projectUrl>https://github.com/wycats/handlebars.js/</projectUrl>
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<description>Extension of the Mustache logicless template language</description>
|
||||
<releaseNotes></releaseNotes>
|
||||
<tags>handlebars mustache template html</tags>
|
||||
</metadata>
|
||||
<files>
|
||||
<file src="handlebars.js" target="Content\Scripts" />
|
||||
</files>
|
||||
</package>
|
||||
@@ -0,0 +1,11 @@
|
||||
module Handlebars
|
||||
module Source
|
||||
def self.bundled_path
|
||||
File.expand_path("../../../handlebars.js", __FILE__)
|
||||
end
|
||||
|
||||
def self.runtime_bundled_path
|
||||
File.expand_path("../../../handlebars.runtime.js", __FILE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
+27
-10
@@ -1,16 +1,33 @@
|
||||
var Handlebars = require("handlebars/base");
|
||||
module.exports = Handlebars;
|
||||
/*globals Handlebars: true */
|
||||
import Handlebars from "./handlebars.runtime";
|
||||
|
||||
require("handlebars/utils");
|
||||
// Compiler imports
|
||||
import AST from "./handlebars/compiler/ast";
|
||||
import { parser as Parser, parse } from "./handlebars/compiler/base";
|
||||
import { Compiler, compile, precompile } from "./handlebars/compiler/compiler";
|
||||
import JavaScriptCompiler from "./handlebars/compiler/javascript-compiler";
|
||||
|
||||
require("handlebars/ast");
|
||||
require("handlebars/printer");
|
||||
require("handlebars/visitor");
|
||||
var _create = Handlebars.create;
|
||||
var create = function() {
|
||||
var hb = _create();
|
||||
|
||||
require("handlebars/runtime");
|
||||
require("handlebars/vm");
|
||||
hb.compile = function(input, options) {
|
||||
return compile(input, options, hb);
|
||||
};
|
||||
hb.precompile = function (input, options) {
|
||||
return precompile(input, options, hb);
|
||||
};
|
||||
|
||||
// BEGIN(BROWSER)
|
||||
hb.AST = AST;
|
||||
hb.Compiler = Compiler;
|
||||
hb.JavaScriptCompiler = JavaScriptCompiler;
|
||||
hb.Parser = Parser;
|
||||
hb.parse = parse;
|
||||
|
||||
// END(BROWSER)
|
||||
return hb;
|
||||
};
|
||||
|
||||
Handlebars = create();
|
||||
Handlebars.create = create;
|
||||
|
||||
export default Handlebars;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*globals Handlebars: true */
|
||||
module base from "./handlebars/base";
|
||||
|
||||
// Each of these augment the Handlebars object. No need to setup here.
|
||||
// (This is done to easily share code between commonjs and browse envs)
|
||||
import SafeString from "./handlebars/safe-string";
|
||||
import Exception from "./handlebars/exception";
|
||||
module Utils from "./handlebars/utils";
|
||||
module runtime from "./handlebars/runtime";
|
||||
|
||||
// For compatibility and usage outside of module systems, make the Handlebars object a namespace
|
||||
var create = function() {
|
||||
var hb = new base.HandlebarsEnvironment();
|
||||
|
||||
Utils.extend(hb, base);
|
||||
hb.SafeString = SafeString;
|
||||
hb.Exception = Exception;
|
||||
hb.Utils = Utils;
|
||||
|
||||
hb.VM = runtime;
|
||||
hb.template = function(spec) {
|
||||
return runtime.template(spec, hb);
|
||||
};
|
||||
|
||||
return hb;
|
||||
};
|
||||
|
||||
var Handlebars = create();
|
||||
Handlebars.create = create;
|
||||
|
||||
export default Handlebars;
|
||||
@@ -1,86 +0,0 @@
|
||||
var Handlebars = require("handlebars");
|
||||
|
||||
// BEGIN(BROWSER)
|
||||
(function() {
|
||||
|
||||
Handlebars.AST = {};
|
||||
|
||||
Handlebars.AST.ProgramNode = function(statements, inverse) {
|
||||
this.type = "program";
|
||||
this.statements = statements;
|
||||
if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); }
|
||||
};
|
||||
|
||||
Handlebars.AST.MustacheNode = function(params, unescaped) {
|
||||
this.type = "mustache";
|
||||
this.id = params[0];
|
||||
this.params = params.slice(1);
|
||||
this.escaped = !unescaped;
|
||||
};
|
||||
|
||||
Handlebars.AST.PartialNode = function(id, context) {
|
||||
this.type = "partial";
|
||||
|
||||
// TODO: disallow complex IDs
|
||||
|
||||
this.id = id;
|
||||
this.context = context;
|
||||
};
|
||||
|
||||
var verifyMatch = function(open, close) {
|
||||
if(open.original !== close.original) {
|
||||
throw new Handlebars.Exception(open.original + " doesn't match " + close.original);
|
||||
}
|
||||
};
|
||||
|
||||
Handlebars.AST.BlockNode = function(mustache, program, close) {
|
||||
verifyMatch(mustache.id, close);
|
||||
this.type = "block";
|
||||
this.mustache = mustache;
|
||||
this.program = program;
|
||||
};
|
||||
|
||||
Handlebars.AST.InverseNode = function(mustache, program, close) {
|
||||
verifyMatch(mustache.id, close);
|
||||
this.type = "inverse";
|
||||
this.mustache = mustache;
|
||||
this.program = program;
|
||||
};
|
||||
|
||||
Handlebars.AST.ContentNode = function(string) {
|
||||
this.type = "content";
|
||||
this.string = string;
|
||||
};
|
||||
|
||||
Handlebars.AST.IdNode = function(parts) {
|
||||
this.type = "ID";
|
||||
this.original = parts.join("/");
|
||||
|
||||
var dig = [], depth = 0;
|
||||
|
||||
for(var i=0,l=parts.length; i<l; i++) {
|
||||
var part = parts[i];
|
||||
|
||||
if(part === "..") { depth++; }
|
||||
else if(part === "." || part === "this") { continue; }
|
||||
else { dig.push(part); }
|
||||
}
|
||||
|
||||
this.parts = dig;
|
||||
this.depth = depth;
|
||||
this.isSimple = (dig.length === 1) && (depth === 0);
|
||||
};
|
||||
|
||||
Handlebars.AST.StringNode = function(string) {
|
||||
this.type = "STRING";
|
||||
this.string = string;
|
||||
};
|
||||
|
||||
Handlebars.AST.CommentNode = function(comment) {
|
||||
this.type = "comment";
|
||||
this.comment = comment;
|
||||
};
|
||||
|
||||
})();
|
||||
// END(BROWSER)
|
||||
|
||||
+206
-92
@@ -1,116 +1,230 @@
|
||||
var handlebars = require("handlebars/parser").parser;
|
||||
module Utils from "./utils";
|
||||
import Exception from "./exception";
|
||||
|
||||
// BEGIN(BROWSER)
|
||||
var Handlebars = {};
|
||||
export var VERSION = "v2.0.0-alpha.1";
|
||||
export var COMPILER_REVISION = 5;
|
||||
|
||||
Handlebars.Parser = handlebars;
|
||||
|
||||
Handlebars.parse = function(string) {
|
||||
Handlebars.Parser.yy = Handlebars.AST;
|
||||
return Handlebars.Parser.parse(string);
|
||||
export var REVISION_CHANGES = {
|
||||
1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it
|
||||
2: '== 1.0.0-rc.3',
|
||||
3: '== 1.0.0-rc.4',
|
||||
4: '== 1.x.x',
|
||||
5: '>= 2.0.0'
|
||||
};
|
||||
|
||||
Handlebars.print = function(ast) {
|
||||
return new Handlebars.PrintVisitor().accept(ast);
|
||||
};
|
||||
var isArray = Utils.isArray,
|
||||
isFunction = Utils.isFunction,
|
||||
toString = Utils.toString,
|
||||
objectType = '[object Object]';
|
||||
|
||||
Handlebars.Runtime = {};
|
||||
export function HandlebarsEnvironment(helpers, partials) {
|
||||
this.helpers = helpers || {};
|
||||
this.partials = partials || {};
|
||||
|
||||
Handlebars.Runtime.compile = function(string) {
|
||||
var ast = Handlebars.parse(string);
|
||||
registerDefaultHelpers(this);
|
||||
}
|
||||
|
||||
return function(context, helpers, partials) {
|
||||
helpers = helpers || Handlebars.helpers;
|
||||
partials = partials || Handlebars.partials;
|
||||
HandlebarsEnvironment.prototype = {
|
||||
constructor: HandlebarsEnvironment,
|
||||
|
||||
var internalContext = new Handlebars.Context(context, helpers, partials);
|
||||
var runtime = new Handlebars.Runtime(internalContext);
|
||||
runtime.accept(ast);
|
||||
return runtime.buffer;
|
||||
};
|
||||
};
|
||||
logger: logger,
|
||||
log: log,
|
||||
|
||||
Handlebars.helpers = {};
|
||||
Handlebars.partials = {};
|
||||
registerHelper: function(name, fn, inverse) {
|
||||
if (toString.call(name) === objectType) {
|
||||
if (inverse || fn) { throw new Exception('Arg not supported with multiple helpers'); }
|
||||
Utils.extend(this.helpers, name);
|
||||
} else {
|
||||
if (inverse) { fn.not = inverse; }
|
||||
this.helpers[name] = fn;
|
||||
}
|
||||
},
|
||||
unregisterHelper: function(name) {
|
||||
delete this.helpers[name];
|
||||
},
|
||||
|
||||
Handlebars.registerHelper = function(name, fn, inverse) {
|
||||
if(inverse) { fn.not = inverse; }
|
||||
this.helpers[name] = fn;
|
||||
};
|
||||
|
||||
Handlebars.registerPartial = function(name, str) {
|
||||
this.partials[name] = str;
|
||||
};
|
||||
|
||||
Handlebars.registerHelper('blockHelperMissing', function(context, fn, inverse) {
|
||||
inverse = inverse || function() {};
|
||||
|
||||
var ret = "";
|
||||
var type = Object.prototype.toString.call(context);
|
||||
|
||||
if(type === "[object Function]") {
|
||||
context = context();
|
||||
registerPartial: function(name, str) {
|
||||
if (toString.call(name) === objectType) {
|
||||
Utils.extend(this.partials, name);
|
||||
} else {
|
||||
this.partials[name] = str;
|
||||
}
|
||||
},
|
||||
unregisterPartial: function(name) {
|
||||
delete this.partials[name];
|
||||
}
|
||||
};
|
||||
|
||||
if(context === true) {
|
||||
return fn(this);
|
||||
} else if(context === false || context == null) {
|
||||
return inverse(this);
|
||||
} else if(type === "[object Array]") {
|
||||
if(context.length > 0) {
|
||||
for(var i=0, j=context.length; i<j; i++) {
|
||||
ret = ret + fn(context[i]);
|
||||
function registerDefaultHelpers(instance) {
|
||||
instance.registerHelper('helperMissing', function(/* [args, ]options */) {
|
||||
if(arguments.length === 1) {
|
||||
// A missing field in a {{foo}} constuct.
|
||||
return undefined;
|
||||
} else {
|
||||
// Someone is actually trying to call something, blow up.
|
||||
throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'");
|
||||
}
|
||||
});
|
||||
|
||||
instance.registerHelper('blockHelperMissing', function(context, options) {
|
||||
var inverse = options.inverse || function() {}, fn = options.fn;
|
||||
|
||||
if (isFunction(context)) { context = context.call(this); }
|
||||
|
||||
if(context === true) {
|
||||
return fn(this);
|
||||
} else if(context === false || context == null) {
|
||||
return inverse(this);
|
||||
} else if (isArray(context)) {
|
||||
if(context.length > 0) {
|
||||
if (options.ids) {
|
||||
options.ids = [options.name];
|
||||
}
|
||||
|
||||
return instance.helpers.each(context, options);
|
||||
} else {
|
||||
return inverse(this);
|
||||
}
|
||||
} else {
|
||||
if (options.data && options.ids) {
|
||||
var data = createFrame(options.data);
|
||||
data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name);
|
||||
options = {data: data};
|
||||
}
|
||||
|
||||
return fn(context, options);
|
||||
}
|
||||
});
|
||||
|
||||
instance.registerHelper('each', function(context, options) {
|
||||
// Allow for {{#each}}
|
||||
if (!options) {
|
||||
options = context;
|
||||
context = this;
|
||||
}
|
||||
|
||||
var fn = options.fn, inverse = options.inverse;
|
||||
var i = 0, ret = "", data;
|
||||
|
||||
var contextPath;
|
||||
if (options.data && options.ids) {
|
||||
contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.';
|
||||
}
|
||||
|
||||
if (isFunction(context)) { context = context.call(this); }
|
||||
|
||||
if (options.data) {
|
||||
data = createFrame(options.data);
|
||||
}
|
||||
|
||||
if(context && typeof context === 'object') {
|
||||
if (isArray(context)) {
|
||||
for(var j = context.length; i<j; i++) {
|
||||
if (data) {
|
||||
data.index = i;
|
||||
data.first = (i === 0);
|
||||
data.last = (i === (context.length-1));
|
||||
|
||||
if (contextPath) {
|
||||
data.contextPath = contextPath + i;
|
||||
}
|
||||
}
|
||||
ret = ret + fn(context[i], { data: data });
|
||||
}
|
||||
} else {
|
||||
for(var key in context) {
|
||||
if(context.hasOwnProperty(key)) {
|
||||
if(data) {
|
||||
data.key = key;
|
||||
data.index = i;
|
||||
data.first = (i === 0);
|
||||
|
||||
if (contextPath) {
|
||||
data.contextPath = contextPath + key;
|
||||
}
|
||||
}
|
||||
ret = ret + fn(context[key], {data: data});
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(i === 0){
|
||||
ret = inverse(this);
|
||||
}
|
||||
|
||||
return ret;
|
||||
} else {
|
||||
return fn(context);
|
||||
}
|
||||
}, function(context, fn) {
|
||||
return fn(context);
|
||||
});
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('each', function(context, fn, inverse) {
|
||||
var ret = "";
|
||||
instance.registerHelper('if', function(conditional, options) {
|
||||
if (isFunction(conditional)) { conditional = conditional.call(this); }
|
||||
|
||||
if(context && context.length > 0) {
|
||||
for(var i=0, j=context.length; i<j; i++) {
|
||||
ret = ret + fn(context[i]);
|
||||
// Default behavior is to render the positive path if the value is truthy and not empty.
|
||||
// The `includeZero` option may be set to treat the condtional as purely not empty based on the
|
||||
// behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative.
|
||||
if ((!options.hash.includeZero && !conditional) || Utils.isEmpty(conditional)) {
|
||||
return options.inverse(this);
|
||||
} else {
|
||||
return options.fn(this);
|
||||
}
|
||||
});
|
||||
|
||||
instance.registerHelper('unless', function(conditional, options) {
|
||||
return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash});
|
||||
});
|
||||
|
||||
instance.registerHelper('with', function(context, options) {
|
||||
if (isFunction(context)) { context = context.call(this); }
|
||||
|
||||
var fn = options.fn;
|
||||
|
||||
if (!Utils.isEmpty(context)) {
|
||||
if (options.data && options.ids) {
|
||||
var data = createFrame(options.data);
|
||||
data.contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]);
|
||||
options = {data:data};
|
||||
}
|
||||
|
||||
return fn(context, options);
|
||||
}
|
||||
});
|
||||
|
||||
instance.registerHelper('log', function(context, options) {
|
||||
var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1;
|
||||
instance.log(level, context);
|
||||
});
|
||||
|
||||
instance.registerHelper('lookup', function(obj, field, options) {
|
||||
return obj && obj[field];
|
||||
});
|
||||
}
|
||||
|
||||
export var logger = {
|
||||
methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' },
|
||||
|
||||
// State enum
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
WARN: 2,
|
||||
ERROR: 3,
|
||||
level: 3,
|
||||
|
||||
// can be overridden in the host environment
|
||||
log: function(level, obj) {
|
||||
if (logger.level <= level) {
|
||||
var method = logger.methodMap[level];
|
||||
if (typeof console !== 'undefined' && console[method]) {
|
||||
console[method].call(console, obj);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ret = inverse(this);
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('if', function(context, fn, inverse) {
|
||||
if(!context || context == []) {
|
||||
return inverse(this);
|
||||
} else {
|
||||
return fn(this);
|
||||
}
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('unless', function(context, fn, inverse) {
|
||||
Handlebars.helpers['if'].call(this, context, inverse, fn);
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('with', function(context, fn) {
|
||||
return fn(context);
|
||||
});
|
||||
|
||||
Handlebars.logger = {
|
||||
DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3,
|
||||
|
||||
// override in the host environment
|
||||
log: function(level, str) {}
|
||||
};
|
||||
|
||||
Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); };
|
||||
|
||||
// END(BROWSER)
|
||||
|
||||
module.exports = Handlebars;
|
||||
export function log(level, obj) { logger.log(level, obj); }
|
||||
|
||||
export var createFrame = function(object) {
|
||||
var frame = Utils.extend({}, object);
|
||||
frame._parent = object;
|
||||
return frame;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import Exception from "../exception";
|
||||
|
||||
function LocationInfo(locInfo){
|
||||
locInfo = locInfo || {};
|
||||
this.firstLine = locInfo.first_line;
|
||||
this.firstColumn = locInfo.first_column;
|
||||
this.lastColumn = locInfo.last_column;
|
||||
this.lastLine = locInfo.last_line;
|
||||
}
|
||||
|
||||
var AST = {
|
||||
ProgramNode: function(statements, inverseStrip, inverse, locInfo) {
|
||||
var inverseLocationInfo, firstInverseNode;
|
||||
if (arguments.length === 3) {
|
||||
locInfo = inverse;
|
||||
inverse = null;
|
||||
} else if (arguments.length === 2) {
|
||||
locInfo = inverseStrip;
|
||||
inverseStrip = null;
|
||||
}
|
||||
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "program";
|
||||
this.statements = statements;
|
||||
this.strip = {};
|
||||
|
||||
if(inverse) {
|
||||
firstInverseNode = inverse[0];
|
||||
if (firstInverseNode) {
|
||||
inverseLocationInfo = {
|
||||
first_line: firstInverseNode.firstLine,
|
||||
last_line: firstInverseNode.lastLine,
|
||||
last_column: firstInverseNode.lastColumn,
|
||||
first_column: firstInverseNode.firstColumn
|
||||
};
|
||||
this.inverse = new AST.ProgramNode(inverse, inverseStrip, inverseLocationInfo);
|
||||
} else {
|
||||
this.inverse = new AST.ProgramNode(inverse, inverseStrip);
|
||||
}
|
||||
this.strip.right = inverseStrip.left;
|
||||
} else if (inverseStrip) {
|
||||
this.strip.left = inverseStrip.right;
|
||||
}
|
||||
},
|
||||
|
||||
MustacheNode: function(rawParams, hash, open, strip, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "mustache";
|
||||
this.strip = strip;
|
||||
|
||||
// Open may be a string parsed from the parser or a passed boolean flag
|
||||
if (open != null && open.charAt) {
|
||||
// Must use charAt to support IE pre-10
|
||||
var escapeFlag = open.charAt(3) || open.charAt(2);
|
||||
this.escaped = escapeFlag !== '{' && escapeFlag !== '&';
|
||||
} else {
|
||||
this.escaped = !!open;
|
||||
}
|
||||
|
||||
if (rawParams instanceof AST.SexprNode) {
|
||||
this.sexpr = rawParams;
|
||||
} else {
|
||||
// Support old AST API
|
||||
this.sexpr = new AST.SexprNode(rawParams, hash);
|
||||
}
|
||||
|
||||
this.sexpr.isRoot = true;
|
||||
|
||||
// Support old AST API that stored this info in MustacheNode
|
||||
this.id = this.sexpr.id;
|
||||
this.params = this.sexpr.params;
|
||||
this.hash = this.sexpr.hash;
|
||||
this.eligibleHelper = this.sexpr.eligibleHelper;
|
||||
this.isHelper = this.sexpr.isHelper;
|
||||
},
|
||||
|
||||
SexprNode: function(rawParams, hash, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
|
||||
this.type = "sexpr";
|
||||
this.hash = hash;
|
||||
|
||||
var id = this.id = rawParams[0];
|
||||
var params = this.params = rawParams.slice(1);
|
||||
|
||||
// a mustache is definitely a helper if:
|
||||
// * it is an eligible helper, and
|
||||
// * it has at least one parameter or hash segment
|
||||
this.isHelper = params.length || hash;
|
||||
|
||||
// a mustache is an eligible helper if:
|
||||
// * its id is simple (a single part, not `this` or `..`)
|
||||
this.eligibleHelper = this.isHelper || id.isSimple;
|
||||
|
||||
// if a mustache is an eligible helper but not a definite
|
||||
// helper, it is ambiguous, and will be resolved in a later
|
||||
// pass or at runtime.
|
||||
},
|
||||
|
||||
PartialNode: function(partialName, context, hash, strip, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "partial";
|
||||
this.partialName = partialName;
|
||||
this.context = context;
|
||||
this.hash = hash;
|
||||
this.strip = strip;
|
||||
},
|
||||
|
||||
BlockNode: function(mustache, program, inverse, close, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
|
||||
if(mustache.sexpr.id.original !== close.path.original) {
|
||||
throw new Exception(mustache.sexpr.id.original + " doesn't match " + close.path.original, this);
|
||||
}
|
||||
|
||||
this.type = 'block';
|
||||
this.mustache = mustache;
|
||||
this.program = program;
|
||||
this.inverse = inverse;
|
||||
|
||||
this.strip = {
|
||||
left: mustache.strip.left,
|
||||
right: close.strip.right
|
||||
};
|
||||
|
||||
(program || inverse).strip.left = mustache.strip.right;
|
||||
(inverse || program).strip.right = close.strip.left;
|
||||
|
||||
if (inverse && !program) {
|
||||
this.isInverse = true;
|
||||
}
|
||||
},
|
||||
|
||||
RawBlockNode: function(mustache, content, close, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
|
||||
if (mustache.sexpr.id.original !== close) {
|
||||
throw new Exception(mustache.sexpr.id.original + " doesn't match " + close, this);
|
||||
}
|
||||
|
||||
content = new AST.ContentNode(content, locInfo);
|
||||
|
||||
this.type = 'block';
|
||||
this.mustache = mustache;
|
||||
this.program = new AST.ProgramNode([content], locInfo);
|
||||
},
|
||||
|
||||
ContentNode: function(string, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "content";
|
||||
this.string = string;
|
||||
},
|
||||
|
||||
HashNode: function(pairs, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "hash";
|
||||
this.pairs = pairs;
|
||||
},
|
||||
|
||||
IdNode: function(parts, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "ID";
|
||||
|
||||
var original = "",
|
||||
dig = [],
|
||||
depth = 0,
|
||||
depthString = '';
|
||||
|
||||
for(var i=0,l=parts.length; i<l; i++) {
|
||||
var part = parts[i].part;
|
||||
original += (parts[i].separator || '') + part;
|
||||
|
||||
if (part === ".." || part === "." || part === "this") {
|
||||
if (dig.length > 0) {
|
||||
throw new Exception("Invalid path: " + original, this);
|
||||
} else if (part === "..") {
|
||||
depth++;
|
||||
depthString += '../';
|
||||
} else {
|
||||
this.isScoped = true;
|
||||
}
|
||||
} else {
|
||||
dig.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
this.original = original;
|
||||
this.parts = dig;
|
||||
this.string = dig.join('.');
|
||||
this.depth = depth;
|
||||
this.idName = depthString + this.string;
|
||||
|
||||
// an ID is simple if it only has one part, and that part is not
|
||||
// `..` or `this`.
|
||||
this.isSimple = parts.length === 1 && !this.isScoped && depth === 0;
|
||||
|
||||
this.stringModeValue = this.string;
|
||||
},
|
||||
|
||||
PartialNameNode: function(name, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "PARTIAL_NAME";
|
||||
this.name = name.original;
|
||||
},
|
||||
|
||||
DataNode: function(id, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "DATA";
|
||||
this.id = id;
|
||||
this.stringModeValue = id.stringModeValue;
|
||||
this.idName = '@' + id.stringModeValue;
|
||||
},
|
||||
|
||||
StringNode: function(string, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "STRING";
|
||||
this.original =
|
||||
this.string =
|
||||
this.stringModeValue = string;
|
||||
},
|
||||
|
||||
NumberNode: function(number, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "NUMBER";
|
||||
this.original =
|
||||
this.number = number;
|
||||
this.stringModeValue = Number(number);
|
||||
},
|
||||
|
||||
BooleanNode: function(bool, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "BOOLEAN";
|
||||
this.bool = bool;
|
||||
this.stringModeValue = bool === "true";
|
||||
},
|
||||
|
||||
CommentNode: function(comment, locInfo) {
|
||||
LocationInfo.call(this, locInfo);
|
||||
this.type = "comment";
|
||||
this.comment = comment;
|
||||
}
|
||||
};
|
||||
|
||||
// Must be exported as an object rather than the root of the module as the jison lexer
|
||||
// most modify the object to operate properly.
|
||||
export default AST;
|
||||
@@ -0,0 +1,12 @@
|
||||
import parser from "./parser";
|
||||
import AST from "./ast";
|
||||
|
||||
export { parser };
|
||||
|
||||
export function parse(input) {
|
||||
// Just return if an already-compile AST was passed in.
|
||||
if(input.constructor === AST.ProgramNode) { return input; }
|
||||
|
||||
parser.yy = AST;
|
||||
return parser.parse(input);
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
import Exception from "../exception";
|
||||
|
||||
export function Compiler() {}
|
||||
|
||||
// the foundHelper register will disambiguate helper lookup from finding a
|
||||
// function in a context. This is necessary for mustache compatibility, which
|
||||
// requires that context functions in blocks are evaluated by blockHelperMissing,
|
||||
// and then proceed as if the resulting value was provided to blockHelperMissing.
|
||||
|
||||
Compiler.prototype = {
|
||||
compiler: Compiler,
|
||||
|
||||
disassemble: function() {
|
||||
var opcodes = this.opcodes, opcode, out = [], params, param;
|
||||
|
||||
for (var i=0, l=opcodes.length; i<l; i++) {
|
||||
opcode = opcodes[i];
|
||||
|
||||
if (opcode.opcode === 'DECLARE') {
|
||||
out.push("DECLARE " + opcode.name + "=" + opcode.value);
|
||||
} else {
|
||||
params = [];
|
||||
for (var j=0; j<opcode.args.length; j++) {
|
||||
param = opcode.args[j];
|
||||
if (typeof param === "string") {
|
||||
param = "\"" + param.replace("\n", "\\n") + "\"";
|
||||
}
|
||||
params.push(param);
|
||||
}
|
||||
out.push(opcode.opcode + " " + params.join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
return out.join("\n");
|
||||
},
|
||||
|
||||
equals: function(other) {
|
||||
var len = this.opcodes.length;
|
||||
if (other.opcodes.length !== len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
var opcode = this.opcodes[i],
|
||||
otherOpcode = other.opcodes[i];
|
||||
if (opcode.opcode !== otherOpcode.opcode || opcode.args.length !== otherOpcode.args.length) {
|
||||
return false;
|
||||
}
|
||||
for (var j = 0; j < opcode.args.length; j++) {
|
||||
if (opcode.args[j] !== otherOpcode.args[j]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
len = this.children.length;
|
||||
if (other.children.length !== len) {
|
||||
return false;
|
||||
}
|
||||
for (i = 0; i < len; i++) {
|
||||
if (!this.children[i].equals(other.children[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
guid: 0,
|
||||
|
||||
compile: function(program, options) {
|
||||
this.opcodes = [];
|
||||
this.children = [];
|
||||
this.depths = {list: []};
|
||||
this.options = options;
|
||||
this.stringParams = options.stringParams;
|
||||
this.trackIds = options.trackIds;
|
||||
|
||||
// These changes will propagate to the other compiler components
|
||||
var knownHelpers = this.options.knownHelpers;
|
||||
this.options.knownHelpers = {
|
||||
'helperMissing': true,
|
||||
'blockHelperMissing': true,
|
||||
'each': true,
|
||||
'if': true,
|
||||
'unless': true,
|
||||
'with': true,
|
||||
'log': true,
|
||||
'lookup': true
|
||||
};
|
||||
if (knownHelpers) {
|
||||
for (var name in knownHelpers) {
|
||||
this.options.knownHelpers[name] = knownHelpers[name];
|
||||
}
|
||||
}
|
||||
|
||||
return this.accept(program);
|
||||
},
|
||||
|
||||
accept: function(node) {
|
||||
var strip = node.strip || {},
|
||||
ret;
|
||||
if (strip.left) {
|
||||
this.opcode('strip');
|
||||
}
|
||||
|
||||
ret = this[node.type](node);
|
||||
|
||||
if (strip.right) {
|
||||
this.opcode('strip');
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
program: function(program) {
|
||||
var statements = program.statements;
|
||||
|
||||
for(var i=0, l=statements.length; i<l; i++) {
|
||||
this.accept(statements[i]);
|
||||
}
|
||||
this.isSimple = l === 1;
|
||||
|
||||
this.depths.list = this.depths.list.sort(function(a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
compileProgram: function(program) {
|
||||
var result = new this.compiler().compile(program, this.options);
|
||||
var guid = this.guid++, depth;
|
||||
|
||||
this.usePartial = this.usePartial || result.usePartial;
|
||||
|
||||
this.children[guid] = result;
|
||||
|
||||
for(var i=0, l=result.depths.list.length; i<l; i++) {
|
||||
depth = result.depths.list[i];
|
||||
|
||||
if(depth < 2) { continue; }
|
||||
else { this.addDepth(depth - 1); }
|
||||
}
|
||||
|
||||
return guid;
|
||||
},
|
||||
|
||||
block: function(block) {
|
||||
var mustache = block.mustache,
|
||||
program = block.program,
|
||||
inverse = block.inverse;
|
||||
|
||||
if (program) {
|
||||
program = this.compileProgram(program);
|
||||
}
|
||||
|
||||
if (inverse) {
|
||||
inverse = this.compileProgram(inverse);
|
||||
}
|
||||
|
||||
var sexpr = mustache.sexpr;
|
||||
var type = this.classifySexpr(sexpr);
|
||||
|
||||
if (type === "helper") {
|
||||
this.helperSexpr(sexpr, program, inverse);
|
||||
} else if (type === "simple") {
|
||||
this.simpleSexpr(sexpr);
|
||||
|
||||
// now that the simple mustache is resolved, we need to
|
||||
// evaluate it by executing `blockHelperMissing`
|
||||
this.opcode('pushProgram', program);
|
||||
this.opcode('pushProgram', inverse);
|
||||
this.opcode('emptyHash');
|
||||
this.opcode('blockValue', sexpr.id.original);
|
||||
} else {
|
||||
this.ambiguousSexpr(sexpr, program, inverse);
|
||||
|
||||
// now that the simple mustache is resolved, we need to
|
||||
// evaluate it by executing `blockHelperMissing`
|
||||
this.opcode('pushProgram', program);
|
||||
this.opcode('pushProgram', inverse);
|
||||
this.opcode('emptyHash');
|
||||
this.opcode('ambiguousBlockValue');
|
||||
}
|
||||
|
||||
this.opcode('append');
|
||||
},
|
||||
|
||||
hash: function(hash) {
|
||||
var pairs = hash.pairs, pair;
|
||||
|
||||
this.opcode('pushHash');
|
||||
|
||||
for(var i=0, l=pairs.length; i<l; i++) {
|
||||
pair = pairs[i];
|
||||
|
||||
this.pushParam(pair[1]);
|
||||
this.opcode('assignToHash', pair[0]);
|
||||
}
|
||||
this.opcode('popHash');
|
||||
},
|
||||
|
||||
partial: function(partial) {
|
||||
var partialName = partial.partialName;
|
||||
this.usePartial = true;
|
||||
|
||||
if (partial.hash) {
|
||||
this.accept(partial.hash);
|
||||
} else {
|
||||
this.opcode('push', 'undefined');
|
||||
}
|
||||
|
||||
if (partial.context) {
|
||||
this.accept(partial.context);
|
||||
} else {
|
||||
this.opcode('push', 'depth0');
|
||||
}
|
||||
|
||||
this.opcode('invokePartial', partialName.name);
|
||||
this.opcode('append');
|
||||
},
|
||||
|
||||
content: function(content) {
|
||||
this.opcode('appendContent', content.string);
|
||||
},
|
||||
|
||||
mustache: function(mustache) {
|
||||
this.sexpr(mustache.sexpr);
|
||||
|
||||
if(mustache.escaped && !this.options.noEscape) {
|
||||
this.opcode('appendEscaped');
|
||||
} else {
|
||||
this.opcode('append');
|
||||
}
|
||||
},
|
||||
|
||||
ambiguousSexpr: function(sexpr, program, inverse) {
|
||||
var id = sexpr.id,
|
||||
name = id.parts[0],
|
||||
isBlock = program != null || inverse != null;
|
||||
|
||||
this.opcode('getContext', id.depth);
|
||||
|
||||
this.opcode('pushProgram', program);
|
||||
this.opcode('pushProgram', inverse);
|
||||
|
||||
this.opcode('invokeAmbiguous', name, isBlock);
|
||||
},
|
||||
|
||||
simpleSexpr: function(sexpr) {
|
||||
var id = sexpr.id;
|
||||
|
||||
if (id.type === 'DATA') {
|
||||
this.DATA(id);
|
||||
} else if (id.parts.length) {
|
||||
this.ID(id);
|
||||
} else {
|
||||
// Simplified ID for `this`
|
||||
this.addDepth(id.depth);
|
||||
this.opcode('getContext', id.depth);
|
||||
this.opcode('pushContext');
|
||||
}
|
||||
|
||||
this.opcode('resolvePossibleLambda');
|
||||
},
|
||||
|
||||
helperSexpr: function(sexpr, program, inverse) {
|
||||
var params = this.setupFullMustacheParams(sexpr, program, inverse),
|
||||
id = sexpr.id,
|
||||
name = id.parts[0];
|
||||
|
||||
if (this.options.knownHelpers[name]) {
|
||||
this.opcode('invokeKnownHelper', params.length, name);
|
||||
} else if (this.options.knownHelpersOnly) {
|
||||
throw new Exception("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
|
||||
} else {
|
||||
this.ID(id);
|
||||
this.opcode('invokeHelper', params.length, name, sexpr.isRoot);
|
||||
}
|
||||
},
|
||||
|
||||
sexpr: function(sexpr) {
|
||||
var type = this.classifySexpr(sexpr);
|
||||
|
||||
if (type === "simple") {
|
||||
this.simpleSexpr(sexpr);
|
||||
} else if (type === "helper") {
|
||||
this.helperSexpr(sexpr);
|
||||
} else {
|
||||
this.ambiguousSexpr(sexpr);
|
||||
}
|
||||
},
|
||||
|
||||
ID: function(id) {
|
||||
this.addDepth(id.depth);
|
||||
this.opcode('getContext', id.depth);
|
||||
|
||||
var name = id.parts[0];
|
||||
if (!name) {
|
||||
this.opcode('pushContext');
|
||||
} else {
|
||||
this.opcode('lookupOnContext', id.parts[0]);
|
||||
}
|
||||
|
||||
for(var i=1, l=id.parts.length; i<l; i++) {
|
||||
this.opcode('lookup', id.parts[i]);
|
||||
}
|
||||
},
|
||||
|
||||
DATA: function(data) {
|
||||
this.options.data = true;
|
||||
this.opcode('lookupData', data.id.depth);
|
||||
var parts = data.id.parts;
|
||||
for(var i=0, l=parts.length; i<l; i++) {
|
||||
this.opcode('lookup', parts[i]);
|
||||
}
|
||||
},
|
||||
|
||||
STRING: function(string) {
|
||||
this.opcode('pushString', string.string);
|
||||
},
|
||||
|
||||
NUMBER: function(number) {
|
||||
this.opcode('pushLiteral', number.number);
|
||||
},
|
||||
|
||||
BOOLEAN: function(bool) {
|
||||
this.opcode('pushLiteral', bool.bool);
|
||||
},
|
||||
|
||||
comment: function() {},
|
||||
|
||||
// HELPERS
|
||||
opcode: function(name) {
|
||||
this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) });
|
||||
},
|
||||
|
||||
declare: function(name, value) {
|
||||
this.opcodes.push({ opcode: 'DECLARE', name: name, value: value });
|
||||
},
|
||||
|
||||
addDepth: function(depth) {
|
||||
if(depth === 0) { return; }
|
||||
|
||||
if(!this.depths[depth]) {
|
||||
this.depths[depth] = true;
|
||||
this.depths.list.push(depth);
|
||||
}
|
||||
},
|
||||
|
||||
classifySexpr: function(sexpr) {
|
||||
var isHelper = sexpr.isHelper;
|
||||
var isEligible = sexpr.eligibleHelper;
|
||||
var options = this.options;
|
||||
|
||||
// if ambiguous, we can possibly resolve the ambiguity now
|
||||
// An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc.
|
||||
if (isEligible && !isHelper) {
|
||||
var name = sexpr.id.parts[0];
|
||||
|
||||
if (options.knownHelpers[name]) {
|
||||
isHelper = true;
|
||||
} else if (options.knownHelpersOnly) {
|
||||
isEligible = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHelper) { return "helper"; }
|
||||
else if (isEligible) { return "ambiguous"; }
|
||||
else { return "simple"; }
|
||||
},
|
||||
|
||||
pushParams: function(params) {
|
||||
var i = params.length;
|
||||
|
||||
while(i--) {
|
||||
this.pushParam(params[i]);
|
||||
}
|
||||
},
|
||||
|
||||
pushParam: function(val) {
|
||||
if (this.stringParams) {
|
||||
if(val.depth) {
|
||||
this.addDepth(val.depth);
|
||||
}
|
||||
this.opcode('getContext', val.depth || 0);
|
||||
this.opcode('pushStringParam', val.stringModeValue, val.type);
|
||||
|
||||
if (val.type === 'sexpr') {
|
||||
// Subexpressions get evaluated and passed in
|
||||
// in string params mode.
|
||||
this.sexpr(val);
|
||||
}
|
||||
} else {
|
||||
if (this.trackIds) {
|
||||
this.opcode('pushId', val.type, val.idName || val.stringModeValue);
|
||||
}
|
||||
this.accept(val);
|
||||
}
|
||||
},
|
||||
|
||||
setupFullMustacheParams: function(sexpr, program, inverse) {
|
||||
var params = sexpr.params;
|
||||
this.pushParams(params);
|
||||
|
||||
this.opcode('pushProgram', program);
|
||||
this.opcode('pushProgram', inverse);
|
||||
|
||||
if (sexpr.hash) {
|
||||
this.hash(sexpr.hash);
|
||||
} else {
|
||||
this.opcode('emptyHash');
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
};
|
||||
|
||||
export function precompile(input, options, env) {
|
||||
if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) {
|
||||
throw new Exception("You must pass a string or Handlebars AST to Handlebars.precompile. You passed " + input);
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
if (!('data' in options)) {
|
||||
options.data = true;
|
||||
}
|
||||
|
||||
var ast = env.parse(input);
|
||||
var environment = new env.Compiler().compile(ast, options);
|
||||
return new env.JavaScriptCompiler().compile(environment, options);
|
||||
}
|
||||
|
||||
export function compile(input, options, env) {
|
||||
if (input == null || (typeof input !== 'string' && input.constructor !== env.AST.ProgramNode)) {
|
||||
throw new Exception("You must pass a string or Handlebars AST to Handlebars.compile. You passed " + input);
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (!('data' in options)) {
|
||||
options.data = true;
|
||||
}
|
||||
|
||||
var compiled;
|
||||
|
||||
function compileInput() {
|
||||
var ast = env.parse(input);
|
||||
var environment = new env.Compiler().compile(ast, options);
|
||||
var templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true);
|
||||
return env.template(templateSpec);
|
||||
}
|
||||
|
||||
// Template is only compiled on first use and cached after that point.
|
||||
var ret = function(context, options) {
|
||||
if (!compiled) {
|
||||
compiled = compileInput();
|
||||
}
|
||||
return compiled.call(this, context, options);
|
||||
};
|
||||
ret.child = function(i) {
|
||||
if (!compiled) {
|
||||
compiled = compileInput();
|
||||
}
|
||||
return compiled.child(i);
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,989 @@
|
||||
import { COMPILER_REVISION, REVISION_CHANGES, log } from "../base";
|
||||
import Exception from "../exception";
|
||||
|
||||
function Literal(value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
function JavaScriptCompiler() {}
|
||||
|
||||
JavaScriptCompiler.prototype = {
|
||||
// PUBLIC API: You can override these methods in a subclass to provide
|
||||
// alternative compiled forms for name lookup and buffering semantics
|
||||
nameLookup: function(parent, name /* , type*/) {
|
||||
var wrap,
|
||||
ret;
|
||||
if (parent.indexOf('depth') === 0) {
|
||||
wrap = true;
|
||||
}
|
||||
|
||||
if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
|
||||
ret = parent + "." + name;
|
||||
} else {
|
||||
ret = parent + "['" + name + "']";
|
||||
}
|
||||
|
||||
if (wrap) {
|
||||
return '(' + parent + ' && ' + ret + ')';
|
||||
} else {
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
|
||||
compilerInfo: function() {
|
||||
var revision = COMPILER_REVISION,
|
||||
versions = REVISION_CHANGES[revision];
|
||||
return [revision, versions];
|
||||
},
|
||||
|
||||
appendToBuffer: function(string) {
|
||||
if (this.environment.isSimple) {
|
||||
return "return " + string + ";";
|
||||
} else {
|
||||
return {
|
||||
appendToBuffer: true,
|
||||
content: string,
|
||||
toString: function() { return "buffer += " + string + ";"; }
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
initializeBuffer: function() {
|
||||
return this.quotedString("");
|
||||
},
|
||||
|
||||
namespace: "Handlebars",
|
||||
// END PUBLIC API
|
||||
|
||||
compile: function(environment, options, context, asObject) {
|
||||
this.environment = environment;
|
||||
this.options = options || {};
|
||||
this.stringParams = this.options.stringParams;
|
||||
this.trackIds = this.options.trackIds;
|
||||
this.precompile = !asObject;
|
||||
|
||||
log('debug', this.environment.disassemble() + "\n\n");
|
||||
|
||||
this.name = this.environment.name;
|
||||
this.isChild = !!context;
|
||||
this.context = context || {
|
||||
programs: [],
|
||||
environments: []
|
||||
};
|
||||
|
||||
this.preamble();
|
||||
|
||||
this.stackSlot = 0;
|
||||
this.stackVars = [];
|
||||
this.aliases = {};
|
||||
this.registers = { list: [] };
|
||||
this.hashes = [];
|
||||
this.compileStack = [];
|
||||
this.inlineStack = [];
|
||||
|
||||
this.compileChildren(environment, options);
|
||||
|
||||
var opcodes = environment.opcodes, opcode;
|
||||
|
||||
this.i = 0;
|
||||
|
||||
for(var l=opcodes.length; this.i<l; this.i++) {
|
||||
opcode = opcodes[this.i];
|
||||
|
||||
if(opcode.opcode === 'DECLARE') {
|
||||
this[opcode.name] = opcode.value;
|
||||
} else {
|
||||
this[opcode.opcode].apply(this, opcode.args);
|
||||
}
|
||||
|
||||
// Reset the stripNext flag if it was not set by this operation.
|
||||
if (opcode.opcode !== this.stripNext) {
|
||||
this.stripNext = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush any trailing content that might be pending.
|
||||
this.pushSource('');
|
||||
|
||||
if (this.stackSlot || this.inlineStack.length || this.compileStack.length) {
|
||||
throw new Exception('Compile completed with content left on stack');
|
||||
}
|
||||
|
||||
var fn = this.createFunctionContext(asObject);
|
||||
if (!this.isChild) {
|
||||
var ret = {
|
||||
compiler: this.compilerInfo(),
|
||||
main: fn
|
||||
};
|
||||
this.context.programs.map(function(program, index) {
|
||||
if (program) {
|
||||
ret[index] = program;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.environment.usePartial) {
|
||||
ret.usePartial = true;
|
||||
}
|
||||
if (this.options.data) {
|
||||
ret.useData = true;
|
||||
}
|
||||
|
||||
if (!asObject) {
|
||||
ret.compiler = JSON.stringify(ret.compiler);
|
||||
ret = this.objectLiteral(ret);
|
||||
}
|
||||
|
||||
return ret;
|
||||
} else {
|
||||
return fn;
|
||||
}
|
||||
},
|
||||
|
||||
preamble: function() {
|
||||
// track the last context pushed into place to allow skipping the
|
||||
// getContext opcode when it would be a noop
|
||||
this.lastContext = 0;
|
||||
this.source = [];
|
||||
},
|
||||
|
||||
createFunctionContext: function(asObject) {
|
||||
var varDeclarations = '';
|
||||
|
||||
var locals = this.stackVars.concat(this.registers.list);
|
||||
if(locals.length > 0) {
|
||||
varDeclarations += ", " + locals.join(", ");
|
||||
}
|
||||
|
||||
// Generate minimizer alias mappings
|
||||
for (var alias in this.aliases) {
|
||||
if (this.aliases.hasOwnProperty(alias)) {
|
||||
varDeclarations += ', ' + alias + '=' + this.aliases[alias];
|
||||
}
|
||||
}
|
||||
|
||||
var params = ["depth0", "helpers", "partials", "data"];
|
||||
|
||||
for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
|
||||
params.push("depth" + this.environment.depths.list[i]);
|
||||
}
|
||||
|
||||
// Perform a second pass over the output to merge content when possible
|
||||
var source = this.mergeSource(varDeclarations);
|
||||
|
||||
if (asObject) {
|
||||
params.push(source);
|
||||
|
||||
return Function.apply(this, params);
|
||||
} else {
|
||||
return 'function(' + params.join(',') + ') {\n ' + source + '}';
|
||||
}
|
||||
},
|
||||
mergeSource: function(varDeclarations) {
|
||||
var source = '',
|
||||
buffer,
|
||||
appendOnly = !this.forceBuffer,
|
||||
appendFirst;
|
||||
|
||||
for (var i = 0, len = this.source.length; i < len; i++) {
|
||||
var line = this.source[i];
|
||||
if (line.appendToBuffer) {
|
||||
if (buffer) {
|
||||
buffer = buffer + '\n + ' + line.content;
|
||||
} else {
|
||||
buffer = line.content;
|
||||
}
|
||||
} else {
|
||||
if (buffer) {
|
||||
if (!source) {
|
||||
appendFirst = true;
|
||||
source = buffer + ';\n ';
|
||||
} else {
|
||||
source += 'buffer += ' + buffer + ';\n ';
|
||||
}
|
||||
buffer = undefined;
|
||||
}
|
||||
source += line + '\n ';
|
||||
|
||||
if (!this.environment.isSimple) {
|
||||
appendOnly = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (appendOnly) {
|
||||
if (buffer || !source) {
|
||||
source += 'return ' + (buffer || '""') + ';\n';
|
||||
}
|
||||
} else {
|
||||
varDeclarations += ", buffer = " + (appendFirst ? '' : this.initializeBuffer());
|
||||
if (buffer) {
|
||||
source += 'return buffer + ' + buffer + ';\n';
|
||||
} else {
|
||||
source += 'return buffer;\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (varDeclarations) {
|
||||
source = 'var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n ') + source;
|
||||
}
|
||||
|
||||
return source;
|
||||
},
|
||||
|
||||
// [blockValue]
|
||||
//
|
||||
// On stack, before: hash, inverse, program, value
|
||||
// On stack, after: return value of blockHelperMissing
|
||||
//
|
||||
// The purpose of this opcode is to take a block of the form
|
||||
// `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and
|
||||
// replace it on the stack with the result of properly
|
||||
// invoking blockHelperMissing.
|
||||
blockValue: function(name) {
|
||||
this.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
|
||||
|
||||
var params = ["depth0"];
|
||||
this.setupParams(name, 0, params);
|
||||
|
||||
this.replaceStack(function(current) {
|
||||
params.splice(1, 0, current);
|
||||
return "blockHelperMissing.call(" + params.join(", ") + ")";
|
||||
});
|
||||
},
|
||||
|
||||
// [ambiguousBlockValue]
|
||||
//
|
||||
// On stack, before: hash, inverse, program, value
|
||||
// Compiler value, before: lastHelper=value of last found helper, if any
|
||||
// On stack, after, if no lastHelper: same as [blockValue]
|
||||
// On stack, after, if lastHelper: value
|
||||
ambiguousBlockValue: function() {
|
||||
this.aliases.blockHelperMissing = 'helpers.blockHelperMissing';
|
||||
|
||||
// We're being a bit cheeky and reusing the options value from the prior exec
|
||||
var params = ["depth0"];
|
||||
this.setupParams('', 0, params, true);
|
||||
|
||||
this.flushInline();
|
||||
|
||||
var current = this.topStack();
|
||||
params.splice(1, 0, current);
|
||||
|
||||
this.pushSource("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }");
|
||||
},
|
||||
|
||||
// [appendContent]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: ...
|
||||
//
|
||||
// Appends the string value of `content` to the current buffer
|
||||
appendContent: function(content) {
|
||||
if (this.pendingContent) {
|
||||
content = this.pendingContent + content;
|
||||
}
|
||||
if (this.stripNext) {
|
||||
content = content.replace(/^\s+/, '');
|
||||
}
|
||||
|
||||
this.pendingContent = content;
|
||||
},
|
||||
|
||||
// [strip]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: ...
|
||||
//
|
||||
// Removes any trailing whitespace from the prior content node and flags
|
||||
// the next operation for stripping if it is a content node.
|
||||
strip: function() {
|
||||
if (this.pendingContent) {
|
||||
this.pendingContent = this.pendingContent.replace(/\s+$/, '');
|
||||
}
|
||||
this.stripNext = 'strip';
|
||||
},
|
||||
|
||||
// [append]
|
||||
//
|
||||
// On stack, before: value, ...
|
||||
// On stack, after: ...
|
||||
//
|
||||
// Coerces `value` to a String and appends it to the current buffer.
|
||||
//
|
||||
// If `value` is truthy, or 0, it is coerced into a string and appended
|
||||
// Otherwise, the empty string is appended
|
||||
append: function() {
|
||||
// Force anything that is inlined onto the stack so we don't have duplication
|
||||
// when we examine local
|
||||
this.flushInline();
|
||||
var local = this.popStack();
|
||||
this.pushSource("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
|
||||
if (this.environment.isSimple) {
|
||||
this.pushSource("else { " + this.appendToBuffer("''") + " }");
|
||||
}
|
||||
},
|
||||
|
||||
// [appendEscaped]
|
||||
//
|
||||
// On stack, before: value, ...
|
||||
// On stack, after: ...
|
||||
//
|
||||
// Escape `value` and append it to the buffer
|
||||
appendEscaped: function() {
|
||||
this.aliases.escapeExpression = 'this.escapeExpression';
|
||||
|
||||
this.pushSource(this.appendToBuffer("escapeExpression(" + this.popStack() + ")"));
|
||||
},
|
||||
|
||||
// [getContext]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: ...
|
||||
// Compiler value, after: lastContext=depth
|
||||
//
|
||||
// Set the value of the `lastContext` compiler value to the depth
|
||||
getContext: function(depth) {
|
||||
if(this.lastContext !== depth) {
|
||||
this.lastContext = depth;
|
||||
}
|
||||
},
|
||||
|
||||
// [lookupOnContext]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: currentContext[name], ...
|
||||
//
|
||||
// Looks up the value of `name` on the current context and pushes
|
||||
// it onto the stack.
|
||||
lookupOnContext: function(name) {
|
||||
this.push(this.nameLookup('depth' + this.lastContext, name, 'context'));
|
||||
},
|
||||
|
||||
// [pushContext]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: currentContext, ...
|
||||
//
|
||||
// Pushes the value of the current context onto the stack.
|
||||
pushContext: function() {
|
||||
this.pushStackLiteral('depth' + this.lastContext);
|
||||
},
|
||||
|
||||
// [resolvePossibleLambda]
|
||||
//
|
||||
// On stack, before: value, ...
|
||||
// On stack, after: resolved value, ...
|
||||
//
|
||||
// If the `value` is a lambda, replace it on the stack by
|
||||
// the return value of the lambda
|
||||
resolvePossibleLambda: function() {
|
||||
this.aliases.functionType = '"function"';
|
||||
|
||||
this.replaceStack(function(current) {
|
||||
return "typeof " + current + " === functionType ? " + current + ".apply(depth0) : " + current;
|
||||
});
|
||||
},
|
||||
|
||||
// [lookup]
|
||||
//
|
||||
// On stack, before: value, ...
|
||||
// On stack, after: value[name], ...
|
||||
//
|
||||
// Replace the value on the stack with the result of looking
|
||||
// up `name` on `value`
|
||||
lookup: function(name) {
|
||||
this.replaceStack(function(current) {
|
||||
return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context');
|
||||
});
|
||||
},
|
||||
|
||||
// [lookupData]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: data, ...
|
||||
//
|
||||
// Push the data lookup operator
|
||||
lookupData: function(depth) {
|
||||
if (!depth) {
|
||||
this.pushStackLiteral('data');
|
||||
} else {
|
||||
this.pushStackLiteral('this.data(data, ' + depth + ')');
|
||||
}
|
||||
},
|
||||
|
||||
// [pushStringParam]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: string, currentContext, ...
|
||||
//
|
||||
// This opcode is designed for use in string mode, which
|
||||
// provides the string value of a parameter along with its
|
||||
// depth rather than resolving it immediately.
|
||||
pushStringParam: function(string, type) {
|
||||
this.pushStackLiteral('depth' + this.lastContext);
|
||||
|
||||
this.pushString(type);
|
||||
|
||||
// If it's a subexpression, the string result
|
||||
// will be pushed after this opcode.
|
||||
if (type !== 'sexpr') {
|
||||
if (typeof string === 'string') {
|
||||
this.pushString(string);
|
||||
} else {
|
||||
this.pushStackLiteral(string);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
emptyHash: function() {
|
||||
this.pushStackLiteral('{}');
|
||||
|
||||
if (this.trackIds) {
|
||||
this.push('{}'); // hashIds
|
||||
}
|
||||
if (this.stringParams) {
|
||||
this.push('{}'); // hashContexts
|
||||
this.push('{}'); // hashTypes
|
||||
}
|
||||
},
|
||||
pushHash: function() {
|
||||
if (this.hash) {
|
||||
this.hashes.push(this.hash);
|
||||
}
|
||||
this.hash = {values: [], types: [], contexts: [], ids: []};
|
||||
},
|
||||
popHash: function() {
|
||||
var hash = this.hash;
|
||||
this.hash = this.hashes.pop();
|
||||
|
||||
if (this.trackIds) {
|
||||
this.push('{' + hash.ids.join(',') + '}');
|
||||
}
|
||||
if (this.stringParams) {
|
||||
this.push('{' + hash.contexts.join(',') + '}');
|
||||
this.push('{' + hash.types.join(',') + '}');
|
||||
}
|
||||
|
||||
this.push('{\n ' + hash.values.join(',\n ') + '\n }');
|
||||
},
|
||||
|
||||
// [pushString]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: quotedString(string), ...
|
||||
//
|
||||
// Push a quoted version of `string` onto the stack
|
||||
pushString: function(string) {
|
||||
this.pushStackLiteral(this.quotedString(string));
|
||||
},
|
||||
|
||||
// [push]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: expr, ...
|
||||
//
|
||||
// Push an expression onto the stack
|
||||
push: function(expr) {
|
||||
this.inlineStack.push(expr);
|
||||
return expr;
|
||||
},
|
||||
|
||||
// [pushLiteral]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: value, ...
|
||||
//
|
||||
// Pushes a value onto the stack. This operation prevents
|
||||
// the compiler from creating a temporary variable to hold
|
||||
// it.
|
||||
pushLiteral: function(value) {
|
||||
this.pushStackLiteral(value);
|
||||
},
|
||||
|
||||
// [pushProgram]
|
||||
//
|
||||
// On stack, before: ...
|
||||
// On stack, after: program(guid), ...
|
||||
//
|
||||
// Push a program expression onto the stack. This takes
|
||||
// a compile-time guid and converts it into a runtime-accessible
|
||||
// expression.
|
||||
pushProgram: function(guid) {
|
||||
if (guid != null) {
|
||||
this.pushStackLiteral(this.programExpression(guid));
|
||||
} else {
|
||||
this.pushStackLiteral(null);
|
||||
}
|
||||
},
|
||||
|
||||
// [invokeHelper]
|
||||
//
|
||||
// On stack, before: hash, inverse, program, params..., ...
|
||||
// On stack, after: result of helper invocation
|
||||
//
|
||||
// Pops off the helper's parameters, invokes the helper,
|
||||
// and pushes the helper's return value onto the stack.
|
||||
//
|
||||
// If the helper is not found, `helperMissing` is called.
|
||||
invokeHelper: function(paramSize, name, isRoot) {
|
||||
this.aliases.helperMissing = 'helpers.helperMissing';
|
||||
this.useRegister('helper');
|
||||
|
||||
var nonHelper = this.popStack();
|
||||
var helper = this.setupHelper(paramSize, name);
|
||||
|
||||
var lookup = 'helper = ' + helper.name + ' || ' + nonHelper + ' || helperMissing';
|
||||
if (helper.paramsInit) {
|
||||
lookup += ',' + helper.paramsInit;
|
||||
}
|
||||
|
||||
this.push('(' + lookup + ',helper.call(' + helper.callParams + '))');
|
||||
|
||||
// Always flush subexpressions. This is both to prevent the compounding size issue that
|
||||
// occurs when the code has to be duplicated for inlining and also to prevent errors
|
||||
// due to the incorrect options object being passed due to the shared register.
|
||||
if (!isRoot) {
|
||||
this.flushInline();
|
||||
}
|
||||
},
|
||||
|
||||
// [invokeKnownHelper]
|
||||
//
|
||||
// On stack, before: hash, inverse, program, params..., ...
|
||||
// On stack, after: result of helper invocation
|
||||
//
|
||||
// This operation is used when the helper is known to exist,
|
||||
// so a `helperMissing` fallback is not required.
|
||||
invokeKnownHelper: function(paramSize, name) {
|
||||
var helper = this.setupHelper(paramSize, name);
|
||||
this.push(helper.name + ".call(" + helper.callParams + ")");
|
||||
},
|
||||
|
||||
// [invokeAmbiguous]
|
||||
//
|
||||
// On stack, before: hash, inverse, program, params..., ...
|
||||
// On stack, after: result of disambiguation
|
||||
//
|
||||
// This operation is used when an expression like `{{foo}}`
|
||||
// is provided, but we don't know at compile-time whether it
|
||||
// is a helper or a path.
|
||||
//
|
||||
// This operation emits more code than the other options,
|
||||
// and can be avoided by passing the `knownHelpers` and
|
||||
// `knownHelpersOnly` flags at compile-time.
|
||||
invokeAmbiguous: function(name, helperCall) {
|
||||
this.aliases.functionType = '"function"';
|
||||
this.useRegister('helper');
|
||||
|
||||
this.emptyHash();
|
||||
var helper = this.setupHelper(0, name, helperCall);
|
||||
|
||||
var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper');
|
||||
var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context');
|
||||
|
||||
this.push(
|
||||
'((helper = ' + helperName + ' || ' + nonHelper
|
||||
+ (helper.paramsInit ? '),(' + helper.paramsInit : '') + '),'
|
||||
+ '(typeof helper === functionType ? helper.call(' + helper.callParams + ') : helper))');
|
||||
},
|
||||
|
||||
// [invokePartial]
|
||||
//
|
||||
// On stack, before: context, ...
|
||||
// On stack after: result of partial invocation
|
||||
//
|
||||
// This operation pops off a context, invokes a partial with that context,
|
||||
// and pushes the result of the invocation back.
|
||||
invokePartial: function(name) {
|
||||
var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), this.popStack(), "helpers", "partials"];
|
||||
|
||||
if (this.options.data) {
|
||||
params.push("data");
|
||||
}
|
||||
|
||||
this.push("this.invokePartial(" + params.join(", ") + ")");
|
||||
},
|
||||
|
||||
// [assignToHash]
|
||||
//
|
||||
// On stack, before: value, hash, ...
|
||||
// On stack, after: hash, ...
|
||||
//
|
||||
// Pops a value and hash off the stack, assigns `hash[key] = value`
|
||||
// and pushes the hash back onto the stack.
|
||||
assignToHash: function(key) {
|
||||
var value = this.popStack(),
|
||||
context,
|
||||
type,
|
||||
id;
|
||||
|
||||
if (this.trackIds) {
|
||||
id = this.popStack();
|
||||
}
|
||||
if (this.stringParams) {
|
||||
type = this.popStack();
|
||||
context = this.popStack();
|
||||
}
|
||||
|
||||
var hash = this.hash;
|
||||
if (context) {
|
||||
hash.contexts.push("'" + key + "': " + context);
|
||||
}
|
||||
if (type) {
|
||||
hash.types.push("'" + key + "': " + type);
|
||||
}
|
||||
if (id) {
|
||||
hash.ids.push("'" + key + "': " + id);
|
||||
}
|
||||
hash.values.push("'" + key + "': (" + value + ")");
|
||||
},
|
||||
|
||||
pushId: function(type, name) {
|
||||
if (type === 'ID' || type === 'DATA') {
|
||||
this.pushString(name);
|
||||
} else if (type === 'sexpr') {
|
||||
this.pushStackLiteral('true');
|
||||
} else {
|
||||
this.pushStackLiteral('null');
|
||||
}
|
||||
},
|
||||
|
||||
// HELPERS
|
||||
|
||||
compiler: JavaScriptCompiler,
|
||||
|
||||
compileChildren: function(environment, options) {
|
||||
var children = environment.children, child, compiler;
|
||||
|
||||
for(var i=0, l=children.length; i<l; i++) {
|
||||
child = children[i];
|
||||
compiler = new this.compiler();
|
||||
|
||||
var index = this.matchExistingProgram(child);
|
||||
|
||||
if (index == null) {
|
||||
this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children
|
||||
index = this.context.programs.length;
|
||||
child.index = index;
|
||||
child.name = 'program' + index;
|
||||
this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile);
|
||||
this.context.environments[index] = child;
|
||||
} else {
|
||||
child.index = index;
|
||||
child.name = 'program' + index;
|
||||
}
|
||||
}
|
||||
},
|
||||
matchExistingProgram: function(child) {
|
||||
for (var i = 0, len = this.context.environments.length; i < len; i++) {
|
||||
var environment = this.context.environments[i];
|
||||
if (environment && environment.equals(child)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
programExpression: function(guid) {
|
||||
if(guid == null) {
|
||||
return 'this.noop';
|
||||
}
|
||||
|
||||
var child = this.environment.children[guid],
|
||||
depths = child.depths.list, depth;
|
||||
|
||||
var programParams = [child.index, 'data'];
|
||||
|
||||
for(var i=0, l = depths.length; i<l; i++) {
|
||||
depth = depths[i];
|
||||
|
||||
programParams.push('depth' + (depth - 1));
|
||||
}
|
||||
|
||||
return (depths.length === 0 ? 'this.program(' : 'this.programWithDepth(') + programParams.join(', ') + ')';
|
||||
},
|
||||
|
||||
register: function(name, val) {
|
||||
this.useRegister(name);
|
||||
this.pushSource(name + " = " + val + ";");
|
||||
},
|
||||
|
||||
useRegister: function(name) {
|
||||
if(!this.registers[name]) {
|
||||
this.registers[name] = true;
|
||||
this.registers.list.push(name);
|
||||
}
|
||||
},
|
||||
|
||||
pushStackLiteral: function(item) {
|
||||
return this.push(new Literal(item));
|
||||
},
|
||||
|
||||
pushSource: function(source) {
|
||||
if (this.pendingContent) {
|
||||
this.source.push(this.appendToBuffer(this.quotedString(this.pendingContent)));
|
||||
this.pendingContent = undefined;
|
||||
}
|
||||
|
||||
if (source) {
|
||||
this.source.push(source);
|
||||
}
|
||||
},
|
||||
|
||||
pushStack: function(item) {
|
||||
this.flushInline();
|
||||
|
||||
var stack = this.incrStack();
|
||||
if (item) {
|
||||
this.pushSource(stack + " = " + item + ";");
|
||||
}
|
||||
this.compileStack.push(stack);
|
||||
return stack;
|
||||
},
|
||||
|
||||
replaceStack: function(callback) {
|
||||
var prefix = '',
|
||||
inline = this.isInline(),
|
||||
stack,
|
||||
createdStack,
|
||||
usedLiteral;
|
||||
|
||||
// If we are currently inline then we want to merge the inline statement into the
|
||||
// replacement statement via ','
|
||||
if (inline) {
|
||||
var top = this.popStack(true);
|
||||
|
||||
if (top instanceof Literal) {
|
||||
// Literals do not need to be inlined
|
||||
stack = top.value;
|
||||
usedLiteral = true;
|
||||
} else {
|
||||
// Get or create the current stack name for use by the inline
|
||||
createdStack = !this.stackSlot;
|
||||
var name = !createdStack ? this.topStackName() : this.incrStack();
|
||||
|
||||
prefix = '(' + this.push(name) + ' = ' + top + '),';
|
||||
stack = this.topStack();
|
||||
}
|
||||
} else {
|
||||
stack = this.topStack();
|
||||
}
|
||||
|
||||
var item = callback.call(this, stack);
|
||||
|
||||
if (inline) {
|
||||
if (!usedLiteral) {
|
||||
this.popStack();
|
||||
}
|
||||
if (createdStack) {
|
||||
this.stackSlot--;
|
||||
}
|
||||
this.push('(' + prefix + item + ')');
|
||||
} else {
|
||||
// Prevent modification of the context depth variable. Through replaceStack
|
||||
if (!/^stack/.test(stack)) {
|
||||
stack = this.nextStack();
|
||||
}
|
||||
|
||||
this.pushSource(stack + " = (" + prefix + item + ");");
|
||||
}
|
||||
return stack;
|
||||
},
|
||||
|
||||
nextStack: function() {
|
||||
return this.pushStack();
|
||||
},
|
||||
|
||||
incrStack: function() {
|
||||
this.stackSlot++;
|
||||
if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
|
||||
return this.topStackName();
|
||||
},
|
||||
topStackName: function() {
|
||||
return "stack" + this.stackSlot;
|
||||
},
|
||||
flushInline: function() {
|
||||
var inlineStack = this.inlineStack;
|
||||
if (inlineStack.length) {
|
||||
this.inlineStack = [];
|
||||
for (var i = 0, len = inlineStack.length; i < len; i++) {
|
||||
var entry = inlineStack[i];
|
||||
if (entry instanceof Literal) {
|
||||
this.compileStack.push(entry);
|
||||
} else {
|
||||
this.pushStack(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isInline: function() {
|
||||
return this.inlineStack.length;
|
||||
},
|
||||
|
||||
popStack: function(wrapped) {
|
||||
var inline = this.isInline(),
|
||||
item = (inline ? this.inlineStack : this.compileStack).pop();
|
||||
|
||||
if (!wrapped && (item instanceof Literal)) {
|
||||
return item.value;
|
||||
} else {
|
||||
if (!inline) {
|
||||
if (!this.stackSlot) {
|
||||
throw new Exception('Invalid stack pop');
|
||||
}
|
||||
this.stackSlot--;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
},
|
||||
|
||||
topStack: function(wrapped) {
|
||||
var stack = (this.isInline() ? this.inlineStack : this.compileStack),
|
||||
item = stack[stack.length - 1];
|
||||
|
||||
if (!wrapped && (item instanceof Literal)) {
|
||||
return item.value;
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
},
|
||||
|
||||
quotedString: function(str) {
|
||||
return '"' + str
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4
|
||||
.replace(/\u2029/g, '\\u2029') + '"';
|
||||
},
|
||||
|
||||
objectLiteral: function(obj) {
|
||||
var pairs = [];
|
||||
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
pairs.push(this.quotedString(key) + ':' + obj[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return '{' + pairs.join(',') + '}';
|
||||
},
|
||||
|
||||
setupHelper: function(paramSize, name, blockHelper) {
|
||||
var params = [],
|
||||
paramsInit = this.setupParams(name, paramSize, params, blockHelper);
|
||||
var foundHelper = this.nameLookup('helpers', name, 'helper');
|
||||
|
||||
return {
|
||||
params: params,
|
||||
paramsInit: paramsInit,
|
||||
name: foundHelper,
|
||||
callParams: ["depth0"].concat(params).join(", ")
|
||||
};
|
||||
},
|
||||
|
||||
setupOptions: function(helper, paramSize, params) {
|
||||
var options = {}, contexts = [], types = [], ids = [], param, inverse, program;
|
||||
|
||||
options.name = this.quotedString(helper);
|
||||
options.hash = this.popStack();
|
||||
|
||||
if (this.trackIds) {
|
||||
options.hashIds = this.popStack();
|
||||
}
|
||||
if (this.stringParams) {
|
||||
options.hashTypes = this.popStack();
|
||||
options.hashContexts = this.popStack();
|
||||
}
|
||||
|
||||
inverse = this.popStack();
|
||||
program = this.popStack();
|
||||
|
||||
// Avoid setting fn and inverse if neither are set. This allows
|
||||
// helpers to do a check for `if (options.fn)`
|
||||
if (program || inverse) {
|
||||
if (!program) {
|
||||
program = 'this.noop';
|
||||
}
|
||||
|
||||
if (!inverse) {
|
||||
inverse = 'this.noop';
|
||||
}
|
||||
|
||||
options.fn = program;
|
||||
options.inverse = inverse;
|
||||
}
|
||||
|
||||
for (var i = 0; i < paramSize; i++) {
|
||||
param = this.popStack();
|
||||
params.push(param);
|
||||
|
||||
if (this.trackIds) {
|
||||
ids.push(this.popStack());
|
||||
}
|
||||
if (this.stringParams) {
|
||||
types.push(this.popStack());
|
||||
contexts.push(this.popStack());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.trackIds) {
|
||||
options.ids = "[" + ids.join(",") + "]";
|
||||
}
|
||||
if (this.stringParams) {
|
||||
options.types = "[" + types.join(",") + "]";
|
||||
options.contexts = "[" + contexts.join(",") + "]";
|
||||
}
|
||||
|
||||
if (this.options.data) {
|
||||
options.data = "data";
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
||||
// the params and contexts arguments are passed in arrays
|
||||
// to fill in
|
||||
setupParams: function(helperName, paramSize, params, useRegister) {
|
||||
var options = this.objectLiteral(this.setupOptions(helperName, paramSize, params));
|
||||
|
||||
if (useRegister) {
|
||||
this.useRegister('options');
|
||||
params.push('options');
|
||||
return 'options=' + options;
|
||||
} else {
|
||||
params.push(options);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var reservedWords = (
|
||||
"break else new var" +
|
||||
" case finally return void" +
|
||||
" catch for switch while" +
|
||||
" continue function this with" +
|
||||
" default if throw" +
|
||||
" delete in try" +
|
||||
" do instanceof typeof" +
|
||||
" abstract enum int short" +
|
||||
" boolean export interface static" +
|
||||
" byte extends long super" +
|
||||
" char final native synchronized" +
|
||||
" class float package throws" +
|
||||
" const goto private transient" +
|
||||
" debugger implements protected volatile" +
|
||||
" double import public let yield"
|
||||
).split(" ");
|
||||
|
||||
var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
|
||||
|
||||
for(var i=0, l=reservedWords.length; i<l; i++) {
|
||||
compilerWords[reservedWords[i]] = true;
|
||||
}
|
||||
|
||||
JavaScriptCompiler.isValidJavaScriptVariableName = function(name) {
|
||||
return !JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name);
|
||||
};
|
||||
|
||||
export default JavaScriptCompiler;
|
||||
@@ -0,0 +1,143 @@
|
||||
import Visitor from "./visitor";
|
||||
|
||||
export function print(ast) {
|
||||
return new PrintVisitor().accept(ast);
|
||||
}
|
||||
|
||||
export function PrintVisitor() {
|
||||
this.padding = 0;
|
||||
}
|
||||
|
||||
PrintVisitor.prototype = new Visitor();
|
||||
|
||||
PrintVisitor.prototype.pad = function(string, newline) {
|
||||
var out = "";
|
||||
|
||||
for(var i=0,l=this.padding; i<l; i++) {
|
||||
out = out + " ";
|
||||
}
|
||||
|
||||
out = out + string;
|
||||
|
||||
if(newline !== false) { out = out + "\n"; }
|
||||
return out;
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.program = function(program) {
|
||||
var out = "",
|
||||
statements = program.statements,
|
||||
i, l;
|
||||
|
||||
for(i=0, l=statements.length; i<l; i++) {
|
||||
out = out + this.accept(statements[i]);
|
||||
}
|
||||
|
||||
this.padding--;
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.block = function(block) {
|
||||
var out = "";
|
||||
|
||||
out = out + this.pad("BLOCK:");
|
||||
this.padding++;
|
||||
out = out + this.accept(block.mustache);
|
||||
if (block.program) {
|
||||
out = out + this.pad("PROGRAM:");
|
||||
this.padding++;
|
||||
out = out + this.accept(block.program);
|
||||
this.padding--;
|
||||
}
|
||||
if (block.inverse) {
|
||||
if (block.program) { this.padding++; }
|
||||
out = out + this.pad("{{^}}");
|
||||
this.padding++;
|
||||
out = out + this.accept(block.inverse);
|
||||
this.padding--;
|
||||
if (block.program) { this.padding--; }
|
||||
}
|
||||
this.padding--;
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.sexpr = function(sexpr) {
|
||||
var params = sexpr.params, paramStrings = [], hash;
|
||||
|
||||
for(var i=0, l=params.length; i<l; i++) {
|
||||
paramStrings.push(this.accept(params[i]));
|
||||
}
|
||||
|
||||
params = "[" + paramStrings.join(", ") + "]";
|
||||
|
||||
hash = sexpr.hash ? " " + this.accept(sexpr.hash) : "";
|
||||
|
||||
return this.accept(sexpr.id) + " " + params + hash;
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.mustache = function(mustache) {
|
||||
return this.pad("{{ " + this.accept(mustache.sexpr) + " }}");
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.partial = function(partial) {
|
||||
var content = this.accept(partial.partialName);
|
||||
if(partial.context) {
|
||||
content += " " + this.accept(partial.context);
|
||||
}
|
||||
if (partial.hash) {
|
||||
content += " " + this.accept(partial.hash);
|
||||
}
|
||||
return this.pad("{{> " + content + " }}");
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.hash = function(hash) {
|
||||
var pairs = hash.pairs;
|
||||
var joinedPairs = [], left, right;
|
||||
|
||||
for(var i=0, l=pairs.length; i<l; i++) {
|
||||
left = pairs[i][0];
|
||||
right = this.accept(pairs[i][1]);
|
||||
joinedPairs.push( left + "=" + right );
|
||||
}
|
||||
|
||||
return "HASH{" + joinedPairs.join(", ") + "}";
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.STRING = function(string) {
|
||||
return '"' + string.string + '"';
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.NUMBER = function(number) {
|
||||
return "NUMBER{" + number.number + "}";
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.BOOLEAN = function(bool) {
|
||||
return "BOOLEAN{" + bool.bool + "}";
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.ID = function(id) {
|
||||
var path = id.parts.join("/");
|
||||
if(id.parts.length > 1) {
|
||||
return "PATH:" + path;
|
||||
} else {
|
||||
return "ID:" + path;
|
||||
}
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.PARTIAL_NAME = function(partialName) {
|
||||
return "PARTIAL:" + partialName.name;
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.DATA = function(data) {
|
||||
return "@" + this.accept(data.id);
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.content = function(content) {
|
||||
return this.pad("CONTENT[ '" + content.string + "' ]");
|
||||
};
|
||||
|
||||
PrintVisitor.prototype.comment = function(comment) {
|
||||
return this.pad("{{! '" + comment.comment + "' }}");
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
function Visitor() {}
|
||||
|
||||
Visitor.prototype = {
|
||||
constructor: Visitor,
|
||||
|
||||
accept: function(object) {
|
||||
return this[object.type](object);
|
||||
}
|
||||
};
|
||||
|
||||
export default Visitor;
|
||||
@@ -1,29 +0,0 @@
|
||||
var Handlebars = require("handlebars");
|
||||
|
||||
// BEGIN(BROWSER)
|
||||
(function() {
|
||||
var classes = ["Lexer", "PrintVisitor", "Context", "Runtime", "Exception"];
|
||||
var prop;
|
||||
|
||||
for(var i=0, l=classes.length; i<l; i++) {
|
||||
var className = classes[i], klass = Handlebars[className];
|
||||
klass.displayName = "new Handlebars." + className;
|
||||
|
||||
for(prop in klass) {
|
||||
if(klass.hasOwnProperty(prop)) {
|
||||
klass[prop].displayName = "Handlebars." + className + "#" + prop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(prop in Handlebars.Utils) {
|
||||
if(Handlebars.Utils.hasOwnProperty(prop)) {
|
||||
Handlebars.Utils[prop].displayName = "Handlebars.Utils." + prop;
|
||||
}
|
||||
}
|
||||
|
||||
Handlebars.parse.displayName = "Handlebars.parse";
|
||||
Handlebars.print.displayName = "Handlebars.print";
|
||||
Handlebars.compile.displayName = "Handlebars.compile";
|
||||
})();
|
||||
// END(BROWSER)
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack'];
|
||||
|
||||
function Exception(message, node) {
|
||||
var line;
|
||||
if (node && node.firstLine) {
|
||||
line = node.firstLine;
|
||||
|
||||
message += ' - ' + line + ':' + node.firstColumn;
|
||||
}
|
||||
|
||||
var tmp = Error.prototype.constructor.call(this, message);
|
||||
|
||||
// Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work.
|
||||
for (var idx = 0; idx < errorProps.length; idx++) {
|
||||
this[errorProps[idx]] = tmp[errorProps[idx]];
|
||||
}
|
||||
|
||||
if (line) {
|
||||
this.lineNumber = line;
|
||||
this.column = node.firstColumn;
|
||||
}
|
||||
}
|
||||
|
||||
Exception.prototype = new Error();
|
||||
|
||||
export default Exception;
|
||||
@@ -1,114 +0,0 @@
|
||||
var Handlebars = require("handlebars");
|
||||
require("handlebars/visitor");
|
||||
|
||||
// BEGIN(BROWSER)
|
||||
Handlebars.PrintVisitor = function() { this.padding = 0; };
|
||||
Handlebars.PrintVisitor.prototype = new Handlebars.Visitor();
|
||||
|
||||
Handlebars.PrintVisitor.prototype.pad = function(string, newline) {
|
||||
var out = "";
|
||||
|
||||
for(var i=0,l=this.padding; i<l; i++) {
|
||||
out = out + " ";
|
||||
}
|
||||
|
||||
out = out + string;
|
||||
|
||||
if(newline !== false) { out = out + "\n"; }
|
||||
return out;
|
||||
};
|
||||
|
||||
Handlebars.PrintVisitor.prototype.program = function(program) {
|
||||
var out = this.pad("PROGRAM:"),
|
||||
statements = program.statements,
|
||||
inverse = program.inverse,
|
||||
i, l;
|
||||
|
||||
this.padding++;
|
||||
|
||||
for(i=0, l=statements.length; i<l; i++) {
|
||||
out = out + this.accept(statements[i]);
|
||||
}
|
||||
|
||||
this.padding--;
|
||||
|
||||
if(inverse) {
|
||||
out = out + this.pad("{{^}}");
|
||||
|
||||
this.padding++;
|
||||
|
||||
for(i=0, l=inverse.statements.length; i<l; i++) {
|
||||
out = out + this.accept(inverse.statements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
this.padding--;
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
Handlebars.PrintVisitor.prototype.block = function(block) {
|
||||
var out = "";
|
||||
|
||||
out = out + this.pad("BLOCK:");
|
||||
this.padding++;
|
||||
out = out + this.accept(block.mustache);
|
||||
out = out + this.accept(block.program);
|
||||
this.padding--;
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
Handlebars.PrintVisitor.prototype.inverse = function(block) {
|
||||
var out = "";
|
||||
|
||||
out = out + this.pad("INVERSE:");
|
||||
this.padding++;
|
||||
out = out + this.accept(block.mustache);
|
||||
out = out + this.accept(block.program);
|
||||
this.padding--;
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
|
||||
Handlebars.PrintVisitor.prototype.mustache = function(mustache) {
|
||||
var params = mustache.params, paramStrings = [];
|
||||
|
||||
for(var i=0, l=params.length; i<l; i++) {
|
||||
paramStrings.push(this.accept(params[i]));
|
||||
}
|
||||
|
||||
params = "[" + paramStrings.join(", ") + "]";
|
||||
return this.pad("{{ " + this.accept(mustache.id) + " " + params + " }}");
|
||||
};
|
||||
|
||||
Handlebars.PrintVisitor.prototype.partial = function(partial) {
|
||||
var content = this.accept(partial.id);
|
||||
if(partial.context) { content = content + " " + this.accept(partial.context); }
|
||||
return this.pad("{{> " + content + " }}");
|
||||
};
|
||||
|
||||
Handlebars.PrintVisitor.prototype.STRING = function(string) {
|
||||
return '"' + string.string + '"';
|
||||
};
|
||||
|
||||
Handlebars.PrintVisitor.prototype.ID = function(id) {
|
||||
var path = id.parts.join("/");
|
||||
if(id.parts.length > 1) {
|
||||
return "PATH:" + path;
|
||||
} else {
|
||||
return "ID:" + path;
|
||||
}
|
||||
};
|
||||
|
||||
Handlebars.PrintVisitor.prototype.content = function(content) {
|
||||
return this.pad("CONTENT[ '" + content.string + "' ]");
|
||||
};
|
||||
|
||||
Handlebars.PrintVisitor.prototype.comment = function(comment) {
|
||||
return this.pad("{{! '" + comment.comment + "' }}");
|
||||
};
|
||||
// END(BROWSER)
|
||||
|
||||
exports.PrintVisitor = Handlebars.PrintVisitor;
|
||||
+157
-258
@@ -1,267 +1,166 @@
|
||||
var inspect = function(obj) {
|
||||
require("sys").print(require("sys").inspect(obj) + "\n");
|
||||
};
|
||||
module Utils from "./utils";
|
||||
import Exception from "./exception";
|
||||
import { COMPILER_REVISION, REVISION_CHANGES, createFrame } from "./base";
|
||||
|
||||
var Handlebars = require("handlebars");
|
||||
export function checkRevision(compilerInfo) {
|
||||
var compilerRevision = compilerInfo && compilerInfo[0] || 1,
|
||||
currentRevision = COMPILER_REVISION;
|
||||
|
||||
// BEGIN(BROWSER)
|
||||
// A Context wraps data, and makes it possible to extract a
|
||||
// new Context given a path. For instance, if the data
|
||||
// is { person: { name: "Alan" } }, a Context wrapping
|
||||
// "Alan" can be extracted by searching for "person/name"
|
||||
Handlebars.Context = function(data, helpers, partials) {
|
||||
this.data = data;
|
||||
this.helpers = helpers || {};
|
||||
this.partials = partials || {};
|
||||
};
|
||||
|
||||
Handlebars.Context.prototype = {
|
||||
isContext: true,
|
||||
|
||||
// Make a shallow copy of the Context
|
||||
clone: function() {
|
||||
return new Handlebars.Context(this.data, this.helpers, this.partials);
|
||||
},
|
||||
|
||||
// Search for an object inside the Context's data. The
|
||||
// path parameter is an object with parts
|
||||
// ("person/name" represented as ["person", "name"]),
|
||||
// and depth (the amount of levels to go up the stack,
|
||||
// originally represented as ..). The stack parameter
|
||||
// is the objects already searched from the root of
|
||||
// the original Context in order to get to this point.
|
||||
//
|
||||
// Return a new Context wrapping the data found in
|
||||
// the search.
|
||||
evaluate: function(path, stack) {
|
||||
var context = this.clone();
|
||||
var depth = path.depth, parts = path.parts;
|
||||
|
||||
if(depth > stack.length) { context.data = null; }
|
||||
else if(depth > 0) { context = stack[stack.length - depth].clone(); }
|
||||
|
||||
for(var i=0,l=parts.length; i<l && context.data != null; i++) {
|
||||
context.data = context.data[parts[i]];
|
||||
if (compilerRevision !== currentRevision) {
|
||||
if (compilerRevision < currentRevision) {
|
||||
var runtimeVersions = REVISION_CHANGES[currentRevision],
|
||||
compilerVersions = REVISION_CHANGES[compilerRevision];
|
||||
throw new Exception("Template was precompiled with an older version of Handlebars than the current runtime. "+
|
||||
"Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+").");
|
||||
} else {
|
||||
// Use the embedded version info since the runtime doesn't know about this revision yet
|
||||
throw new Exception("Template was precompiled with a newer version of Handlebars than the current runtime. "+
|
||||
"Please update your runtime to a newer version ("+compilerInfo[1]+").");
|
||||
}
|
||||
|
||||
if(context.data !== undefined) { return context; }
|
||||
|
||||
if(parts.length === 1 && context.data === undefined) {
|
||||
context.data = context.helpers[parts[0]];
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Handlebars.K = function() { return this; };
|
||||
// TODO: Remove this line and break up compilePartial
|
||||
|
||||
Handlebars.proxy = function(obj) {
|
||||
var Proxy = this.K;
|
||||
Proxy.prototype = obj;
|
||||
return new Proxy();
|
||||
};
|
||||
|
||||
Handlebars.Runtime = function(context, stack) {
|
||||
this.stack = stack || [];
|
||||
this.buffer = "";
|
||||
|
||||
this.context = context;
|
||||
};
|
||||
|
||||
Handlebars.Runtime.prototype = {
|
||||
accept: Handlebars.Visitor.prototype.accept,
|
||||
|
||||
ID: function(path) {
|
||||
return this.context.evaluate(path, this.stack);
|
||||
},
|
||||
|
||||
STRING: function(string) {
|
||||
return { data: string.string };
|
||||
},
|
||||
|
||||
program: function(program) {
|
||||
var statements = program.statements;
|
||||
|
||||
for(var i=0, l=statements.length; i<l; i++) {
|
||||
var statement = statements[i];
|
||||
this[statement.type](statement);
|
||||
}
|
||||
|
||||
return this.buffer;
|
||||
},
|
||||
|
||||
mustache: function(mustache) {
|
||||
var idObj = this.ID(mustache.id);
|
||||
var params = mustache.params.slice(0);
|
||||
var buf;
|
||||
|
||||
for(var i=0, l=params.length; i<l; i++) {
|
||||
var param = params[i];
|
||||
params[i] = this[param.type](param).data;
|
||||
}
|
||||
|
||||
var data = idObj.data;
|
||||
|
||||
var type = Object.prototype.toString.call(data);
|
||||
var functionType = (type === "[object Function]");
|
||||
|
||||
if(!functionType && params.length) {
|
||||
params = params.slice(0);
|
||||
params.unshift(data || mustache.id.original);
|
||||
data = this.context.helpers.helperMissing;
|
||||
functionType = true;
|
||||
}
|
||||
|
||||
if(functionType) {
|
||||
buf = data.apply(this.wrapContext(), params);
|
||||
} else {
|
||||
buf = data;
|
||||
}
|
||||
|
||||
if(buf && mustache.escaped) { buf = Handlebars.Utils.escapeExpression(buf); }
|
||||
|
||||
this.buffer = this.buffer + ((!buf && buf !== 0) ? '' : buf);
|
||||
},
|
||||
|
||||
block: function(block) {
|
||||
var mustache = block.mustache, data;
|
||||
|
||||
var id = mustache.id,
|
||||
idObj = this.ID(mustache.id),
|
||||
data = idObj.data;
|
||||
|
||||
var result;
|
||||
|
||||
if(typeof data === "function") {
|
||||
params = this.evaluateParams(mustache.params);
|
||||
} else {
|
||||
params = [data];
|
||||
data = this.context.helpers.blockHelperMissing;
|
||||
}
|
||||
|
||||
params.push(this.wrapProgram(block.program));
|
||||
result = data.apply(this.wrapContext(), params);
|
||||
this.buffer = this.buffer + ((result === undefined) ? "" : result);
|
||||
|
||||
if(block.program.inverse) {
|
||||
params.pop();
|
||||
params.push(this.wrapProgram(block.program.inverse));
|
||||
result = data.not ? data.not.apply(this.wrapContext(), params) : "";
|
||||
this.buffer = this.buffer + result;
|
||||
}
|
||||
},
|
||||
|
||||
partial: function(partial) {
|
||||
var partials = this.context.partials || {};
|
||||
var id = partial.id.original;
|
||||
|
||||
var partialBody = partials[partial.id.original];
|
||||
var program, context;
|
||||
|
||||
if(!partialBody) {
|
||||
throw new Handlebars.Exception("The partial " + partial.id.original + " does not exist");
|
||||
}
|
||||
|
||||
if(typeof partialBody === "string") {
|
||||
program = Handlebars.parse(partialBody);
|
||||
partials[id] = program;
|
||||
} else {
|
||||
program = partialBody;
|
||||
}
|
||||
|
||||
if(partial.context) {
|
||||
context = this.ID(partial.context);
|
||||
} else {
|
||||
context = this.context;
|
||||
}
|
||||
var runtime = new Handlebars.Runtime(context, this.stack);
|
||||
this.buffer = this.buffer + runtime.program(program);
|
||||
},
|
||||
|
||||
not: function(context, fn) {
|
||||
return fn(context);
|
||||
},
|
||||
|
||||
// TODO: Write down the actual spec for inverse sections...
|
||||
inverse: function(block) {
|
||||
var mustache = block.mustache,
|
||||
id = mustache.id,
|
||||
not;
|
||||
|
||||
var idObj = this.ID(id),
|
||||
data = idObj.data,
|
||||
isInverse = Handlebars.Utils.isEmpty(data);
|
||||
|
||||
|
||||
var context = this.wrapContext();
|
||||
|
||||
if(Object.prototype.toString.call(data) === "[object Function]") {
|
||||
params = this.evaluateParams(mustache.params);
|
||||
id = id.parts.join("/");
|
||||
|
||||
data = data.apply(context, params);
|
||||
if(Handlebars.Utils.isEmpty(data)) { isInverse = true; }
|
||||
if(data.not) { not = data.not; } else { not = this.not; }
|
||||
} else {
|
||||
not = this.not;
|
||||
}
|
||||
|
||||
var result = not(context, this.wrapProgram(block.program));
|
||||
if(result != null) { this.buffer = this.buffer + result; }
|
||||
return;
|
||||
},
|
||||
|
||||
content: function(content) {
|
||||
this.buffer += content.string;
|
||||
},
|
||||
|
||||
comment: function() {},
|
||||
|
||||
evaluateParams: function(params) {
|
||||
var ret = [];
|
||||
|
||||
for(var i=0, l=params.length; i<l; i++) {
|
||||
var param = params[i];
|
||||
ret[i] = this[param.type](param).data;
|
||||
}
|
||||
|
||||
if(ret.length === 0) { ret = [this.wrapContext()]; }
|
||||
return ret;
|
||||
},
|
||||
|
||||
wrapContext: function() {
|
||||
var data = this.context.data;
|
||||
var proxy = Handlebars.proxy(data);
|
||||
var context = proxy.__context__ = this.context;
|
||||
var stack = proxy.__stack__ = this.stack.slice(0);
|
||||
|
||||
proxy.__get__ = function(path) {
|
||||
path = new Handlebars.AST.IdNode(path.split("/"));
|
||||
return context.evaluate(path, stack).data;
|
||||
};
|
||||
|
||||
proxy.isWrappedContext = true;
|
||||
proxy.__data__ = data;
|
||||
|
||||
return proxy;
|
||||
},
|
||||
|
||||
wrapProgram: function(program) {
|
||||
var currentContext = this.context;
|
||||
var stack = this.stack.slice(0);
|
||||
|
||||
return function(context) {
|
||||
if(context && context.isWrappedContext) { context = context.__data__; }
|
||||
|
||||
stack.push(currentContext);
|
||||
var newContext = new Handlebars.Context(context, currentContext.helpers, currentContext.partials);
|
||||
var runtime = new Handlebars.Runtime(newContext, stack);
|
||||
runtime.program(program);
|
||||
return runtime.buffer;
|
||||
};
|
||||
export function template(templateSpec, env) {
|
||||
if (!env) {
|
||||
throw new Exception("No environment passed to template");
|
||||
}
|
||||
|
||||
};
|
||||
// END(BROWSER)
|
||||
// Note: Using env.VM references rather than local var references throughout this section to allow
|
||||
// for external users to override these as psuedo-supported APIs.
|
||||
env.VM.checkRevision(templateSpec.compiler);
|
||||
|
||||
var invokePartialWrapper = function(partial, name, context, hash, helpers, partials, data) {
|
||||
if (hash) {
|
||||
context = Utils.extend({}, context, hash);
|
||||
}
|
||||
|
||||
var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data);
|
||||
if (result != null) { return result; }
|
||||
|
||||
if (env.compile) {
|
||||
var options = { helpers: helpers, partials: partials, data: data };
|
||||
partials[name] = env.compile(partial, { data: data !== undefined }, env);
|
||||
return partials[name](context, options);
|
||||
} else {
|
||||
throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode");
|
||||
}
|
||||
};
|
||||
|
||||
// Just add water
|
||||
var container = {
|
||||
escapeExpression: Utils.escapeExpression,
|
||||
invokePartial: invokePartialWrapper,
|
||||
|
||||
fn: function(i) {
|
||||
return templateSpec[i];
|
||||
},
|
||||
|
||||
programs: [],
|
||||
program: function(i, data) {
|
||||
var programWrapper = this.programs[i],
|
||||
fn = this.fn(i);
|
||||
if(data) {
|
||||
programWrapper = program(this, i, fn, data);
|
||||
} else if (!programWrapper) {
|
||||
programWrapper = this.programs[i] = program(this, i, fn);
|
||||
}
|
||||
return programWrapper;
|
||||
},
|
||||
programWithDepth: env.VM.programWithDepth,
|
||||
|
||||
initData: function(context, data) {
|
||||
if (!data || !('root' in data)) {
|
||||
data = data ? createFrame(data) : {};
|
||||
data.root = context;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
data: function(data, depth) {
|
||||
while (data && depth--) {
|
||||
data = data._parent;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
merge: function(param, common) {
|
||||
var ret = param || common;
|
||||
|
||||
if (param && common && (param !== common)) {
|
||||
ret = Utils.extend({}, common, param);
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
noop: env.VM.noop,
|
||||
compilerInfo: templateSpec.compiler
|
||||
};
|
||||
|
||||
var ret = function(context, options) {
|
||||
options = options || {};
|
||||
var namespace = options.partial ? options : env,
|
||||
helpers,
|
||||
partials,
|
||||
data = options.data;
|
||||
|
||||
if (!options.partial) {
|
||||
helpers = container.helpers = container.merge(options.helpers, namespace.helpers);
|
||||
|
||||
if (templateSpec.usePartial) {
|
||||
partials = container.partials = container.merge(options.partials, namespace.partials);
|
||||
}
|
||||
if (templateSpec.useData) {
|
||||
data = container.initData(context, data);
|
||||
}
|
||||
} else {
|
||||
helpers = container.helpers = options.helpers;
|
||||
partials = container.partials = options.partials;
|
||||
}
|
||||
return templateSpec.main.call(container, context, helpers, partials, data);
|
||||
};
|
||||
|
||||
ret.child = function(i) {
|
||||
return container.programWithDepth(i);
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function programWithDepth(i, data /*, $depth */) {
|
||||
/*jshint -W040 */
|
||||
var args = Array.prototype.slice.call(arguments, 2),
|
||||
container = this,
|
||||
fn = container.fn(i);
|
||||
|
||||
var prog = function(context, options) {
|
||||
options = options || {};
|
||||
|
||||
return fn.apply(container, [context, container.helpers, container.partials, options.data || data].concat(args));
|
||||
};
|
||||
prog.program = i;
|
||||
prog.depth = args.length;
|
||||
return prog;
|
||||
}
|
||||
|
||||
export function program(container, i, fn, data) {
|
||||
var prog = function(context, options) {
|
||||
options = options || {};
|
||||
|
||||
return fn.call(container, context, container.helpers, container.partials, options.data || data);
|
||||
};
|
||||
prog.program = i;
|
||||
prog.depth = 0;
|
||||
return prog;
|
||||
}
|
||||
|
||||
export function invokePartial(partial, name, context, helpers, partials, data) {
|
||||
var options = { partial: true, helpers: helpers, partials: partials, data: data };
|
||||
|
||||
if(partial === undefined) {
|
||||
throw new Exception("The partial " + name + " could not be found");
|
||||
} else if(partial instanceof Function) {
|
||||
return partial(context, options);
|
||||
}
|
||||
}
|
||||
|
||||
export function noop() { return ""; }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Build out our basic SafeString type
|
||||
function SafeString(string) {
|
||||
this.string = string;
|
||||
}
|
||||
|
||||
SafeString.prototype.toString = function() {
|
||||
return "" + this.string;
|
||||
};
|
||||
|
||||
export default SafeString;
|
||||
+73
-50
@@ -1,58 +1,81 @@
|
||||
var Handlebars = require("handlebars");
|
||||
/*jshint -W004 */
|
||||
import SafeString from "./safe-string";
|
||||
|
||||
// BEGIN(BROWSER)
|
||||
Handlebars.Exception = function(message) {
|
||||
this.message = message;
|
||||
var escape = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"`": "`"
|
||||
};
|
||||
|
||||
// Build out our basic SafeString type
|
||||
Handlebars.SafeString = function(string) {
|
||||
this.string = string;
|
||||
};
|
||||
Handlebars.SafeString.prototype.toString = function() {
|
||||
return this.string.toString();
|
||||
};
|
||||
var badChars = /[&<>"'`]/g;
|
||||
var possible = /[&<>"'`]/;
|
||||
|
||||
(function() {
|
||||
var escape = {
|
||||
"<": "<",
|
||||
">": ">"
|
||||
};
|
||||
function escapeChar(chr) {
|
||||
return escape[chr] || "&";
|
||||
}
|
||||
|
||||
var badChars = /&(?!\w+;)|[<>]/g;
|
||||
var possible = /[&<>]/
|
||||
|
||||
var escapeChar = function(chr) {
|
||||
return escape[chr] || "&"
|
||||
};
|
||||
|
||||
Handlebars.Utils = {
|
||||
escapeExpression: function(string) {
|
||||
// don't escape SafeStrings, since they're already safe
|
||||
if (string instanceof Handlebars.SafeString) {
|
||||
return string.toString();
|
||||
} else if (string == null || string === false) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if(!possible.test(string)) { return string; }
|
||||
return string.replace(badChars, escapeChar);
|
||||
},
|
||||
|
||||
isEmpty: function(value) {
|
||||
if (typeof value === "undefined") {
|
||||
return true;
|
||||
} else if (value === null) {
|
||||
return true;
|
||||
} else if (value === false) {
|
||||
return true;
|
||||
} else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
export function extend(obj /* , ...source */) {
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
for (var key in arguments[i]) {
|
||||
if (Object.prototype.hasOwnProperty.call(arguments[i], key)) {
|
||||
obj[key] = arguments[i][key];
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
// END(BROWSER)
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
export var toString = Object.prototype.toString;
|
||||
|
||||
// Sourced from lodash
|
||||
// https://github.com/bestiejs/lodash/blob/master/LICENSE.txt
|
||||
var isFunction = function(value) {
|
||||
return typeof value === 'function';
|
||||
};
|
||||
// fallback for older versions of Chrome and Safari
|
||||
if (isFunction(/x/)) {
|
||||
isFunction = function(value) {
|
||||
return typeof value === 'function' && toString.call(value) === '[object Function]';
|
||||
};
|
||||
}
|
||||
export var isFunction;
|
||||
|
||||
export var isArray = Array.isArray || function(value) {
|
||||
return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false;
|
||||
};
|
||||
|
||||
|
||||
export function escapeExpression(string) {
|
||||
// don't escape SafeStrings, since they're already safe
|
||||
if (string instanceof SafeString) {
|
||||
return string.toString();
|
||||
} else if (!string && string !== 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Force a string conversion as this will be done by the append regardless and
|
||||
// the regex test will do this transparently behind the scenes, causing issues if
|
||||
// an object's to string has escaped characters in it.
|
||||
string = "" + string;
|
||||
|
||||
if(!possible.test(string)) { return string; }
|
||||
return string.replace(badChars, escapeChar);
|
||||
}
|
||||
|
||||
export function isEmpty(value) {
|
||||
if (!value && value !== 0) {
|
||||
return true;
|
||||
} else if (isArray(value) && value.length === 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function appendContextPath(contextPath, id) {
|
||||
return (contextPath ? contextPath + '.' : '') + id;
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
var Handlebars = require("handlebars");
|
||||
|
||||
// BEGIN(BROWSER)
|
||||
|
||||
Handlebars.Visitor = function() {};
|
||||
|
||||
Handlebars.Visitor.prototype = {
|
||||
accept: function(object) {
|
||||
return this[object.type](object);
|
||||
}
|
||||
};
|
||||
// END(BROWSER)
|
||||
|
||||
@@ -1,659 +0,0 @@
|
||||
var Handlebars = require("handlebars");
|
||||
|
||||
// BEGIN(BROWSER)
|
||||
Handlebars.Compiler = function() {};
|
||||
Handlebars.JavaScriptCompiler = function() {};
|
||||
|
||||
(function(Compiler, JavaScriptCompiler) {
|
||||
Compiler.OPCODE_MAP = {
|
||||
appendContent: 1,
|
||||
getContext: 2,
|
||||
lookupWithFallback: 3,
|
||||
lookup: 4,
|
||||
append: 5,
|
||||
invokeMustache: 6,
|
||||
appendEscaped: 7,
|
||||
pushString: 8,
|
||||
truthyOrFallback: 9,
|
||||
functionOrFallback: 10,
|
||||
invokeProgram: 11,
|
||||
invokePartial: 12,
|
||||
push: 13,
|
||||
invokeInverse: 14
|
||||
};
|
||||
|
||||
Compiler.MULTI_PARAM_OPCODES = {
|
||||
appendContent: 1,
|
||||
getContext: 1,
|
||||
lookupWithFallback: 1,
|
||||
lookup: 1,
|
||||
invokeMustache: 2,
|
||||
pushString: 1,
|
||||
truthyOrFallback: 1,
|
||||
functionOrFallback: 1,
|
||||
invokeProgram: 2,
|
||||
invokePartial: 1,
|
||||
push: 1,
|
||||
invokeInverse: 1
|
||||
};
|
||||
|
||||
Compiler.DISASSEMBLE_MAP = {};
|
||||
|
||||
for(var prop in Compiler.OPCODE_MAP) {
|
||||
var value = Compiler.OPCODE_MAP[prop];
|
||||
Compiler.DISASSEMBLE_MAP[value] = prop;
|
||||
}
|
||||
|
||||
Compiler.multiParamSize = function(code) {
|
||||
return Compiler.MULTI_PARAM_OPCODES[Compiler.DISASSEMBLE_MAP[code]];
|
||||
};
|
||||
|
||||
Compiler.prototype = {
|
||||
disassemble: function() {
|
||||
var opcodes = this.opcodes, opcode, nextCode;
|
||||
var out = [], str, name, value;
|
||||
|
||||
for(var i=0, l=opcodes.length; i<l; i++) {
|
||||
opcode = opcodes[i];
|
||||
|
||||
if(opcode === 'DECLARE') {
|
||||
name = opcodes[++i];
|
||||
value = opcodes[++i];
|
||||
out.push("DECLARE " + name + " = " + value);
|
||||
} else {
|
||||
str = Compiler.DISASSEMBLE_MAP[opcode];
|
||||
|
||||
var extraParams = Compiler.multiParamSize(opcode);
|
||||
var codes = [];
|
||||
|
||||
for(var j=0; j<extraParams; j++) {
|
||||
nextCode = opcodes[++i];
|
||||
|
||||
if(typeof nextCode === "string") {
|
||||
nextCode = "\"" + nextCode.replace("\n", "\\n") + "\"";
|
||||
}
|
||||
|
||||
codes.push(nextCode);
|
||||
}
|
||||
|
||||
str = str + " " + codes.join(" ");
|
||||
|
||||
out.push(str);
|
||||
}
|
||||
}
|
||||
|
||||
return out.join("\n");
|
||||
},
|
||||
|
||||
guid: 0,
|
||||
|
||||
compile: function(program) {
|
||||
this.children = [];
|
||||
this.depths = {list: []};
|
||||
return this.program(program);
|
||||
},
|
||||
|
||||
accept: function(node) {
|
||||
return this[node.type](node);
|
||||
},
|
||||
|
||||
program: function(program) {
|
||||
var statements = program.statements, statement;
|
||||
this.opcodes = [];
|
||||
|
||||
for(var i=0, l=statements.length; i<l; i++) {
|
||||
statement = statements[i];
|
||||
this[statement.type](statement);
|
||||
}
|
||||
|
||||
this.depths.list = this.depths.list.sort(function(a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
compileProgram: function(program) {
|
||||
var result = new Compiler().compile(program);
|
||||
var guid = this.guid++;
|
||||
|
||||
this.usePartial = this.usePartial || result.usePartial;
|
||||
|
||||
this.children[guid] = result;
|
||||
|
||||
for(var i=0, l=result.depths.list.length; i<l; i++) {
|
||||
depth = result.depths.list[i];
|
||||
|
||||
if(depth < 2) { continue; }
|
||||
else { this.addDepth(depth - 1); }
|
||||
}
|
||||
|
||||
return guid;
|
||||
},
|
||||
|
||||
block: function(block) {
|
||||
var mustache = block.mustache;
|
||||
var params = mustache.params, depth, child, inverse, inverseGuid;
|
||||
|
||||
this.pushParams(params);
|
||||
|
||||
// ID lookup is now on the stack
|
||||
this.ID(mustache.id);
|
||||
|
||||
var programGuid = this.compileProgram(block.program);
|
||||
|
||||
if(block.program.inverse) {
|
||||
inverseGuid = this.compileProgram(block.program.inverse);
|
||||
}
|
||||
|
||||
if(block.program.inverse) {
|
||||
this.declare('inverse', inverseGuid);
|
||||
}
|
||||
|
||||
this.opcode('invokeProgram', programGuid, params.length);
|
||||
this.declare('inverse', null);
|
||||
this.opcode('append');
|
||||
},
|
||||
|
||||
inverse: function(block) {
|
||||
this.ID(block.mustache.id);
|
||||
var programGuid = this.compileProgram(block.program);
|
||||
|
||||
this.opcode('invokeInverse', programGuid);
|
||||
this.opcode('append');
|
||||
},
|
||||
|
||||
partial: function(partial) {
|
||||
var id = partial.id;
|
||||
this.usePartial = true;
|
||||
|
||||
if(partial.context) {
|
||||
this.ID(partial.context);
|
||||
} else {
|
||||
this.opcode('push', 'context');
|
||||
}
|
||||
|
||||
this.opcode('invokePartial', id.original);
|
||||
this.opcode('append');
|
||||
},
|
||||
|
||||
content: function(content) {
|
||||
this.opcode('appendContent', content.string);
|
||||
},
|
||||
|
||||
mustache: function(mustache) {
|
||||
var params = mustache.params;
|
||||
|
||||
this.pushParams(params);
|
||||
this.ID(mustache.id);
|
||||
|
||||
this.opcode('invokeMustache', params.length, mustache.id.original);
|
||||
|
||||
if(mustache.escaped) {
|
||||
this.opcode('appendEscaped');
|
||||
} else {
|
||||
this.opcode('append');
|
||||
}
|
||||
},
|
||||
|
||||
ID: function(id) {
|
||||
this.addDepth(id.depth);
|
||||
|
||||
this.opcode('getContext', id.depth);
|
||||
|
||||
this.opcode('lookupWithFallback', id.parts[0] || null);
|
||||
|
||||
for(var i=1, l=id.parts.length; i<l; i++) {
|
||||
this.opcode('lookup', id.parts[i]);
|
||||
}
|
||||
},
|
||||
|
||||
STRING: function(string) {
|
||||
this.opcode('pushString', string.string);
|
||||
},
|
||||
|
||||
comment: function() {},
|
||||
|
||||
pushParams: function(params) {
|
||||
var i = params.length, param;
|
||||
|
||||
while(i--) {
|
||||
param = params[i];
|
||||
this[param.type](param);
|
||||
}
|
||||
},
|
||||
|
||||
opcode: function(name, val1, val2) {
|
||||
this.opcodes.push(Compiler.OPCODE_MAP[name]);
|
||||
if(val1 !== undefined) { this.opcodes.push(val1); }
|
||||
if(val2 !== undefined) { this.opcodes.push(val2); }
|
||||
},
|
||||
|
||||
declare: function(name, value) {
|
||||
this.opcodes.push('DECLARE');
|
||||
this.opcodes.push(name);
|
||||
this.opcodes.push(value);
|
||||
},
|
||||
|
||||
addDepth: function(depth) {
|
||||
if(depth === 0) { return; }
|
||||
|
||||
if(!this.depths[depth]) {
|
||||
this.depths[depth] = true;
|
||||
this.depths.list.push(depth);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
JavaScriptCompiler.prototype = {
|
||||
// PUBLIC API: You can override these methods in a subclass to provide
|
||||
// alternative compiled forms for name lookup and buffering semantics
|
||||
nameLookup: function(parent, name, type) {
|
||||
if(JavaScriptCompiler.RESERVED_WORDS[name]) {
|
||||
return parent + "['" + name + "']";
|
||||
} else {
|
||||
return parent + "." + name;
|
||||
}
|
||||
},
|
||||
|
||||
appendToBuffer: function(string) {
|
||||
return "buffer = buffer + " + string + ";";
|
||||
},
|
||||
|
||||
initializeBuffer: function() {
|
||||
return this.quotedString("");
|
||||
},
|
||||
// END PUBLIC API
|
||||
|
||||
compile: function(environment, data) {
|
||||
this.environment = environment;
|
||||
this.data = data;
|
||||
|
||||
this.preamble();
|
||||
|
||||
this.stackSlot = 0;
|
||||
this.stackVars = [];
|
||||
this.registers = {list: []};
|
||||
|
||||
this.compileChildren(environment, data);
|
||||
|
||||
Handlebars.log(Handlebars.logger.DEBUG, environment.disassemble() + "\n\n");
|
||||
|
||||
var opcodes = environment.opcodes, opcode, name, declareName, declareVal;
|
||||
|
||||
this.i = 0;
|
||||
|
||||
for(l=opcodes.length; this.i<l; this.i++) {
|
||||
opcode = this.nextOpcode(0);
|
||||
|
||||
if(opcode[0] === 'DECLARE') {
|
||||
this.i = this.i + 2;
|
||||
this[opcode[1]] = opcode[2];
|
||||
} else {
|
||||
this.i = this.i + opcode[1].length;
|
||||
this[opcode[0]].apply(this, opcode[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return this.createFunction();
|
||||
},
|
||||
|
||||
nextOpcode: function(n) {
|
||||
var opcodes = this.environment.opcodes, opcode = opcodes[this.i + n], name, val;
|
||||
var extraParams, codes;
|
||||
|
||||
if(opcode === 'DECLARE') {
|
||||
name = opcodes[this.i + 1];
|
||||
val = opcodes[this.i + 2];
|
||||
return ['DECLARE', name, val];
|
||||
} else {
|
||||
name = Compiler.DISASSEMBLE_MAP[opcode];
|
||||
|
||||
extraParams = Compiler.multiParamSize(opcode);
|
||||
codes = [];
|
||||
|
||||
for(var j=0; j<extraParams; j++) {
|
||||
codes.push(opcodes[this.i + j + 1 + n]);
|
||||
}
|
||||
|
||||
return [name, codes];
|
||||
}
|
||||
},
|
||||
|
||||
eat: function(opcode) {
|
||||
this.i = this.i + opcode.length;
|
||||
},
|
||||
|
||||
preamble: function() {
|
||||
var out = [];
|
||||
out.push("var buffer = " + this.initializeBuffer() + ", currentContext = context");
|
||||
|
||||
var copies = "helpers = helpers || Handlebars.helpers;";
|
||||
if(this.environment.usePartial) { copies = copies + " partials = partials || Handlebars.partials;"; }
|
||||
out.push(copies);
|
||||
|
||||
// track the last context pushed into place to allow skipping the
|
||||
// getContext opcode when it would be a noop
|
||||
this.lastContext = 0;
|
||||
this.source = out;
|
||||
},
|
||||
|
||||
createFunction: function() {
|
||||
var container = {
|
||||
escapeExpression: Handlebars.Utils.escapeExpression,
|
||||
invokePartial: Handlebars.VM.invokePartial,
|
||||
programs: [],
|
||||
program: function(i, helpers, partials, data) {
|
||||
var programWrapper = this.programs[i];
|
||||
if(data) {
|
||||
return Handlebars.VM.program(this.children[i], helpers, partials, data);
|
||||
} else if(programWrapper) {
|
||||
return programWrapper;
|
||||
} else {
|
||||
programWrapper = this.programs[i] = Handlebars.VM.program(this.children[i], helpers, partials);
|
||||
return programWrapper;
|
||||
}
|
||||
},
|
||||
programWithDepth: Handlebars.VM.programWithDepth,
|
||||
noop: Handlebars.VM.noop
|
||||
};
|
||||
var locals = this.stackVars.concat(this.registers.list);
|
||||
|
||||
if(locals.length > 0) {
|
||||
this.source[0] = this.source[0] + ", " + locals.join(", ");
|
||||
}
|
||||
|
||||
this.source[0] = this.source[0] + ";";
|
||||
|
||||
this.source.push("return buffer;");
|
||||
|
||||
var params = ["Handlebars", "context", "helpers", "partials"];
|
||||
|
||||
if(this.data) { params.push("data"); }
|
||||
|
||||
for(var i=0, l=this.environment.depths.list.length; i<l; i++) {
|
||||
params.push("depth" + this.environment.depths.list[i]);
|
||||
}
|
||||
|
||||
|
||||
if(params.length === 4 && !this.environment.usePartial) { params.pop(); }
|
||||
|
||||
params.push(this.source.join("\n"));
|
||||
|
||||
var fn = Function.apply(this, params);
|
||||
fn.displayName = "Handlebars.js";
|
||||
|
||||
Handlebars.log(Handlebars.logger.DEBUG, fn.toString() + "\n\n");
|
||||
|
||||
container.render = fn;
|
||||
|
||||
container.children = this.environment.children;
|
||||
|
||||
return function(context, helpers, partials, data, $depth) {
|
||||
try {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
args.unshift(Handlebars);
|
||||
return container.render.apply(container, args);
|
||||
} catch(e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
appendContent: function(content) {
|
||||
this.source.push(this.appendToBuffer(this.quotedString(content)));
|
||||
},
|
||||
|
||||
append: function() {
|
||||
var local = this.popStack();
|
||||
this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }");
|
||||
},
|
||||
|
||||
appendEscaped: function() {
|
||||
var opcode = this.nextOpcode(1), extra = "";
|
||||
|
||||
if(opcode[0] === 'appendContent') {
|
||||
extra = " + " + this.quotedString(opcode[1][0]);
|
||||
this.eat(opcode);
|
||||
}
|
||||
|
||||
this.source.push(this.appendToBuffer("this.escapeExpression(" + this.popStack() + ")" + extra));
|
||||
},
|
||||
|
||||
getContext: function(depth) {
|
||||
if(this.lastContext !== depth) {
|
||||
this.lastContext = depth;
|
||||
|
||||
if(depth === 0) {
|
||||
this.source.push("currentContext = context;");
|
||||
} else {
|
||||
this.source.push("currentContext = depth" + depth + ";");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
lookupWithFallback: function(name) {
|
||||
if(name) {
|
||||
this.pushStack(this.nameLookup('currentContext', name, 'context'));
|
||||
var topStack = this.topStack();
|
||||
this.source.push("if(" + topStack + " === undefined) { " + topStack + " = " + this.nameLookup('helpers', name, 'helper') + "; }");
|
||||
} else {
|
||||
this.pushStack("currentContext");
|
||||
}
|
||||
},
|
||||
|
||||
lookup: function(name) {
|
||||
var topStack = this.topStack();
|
||||
this.source.push(topStack + " = " + this.nameLookup(topStack, name, 'context') + ";");
|
||||
},
|
||||
|
||||
pushString: function(string) {
|
||||
this.pushStack(this.quotedString(string));
|
||||
},
|
||||
|
||||
push: function(name) {
|
||||
this.pushStack(name);
|
||||
},
|
||||
|
||||
invokeMustache: function(paramSize, original) {
|
||||
var params = ["context"], fn, slot;
|
||||
|
||||
fn = this.popStack();
|
||||
|
||||
for(var i=0; i<paramSize; i++) {
|
||||
params.push(this.popStack());
|
||||
}
|
||||
|
||||
if(this.data) { params.push("data"); }
|
||||
|
||||
var paramString = params.join(", ");
|
||||
var helperMissing = ["context"].concat(this.quotedString(original)).concat(params.slice(1));
|
||||
|
||||
slot = this.nextStack();
|
||||
|
||||
if(paramSize === 0) {
|
||||
// TODO: This case is not listed in the mustache spec. Is it important?
|
||||
this.source.push("if(typeof " + fn + " === 'function') { " + slot + " = " + fn + ".call(" + paramString + "); }");
|
||||
} else {
|
||||
this.source.push("if(typeof " + fn + " === 'function') { " + slot + " = " + fn + ".call(" + paramString + "); } else { " + slot + " = helpers.helperMissing.call(" + helperMissing + ") }");
|
||||
}
|
||||
},
|
||||
|
||||
invokeProgram: function(guid, paramSize) {
|
||||
var inverse = this.programExpression(this.inverse);
|
||||
|
||||
var fn = this.popStack();
|
||||
var id = fn;
|
||||
|
||||
var params = ["context"];
|
||||
var blockMissingParams = ["context", id];
|
||||
|
||||
for(var i=0; i<paramSize; i++) {
|
||||
var param = this.popStack();
|
||||
params.push(param);
|
||||
blockMissingParams.push(param);
|
||||
}
|
||||
|
||||
var mainProgram = this.programExpression(guid);
|
||||
|
||||
params.push(mainProgram, inverse);
|
||||
blockMissingParams.push(mainProgram, inverse);
|
||||
|
||||
if(this.data) {
|
||||
params.push("data");
|
||||
blockMissingParams.push("data");
|
||||
}
|
||||
|
||||
var nextStack = this.nextStack();
|
||||
|
||||
this.source.push("if(typeof " + id + " === 'function') { " + nextStack + " = " + id + ".call(" + params.join(", ") + "); }");
|
||||
this.source.push("else { " + nextStack + " = helpers.blockHelperMissing.call(" + blockMissingParams.join(", ") + "); }");
|
||||
},
|
||||
|
||||
invokeInverse: function(guid) {
|
||||
var program = this.programExpression(guid);
|
||||
|
||||
var blockMissingParams = ["context", this.topStack(), "this.noop", program];
|
||||
this.pushStack("helpers.blockHelperMissing.call(" + blockMissingParams.join(", ") + ")");
|
||||
},
|
||||
|
||||
invokePartial: function(context) {
|
||||
this.pushStack("this.invokePartial(" + this.nameLookup('partials', context, 'partial') + ", '" + context + "', " + this.popStack() + ", helpers, partials);");
|
||||
},
|
||||
|
||||
// HELPERS
|
||||
|
||||
compiler: JavaScriptCompiler,
|
||||
|
||||
compileChildren: function(environment, data) {
|
||||
var children = environment.children, child, compiler;
|
||||
var compiled = [];
|
||||
|
||||
for(var i=0, l=children.length; i<l; i++) {
|
||||
child = children[i];
|
||||
compiler = new this.compiler();
|
||||
|
||||
compiled[i] = compiler.compile(child, data);
|
||||
}
|
||||
|
||||
environment.rawChildren = children;
|
||||
environment.children = compiled;
|
||||
},
|
||||
|
||||
programExpression: function(guid) {
|
||||
if(guid == null) { return "this.noop"; }
|
||||
|
||||
var programParams = [guid, "helpers", "partials"];
|
||||
|
||||
var depths = this.environment.rawChildren[guid].depths.list;
|
||||
|
||||
if(this.data) { programParams.push("data"); }
|
||||
|
||||
for(var i=0, l = depths.length; i<l; i++) {
|
||||
depth = depths[i];
|
||||
|
||||
if(depth === 1) { programParams.push("context"); }
|
||||
else { programParams.push("depth" + (depth - 1)); }
|
||||
}
|
||||
|
||||
if(!this.environment.usePartial) {
|
||||
if(programParams[3]) {
|
||||
programParams[2] = "null";
|
||||
} else {
|
||||
programParams.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if(depths.length === 0) {
|
||||
return "this.program(" + programParams.join(", ") + ")";
|
||||
} else {
|
||||
programParams[0] = "this.children[" + guid + "]";
|
||||
return "this.programWithDepth(" + programParams.join(", ") + ")";
|
||||
}
|
||||
},
|
||||
|
||||
register: function(name, val) {
|
||||
this.useRegister(name);
|
||||
this.source.push(name + " = " + val + ";");
|
||||
},
|
||||
|
||||
useRegister: function(name) {
|
||||
if(!this.registers[name]) {
|
||||
this.registers[name] = true;
|
||||
this.registers.list.push(name);
|
||||
}
|
||||
},
|
||||
|
||||
pushStack: function(item) {
|
||||
this.source.push(this.nextStack() + " = " + item + ";");
|
||||
return "stack" + this.stackSlot;
|
||||
},
|
||||
|
||||
nextStack: function() {
|
||||
this.stackSlot++;
|
||||
if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); }
|
||||
return "stack" + this.stackSlot;
|
||||
},
|
||||
|
||||
popStack: function() {
|
||||
return "stack" + this.stackSlot--;
|
||||
},
|
||||
|
||||
topStack: function() {
|
||||
return "stack" + this.stackSlot;
|
||||
},
|
||||
|
||||
quotedString: function(str) {
|
||||
return '"' + str
|
||||
.replace(/\\/, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r') + '"';
|
||||
}
|
||||
};
|
||||
|
||||
var reservedWords = ("break case catch continue default delete do else finally " +
|
||||
"for function if in instanceof new return switch this throw " +
|
||||
"try typeof var void while with null true false").split(" ");
|
||||
|
||||
compilerWords = JavaScriptCompiler.RESERVED_WORDS = {};
|
||||
|
||||
for(var i=0, l=reservedWords.length; i<l; i++) {
|
||||
compilerWords[reservedWords[i]] = true;
|
||||
}
|
||||
|
||||
})(Handlebars.Compiler, Handlebars.JavaScriptCompiler);
|
||||
|
||||
Handlebars.VM = {
|
||||
programWithDepth: function(fn) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
return function(context) {
|
||||
return fn.apply(this, [context].concat(args));
|
||||
};
|
||||
},
|
||||
program: function(fn, helpers, partials, data) {
|
||||
return function(context) {
|
||||
return fn(context, helpers, partials, data);
|
||||
};
|
||||
},
|
||||
noop: function() { return ""; },
|
||||
compile: function(string, data) {
|
||||
var ast = Handlebars.parse(string);
|
||||
var environment = new Handlebars.Compiler().compile(ast);
|
||||
return new Handlebars.JavaScriptCompiler().compile(environment, data);
|
||||
},
|
||||
invokePartial: function(partial, name, context, helpers, partials) {
|
||||
if(partial === undefined) {
|
||||
throw new Handlebars.Exception("The partial " + name + " could not be found");
|
||||
} else if(partial instanceof Function) {
|
||||
return partial(context, helpers, partials);
|
||||
} else {
|
||||
partials[name] = Handlebars.VM.compile(partial);
|
||||
return partials[name](context, helpers, partials);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Handlebars.compile = Handlebars.VM.compile;
|
||||
// END(BROWSER)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// USAGE:
|
||||
// var handlebars = require('handlebars');
|
||||
|
||||
// var local = handlebars.create();
|
||||
|
||||
var handlebars = require('../dist/cjs/handlebars')["default"];
|
||||
|
||||
handlebars.Visitor = require('../dist/cjs/handlebars/compiler/visitor')["default"];
|
||||
|
||||
var printer = require('../dist/cjs/handlebars/compiler/printer');
|
||||
handlebars.PrintVisitor = printer.PrintVisitor;
|
||||
handlebars.print = printer.print;
|
||||
|
||||
module.exports = handlebars;
|
||||
|
||||
// Publish a Node.js require() handler for .handlebars and .hbs files
|
||||
if (typeof require !== 'undefined' && require.extensions) {
|
||||
var extension = function(module, filename) {
|
||||
var fs = require("fs");
|
||||
var templateString = fs.readFileSync(filename, "utf8");
|
||||
module.exports = handlebars.compile(templateString);
|
||||
};
|
||||
require.extensions[".handlebars"] = extension;
|
||||
require.extensions[".hbs"] = extension;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "handlebars",
|
||||
"barename": "handlebars",
|
||||
"version": "v2.0.0-alpha.1",
|
||||
"description": "Handlebars provides the power necessary to let you build semantic templates effectively with no frustration",
|
||||
"homepage": "http://www.handlebarsjs.com/",
|
||||
"keywords": [
|
||||
"handlebars",
|
||||
"mustache",
|
||||
"template",
|
||||
"html"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/wycats/handlebars.js.git"
|
||||
},
|
||||
"author": "Yehuda Katz",
|
||||
"license": "MIT",
|
||||
"readmeFilename": "README.md",
|
||||
"engines": {
|
||||
"node": ">=0.4.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"optimist": "~0.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"uglify-js": "~2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"async": "~0.2.9",
|
||||
"aws-sdk": "~1.5.0",
|
||||
"benchmark": "~1.0",
|
||||
"dustjs-linkedin": "~2.0.2",
|
||||
"eco": "~1.1.0-rc-3",
|
||||
"grunt": "~0.4.1",
|
||||
"grunt-cli": "~0.1.10",
|
||||
"grunt-contrib-clean": "~0.4.1",
|
||||
"grunt-contrib-concat": "~0.3.0",
|
||||
"grunt-contrib-connect": "~0.5.0",
|
||||
"grunt-contrib-copy": "~0.4.1",
|
||||
"grunt-contrib-jshint": "0.x",
|
||||
"grunt-contrib-requirejs": "~0.4.1",
|
||||
"grunt-contrib-uglify": "~0.2.2",
|
||||
"grunt-contrib-watch": "~0.5.3",
|
||||
"grunt-saucelabs": "~4.1.2",
|
||||
"es6-module-packager": "1.x",
|
||||
"jison": "~0.3.0",
|
||||
"keen.io": "0.0.3",
|
||||
"mocha": "*",
|
||||
"mustache": "~0.7.2",
|
||||
"semver": "~2.1.0",
|
||||
"underscore": "~1.5.1"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"bin": {
|
||||
"handlebars": "bin/handlebars"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
# Release Notes
|
||||
|
||||
## Development
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v2.0.0-alpha.1...master)
|
||||
|
||||
## v2.0.0-alpha.1 - February 10th, 2014
|
||||
- [#182](https://github.com/wycats/handlebars.js/pull/182) - Allow passing hash parameters to partials ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#392](https://github.com/wycats/handlebars.js/pull/392) - Access to root context in partials and helpers ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#472](https://github.com/wycats/handlebars.js/issues/472) - Helpers cannot have decimal parameters ([@kayleg](https://api.github.com/users/kayleg))
|
||||
- [#569](https://github.com/wycats/handlebars.js/pull/569) - Unable to lookup array values using @index ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#491](https://github.com/wycats/handlebars.js/pull/491) - For nested helpers: get the @ variables of the outer helper from the inner one ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#669](https://github.com/wycats/handlebars.js/issues/669) - Ability to unregister a helper ([@dbachrach](https://api.github.com/users/dbachrach))
|
||||
- [#730](https://github.com/wycats/handlebars.js/pull/730) - Raw block helpers ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#634](https://github.com/wycats/handlebars.js/pull/634) - It would be great to have the helper name passed to `blockHelperMissing` ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#729](https://github.com/wycats/handlebars.js/pull/729) - Convert template spec to object literal ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
|
||||
- [#658](https://github.com/wycats/handlebars.js/issues/658) - Depthed helpers do not work after an upgrade from 1.0.0 ([@xibxor](https://api.github.com/users/xibxor))
|
||||
- [#671](https://github.com/wycats/handlebars.js/issues/671) - Crashes on no-parameter {{#each}} ([@stepancheg](https://api.github.com/users/stepancheg))
|
||||
- [#689](https://github.com/wycats/handlebars.js/issues/689) - broken template precompilation ([@AAS](https://api.github.com/users/AAS))
|
||||
- [#698](https://github.com/wycats/handlebars.js/pull/698) - Fix parser generation under windows ([@osiris43](https://api.github.com/users/osiris43))
|
||||
- [#699](https://github.com/wycats/handlebars.js/issues/699) - @DATA not compiles to invalid JS in stringParams mode ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#705](https://github.com/wycats/handlebars.js/issues/705) - 1.3.0 can not be wrapped in an IIFE ([@craigteegarden](https://api.github.com/users/craigteegarden))
|
||||
- [#706](https://github.com/wycats/handlebars.js/pull/706) - README: Use with helper instead of relying on blockHelperMissing ([@scottgonzalez](https://api.github.com/users/scottgonzalez))
|
||||
|
||||
- [#700](https://github.com/wycats/handlebars.js/pull/700) - Remove redundant conditions ([@blakeembrey](https://api.github.com/users/blakeembrey))
|
||||
- [#704](https://github.com/wycats/handlebars.js/pull/704) - JavaScript Compiler Cleanup ([@blakeembrey](https://api.github.com/users/blakeembrey))
|
||||
|
||||
Compatibility notes:
|
||||
- `helperMissing` helper no longer has the indexed name argument. Helper name is now available via `options.name`.
|
||||
- Precompiler output has changed, which breaks compatibility with prior versions of the runtime and precompiled output.
|
||||
- `JavaScriptCompiler.compilerInfo` now returns generic objects rather than javascript source.
|
||||
- AST changes
|
||||
- INTEGER -> NUMBER
|
||||
- Additional PartialNode hash parameter
|
||||
- New RawBlockNode type
|
||||
- Data frames now have a `_parent` field. This is internal but is enumerable for performance/compatability reasons.
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.3.0...v2.0.0-alpha.1)
|
||||
|
||||
## v1.3.0 - January 1st, 2014
|
||||
- [#690](https://github.com/wycats/handlebars.js/pull/690) - Added support for subexpressions ([@machty](https://api.github.com/users/machty))
|
||||
- [#696](https://github.com/wycats/handlebars.js/pull/696) - Fix for reserved keyword "default" ([@nateirwin](https://api.github.com/users/nateirwin))
|
||||
- [#692](https://github.com/wycats/handlebars.js/pull/692) - add line numbers to nodes when parsing ([@fivetanley](https://api.github.com/users/fivetanley))
|
||||
- [#695](https://github.com/wycats/handlebars.js/pull/695) - Pull options out from param setup to allow easier extension ([@blakeembrey](https://api.github.com/users/blakeembrey))
|
||||
- [#694](https://github.com/wycats/handlebars.js/pull/694) - Make the environment reusable ([@blakeembrey](https://api.github.com/users/blakeembrey))
|
||||
- [#636](https://github.com/wycats/handlebars.js/issues/636) - Print line and column of errors ([@sgronblo](https://api.github.com/users/sgronblo))
|
||||
- Use literal for data lookup - c1a93d3
|
||||
- Add stack handling sanity checks - cd885bf
|
||||
- Fix stack id "leak" on replaceStack - ddfe457
|
||||
- Fix incorrect stack pop when replacing literals - f4d337d
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.2.1...v1.3.0)
|
||||
|
||||
## v1.2.1 - December 26th, 2013
|
||||
- [#684](https://github.com/wycats/handlebars.js/pull/684) - Allow any number of trailing characters for valid JavaScript variable ([@blakeembrey](https://api.github.com/users/blakeembrey))
|
||||
- [#686](https://github.com/wycats/handlebars.js/pull/686) - Falsy AMD module names in version 1.2.0 ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.2.0...v1.2.1)
|
||||
|
||||
## v1.2.0 - December 23rd, 2013
|
||||
- [#675](https://github.com/wycats/handlebars.js/issues/675) - Cannot compile empty template for partial ([@erwinw](https://api.github.com/users/erwinw))
|
||||
- [#677](https://github.com/wycats/handlebars.js/issues/677) - Triple brace statements fail under IE ([@hamzaCM](https://api.github.com/users/hamzaCM))
|
||||
- [#655](https://github.com/wycats/handlebars.js/issues/655) - Loading Handlebars using bower ([@niki4810](https://api.github.com/users/niki4810))
|
||||
- [#657](https://github.com/wycats/handlebars.js/pull/657) - Fixes issue where cli compiles non handlebars templates ([@chrishoage](https://api.github.com/users/chrishoage))
|
||||
- [#681](https://github.com/wycats/handlebars.js/pull/681) - Adds in-browser testing and Saucelabs CI ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#661](https://github.com/wycats/handlebars.js/pull/661) - Add @first and @index to #each object iteration ([@cgp](https://api.github.com/users/cgp))
|
||||
- [#650](https://github.com/wycats/handlebars.js/pull/650) - Handlebars is MIT-licensed ([@thomasboyt](https://api.github.com/users/thomasboyt))
|
||||
- [#641](https://github.com/wycats/handlebars.js/pull/641) - Document ember testing process ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#662](https://github.com/wycats/handlebars.js/issues/662) - handlebars-source 1.1.2 is missing from RubyGems.
|
||||
- [#656](https://github.com/wycats/handlebars.js/issues/656) - Expose COMPILER_REVISION checks as a hook ([@machty](https://api.github.com/users/machty))
|
||||
- [#668](https://github.com/wycats/handlebars.js/issues/668) - Consider publishing handlebars-runtime as a separate module on npm ([@dlmanning](https://api.github.com/users/dlmanning))
|
||||
- [#679](https://github.com/wycats/handlebars.js/issues/679) - Unable to override invokePartial ([@mattbrailsford](https://api.github.com/users/mattbrailsford))
|
||||
- [#646](https://github.com/wycats/handlebars.js/pull/646) - Fix "\\{{" immediately following "\{{" ([@dmarcotte](https://api.github.com/users/dmarcotte))
|
||||
- Allow extend to work with non-prototyped objects - eb53f2e
|
||||
- Add JavascriptCompiler public API tests - 1a751b2
|
||||
- Add AST test coverage for more complex paths - ddea5be
|
||||
- Fix handling of boolean escape in MustacheNode - b4968bb
|
||||
|
||||
Compatibility notes:
|
||||
- `@index` and `@first` are now supported for `each` iteration on objects
|
||||
- `Handlebars.VM.checkRevision` and `Handlebars.JavaScriptCompiler.prototype.compilerInfo` now available to modify the version checking behavior.
|
||||
- Browserify users may link to the runtime library via `require('handlebars/runtime')`
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.1.2...v1.2.0)
|
||||
|
||||
## v1.1.2 - November 5th, 2013
|
||||
|
||||
- [#645](https://github.com/wycats/handlebars.js/issues/645) - 1.1.1 fails under IE8 ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#644](https://github.com/wycats/handlebars.js/issues/644) - Using precompiled templates (AMD mode) with handlebars.runtime 1.1.1 ([@fddima](https://api.github.com/users/fddima))
|
||||
|
||||
- Add simple binary utility tests - 96a45a4
|
||||
- Fix empty string compilation - eea708a
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.1.1...v1.1.2)
|
||||
|
||||
## v1.1.1 - November 4th, 2013
|
||||
|
||||
- [#642](https://github.com/wycats/handlebars.js/issues/642) - handlebars 1.1.0 are broken with nodejs
|
||||
|
||||
- Fix release notes link - 17ba258
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.1.0...v1.1.1)
|
||||
|
||||
## v1.1.0 - November 3rd, 2013
|
||||
|
||||
- [#628](https://github.com/wycats/handlebars.js/pull/628) - Convert code to ES6 modules ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#336](https://github.com/wycats/handlebars.js/pull/336) - Add whitespace control syntax ([@kpdecker](https://api.github.com/users/kpdecker))
|
||||
- [#535](https://github.com/wycats/handlebars.js/pull/535) - Fix for probable JIT error under Safari ([@sorentwo](https://api.github.com/users/sorentwo))
|
||||
- [#483](https://github.com/wycats/handlebars.js/issues/483) - Add first and last @ vars to each helper ([@denniskuczynski](https://api.github.com/users/denniskuczynski))
|
||||
- [#557](https://github.com/wycats/handlebars.js/pull/557) - `\\{{foo}}` escaping only works in some situations ([@dmarcotte](https://api.github.com/users/dmarcotte))
|
||||
- [#552](https://github.com/wycats/handlebars.js/pull/552) - Added BOM removal flag. ([@blessenm](https://api.github.com/users/blessenm))
|
||||
- [#543](https://github.com/wycats/handlebars.js/pull/543) - publish passing master builds to s3 ([@fivetanley](https://api.github.com/users/fivetanley))
|
||||
|
||||
- [#608](https://github.com/wycats/handlebars.js/issues/608) - Add `includeZero` flag to `if` conditional
|
||||
- [#498](https://github.com/wycats/handlebars.js/issues/498) - `Handlebars.compile` fails on empty string although a single blank works fine
|
||||
- [#599](https://github.com/wycats/handlebars.js/issues/599) - lambda helpers only receive options if used with arguments
|
||||
- [#592](https://github.com/wycats/handlebars.js/issues/592) - Optimize array and subprogram performance
|
||||
- [#571](https://github.com/wycats/handlebars.js/issues/571) - uglify upgrade breaks compatibility with older versions of node
|
||||
- [#587](https://github.com/wycats/handlebars.js/issues/587) - Partial inside partial breaks?
|
||||
|
||||
|
||||
Compatibility notes:
|
||||
- The project now includes separate artifacts for AMD, CommonJS, and global objects.
|
||||
- AMD: Users may load the bundled `handlebars.amd.js` or `handlebars.runtime.amd.js` files or load individual modules directly. AMD users should also note that the handlebars object is exposed via the `default` field on the imported object. This [gist](https://gist.github.com/wycats/7417be0dc361a69d5916) provides some discussion of possible compatibility shims.
|
||||
- CommonJS/Node: Node loading occurs as normal via `require`
|
||||
- Globals: The `handlebars.js` and `handlebars.runtime.js` files should behave in the same manner as the v1.0.12 / 1.0.0 release.
|
||||
- Build artifacts have been removed from the repository. [npm][npm], [components/handlebars.js][components], [cdnjs][cdnjs], or the [builds page][builds-page] should now be used as the source of built artifacts.
|
||||
- Context-stored helpers are now always passed the `options` hash. Previously no-argument helpers did not have this argument.
|
||||
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.0.12...v1.1.0)
|
||||
|
||||
## v1.0.12 / 1.0.0 - May 31 2013
|
||||
|
||||
- [#515](https://github.com/wycats/handlebars.js/issues/515) - Add node require extensions support ([@jjclark1982](https://github.com/jjclark1982))
|
||||
- [#517](https://github.com/wycats/handlebars.js/issues/517) - Fix amd precompiler output with directories ([@blessenm](https://github.com/blessenm))
|
||||
- [#433](https://github.com/wycats/handlebars.js/issues/433) - Add support for unicode ids
|
||||
- [#469](https://github.com/wycats/handlebars.js/issues/469) - Add support for `?` in ids
|
||||
- [#534](https://github.com/wycats/handlebars.js/issues/534) - Protect from object prototype modifications
|
||||
- [#519](https://github.com/wycats/handlebars.js/issues/519) - Fix partials with . name ([@jamesgorrie](https://github.com/jamesgorrie))
|
||||
- [#519](https://github.com/wycats/handlebars.js/issues/519) - Allow ID or strings in partial names
|
||||
- [#437](https://github.com/wycats/handlebars.js/issues/437) - Require matching brace counts in escaped expressions
|
||||
- Merge passed partials and helpers with global namespace values
|
||||
- Add support for complex ids in @data references
|
||||
- Docs updates
|
||||
|
||||
Compatibility notes:
|
||||
- The parser is now stricter on `{{{`, requiring that the end token be `}}}`. Templates that do not
|
||||
follow this convention should add the additional brace value.
|
||||
- Code that relies on global the namespace being muted when custom helpers or partials are passed will need to explicitly pass an `undefined` value for any helpers that should not be available.
|
||||
- The compiler version has changed. Precompiled templates with 1.0.12 or higher must use the 1.0.0 or higher runtime.
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.0.11...v1.0.12)
|
||||
|
||||
## v1.0.11 / 1.0.0-rc4 - May 13 2013
|
||||
|
||||
- [#458](https://github.com/wycats/handlebars.js/issues/458) - Fix `./foo` syntax ([@jpfiset](https://github.com/jpfiset))
|
||||
- [#460](https://github.com/wycats/handlebars.js/issues/460) - Allow `:` in unescaped identifers ([@jpfiset](https://github.com/jpfiset))
|
||||
- [#471](https://github.com/wycats/handlebars.js/issues/471) - Create release notes (These!)
|
||||
- [#456](https://github.com/wycats/handlebars.js/issues/456) - Allow escaping of `\\`
|
||||
- [#211](https://github.com/wycats/handlebars.js/issues/211) - Fix exception in `escapeExpression`
|
||||
- [#375](https://github.com/wycats/handlebars.js/issues/375) - Escape unicode newlines
|
||||
- [#461](https://github.com/wycats/handlebars.js/issues/461) - Do not fail when compiling `""`
|
||||
- [#302](https://github.com/wycats/handlebars.js/issues/302) - Fix sanity check in knownHelpersOnly mode
|
||||
- [#369](https://github.com/wycats/handlebars.js/issues/369) - Allow registration of multiple helpers and partial by passing definition object
|
||||
- Add bower package declaration ([@DevinClark](https://github.com/DevinClark))
|
||||
- Add NuSpec package declaration ([@MikeMayer](https://github.com/MikeMayer))
|
||||
- Handle empty context in `with` ([@thejohnfreeman](https://github.com/thejohnfreeman))
|
||||
- Support custom template extensions in CLI ([@matteoagosti](https://github.com/matteoagosti))
|
||||
- Fix Rhino support ([@broady](https://github.com/broady))
|
||||
- Include contexts in string mode ([@leshill](https://github.com/leshill))
|
||||
- Return precompiled scripts when compiling to AMD ([@JamesMaroney](https://github.com/JamesMaroney))
|
||||
- Docs updates ([@iangreenleaf](https://github.com/iangreenleaf), [@gilesbowkett](https://github.com/gilesbowkett), [@utkarsh2012](https://github.com/utkarsh2012))
|
||||
- Fix `toString` handling under IE and browserify ([@tommydudebreaux](https://github.com/tommydudebreaux))
|
||||
- Add program metadata
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.0.10...v1.0.11)
|
||||
|
||||
## v1.0.10 - Node - Feb 27 2013
|
||||
|
||||
- [#428](https://github.com/wycats/handlebars.js/issues/428) - Fix incorrect rendering of nested programs
|
||||
- Fix exception message ([@tricknotes](https://github.com/tricknotes))
|
||||
- Added negative number literal support
|
||||
- Concert library to single IIFE
|
||||
- Add handlebars-source gemspec ([@machty](https://github.com/machty))
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.0.9...v1.0.10)
|
||||
|
||||
## v1.0.9 - Node - Feb 15 2013
|
||||
|
||||
- Added `Handlebars.create` API in node module for sandboxed instances ([@tommydudebreaux](https://github.com/tommydudebreaux))
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/1.0.0-rc.3...v1.0.9)
|
||||
|
||||
## 1.0.0-rc3 - Browser - Feb 14 2013
|
||||
|
||||
- Prevent use of `this` or `..` in illogical place ([@leshill](https://github.com/leshill))
|
||||
- Allow AST passing for `parse`/`compile`/`precompile` ([@machty](https://github.com/machty))
|
||||
- Optimize generated output by inlining statements where possible
|
||||
- Check compiler version when evaluating templates
|
||||
- Package browser dist in npm package
|
||||
|
||||
[Commits](https://github.com/wycats/handlebars.js/compare/v1.0.8...1.0.0-rc.3)
|
||||
|
||||
## Prior Versions
|
||||
|
||||
When upgrading from the Handlebars 0.9 series, be aware that the
|
||||
signature for passing custom helpers or partials to templates has
|
||||
changed.
|
||||
|
||||
Instead of:
|
||||
|
||||
```js
|
||||
template(context, helpers, partials, [data])
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```js
|
||||
template(context, {helpers: helpers, partials: partials, data: data})
|
||||
```
|
||||
|
||||
[builds-page]: http://builds.handlebarsjs.com.s3.amazonaws.com/index.html
|
||||
[cdnjs]: http://cdnjs.com/libraries/handlebars.js/
|
||||
[components]: https://github.com/components/handlebars.js
|
||||
[npm]: https://npmjs.org/package/handlebars
|
||||
@@ -0,0 +1,3 @@
|
||||
// Create a simple path alias to allow browserify to resolve
|
||||
// the runtime on a supported path.
|
||||
module.exports = require('./dist/cjs/handlebars.runtime');
|
||||
@@ -1,100 +0,0 @@
|
||||
require "spec_helper"
|
||||
|
||||
class TestContext
|
||||
class TestModule
|
||||
attr_reader :name, :tests
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
@tests = []
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :modules
|
||||
|
||||
def initialize
|
||||
@modules = []
|
||||
end
|
||||
|
||||
def module(name)
|
||||
@modules << TestModule.new(name)
|
||||
end
|
||||
|
||||
def test(name, function)
|
||||
@modules.last.tests << [name, function]
|
||||
end
|
||||
end
|
||||
|
||||
test_context = TestContext.new
|
||||
js_context = Handlebars::Spec::CONTEXT
|
||||
|
||||
Module.new do
|
||||
extend Test::Unit::Assertions
|
||||
|
||||
def self.js_backtrace(context)
|
||||
begin
|
||||
context.eval("throw")
|
||||
rescue V8::JSError => e
|
||||
return e.backtrace(:javascript)
|
||||
end
|
||||
end
|
||||
|
||||
js_context["p"] = proc do |str|
|
||||
p str
|
||||
end
|
||||
|
||||
js_context["ok"] = proc do |ok, message|
|
||||
js_context["$$RSPEC1$$"] = ok
|
||||
|
||||
result = js_context.eval("!!$$RSPEC1$$")
|
||||
|
||||
message ||= "#{ok} was not truthy"
|
||||
|
||||
unless result
|
||||
backtrace = js_backtrace(js_context)
|
||||
message << "\n#{backtrace.join("\n")}"
|
||||
end
|
||||
|
||||
assert result, message
|
||||
end
|
||||
|
||||
js_context["equals"] = proc do |first, second, message|
|
||||
js_context["$$RSPEC1$$"] = first
|
||||
js_context["$$RSPEC2$$"] = second
|
||||
|
||||
result = js_context.eval("$$RSPEC1$$ == $$RSPEC2$$")
|
||||
|
||||
message ||= "#{first} did not == #{second}"
|
||||
|
||||
unless result
|
||||
backtrace = js_backtrace(js_context)
|
||||
message << "\n#{backtrace.join("\n")}"
|
||||
end
|
||||
|
||||
assert result, message
|
||||
end
|
||||
|
||||
js_context["equal"] = js_context["equals"]
|
||||
|
||||
js_context["module"] = proc do |name|
|
||||
test_context.module(name)
|
||||
end
|
||||
|
||||
js_context["test"] = proc do |name, function|
|
||||
test_context.test(name, function)
|
||||
end
|
||||
|
||||
local = Regexp.escape(File.expand_path(Dir.pwd))
|
||||
qunit_spec = File.expand_path("../qunit_spec.js", __FILE__)
|
||||
js_context.load(qunit_spec.sub(/^#{local}\//, ''))
|
||||
end
|
||||
|
||||
test_context.modules.each do |mod|
|
||||
describe mod.name do
|
||||
mod.tests.each do |name, function|
|
||||
it name do
|
||||
function.call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Mocha</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
|
||||
<style>
|
||||
.headless .suite > h1,
|
||||
.headless .test.pass {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Show only errors in "headless", non-interactive mode.
|
||||
if (/headless=true/.test(location.href)) {
|
||||
document.documentElement.className = 'headless';
|
||||
}
|
||||
</script>
|
||||
<script src="/node_modules/mocha/mocha.js"></script>
|
||||
<script>
|
||||
mocha.setup('bdd');
|
||||
</script>
|
||||
|
||||
<script src="/spec/env/require.js"></script>
|
||||
|
||||
<script src="/spec/env/common.js"></script>
|
||||
<script>
|
||||
requirejs.config({
|
||||
paths: {
|
||||
handlebars: '/dist/handlebars.amd',
|
||||
tests: '/tmp/tests'
|
||||
}
|
||||
});
|
||||
|
||||
var CompilerContext = {
|
||||
compile: function(template, options) {
|
||||
var templateSpec = handlebarsEnv.precompile(template, options);
|
||||
return handlebarsEnv.template(safeEval(templateSpec));
|
||||
},
|
||||
compileWithPartial: function(template, options) {
|
||||
return handlebarsEnv.compile(template, options);
|
||||
}
|
||||
};
|
||||
|
||||
function safeEval(templateSpec) {
|
||||
try {
|
||||
var ret;
|
||||
eval('ret = ' + templateSpec);
|
||||
return ret;
|
||||
} catch (err) {
|
||||
console.error(templateSpec);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
onload = function(){
|
||||
|
||||
require(['handlebars'], function(Handlebars) {
|
||||
window.Handlebars = Handlebars['default'];
|
||||
|
||||
require(['tests'], function(Handlebars) {
|
||||
// The test harness leaks under FF. We should have decent global leak coverage from other tests
|
||||
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
|
||||
mocha.checkLeaks();
|
||||
}
|
||||
mocha.run();
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
{{foo}}
|
||||
@@ -0,0 +1 @@
|
||||
Hello, {{name}}!
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/*global Handlebars, handlebarsEnv, shouldThrow */
|
||||
describe('ast', function() {
|
||||
if (!Handlebars.AST) {
|
||||
return;
|
||||
}
|
||||
|
||||
var LOCATION_INFO = {
|
||||
last_line: 0,
|
||||
first_line: 0,
|
||||
first_column: 0,
|
||||
last_column: 0
|
||||
};
|
||||
|
||||
function testLocationInfoStorage(node){
|
||||
var properties = [ 'firstLine', 'lastLine', 'firstColumn', 'lastColumn' ],
|
||||
property,
|
||||
propertiesLen = properties.length,
|
||||
i;
|
||||
|
||||
for (i = 0; i < propertiesLen; i++){
|
||||
property = properties[0];
|
||||
equals(node[property], 0);
|
||||
}
|
||||
}
|
||||
|
||||
describe('MustacheNode', function() {
|
||||
function testEscape(open, expected) {
|
||||
var mustache = new handlebarsEnv.AST.MustacheNode([{}], {}, open, false);
|
||||
equals(mustache.escaped, expected);
|
||||
}
|
||||
|
||||
it('should store args', function() {
|
||||
var id = {isSimple: true},
|
||||
hash = {},
|
||||
mustache = new handlebarsEnv.AST.MustacheNode([id, 'param1'], hash, '', false, LOCATION_INFO);
|
||||
equals(mustache.type, 'mustache');
|
||||
equals(mustache.hash, hash);
|
||||
equals(mustache.escaped, true);
|
||||
equals(mustache.id, id);
|
||||
equals(mustache.params.length, 1);
|
||||
equals(mustache.params[0], 'param1');
|
||||
equals(!!mustache.isHelper, true);
|
||||
testLocationInfoStorage(mustache);
|
||||
});
|
||||
it('should accept token for escape', function() {
|
||||
testEscape('{{', true);
|
||||
testEscape('{{~', true);
|
||||
testEscape('{{#', true);
|
||||
testEscape('{{~#', true);
|
||||
testEscape('{{/', true);
|
||||
testEscape('{{~/', true);
|
||||
testEscape('{{^', true);
|
||||
testEscape('{{~^', true);
|
||||
testEscape('{', true);
|
||||
testEscape('{', true);
|
||||
|
||||
testEscape('{{&', false);
|
||||
testEscape('{{~&', false);
|
||||
testEscape('{{{', false);
|
||||
testEscape('{{~{', false);
|
||||
});
|
||||
it('should accept boolean for escape', function() {
|
||||
testEscape(true, true);
|
||||
testEscape({}, true);
|
||||
|
||||
testEscape(false, false);
|
||||
testEscape(undefined, false);
|
||||
});
|
||||
});
|
||||
describe('BlockNode', function() {
|
||||
it('should throw on mustache mismatch (old sexpr-less version)', function() {
|
||||
shouldThrow(function() {
|
||||
var mustacheNode = new handlebarsEnv.AST.MustacheNode([{ original: 'foo'}], null, '{{', {});
|
||||
new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}});
|
||||
}, Handlebars.Exception, "foo doesn't match bar");
|
||||
});
|
||||
it('should throw on mustache mismatch', function() {
|
||||
shouldThrow(function() {
|
||||
var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
|
||||
var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
|
||||
new handlebarsEnv.AST.BlockNode(mustacheNode, {}, {}, {path: {original: 'bar'}}, {first_line: 2, first_column: 2});
|
||||
}, Handlebars.Exception, "foo doesn't match bar - 2:2");
|
||||
});
|
||||
|
||||
it('stores location info', function(){
|
||||
var sexprNode = new handlebarsEnv.AST.SexprNode([{ original: 'foo'}], null);
|
||||
var mustacheNode = new handlebarsEnv.AST.MustacheNode(sexprNode, null, '{{', {});
|
||||
var block = new handlebarsEnv.AST.BlockNode(mustacheNode,
|
||||
{strip: {}}, {strip: {}},
|
||||
{
|
||||
strip: {},
|
||||
path: {original: 'foo'}
|
||||
},
|
||||
LOCATION_INFO);
|
||||
testLocationInfoStorage(block);
|
||||
});
|
||||
});
|
||||
describe('IdNode', function() {
|
||||
it('should throw on invalid path', function() {
|
||||
shouldThrow(function() {
|
||||
new handlebarsEnv.AST.IdNode([
|
||||
{part: 'foo'},
|
||||
{part: '..'},
|
||||
{part: 'bar'}
|
||||
], {first_line: 1, first_column: 1});
|
||||
}, Handlebars.Exception, "Invalid path: foo.. - 1:1");
|
||||
shouldThrow(function() {
|
||||
new handlebarsEnv.AST.IdNode([
|
||||
{part: 'foo'},
|
||||
{part: '.'},
|
||||
{part: 'bar'}
|
||||
], {first_line: 1, first_column: 1});
|
||||
}, Handlebars.Exception, "Invalid path: foo. - 1:1");
|
||||
shouldThrow(function() {
|
||||
new handlebarsEnv.AST.IdNode([
|
||||
{part: 'foo'},
|
||||
{part: 'this'},
|
||||
{part: 'bar'}
|
||||
], {first_line: 1, first_column: 1});
|
||||
}, Handlebars.Exception, "Invalid path: foothis - 1:1");
|
||||
});
|
||||
|
||||
it('stores location info', function(){
|
||||
var idNode = new handlebarsEnv.AST.IdNode([], LOCATION_INFO);
|
||||
testLocationInfoStorage(idNode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("HashNode", function(){
|
||||
|
||||
it('stores location info', function(){
|
||||
var hash = new handlebarsEnv.AST.HashNode([], LOCATION_INFO);
|
||||
testLocationInfoStorage(hash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ContentNode", function(){
|
||||
|
||||
it('stores location info', function(){
|
||||
var content = new handlebarsEnv.AST.ContentNode("HI", LOCATION_INFO);
|
||||
testLocationInfoStorage(content);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CommentNode", function(){
|
||||
|
||||
it('stores location info', function(){
|
||||
var comment = new handlebarsEnv.AST.CommentNode("HI", LOCATION_INFO);
|
||||
testLocationInfoStorage(comment);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NumberNode", function(){
|
||||
|
||||
it('stores location info', function(){
|
||||
var integer = new handlebarsEnv.AST.NumberNode("6", LOCATION_INFO);
|
||||
testLocationInfoStorage(integer);
|
||||
});
|
||||
});
|
||||
|
||||
describe("StringNode", function(){
|
||||
|
||||
it('stores location info', function(){
|
||||
var string = new handlebarsEnv.AST.StringNode("6", LOCATION_INFO);
|
||||
testLocationInfoStorage(string);
|
||||
});
|
||||
});
|
||||
|
||||
describe("BooleanNode", function(){
|
||||
|
||||
it('stores location info', function(){
|
||||
var bool = new handlebarsEnv.AST.BooleanNode("true", LOCATION_INFO);
|
||||
testLocationInfoStorage(bool);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DataNode", function(){
|
||||
|
||||
it('stores location info', function(){
|
||||
var data = new handlebarsEnv.AST.DataNode("YES", LOCATION_INFO);
|
||||
testLocationInfoStorage(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PartialNameNode", function(){
|
||||
|
||||
it('stores location info', function(){
|
||||
var pnn = new handlebarsEnv.AST.PartialNameNode({original: "YES"}, LOCATION_INFO);
|
||||
testLocationInfoStorage(pnn);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PartialNode", function(){
|
||||
|
||||
it('stores location info', function(){
|
||||
var pn = new handlebarsEnv.AST.PartialNode("so_partial", {}, {}, {}, LOCATION_INFO);
|
||||
testLocationInfoStorage(pn);
|
||||
});
|
||||
});
|
||||
describe("ProgramNode", function(){
|
||||
|
||||
describe("storing location info", function(){
|
||||
it("stores when `inverse` argument isn't passed", function(){
|
||||
var pn = new handlebarsEnv.AST.ProgramNode([], LOCATION_INFO);
|
||||
testLocationInfoStorage(pn);
|
||||
});
|
||||
|
||||
it("stores when `inverse` or `stripInverse` arguments passed", function(){
|
||||
var pn = new handlebarsEnv.AST.ProgramNode([], {strip: {}}, undefined, LOCATION_INFO);
|
||||
testLocationInfoStorage(pn);
|
||||
|
||||
var clone = {
|
||||
strip: {},
|
||||
firstLine: 0,
|
||||
lastLine: 0,
|
||||
firstColumn: 0,
|
||||
lastColumn: 0
|
||||
};
|
||||
pn = new handlebarsEnv.AST.ProgramNode([], {strip: {}}, [ clone ], LOCATION_INFO);
|
||||
testLocationInfoStorage(pn);
|
||||
|
||||
// Assert that the newly created ProgramNode has the same location
|
||||
// information as the inverse
|
||||
testLocationInfoStorage(pn.inverse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Line Numbers", function(){
|
||||
var ast, statements;
|
||||
|
||||
function testColumns(node, firstLine, lastLine, firstColumn, lastColumn){
|
||||
equals(node.firstLine, firstLine);
|
||||
equals(node.lastLine, lastLine);
|
||||
equals(node.firstColumn, firstColumn);
|
||||
equals(node.lastColumn, lastColumn);
|
||||
}
|
||||
|
||||
ast = Handlebars.parse("line 1 {{line1Token}}\n line 2 {{line2token}}\n line 3 {{#blockHelperOnLine3}}\nline 4{{line4token}}\n" +
|
||||
"line5{{else}}\n{{line6Token}}\n{{/blockHelperOnLine3}}");
|
||||
statements = ast.statements;
|
||||
|
||||
it('gets ContentNode line numbers', function(){
|
||||
var contentNode = statements[0];
|
||||
testColumns(contentNode, 1, 1, 0, 7);
|
||||
});
|
||||
|
||||
it('gets MustacheNode line numbers', function(){
|
||||
var mustacheNode = statements[1];
|
||||
testColumns(mustacheNode, 1, 1, 7, 21);
|
||||
});
|
||||
|
||||
it('gets line numbers correct when newlines appear', function(){
|
||||
var secondContentNode = statements[2];
|
||||
testColumns(secondContentNode, 1, 2, 21, 8);
|
||||
});
|
||||
|
||||
it('gets MustacheNode line numbers correct across newlines', function(){
|
||||
var secondMustacheNode = statements[3];
|
||||
testColumns(secondMustacheNode, 2, 2, 8, 22);
|
||||
});
|
||||
|
||||
it('gets the block helper information correct', function(){
|
||||
var blockHelperNode = statements[5];
|
||||
testColumns(blockHelperNode, 3, 7, 8, 23);
|
||||
});
|
||||
|
||||
it('correctly records the line numbers of an inverse of a block helper', function(){
|
||||
var blockHelperNode = statements[5],
|
||||
inverse = blockHelperNode.inverse;
|
||||
|
||||
testColumns(inverse, 5, 6, 13, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
/*global CompilerContext, Handlebars, beforeEach, shouldCompileTo */
|
||||
global.handlebarsEnv = null;
|
||||
|
||||
beforeEach(function() {
|
||||
global.handlebarsEnv = Handlebars.create();
|
||||
});
|
||||
|
||||
describe("basic context", function() {
|
||||
it("most basic", function() {
|
||||
shouldCompileTo("{{foo}}", { foo: "foo" }, "foo");
|
||||
});
|
||||
|
||||
it("escaping", function() {
|
||||
shouldCompileTo("\\{{foo}}", { foo: "food" }, "{{foo}}");
|
||||
shouldCompileTo("content \\{{foo}}", { foo: "food" }, "content {{foo}}");
|
||||
shouldCompileTo("\\\\{{foo}}", { foo: "food" }, "\\food");
|
||||
shouldCompileTo("content \\\\{{foo}}", { foo: "food" }, "content \\food");
|
||||
shouldCompileTo("\\\\ {{foo}}", { foo: "food" }, "\\\\ food");
|
||||
});
|
||||
|
||||
it("compiling with a basic context", function() {
|
||||
shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
|
||||
"It works if all the required keys are provided");
|
||||
});
|
||||
|
||||
it("compiling with an undefined context", function() {
|
||||
shouldCompileTo("Goodbye\n{{cruel}}\n{{world.bar}}!", undefined, "Goodbye\n\n!");
|
||||
|
||||
shouldCompileTo("{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}", undefined, "Goodbye");
|
||||
});
|
||||
|
||||
it("comments", function() {
|
||||
shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
|
||||
{cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
|
||||
"comments are ignored");
|
||||
});
|
||||
|
||||
it("boolean", function() {
|
||||
var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!";
|
||||
shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
|
||||
"booleans show the contents when true");
|
||||
|
||||
shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
|
||||
"booleans do not show the contents when false");
|
||||
});
|
||||
|
||||
it("zeros", function() {
|
||||
shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0},
|
||||
"num1: 42, num2: 0");
|
||||
shouldCompileTo("num: {{.}}", 0, "num: 0");
|
||||
shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0");
|
||||
});
|
||||
|
||||
it("newlines", function() {
|
||||
shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest");
|
||||
shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest");
|
||||
});
|
||||
|
||||
it("escaping text", function() {
|
||||
shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes");
|
||||
shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored");
|
||||
shouldCompileTo("Awesome\\\\ foo", {}, "Awesome\\\\ foo", "text is escaped so that it doesn't mess up backslashes");
|
||||
shouldCompileTo("Awesome {{foo}}", {foo: '\\'}, "Awesome \\", "text is escaped so that it doesn't mess up backslashes");
|
||||
shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript");
|
||||
});
|
||||
|
||||
it("escaping expressions", function() {
|
||||
shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>',
|
||||
"expressions with 3 handlebars aren't escaped");
|
||||
|
||||
shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
|
||||
"expressions with {{& handlebars aren't escaped");
|
||||
|
||||
shouldCompileTo("{{awesome}}", {awesome: "&\"'`\\<>"}, '&"'`\\<>',
|
||||
"by default expressions should be escaped");
|
||||
|
||||
shouldCompileTo("{{awesome}}", {awesome: "Escaped, <b> looks like: <b>"}, 'Escaped, <b> looks like: &lt;b&gt;',
|
||||
"escaping should properly handle amperstands");
|
||||
});
|
||||
|
||||
it("functions returning safestrings shouldn't be escaped", function() {
|
||||
var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }};
|
||||
shouldCompileTo("{{awesome}}", hash, '&\"\\<>',
|
||||
"functions returning safestrings aren't escaped");
|
||||
});
|
||||
|
||||
it("functions", function() {
|
||||
shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome",
|
||||
"functions are called and render their output");
|
||||
shouldCompileTo("{{awesome}}", {awesome: function() { return this.more; }, more: "More awesome"}, "More awesome",
|
||||
"functions are bound to the context");
|
||||
});
|
||||
|
||||
it("functions with context argument", function() {
|
||||
shouldCompileTo("{{awesome frank}}",
|
||||
{awesome: function(context) { return context; },
|
||||
frank: "Frank"},
|
||||
"Frank", "functions are called with context arguments");
|
||||
});
|
||||
it("pathed functions with context argument", function() {
|
||||
shouldCompileTo("{{bar.awesome frank}}",
|
||||
{bar: {awesome: function(context) { return context; }},
|
||||
frank: "Frank"},
|
||||
"Frank", "functions are called with context arguments");
|
||||
});
|
||||
it("depthed functions with context argument", function() {
|
||||
shouldCompileTo("{{#with frank}}{{../awesome .}}{{/with}}",
|
||||
{awesome: function(context) { return context; },
|
||||
frank: "Frank"},
|
||||
"Frank", "functions are called with context arguments");
|
||||
});
|
||||
|
||||
it("block functions with context argument", function() {
|
||||
shouldCompileTo("{{#awesome 1}}inner {{.}}{{/awesome}}",
|
||||
{awesome: function(context, options) { return options.fn(context); }},
|
||||
"inner 1", "block functions are called with context and options");
|
||||
});
|
||||
|
||||
it("depthed block functions with context argument", function() {
|
||||
shouldCompileTo("{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}",
|
||||
{value: true, awesome: function(context, options) { return options.fn(context); }},
|
||||
"inner 1", "block functions are called with context and options");
|
||||
});
|
||||
|
||||
it("block functions without context argument", function() {
|
||||
shouldCompileTo("{{#awesome}}inner{{/awesome}}",
|
||||
{awesome: function(options) { return options.fn(this); }},
|
||||
"inner", "block functions are called with options");
|
||||
});
|
||||
it("pathed block functions without context argument", function() {
|
||||
shouldCompileTo("{{#foo.awesome}}inner{{/foo.awesome}}",
|
||||
{foo: {awesome: function(options) { return this; }}},
|
||||
"inner", "block functions are called with options");
|
||||
});
|
||||
it("depthed block functions without context argument", function() {
|
||||
shouldCompileTo("{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}",
|
||||
{value: true, awesome: function(options) { return this; }},
|
||||
"inner", "block functions are called with options");
|
||||
});
|
||||
|
||||
|
||||
it("paths with hyphens", function() {
|
||||
shouldCompileTo("{{foo-bar}}", {"foo-bar": "baz"}, "baz", "Paths can contain hyphens (-)");
|
||||
shouldCompileTo("{{foo.foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)");
|
||||
shouldCompileTo("{{foo/foo-bar}}", {foo: {"foo-bar": "baz"}}, "baz", "Paths can contain hyphens (-)");
|
||||
});
|
||||
|
||||
it("nested paths", function() {
|
||||
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}},
|
||||
"Goodbye beautiful world!", "Nested paths access nested objects");
|
||||
});
|
||||
|
||||
it("nested paths with empty string value", function() {
|
||||
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}},
|
||||
"Goodbye world!", "Nested paths access nested objects with empty string");
|
||||
});
|
||||
|
||||
it("literal paths", function() {
|
||||
shouldCompileTo("Goodbye {{[@alan]/expression}} world!", {"@alan": {expression: "beautiful"}},
|
||||
"Goodbye beautiful world!", "Literal paths can be used");
|
||||
shouldCompileTo("Goodbye {{[foo bar]/expression}} world!", {"foo bar": {expression: "beautiful"}},
|
||||
"Goodbye beautiful world!", "Literal paths can be used");
|
||||
});
|
||||
|
||||
it('literal references', function() {
|
||||
shouldCompileTo("Goodbye {{[foo bar]}} world!", {"foo bar": "beautiful"},
|
||||
"Goodbye beautiful world!", "Literal paths can be used");
|
||||
});
|
||||
|
||||
it("that current context path ({{.}}) doesn't hit helpers", function() {
|
||||
shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: ");
|
||||
});
|
||||
|
||||
it("complex but empty paths", function() {
|
||||
shouldCompileTo("{{person/name}}", {person: {name: null}}, "");
|
||||
shouldCompileTo("{{person/name}}", {person: {}}, "");
|
||||
});
|
||||
|
||||
it("this keyword in paths", function() {
|
||||
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}";
|
||||
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
|
||||
shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE",
|
||||
"This keyword in paths evaluates to current context");
|
||||
|
||||
string = "{{#hellos}}{{this/text}}{{/hellos}}";
|
||||
hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
|
||||
shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths");
|
||||
});
|
||||
|
||||
it("this keyword nested inside path", function() {
|
||||
var string = "{{#hellos}}{{text/this/foo}}{{/hellos}}";
|
||||
shouldThrow(function() {
|
||||
CompilerContext.compile(string);
|
||||
}, Error);
|
||||
});
|
||||
|
||||
it("this keyword in helpers", function() {
|
||||
var helpers = {foo: function(value) {
|
||||
return 'bar ' + value;
|
||||
}};
|
||||
var string = "{{#goodbyes}}{{foo this}}{{/goodbyes}}";
|
||||
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
|
||||
shouldCompileTo(string, [hash, helpers], "bar goodbyebar Goodbyebar GOODBYE",
|
||||
"This keyword in paths evaluates to current context");
|
||||
|
||||
string = "{{#hellos}}{{foo this/text}}{{/hellos}}";
|
||||
hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
|
||||
shouldCompileTo(string, [hash, helpers], "bar hellobar Hellobar HELLO", "This keyword evaluates in more complex paths");
|
||||
});
|
||||
|
||||
it("this keyword nested inside helpers param", function() {
|
||||
var string = "{{#hellos}}{{foo text/this/foo}}{{/hellos}}";
|
||||
shouldThrow(function() {
|
||||
CompilerContext.compile(string);
|
||||
}, Error);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/*global CompilerContext, shouldCompileTo */
|
||||
describe('blocks', function() {
|
||||
it("array", function() {
|
||||
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
"Arrays iterate over the contents when not empty");
|
||||
|
||||
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
|
||||
"Arrays ignore the contents when empty");
|
||||
|
||||
});
|
||||
|
||||
it("array with @index", function() {
|
||||
var string = "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!";
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
|
||||
});
|
||||
|
||||
it("empty block", function() {
|
||||
var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!";
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
shouldCompileTo(string, hash, "cruel world!",
|
||||
"Arrays iterate over the contents when not empty");
|
||||
|
||||
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
|
||||
"Arrays ignore the contents when empty");
|
||||
});
|
||||
|
||||
it("block with complex lookup", function() {
|
||||
var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}";
|
||||
var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]};
|
||||
|
||||
shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
|
||||
"Templates can access variables in contexts up the stack with relative path syntax");
|
||||
});
|
||||
|
||||
it("block with complex lookup using nested context", function() {
|
||||
var string = "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}";
|
||||
|
||||
shouldThrow(function() {
|
||||
CompilerContext.compile(string);
|
||||
}, Error);
|
||||
});
|
||||
|
||||
it("block with deep nested complex lookup", function() {
|
||||
var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}";
|
||||
var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
|
||||
|
||||
shouldCompileTo(string, hash, "Goodbye cruel OMG!");
|
||||
});
|
||||
|
||||
describe('inverted sections', function() {
|
||||
it("inverted sections with unset value", function() {
|
||||
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
|
||||
var hash = {};
|
||||
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set.");
|
||||
});
|
||||
|
||||
it("inverted section with false value", function() {
|
||||
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
|
||||
var hash = {goodbyes: false};
|
||||
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false.");
|
||||
});
|
||||
|
||||
it("inverted section with empty set", function() {
|
||||
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
|
||||
var hash = {goodbyes: []};
|
||||
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set.");
|
||||
});
|
||||
|
||||
it("block inverted sections", function() {
|
||||
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"},
|
||||
"No people");
|
||||
});
|
||||
|
||||
it("block inverted sections with empty arrays", function() {
|
||||
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []},
|
||||
"No people");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
/*global CompilerContext, shouldCompileTo, compileWithPartials, handlebarsEnv */
|
||||
describe('builtin helpers', function() {
|
||||
describe('#if', function() {
|
||||
it("if", function() {
|
||||
var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
|
||||
shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
|
||||
"if with boolean argument shows the contents when true");
|
||||
shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!",
|
||||
"if with string argument shows the contents");
|
||||
shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
|
||||
"if with boolean argument does not show the contents when false");
|
||||
shouldCompileTo(string, {world: "world"}, "cruel world!",
|
||||
"if with undefined does not show the contents");
|
||||
shouldCompileTo(string, {goodbye: ['foo'], world: "world"}, "GOODBYE cruel world!",
|
||||
"if with non-empty array shows the contents");
|
||||
shouldCompileTo(string, {goodbye: [], world: "world"}, "cruel world!",
|
||||
"if with empty array does not show the contents");
|
||||
shouldCompileTo(string, {goodbye: 0, world: "world"}, "cruel world!",
|
||||
"if with zero does not show the contents");
|
||||
shouldCompileTo("{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!",
|
||||
{goodbye: 0, world: "world"}, "GOODBYE cruel world!",
|
||||
"if with zero does not show the contents");
|
||||
});
|
||||
|
||||
it("if with function argument", function() {
|
||||
var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
|
||||
shouldCompileTo(string, {goodbye: function() {return true;}, world: "world"}, "GOODBYE cruel world!",
|
||||
"if with function shows the contents when function returns true");
|
||||
shouldCompileTo(string, {goodbye: function() {return this.world;}, world: "world"}, "GOODBYE cruel world!",
|
||||
"if with function shows the contents when function returns string");
|
||||
shouldCompileTo(string, {goodbye: function() {return false;}, world: "world"}, "cruel world!",
|
||||
"if with function does not show the contents when returns false");
|
||||
shouldCompileTo(string, {goodbye: function() {return this.foo;}, world: "world"}, "cruel world!",
|
||||
"if with function does not show the contents when returns undefined");
|
||||
});
|
||||
});
|
||||
|
||||
describe('#with', function() {
|
||||
it("with", function() {
|
||||
var string = "{{#with person}}{{first}} {{last}}{{/with}}";
|
||||
shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson");
|
||||
});
|
||||
it("with with function argument", function() {
|
||||
var string = "{{#with person}}{{first}} {{last}}{{/with}}";
|
||||
shouldCompileTo(string, {person: function() { return {first: "Alan", last: "Johnson"};}}, "Alan Johnson");
|
||||
});
|
||||
});
|
||||
|
||||
describe('#each', function() {
|
||||
beforeEach(function() {
|
||||
handlebarsEnv.registerHelper('detectDataInsideEach', function(options) {
|
||||
return options.data && options.data.exclaim;
|
||||
});
|
||||
});
|
||||
|
||||
it("each", function() {
|
||||
var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
"each with array argument iterates over the contents when not empty");
|
||||
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
|
||||
"each with array argument ignores the contents when empty");
|
||||
});
|
||||
|
||||
it("each with an object and @key", function() {
|
||||
var string = "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: {"<b>#1</b>": {text: "goodbye"}, 2: {text: "GOODBYE"}}, world: "world"};
|
||||
|
||||
// Object property iteration order is undefined according to ECMA spec,
|
||||
// so we need to check both possible orders
|
||||
// @see http://stackoverflow.com/questions/280713/elements-order-in-a-for-in-loop
|
||||
var actual = compileWithPartials(string, hash);
|
||||
var expected1 = "<b>#1</b>. goodbye! 2. GOODBYE! cruel world!";
|
||||
var expected2 = "2. GOODBYE! <b>#1</b>. goodbye! cruel world!";
|
||||
|
||||
equals(actual === expected1 || actual === expected2, true, "each with object argument iterates over the contents when not empty");
|
||||
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
|
||||
"each with object argument ignores the contents when empty");
|
||||
});
|
||||
|
||||
it("each with @index", function() {
|
||||
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
|
||||
});
|
||||
|
||||
it("each with nested @index", function() {
|
||||
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!";
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, "0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!", "The @index variable is used");
|
||||
});
|
||||
|
||||
it("each object with @index", function() {
|
||||
var string = "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: {'a': {text: "goodbye"}, b: {text: "Goodbye"}, c: {text: "GOODBYE"}}, world: "world"};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", "The @index variable is used");
|
||||
});
|
||||
|
||||
|
||||
it("each with @first", function() {
|
||||
var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, "goodbye! cruel world!", "The @first variable is used");
|
||||
});
|
||||
|
||||
it("each with nested @first", function() {
|
||||
var string = "{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, "(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!", "The @first variable is used");
|
||||
});
|
||||
|
||||
it("each object with @first", function() {
|
||||
var string = "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: {'foo': {text: "goodbye"}, bar: {text: "Goodbye"}}, world: "world"};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, "goodbye! cruel world!", "The @first variable is used");
|
||||
});
|
||||
|
||||
it("each with @last", function() {
|
||||
var string = "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, "GOODBYE! cruel world!", "The @last variable is used");
|
||||
});
|
||||
|
||||
it("each with nested @last", function() {
|
||||
var string = "{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, "(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!", "The @last variable is used");
|
||||
});
|
||||
|
||||
it("each with function argument", function() {
|
||||
var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!";
|
||||
var hash = {goodbyes: function () { return [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];}, world: "world"};
|
||||
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
"each with array function argument iterates over the contents when not empty");
|
||||
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
|
||||
"each with array function argument ignores the contents when empty");
|
||||
});
|
||||
|
||||
it("data passed to helpers", function() {
|
||||
var string = "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}";
|
||||
var hash = {letters: ['a', 'b', 'c']};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash, {
|
||||
data: {
|
||||
exclaim: '!'
|
||||
}
|
||||
});
|
||||
equal(result, 'a!b!c!', 'should output data');
|
||||
});
|
||||
|
||||
it("each on implicit context", function() {
|
||||
var string = "{{#each}}{{text}}! {{/each}}cruel world!";
|
||||
var hash = [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}];
|
||||
shouldCompileTo(string, [hash], "goodbye! Goodbye! GOODBYE! cruel world!");
|
||||
});
|
||||
});
|
||||
|
||||
it("#log", function() {
|
||||
var string = "{{log blah}}";
|
||||
var hash = { blah: "whee" };
|
||||
|
||||
var levelArg, logArg;
|
||||
handlebarsEnv.log = function(level, arg){
|
||||
levelArg = level;
|
||||
logArg = arg;
|
||||
};
|
||||
|
||||
shouldCompileTo(string, hash, "", "log should not display");
|
||||
equals(1, levelArg, "should call log with 1");
|
||||
equals("whee", logArg, "should call log with 'whee'");
|
||||
});
|
||||
|
||||
|
||||
describe('#lookup', function() {
|
||||
it('should lookup arbitrary content', function() {
|
||||
var string = '{{#each goodbyes}}{{lookup ../data .}}{{/each}}',
|
||||
hash = {goodbyes: [0, 1], data: ['foo', 'bar']};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, 'foobar');
|
||||
});
|
||||
it('should not fail on undefined value', function() {
|
||||
var string = '{{#each goodbyes}}{{lookup ../bar .}}{{/each}}',
|
||||
hash = {goodbyes: [0, 1], data: ['foo', 'bar']};
|
||||
|
||||
var template = CompilerContext.compile(string);
|
||||
var result = template(hash);
|
||||
|
||||
equal(result, '');
|
||||
});
|
||||
});
|
||||
});
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
/*global CompilerContext, Handlebars, handlebarsEnv, shouldThrow */
|
||||
describe('data', function() {
|
||||
it("passing in data to a compiled function that expects data - works with helpers", function() {
|
||||
var template = CompilerContext.compile("{{hello}}", {data: true});
|
||||
|
||||
var helpers = {
|
||||
hello: function(options) {
|
||||
return options.data.adjective + " " + this.noun;
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({noun: "cat"}, {helpers: helpers, data: {adjective: "happy"}});
|
||||
equals("happy cat", result, "Data output by helper");
|
||||
});
|
||||
|
||||
it("data can be looked up via @foo", function() {
|
||||
var template = CompilerContext.compile("{{@hello}}");
|
||||
var result = template({}, { data: { hello: "hello" } });
|
||||
equals("hello", result, "@foo retrieves template data");
|
||||
});
|
||||
|
||||
it("deep @foo triggers automatic top-level data", function() {
|
||||
var template = CompilerContext.compile('{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}');
|
||||
|
||||
var helpers = Handlebars.createFrame(handlebarsEnv.helpers);
|
||||
|
||||
helpers.let = function(options) {
|
||||
var frame = Handlebars.createFrame(options.data);
|
||||
|
||||
for (var prop in options.hash) {
|
||||
frame[prop] = options.hash[prop];
|
||||
}
|
||||
return options.fn(this, { data: frame });
|
||||
};
|
||||
|
||||
var result = template({ foo: true }, { helpers: helpers });
|
||||
equals("Hello world", result, "Automatic data was triggered");
|
||||
});
|
||||
|
||||
it("parameter data can be looked up via @foo", function() {
|
||||
var template = CompilerContext.compile("{{hello @world}}");
|
||||
var helpers = {
|
||||
hello: function(noun) {
|
||||
return "Hello " + noun;
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, { helpers: helpers, data: { world: "world" } });
|
||||
equals("Hello world", result, "@foo as a parameter retrieves template data");
|
||||
});
|
||||
|
||||
it("hash values can be looked up via @foo", function() {
|
||||
var template = CompilerContext.compile("{{hello noun=@world}}");
|
||||
var helpers = {
|
||||
hello: function(options) {
|
||||
return "Hello " + options.hash.noun;
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, { helpers: helpers, data: { world: "world" } });
|
||||
equals("Hello world", result, "@foo as a parameter retrieves template data");
|
||||
});
|
||||
|
||||
it("nested parameter data can be looked up via @foo.bar", function() {
|
||||
var template = CompilerContext.compile("{{hello @world.bar}}");
|
||||
var helpers = {
|
||||
hello: function(noun) {
|
||||
return "Hello " + noun;
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, { helpers: helpers, data: { world: {bar: "world" } } });
|
||||
equals("Hello world", result, "@foo as a parameter retrieves template data");
|
||||
});
|
||||
|
||||
it("nested parameter data does not fail with @world.bar", function() {
|
||||
var template = CompilerContext.compile("{{hello @world.bar}}");
|
||||
var helpers = {
|
||||
hello: function(noun) {
|
||||
return "Hello " + noun;
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, { helpers: helpers, data: { foo: {bar: "world" } } });
|
||||
equals("Hello undefined", result, "@foo as a parameter retrieves template data");
|
||||
});
|
||||
|
||||
it("parameter data throws when using complex scope references", function() {
|
||||
var string = "{{#goodbyes}}{{text}} cruel {{@foo/../name}}! {{/goodbyes}}";
|
||||
|
||||
shouldThrow(function() {
|
||||
CompilerContext.compile(string);
|
||||
}, Error);
|
||||
});
|
||||
|
||||
it("data is inherited downstream", function() {
|
||||
var template = CompilerContext.compile("{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}", { data: true });
|
||||
var helpers = {
|
||||
let: function(options) {
|
||||
var frame = Handlebars.createFrame(options.data);
|
||||
for (var prop in options.hash) {
|
||||
frame[prop] = options.hash[prop];
|
||||
}
|
||||
return options.fn(this, {data: frame});
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({ bar: { baz: "hello world" } }, { helpers: helpers, data: {} });
|
||||
equals("2hello world1", result, "data variables are inherited downstream");
|
||||
});
|
||||
|
||||
it("passing in data to a compiled function that expects data - works with helpers in partials", function() {
|
||||
var template = CompilerContext.compile("{{>my_partial}}", {data: true});
|
||||
|
||||
var partials = {
|
||||
my_partial: CompilerContext.compile("{{hello}}", {data: true})
|
||||
};
|
||||
|
||||
var helpers = {
|
||||
hello: function(options) {
|
||||
return options.data.adjective + " " + this.noun;
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({noun: "cat"}, {helpers: helpers, partials: partials, data: {adjective: "happy"}});
|
||||
equals("happy cat", result, "Data output by helper inside partial");
|
||||
});
|
||||
|
||||
it("passing in data to a compiled function that expects data - works with helpers and parameters", function() {
|
||||
var template = CompilerContext.compile("{{hello world}}", {data: true});
|
||||
|
||||
var helpers = {
|
||||
hello: function(noun, options) {
|
||||
return options.data.adjective + " " + noun + (this.exclaim ? "!" : "");
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({exclaim: true, world: "world"}, {helpers: helpers, data: {adjective: "happy"}});
|
||||
equals("happy world!", result, "Data output by helper");
|
||||
});
|
||||
|
||||
it("passing in data to a compiled function that expects data - works with block helpers", function() {
|
||||
var template = CompilerContext.compile("{{#hello}}{{world}}{{/hello}}", {data: true});
|
||||
|
||||
var helpers = {
|
||||
hello: function(options) {
|
||||
return options.fn(this);
|
||||
},
|
||||
world: function(options) {
|
||||
return options.data.adjective + " world" + (this.exclaim ? "!" : "");
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({exclaim: true}, {helpers: helpers, data: {adjective: "happy"}});
|
||||
equals("happy world!", result, "Data output by helper");
|
||||
});
|
||||
|
||||
it("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
|
||||
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
|
||||
|
||||
var helpers = {
|
||||
hello: function(options) {
|
||||
return options.fn({exclaim: "?"});
|
||||
},
|
||||
world: function(thing, options) {
|
||||
return options.data.adjective + " " + thing + (this.exclaim || "");
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
|
||||
equals("happy world?", result, "Data output by helper");
|
||||
});
|
||||
|
||||
it("passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", function() {
|
||||
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
|
||||
|
||||
var helpers = {
|
||||
hello: function(options) {
|
||||
return options.data.accessData + " " + options.fn({exclaim: "?"});
|
||||
},
|
||||
world: function(thing, options) {
|
||||
return options.data.adjective + " " + thing + (this.exclaim || "");
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy", accessData: "#win"}});
|
||||
equals("#win happy world?", result, "Data output by helper");
|
||||
});
|
||||
|
||||
it("you can override inherited data when invoking a helper", function() {
|
||||
var template = CompilerContext.compile("{{#hello}}{{world zomg}}{{/hello}}", {data: true});
|
||||
|
||||
var helpers = {
|
||||
hello: function(options) {
|
||||
return options.fn({exclaim: "?", zomg: "world"}, { data: {adjective: "sad"} });
|
||||
},
|
||||
world: function(thing, options) {
|
||||
return options.data.adjective + " " + thing + (this.exclaim || "");
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({exclaim: true, zomg: "planet"}, {helpers: helpers, data: {adjective: "happy"}});
|
||||
equals("sad world?", result, "Overriden data output by helper");
|
||||
});
|
||||
|
||||
|
||||
it("you can override inherited data when invoking a helper with depth", function() {
|
||||
var template = CompilerContext.compile("{{#hello}}{{world ../zomg}}{{/hello}}", {data: true});
|
||||
|
||||
var helpers = {
|
||||
hello: function(options) {
|
||||
return options.fn({exclaim: "?"}, { data: {adjective: "sad"} });
|
||||
},
|
||||
world: function(thing, options) {
|
||||
return options.data.adjective + " " + thing + (this.exclaim || "");
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({exclaim: true, zomg: "world"}, {helpers: helpers, data: {adjective: "happy"}});
|
||||
equals("sad world?", result, "Overriden data output by helper");
|
||||
});
|
||||
|
||||
describe('@root', function() {
|
||||
it('the root context can be looked up via @root', function() {
|
||||
var template = CompilerContext.compile('{{@root.foo}}');
|
||||
var result = template({foo: 'hello'}, { data: {} });
|
||||
equals('hello', result);
|
||||
|
||||
result = template({foo: 'hello'}, {});
|
||||
equals('hello', result);
|
||||
});
|
||||
it('passed root values take priority', function() {
|
||||
var template = CompilerContext.compile('{{@root.foo}}');
|
||||
var result = template({}, { data: {root: {foo: 'hello'} } });
|
||||
equals('hello', result);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nesting', function() {
|
||||
it('the root context can be looked up via @root', function() {
|
||||
var template = CompilerContext.compile('{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}');
|
||||
var result = template({foo: 'hello'}, {
|
||||
helpers: {
|
||||
helper: function(options) {
|
||||
var frame = Handlebars.createFrame(options.data);
|
||||
frame.depth = options.data.depth + 1;
|
||||
return options.fn(this, {data: frame});
|
||||
}
|
||||
},
|
||||
data: {
|
||||
depth: 0
|
||||
}
|
||||
});
|
||||
equals('2 1 0', result);
|
||||
});
|
||||
});
|
||||
});
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
/*global handlebarsEnv */
|
||||
require('./common');
|
||||
|
||||
var _ = require('underscore'),
|
||||
fs = require('fs'),
|
||||
vm = require('vm');
|
||||
|
||||
global.Handlebars = undefined;
|
||||
vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.js'), 'dist/handlebars.js');
|
||||
|
||||
global.CompilerContext = {
|
||||
compile: function(template, options) {
|
||||
var templateSpec = handlebarsEnv.precompile(template, options);
|
||||
return handlebarsEnv.template(safeEval(templateSpec));
|
||||
},
|
||||
compileWithPartial: function(template, options) {
|
||||
return handlebarsEnv.compile(template, options);
|
||||
}
|
||||
};
|
||||
|
||||
function safeEval(templateSpec) {
|
||||
try {
|
||||
return eval('(' + templateSpec + ')');
|
||||
} catch (err) {
|
||||
console.error(templateSpec);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
global.shouldCompileTo = function(string, hashOrArray, expected, message) {
|
||||
shouldCompileToWithPartials(string, hashOrArray, false, expected, message);
|
||||
};
|
||||
|
||||
global.shouldCompileToWithPartials = function(string, hashOrArray, partials, expected, message) {
|
||||
var result = compileWithPartials(string, hashOrArray, partials);
|
||||
if (result !== expected) {
|
||||
throw new Error("'" + expected + "' should === '" + result + "': " + message);
|
||||
}
|
||||
};
|
||||
|
||||
global.compileWithPartials = function(string, hashOrArray, partials) {
|
||||
var template = CompilerContext[partials ? 'compileWithPartial' : 'compile'](string), ary;
|
||||
if(Object.prototype.toString.call(hashOrArray) === "[object Array]") {
|
||||
ary = [];
|
||||
ary.push(hashOrArray[0]);
|
||||
ary.push({ helpers: hashOrArray[1], partials: hashOrArray[2] });
|
||||
} else {
|
||||
ary = [hashOrArray];
|
||||
}
|
||||
|
||||
return template.apply(this, ary);
|
||||
};
|
||||
|
||||
|
||||
global.equals = global.equal = function(a, b, msg) {
|
||||
if (a !== b) {
|
||||
throw new Error("'" + b + "' should === '" + a + "'" + (msg ? ": " + msg : ''));
|
||||
}
|
||||
};
|
||||
|
||||
global.shouldThrow = function(callback, type, msg) {
|
||||
var failed;
|
||||
try {
|
||||
callback();
|
||||
failed = true;
|
||||
} catch (err) {
|
||||
if (type && !(err instanceof type)) {
|
||||
throw new Error('Type failure');
|
||||
}
|
||||
if (msg && !(msg.test ? msg.test(err.message) : msg === err.message)) {
|
||||
throw new Error('Message failure');
|
||||
}
|
||||
}
|
||||
if (failed) {
|
||||
throw new Error('It failed to throw');
|
||||
}
|
||||
};
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
/*global handlebarsEnv */
|
||||
require('./common');
|
||||
|
||||
global.Handlebars = require('../../lib');
|
||||
|
||||
global.CompilerContext = {
|
||||
compile: function(template, options) {
|
||||
var templateSpec = handlebarsEnv.precompile(template, options);
|
||||
return handlebarsEnv.template(safeEval(templateSpec));
|
||||
},
|
||||
compileWithPartial: function(template, options) {
|
||||
return handlebarsEnv.compile(template, options);
|
||||
}
|
||||
};
|
||||
|
||||
function safeEval(templateSpec) {
|
||||
try {
|
||||
return eval('(' + templateSpec + ')');
|
||||
} catch (err) {
|
||||
console.error(templateSpec);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Vendored
+2054
File diff suppressed because it is too large
Load Diff
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
var fs = require('fs'),
|
||||
Mocha = require('mocha'),
|
||||
path = require('path');
|
||||
|
||||
var errors = 0,
|
||||
testDir = path.dirname(__dirname),
|
||||
grep = process.argv[2];
|
||||
|
||||
var files = [ testDir + "/basic.js" ];
|
||||
|
||||
var files = fs.readdirSync(testDir)
|
||||
.filter(function(name) { return (/.*\.js$/).test(name); })
|
||||
.map(function(name) { return testDir + '/' + name; });
|
||||
|
||||
run('./node', function() {
|
||||
run('./browser', function() {
|
||||
run('./runtime', function() {
|
||||
process.exit(errors);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function run(env, callback) {
|
||||
var mocha = new Mocha();
|
||||
mocha.ui('bdd');
|
||||
mocha.files = files.slice();
|
||||
if (grep) {
|
||||
mocha.grep(grep);
|
||||
}
|
||||
|
||||
files.forEach(function(name) {
|
||||
delete require.cache[name];
|
||||
});
|
||||
|
||||
console.log('Running env: ' + env);
|
||||
require(env);
|
||||
mocha.run(function(errorCount) {
|
||||
errors += errorCount;
|
||||
callback();
|
||||
});
|
||||
}
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
/*global handlebarsEnv */
|
||||
require('./common');
|
||||
|
||||
var _ = require('underscore'),
|
||||
fs = require('fs'),
|
||||
vm = require('vm');
|
||||
|
||||
global.Handlebars = undefined;
|
||||
vm.runInThisContext(fs.readFileSync(__dirname + '/../../dist/handlebars.runtime.js'), 'dist/handlebars.runtime.js');
|
||||
|
||||
var parse = require('../../dist/cjs/handlebars/compiler/base').parse;
|
||||
var compiler = require('../../dist/cjs/handlebars/compiler/compiler');
|
||||
var JavaScriptCompiler = require('../../dist/cjs/handlebars/compiler/javascript-compiler')['default'];
|
||||
|
||||
global.CompilerContext = {
|
||||
compile: function(template, options) {
|
||||
// Hack the compiler on to the environment for these specific tests
|
||||
handlebarsEnv.precompile = function(template, options) {
|
||||
return compiler.precompile(template, options, handlebarsEnv);
|
||||
};
|
||||
handlebarsEnv.parse = parse;
|
||||
handlebarsEnv.Compiler = compiler.Compiler;
|
||||
handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler;
|
||||
|
||||
var templateSpec = handlebarsEnv.precompile(template, options);
|
||||
return handlebarsEnv.template(safeEval(templateSpec));
|
||||
},
|
||||
compileWithPartial: function(template, options) {
|
||||
// Hack the compiler on to the environment for these specific tests
|
||||
handlebarsEnv.compile = function(template, options) {
|
||||
return compiler.compile(template, options, handlebarsEnv);
|
||||
};
|
||||
handlebarsEnv.parse = parse;
|
||||
handlebarsEnv.Compiler = compiler.Compiler;
|
||||
handlebarsEnv.JavaScriptCompiler = JavaScriptCompiler;
|
||||
|
||||
return handlebarsEnv.compile(template, options);
|
||||
}
|
||||
};
|
||||
|
||||
function safeEval(templateSpec) {
|
||||
try {
|
||||
return eval('(' + templateSpec + ')');
|
||||
} catch (err) {
|
||||
console.error(templateSpec);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
define(['handlebars.runtime'], function(Handlebars) {
|
||||
Handlebars = Handlebars["default"]; var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
|
||||
return templates['empty'] = template({"compiler":[5,">= 2.0.0"],"main":function(depth0,helpers,partials,data) {
|
||||
return "";
|
||||
},"useData":true});
|
||||
});
|
||||
+596
@@ -0,0 +1,596 @@
|
||||
/*global CompilerContext, Handlebars, shouldCompileTo, shouldCompileToWithPartials, shouldThrow, handlebarsEnv */
|
||||
describe('helpers', function() {
|
||||
it("helper with complex lookup$", function() {
|
||||
var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}";
|
||||
var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
|
||||
var helpers = {link: function(prefix) {
|
||||
return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>";
|
||||
}};
|
||||
shouldCompileTo(string, [hash, helpers], "<a href='/root/goodbye'>Goodbye</a>");
|
||||
});
|
||||
|
||||
it("helper for raw block gets raw content", function() {
|
||||
var string = "{{{{raw}}}} {{test}} {{{{/raw}}}}";
|
||||
var hash = { test: "hello" };
|
||||
var helpers = { raw: function(options) {
|
||||
return options.fn();
|
||||
} };
|
||||
shouldCompileTo(string, [hash, helpers], " {{test}} ",
|
||||
"raw block helper gets raw content");
|
||||
});
|
||||
|
||||
it("helper for raw block gets parameters", function() {
|
||||
var string = "{{{{raw 1 2 3}}}} {{test}} {{{{/raw}}}}";
|
||||
var hash = { test: "hello" };
|
||||
var helpers = { raw: function(a, b, c, options) {
|
||||
return options.fn() + a + b + c;
|
||||
} };
|
||||
shouldCompileTo(string, [hash, helpers], " {{test}} 123",
|
||||
"raw block helper gets raw content");
|
||||
});
|
||||
|
||||
it("helper block with complex lookup expression", function() {
|
||||
var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}";
|
||||
var hash = {name: "Alan"};
|
||||
var helpers = {goodbyes: function(options) {
|
||||
var out = "";
|
||||
var byes = ["Goodbye", "goodbye", "GOODBYE"];
|
||||
for (var i = 0,j = byes.length; i < j; i++) {
|
||||
out += byes[i] + " " + options.fn(this) + "! ";
|
||||
}
|
||||
return out;
|
||||
}};
|
||||
shouldCompileTo(string, [hash, helpers], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
|
||||
});
|
||||
|
||||
it("helper with complex lookup and nested template", function() {
|
||||
var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
|
||||
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
|
||||
var helpers = {link: function (prefix, options) {
|
||||
return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>";
|
||||
}};
|
||||
shouldCompileToWithPartials(string, [hash, helpers], false, "<a href='/root/goodbye'>Goodbye</a>");
|
||||
});
|
||||
|
||||
it("helper with complex lookup and nested template in VM+Compiler", function() {
|
||||
var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
|
||||
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
|
||||
var helpers = {link: function (prefix, options) {
|
||||
return "<a href='" + prefix + "/" + this.url + "'>" + options.fn(this) + "</a>";
|
||||
}};
|
||||
shouldCompileToWithPartials(string, [hash, helpers], true, "<a href='/root/goodbye'>Goodbye</a>");
|
||||
});
|
||||
|
||||
it("block helper", function() {
|
||||
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
|
||||
var template = CompilerContext.compile(string);
|
||||
|
||||
var result = template({world: "world"}, { helpers: {goodbyes: function(options) { return options.fn({text: "GOODBYE"}); }}});
|
||||
equal(result, "GOODBYE! cruel world!", "Block helper executed");
|
||||
});
|
||||
|
||||
it("block helper staying in the same context", function() {
|
||||
var string = "{{#form}}<p>{{name}}</p>{{/form}}";
|
||||
var template = CompilerContext.compile(string);
|
||||
|
||||
var result = template({name: "Yehuda"}, {helpers: {form: function(options) { return "<form>" + options.fn(this) + "</form>"; } }});
|
||||
equal(result, "<form><p>Yehuda</p></form>", "Block helper executed with current context");
|
||||
});
|
||||
|
||||
it("block helper should have context in this", function() {
|
||||
var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>";
|
||||
var link = function(options) {
|
||||
return '<a href="/people/' + this.id + '">' + options.fn(this) + '</a>';
|
||||
};
|
||||
var data = { "people": [
|
||||
{ "name": "Alan", "id": 1 },
|
||||
{ "name": "Yehuda", "id": 2 }
|
||||
]};
|
||||
|
||||
shouldCompileTo(source, [data, {link: link}], "<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>");
|
||||
});
|
||||
|
||||
it("block helper for undefined value", function() {
|
||||
shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, "");
|
||||
});
|
||||
|
||||
it("block helper passing a new context", function() {
|
||||
var string = "{{#form yehuda}}<p>{{name}}</p>{{/form}}";
|
||||
var template = CompilerContext.compile(string);
|
||||
|
||||
var result = template({yehuda: {name: "Yehuda"}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}});
|
||||
equal(result, "<form><p>Yehuda</p></form>", "Context variable resolved");
|
||||
});
|
||||
|
||||
it("block helper passing a complex path context", function() {
|
||||
var string = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}";
|
||||
var template = CompilerContext.compile(string);
|
||||
|
||||
var result = template({yehuda: {name: "Yehuda", cat: {name: "Harold"}}}, { helpers: {form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }}});
|
||||
equal(result, "<form><p>Harold</p></form>", "Complex path variable resolved");
|
||||
});
|
||||
|
||||
it("nested block helpers", function() {
|
||||
var string = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}";
|
||||
var template = CompilerContext.compile(string);
|
||||
|
||||
var result = template({
|
||||
yehuda: {name: "Yehuda" }
|
||||
}, {
|
||||
helpers: {
|
||||
link: function(options) { return "<a href='" + this.name + "'>" + options.fn(this) + "</a>"; },
|
||||
form: function(context, options) { return "<form>" + options.fn(context) + "</form>"; }
|
||||
}
|
||||
});
|
||||
equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>", "Both blocks executed");
|
||||
});
|
||||
|
||||
it("block helper inverted sections", function() {
|
||||
var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}";
|
||||
var list = function(context, options) {
|
||||
if (context.length > 0) {
|
||||
var out = "<ul>";
|
||||
for(var i = 0,j=context.length; i < j; i++) {
|
||||
out += "<li>";
|
||||
out += options.fn(context[i]);
|
||||
out += "</li>";
|
||||
}
|
||||
out += "</ul>";
|
||||
return out;
|
||||
} else {
|
||||
return "<p>" + options.inverse(this) + "</p>";
|
||||
}
|
||||
};
|
||||
|
||||
var hash = {people: [{name: "Alan"}, {name: "Yehuda"}]};
|
||||
var empty = {people: []};
|
||||
var rootMessage = {
|
||||
people: [],
|
||||
message: "Nobody's here"
|
||||
};
|
||||
|
||||
var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}";
|
||||
|
||||
// the meaning here may be kind of hard to catch, but list.not is always called,
|
||||
// so we should see the output of both
|
||||
shouldCompileTo(string, [hash, { list: list }], "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context");
|
||||
shouldCompileTo(string, [empty, { list: list }], "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called");
|
||||
shouldCompileTo(messageString, [rootMessage, { list: list }], "<p>Nobody's here</p>", "the context of an inverse is the parent of the block");
|
||||
});
|
||||
|
||||
describe("helpers hash", function() {
|
||||
it("providing a helpers hash", function() {
|
||||
shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: function() { return "world"; }}], "Goodbye cruel world!",
|
||||
"helpers hash is available");
|
||||
|
||||
shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: function() { return "world"; }}],
|
||||
"Goodbye cruel world!", "helpers hash is available inside other blocks");
|
||||
});
|
||||
|
||||
it("in cases of conflict, helpers win", function() {
|
||||
shouldCompileTo("{{{lookup}}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
|
||||
"helpers hash has precedence escaped expansion");
|
||||
shouldCompileTo("{{lookup}}", [{lookup: 'Explicit'}, {lookup: function() { return 'helpers'; }}], "helpers",
|
||||
"helpers hash has precedence simple expansion");
|
||||
});
|
||||
|
||||
it("the helpers hash is available is nested contexts", function() {
|
||||
shouldCompileTo("{{#outer}}{{#inner}}{{helper}}{{/inner}}{{/outer}}",
|
||||
[{'outer': {'inner': {'unused':[]}}}, {'helper': function() { return 'helper'; }}], "helper",
|
||||
"helpers hash is available in nested contexts.");
|
||||
});
|
||||
|
||||
it("the helper hash should augment the global hash", function() {
|
||||
handlebarsEnv.registerHelper('test_helper', function() { return 'found it!'; });
|
||||
|
||||
shouldCompileTo(
|
||||
"{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}", [
|
||||
{cruel: "cruel"},
|
||||
{world: function() { return "world!"; }}
|
||||
],
|
||||
"found it! Goodbye cruel world!!");
|
||||
});
|
||||
});
|
||||
|
||||
it("Multiple global helper registration", function() {
|
||||
var helpers = handlebarsEnv.helpers;
|
||||
handlebarsEnv.helpers = {};
|
||||
|
||||
handlebarsEnv.registerHelper({
|
||||
'if': helpers['if'],
|
||||
world: function() { return "world!"; },
|
||||
test_helper: function() { return 'found it!'; }
|
||||
});
|
||||
|
||||
shouldCompileTo(
|
||||
"{{test_helper}} {{#if cruel}}Goodbye {{cruel}} {{world}}!{{/if}}",
|
||||
[{cruel: "cruel"}],
|
||||
"found it! Goodbye cruel world!!");
|
||||
});
|
||||
|
||||
it("decimal number literals work", function() {
|
||||
var string = 'Message: {{hello -1.2 1.2}}';
|
||||
var hash = {};
|
||||
var helpers = {hello: function(times, times2) {
|
||||
if(typeof times !== 'number') { times = "NaN"; }
|
||||
if(typeof times2 !== 'number') { times2 = "NaN"; }
|
||||
return "Hello " + times + " " + times2 + " times";
|
||||
}};
|
||||
shouldCompileTo(string, [hash, helpers], "Message: Hello -1.2 1.2 times", "template with a negative integer literal");
|
||||
});
|
||||
|
||||
it("negative number literals work", function() {
|
||||
var string = 'Message: {{hello -12}}';
|
||||
var hash = {};
|
||||
var helpers = {hello: function(times) {
|
||||
if(typeof times !== 'number') { times = "NaN"; }
|
||||
return "Hello " + times + " times";
|
||||
}};
|
||||
shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal");
|
||||
});
|
||||
|
||||
describe("String literal parameters", function() {
|
||||
it("simple literals work", function() {
|
||||
var string = 'Message: {{hello "world" 12 true false}}';
|
||||
var hash = {};
|
||||
var helpers = {hello: function(param, times, bool1, bool2) {
|
||||
if(typeof times !== 'number') { times = "NaN"; }
|
||||
if(typeof bool1 !== 'boolean') { bool1 = "NaB"; }
|
||||
if(typeof bool2 !== 'boolean') { bool2 = "NaB"; }
|
||||
return "Hello " + param + " " + times + " times: " + bool1 + " " + bool2;
|
||||
}};
|
||||
shouldCompileTo(string, [hash, helpers], "Message: Hello world 12 times: true false", "template with a simple String literal");
|
||||
});
|
||||
|
||||
it("using a quote in the middle of a parameter raises an error", function() {
|
||||
var string = 'Message: {{hello wo"rld"}}';
|
||||
shouldThrow(function() {
|
||||
CompilerContext.compile(string);
|
||||
}, Error);
|
||||
});
|
||||
|
||||
it("escaping a String is possible", function(){
|
||||
var string = 'Message: {{{hello "\\"world\\""}}}';
|
||||
var hash = {};
|
||||
var helpers = {hello: function(param) { return "Hello " + param; }};
|
||||
shouldCompileTo(string, [hash, helpers], "Message: Hello \"world\"", "template with an escaped String literal");
|
||||
});
|
||||
|
||||
it("it works with ' marks", function() {
|
||||
var string = 'Message: {{{hello "Alan\'s world"}}}';
|
||||
var hash = {};
|
||||
var helpers = {hello: function(param) { return "Hello " + param; }};
|
||||
shouldCompileTo(string, [hash, helpers], "Message: Hello Alan's world", "template with a ' mark");
|
||||
});
|
||||
});
|
||||
|
||||
it("negative number literals work", function() {
|
||||
var string = 'Message: {{hello -12}}';
|
||||
var hash = {};
|
||||
var helpers = {hello: function(times) {
|
||||
if(typeof times !== 'number') { times = "NaN"; }
|
||||
return "Hello " + times + " times";
|
||||
}};
|
||||
shouldCompileTo(string, [hash, helpers], "Message: Hello -12 times", "template with a negative integer literal");
|
||||
});
|
||||
|
||||
describe("multiple parameters", function() {
|
||||
it("simple multi-params work", function() {
|
||||
var string = 'Message: {{goodbye cruel world}}';
|
||||
var hash = {cruel: "cruel", world: "world"};
|
||||
var helpers = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }};
|
||||
shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "regular helpers with multiple params");
|
||||
});
|
||||
|
||||
it("block multi-params work", function() {
|
||||
var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
|
||||
var hash = {cruel: "cruel", world: "world"};
|
||||
var helpers = {goodbye: function(cruel, world, options) {
|
||||
return options.fn({greeting: "Goodbye", adj: cruel, noun: world});
|
||||
}};
|
||||
shouldCompileTo(string, [hash, helpers], "Message: Goodbye cruel world", "block helpers with multiple params");
|
||||
});
|
||||
});
|
||||
describe('hash', function() {
|
||||
it("helpers can take an optional hash", function() {
|
||||
var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" times=12}}');
|
||||
|
||||
var helpers = {
|
||||
goodbye: function(options) {
|
||||
return "GOODBYE " + options.hash.cruel + " " + options.hash.world + " " + options.hash.times + " TIMES";
|
||||
}
|
||||
};
|
||||
|
||||
var context = {};
|
||||
|
||||
var result = template(context, {helpers: helpers});
|
||||
equals(result, "GOODBYE CRUEL WORLD 12 TIMES", "Helper output hash");
|
||||
});
|
||||
|
||||
it("helpers can take an optional hash with booleans", function() {
|
||||
var helpers = {
|
||||
goodbye: function(options) {
|
||||
if (options.hash.print === true) {
|
||||
return "GOODBYE " + options.hash.cruel + " " + options.hash.world;
|
||||
} else if (options.hash.print === false) {
|
||||
return "NOT PRINTING";
|
||||
} else {
|
||||
return "THIS SHOULD NOT HAPPEN";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var context = {};
|
||||
|
||||
var template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=true}}');
|
||||
var result = template(context, {helpers: helpers});
|
||||
equals(result, "GOODBYE CRUEL WORLD", "Helper output hash");
|
||||
|
||||
template = CompilerContext.compile('{{goodbye cruel="CRUEL" world="WORLD" print=false}}');
|
||||
result = template(context, {helpers: helpers});
|
||||
equals(result, "NOT PRINTING", "Boolean helper parameter honored");
|
||||
});
|
||||
|
||||
it("block helpers can take an optional hash", function() {
|
||||
var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" times=12}}world{{/goodbye}}');
|
||||
|
||||
var helpers = {
|
||||
goodbye: function(options) {
|
||||
return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, {helpers: helpers});
|
||||
equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output");
|
||||
});
|
||||
|
||||
it("block helpers can take an optional hash with single quoted stings", function() {
|
||||
var template = CompilerContext.compile("{{#goodbye cruel='CRUEL' times=12}}world{{/goodbye}}");
|
||||
|
||||
var helpers = {
|
||||
goodbye: function(options) {
|
||||
return "GOODBYE " + options.hash.cruel + " " + options.fn(this) + " " + options.hash.times + " TIMES";
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, {helpers: helpers});
|
||||
equals(result, "GOODBYE CRUEL world 12 TIMES", "Hash parameters output");
|
||||
});
|
||||
|
||||
it("block helpers can take an optional hash with booleans", function() {
|
||||
var helpers = {
|
||||
goodbye: function(options) {
|
||||
if (options.hash.print === true) {
|
||||
return "GOODBYE " + options.hash.cruel + " " + options.fn(this);
|
||||
} else if (options.hash.print === false) {
|
||||
return "NOT PRINTING";
|
||||
} else {
|
||||
return "THIS SHOULD NOT HAPPEN";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=true}}world{{/goodbye}}');
|
||||
var result = template({}, {helpers: helpers});
|
||||
equals(result, "GOODBYE CRUEL world", "Boolean hash parameter honored");
|
||||
|
||||
template = CompilerContext.compile('{{#goodbye cruel="CRUEL" print=false}}world{{/goodbye}}');
|
||||
result = template({}, {helpers: helpers});
|
||||
equals(result, "NOT PRINTING", "Boolean hash parameter honored");
|
||||
});
|
||||
});
|
||||
|
||||
describe("helperMissing", function() {
|
||||
it("if a context is not found, helperMissing is used", function() {
|
||||
shouldThrow(function() {
|
||||
var template = CompilerContext.compile("{{hello}} {{link_to world}}");
|
||||
template({});
|
||||
}, undefined, /Missing helper: 'link_to'/);
|
||||
});
|
||||
|
||||
it("if a context is not found, custom helperMissing is used", function() {
|
||||
var string = "{{hello}} {{link_to world}}";
|
||||
var context = { hello: "Hello", world: "world" };
|
||||
|
||||
var helpers = {
|
||||
helperMissing: function(mesg, options) {
|
||||
if(options.name === "link_to") {
|
||||
return new Handlebars.SafeString("<a>" + mesg + "</a>");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
shouldCompileTo(string, [context, helpers], "Hello <a>world</a>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("knownHelpers", function() {
|
||||
it("Known helper should render helper", function() {
|
||||
var template = CompilerContext.compile("{{hello}}", {knownHelpers: {"hello" : true}});
|
||||
|
||||
var result = template({}, {helpers: {hello: function() { return "foo"; }}});
|
||||
equal(result, "foo", "'foo' should === '" + result);
|
||||
});
|
||||
|
||||
it("Unknown helper in knownHelpers only mode should be passed as undefined", function() {
|
||||
var template = CompilerContext.compile("{{typeof hello}}", {knownHelpers: {'typeof': true}, knownHelpersOnly: true});
|
||||
|
||||
var result = template({}, {helpers: {'typeof': function(arg) { return typeof arg; }, hello: function() { return "foo"; }}});
|
||||
equal(result, "undefined", "'undefined' should === '" + result);
|
||||
});
|
||||
it("Builtin helpers available in knownHelpers only mode", function() {
|
||||
var template = CompilerContext.compile("{{#unless foo}}bar{{/unless}}", {knownHelpersOnly: true});
|
||||
|
||||
var result = template({});
|
||||
equal(result, "bar", "'bar' should === '" + result);
|
||||
});
|
||||
it("Field lookup works in knownHelpers only mode", function() {
|
||||
var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true});
|
||||
|
||||
var result = template({foo: 'bar'});
|
||||
equal(result, "bar", "'bar' should === '" + result);
|
||||
});
|
||||
it("Conditional blocks work in knownHelpers only mode", function() {
|
||||
var template = CompilerContext.compile("{{#foo}}bar{{/foo}}", {knownHelpersOnly: true});
|
||||
|
||||
var result = template({foo: 'baz'});
|
||||
equal(result, "bar", "'bar' should === '" + result);
|
||||
});
|
||||
it("Invert blocks work in knownHelpers only mode", function() {
|
||||
var template = CompilerContext.compile("{{^foo}}bar{{/foo}}", {knownHelpersOnly: true});
|
||||
|
||||
var result = template({foo: false});
|
||||
equal(result, "bar", "'bar' should === '" + result);
|
||||
});
|
||||
it("Functions are bound to the context in knownHelpers only mode", function() {
|
||||
var template = CompilerContext.compile("{{foo}}", {knownHelpersOnly: true});
|
||||
var result = template({foo: function() { return this.bar; }, bar: 'bar'});
|
||||
equal(result, "bar", "'bar' should === '" + result);
|
||||
});
|
||||
it("Unknown helper call in knownHelpers only mode should throw", function() {
|
||||
shouldThrow(function() {
|
||||
CompilerContext.compile("{{typeof hello}}", {knownHelpersOnly: true});
|
||||
}, Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("blockHelperMissing", function() {
|
||||
it("lambdas are resolved by blockHelperMissing, not handlebars proper", function() {
|
||||
var string = "{{#truthy}}yep{{/truthy}}";
|
||||
var data = { truthy: function() { return true; } };
|
||||
shouldCompileTo(string, data, "yep");
|
||||
});
|
||||
it("lambdas resolved by blockHelperMissing are bound to the context", function() {
|
||||
var string = "{{#truthy}}yep{{/truthy}}";
|
||||
var boundData = { truthy: function() { return this.truthiness(); }, truthiness: function() { return false; } };
|
||||
shouldCompileTo(string, boundData, "");
|
||||
});
|
||||
});
|
||||
|
||||
describe('name field', function() {
|
||||
var context = {};
|
||||
var helpers = {
|
||||
blockHelperMissing: function() {
|
||||
return 'missing: ' + arguments[arguments.length-1].name;
|
||||
},
|
||||
helper: function() {
|
||||
return 'ran: ' + arguments[arguments.length-1].name;
|
||||
}
|
||||
};
|
||||
|
||||
it('should include in ambiguous mustache calls', function() {
|
||||
shouldCompileTo('{{helper}}', [context, helpers], 'ran: helper');
|
||||
});
|
||||
it('should include in helper mustache calls', function() {
|
||||
shouldCompileTo('{{helper 1}}', [context, helpers], 'ran: helper');
|
||||
});
|
||||
it('should include in ambiguous block calls', function() {
|
||||
shouldCompileTo('{{#helper}}{{/helper}}', [context, helpers], 'ran: helper');
|
||||
});
|
||||
it('should include in simple block calls', function() {
|
||||
shouldCompileTo('{{#./helper}}{{/./helper}}', [context, helpers], 'missing: ./helper');
|
||||
});
|
||||
it('should include in helper block calls', function() {
|
||||
shouldCompileTo('{{#helper 1}}{{/helper}}', [context, helpers], 'ran: helper');
|
||||
});
|
||||
it('should include in known helper calls', function() {
|
||||
var template = CompilerContext.compile("{{helper}}", {knownHelpers: {'helper': true}, knownHelpersOnly: true});
|
||||
|
||||
equal(template({}, {helpers: helpers}), 'ran: helper');
|
||||
});
|
||||
|
||||
it('should include full id', function() {
|
||||
shouldCompileTo('{{#foo.helper}}{{/foo.helper}}', [{foo: {}}, helpers], 'missing: foo.helper');
|
||||
});
|
||||
});
|
||||
|
||||
describe('name conflicts', function() {
|
||||
it("helpers take precedence over same-named context properties", function() {
|
||||
var template = CompilerContext.compile("{{goodbye}} {{cruel world}}");
|
||||
|
||||
var helpers = {
|
||||
goodbye: function() {
|
||||
return this.goodbye.toUpperCase();
|
||||
},
|
||||
|
||||
cruel: function(world) {
|
||||
return "cruel " + world.toUpperCase();
|
||||
}
|
||||
};
|
||||
|
||||
var context = {
|
||||
goodbye: "goodbye",
|
||||
world: "world"
|
||||
};
|
||||
|
||||
var result = template(context, {helpers: helpers});
|
||||
equals(result, "GOODBYE cruel WORLD", "Helper executed");
|
||||
});
|
||||
|
||||
it("helpers take precedence over same-named context properties$", function() {
|
||||
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}}");
|
||||
|
||||
var helpers = {
|
||||
goodbye: function(options) {
|
||||
return this.goodbye.toUpperCase() + options.fn(this);
|
||||
},
|
||||
|
||||
cruel: function(world) {
|
||||
return "cruel " + world.toUpperCase();
|
||||
}
|
||||
};
|
||||
|
||||
var context = {
|
||||
goodbye: "goodbye",
|
||||
world: "world"
|
||||
};
|
||||
|
||||
var result = template(context, {helpers: helpers});
|
||||
equals(result, "GOODBYE cruel WORLD", "Helper executed");
|
||||
});
|
||||
|
||||
it("Scoped names take precedence over helpers", function() {
|
||||
var template = CompilerContext.compile("{{this.goodbye}} {{cruel world}} {{cruel this.goodbye}}");
|
||||
|
||||
var helpers = {
|
||||
goodbye: function() {
|
||||
return this.goodbye.toUpperCase();
|
||||
},
|
||||
|
||||
cruel: function(world) {
|
||||
return "cruel " + world.toUpperCase();
|
||||
}
|
||||
};
|
||||
|
||||
var context = {
|
||||
goodbye: "goodbye",
|
||||
world: "world"
|
||||
};
|
||||
|
||||
var result = template(context, {helpers: helpers});
|
||||
equals(result, "goodbye cruel WORLD cruel GOODBYE", "Helper not executed");
|
||||
});
|
||||
|
||||
it("Scoped names take precedence over block helpers", function() {
|
||||
var template = CompilerContext.compile("{{#goodbye}} {{cruel world}}{{/goodbye}} {{this.goodbye}}");
|
||||
|
||||
var helpers = {
|
||||
goodbye: function(options) {
|
||||
return this.goodbye.toUpperCase() + options.fn(this);
|
||||
},
|
||||
|
||||
cruel: function(world) {
|
||||
return "cruel " + world.toUpperCase();
|
||||
}
|
||||
};
|
||||
|
||||
var context = {
|
||||
goodbye: "goodbye",
|
||||
world: "world"
|
||||
};
|
||||
|
||||
var result = template(context, {helpers: helpers});
|
||||
equals(result, "GOODBYE cruel WORLD goodbye", "Helper executed");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Mocha</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
|
||||
<style>
|
||||
.headless .suite > h1,
|
||||
.headless .test.pass {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Show only errors in "headless", non-interactive mode.
|
||||
if (/headless=true/.test(location.href)) {
|
||||
document.documentElement.className = 'headless';
|
||||
}
|
||||
</script>
|
||||
<script src="/node_modules/mocha/mocha.js"></script>
|
||||
<script>
|
||||
mocha.setup('bdd');
|
||||
</script>
|
||||
<script src="/dist/handlebars.js"></script>
|
||||
|
||||
<script src="/spec/env/common.js"></script>
|
||||
<script>
|
||||
var CompilerContext = {
|
||||
compile: function(template, options) {
|
||||
var templateSpec = handlebarsEnv.precompile(template, options);
|
||||
return handlebarsEnv.template(safeEval(templateSpec));
|
||||
},
|
||||
compileWithPartial: function(template, options) {
|
||||
return handlebarsEnv.compile(template, options);
|
||||
}
|
||||
};
|
||||
|
||||
function safeEval(templateSpec) {
|
||||
try {
|
||||
var ret;
|
||||
eval('ret = ' + templateSpec);
|
||||
return ret;
|
||||
} catch (err) {
|
||||
console.error(templateSpec);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="/tmp/tests.js"></script>
|
||||
<script>
|
||||
onload = function(){
|
||||
// The test harness leaks under FF. We should have decent global leak coverage from other tests
|
||||
if (!navigator.userAgent.match(/Firefox\/([\d.]+)/)) {
|
||||
mocha.checkLeaks();
|
||||
}
|
||||
mocha.run();
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,71 @@
|
||||
/*global Handlebars, beforeEach, handlebarsEnv, shouldCompileTo */
|
||||
describe('javascript-compiler api', function() {
|
||||
if (!Handlebars.JavaScriptCompiler) {
|
||||
return;
|
||||
}
|
||||
|
||||
describe('#nameLookup', function() {
|
||||
var $superName;
|
||||
beforeEach(function() {
|
||||
$superName = handlebarsEnv.JavaScriptCompiler.prototype.nameLookup;
|
||||
});
|
||||
afterEach(function() {
|
||||
handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = $superName;
|
||||
});
|
||||
|
||||
it('should allow override', function() {
|
||||
handlebarsEnv.JavaScriptCompiler.prototype.nameLookup = function(parent, name) {
|
||||
return parent + '.bar_' + name;
|
||||
};
|
||||
shouldCompileTo("{{foo}}", { bar_foo: "food" }, "food");
|
||||
});
|
||||
});
|
||||
describe('#compilerInfo', function() {
|
||||
var $superCheck, $superInfo;
|
||||
beforeEach(function() {
|
||||
$superCheck = handlebarsEnv.VM.checkRevision;
|
||||
$superInfo = handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo;
|
||||
});
|
||||
afterEach(function() {
|
||||
handlebarsEnv.VM.checkRevision = $superCheck;
|
||||
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = $superInfo;
|
||||
});
|
||||
it('should allow compilerInfo override', function() {
|
||||
handlebarsEnv.JavaScriptCompiler.prototype.compilerInfo = function() {
|
||||
return 'crazy';
|
||||
};
|
||||
handlebarsEnv.VM.checkRevision = function(compilerInfo) {
|
||||
if (compilerInfo !== 'crazy') {
|
||||
throw new Error('It didn\'t work');
|
||||
}
|
||||
};
|
||||
shouldCompileTo("{{foo}} ", { foo: "food" }, "food ");
|
||||
});
|
||||
});
|
||||
describe('buffer', function() {
|
||||
var $superAppend, $superCreate;
|
||||
beforeEach(function() {
|
||||
handlebarsEnv.JavaScriptCompiler.prototype.forceBuffer = true;
|
||||
$superAppend = handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer;
|
||||
$superCreate = handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer;
|
||||
});
|
||||
afterEach(function() {
|
||||
handlebarsEnv.JavaScriptCompiler.prototype.forceBuffer = false;
|
||||
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = $superAppend;
|
||||
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = $superCreate;
|
||||
});
|
||||
|
||||
it('should allow init buffer override', function() {
|
||||
handlebarsEnv.JavaScriptCompiler.prototype.initializeBuffer = function() {
|
||||
return this.quotedString('foo_');
|
||||
};
|
||||
shouldCompileTo("{{foo}} ", { foo: "food" }, "foo_food ");
|
||||
});
|
||||
it('should allow append buffer override', function() {
|
||||
handlebarsEnv.JavaScriptCompiler.prototype.appendToBuffer = function(string) {
|
||||
return $superAppend.call(this, string + ' + "_foo"');
|
||||
};
|
||||
shouldCompileTo("{{foo}}", { foo: "food" }, "food_foo");
|
||||
});
|
||||
});
|
||||
});
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
/*global Handlebars */
|
||||
describe('parser', function() {
|
||||
if (!Handlebars.print) {
|
||||
return;
|
||||
}
|
||||
|
||||
function ast_for(template) {
|
||||
var ast = Handlebars.parse(template);
|
||||
return Handlebars.print(ast);
|
||||
}
|
||||
|
||||
it('parses simple mustaches', function() {
|
||||
equals(ast_for('{{foo}}'), "{{ ID:foo [] }}\n");
|
||||
equals(ast_for('{{foo?}}'), "{{ ID:foo? [] }}\n");
|
||||
equals(ast_for('{{foo_}}'), "{{ ID:foo_ [] }}\n");
|
||||
equals(ast_for('{{foo-}}'), "{{ ID:foo- [] }}\n");
|
||||
equals(ast_for('{{foo:}}'), "{{ ID:foo: [] }}\n");
|
||||
});
|
||||
|
||||
it('parses simple mustaches with data', function() {
|
||||
equals(ast_for("{{@foo}}"), "{{ @ID:foo [] }}\n");
|
||||
});
|
||||
|
||||
it('parses simple mustaches with data paths', function() {
|
||||
equals(ast_for("{{@../foo}}"), "{{ @ID:foo [] }}\n");
|
||||
});
|
||||
|
||||
it('parses mustaches with paths', function() {
|
||||
equals(ast_for("{{foo/bar}}"), "{{ PATH:foo/bar [] }}\n");
|
||||
});
|
||||
|
||||
it('parses mustaches with this/foo', function() {
|
||||
equals(ast_for("{{this/foo}}"), "{{ ID:foo [] }}\n");
|
||||
});
|
||||
|
||||
it('parses mustaches with - in a path', function() {
|
||||
equals(ast_for("{{foo-bar}}"), "{{ ID:foo-bar [] }}\n");
|
||||
});
|
||||
|
||||
it('parses mustaches with parameters', function() {
|
||||
equals(ast_for("{{foo bar}}"), "{{ ID:foo [ID:bar] }}\n");
|
||||
});
|
||||
|
||||
it('parses mustaches with string parameters', function() {
|
||||
equals(ast_for("{{foo bar \"baz\" }}"), '{{ ID:foo [ID:bar, "baz"] }}\n');
|
||||
});
|
||||
|
||||
it('parses mustaches with NUMBER parameters', function() {
|
||||
equals(ast_for("{{foo 1}}"), "{{ ID:foo [NUMBER{1}] }}\n");
|
||||
});
|
||||
|
||||
it('parses mustaches with BOOLEAN parameters', function() {
|
||||
equals(ast_for("{{foo true}}"), "{{ ID:foo [BOOLEAN{true}] }}\n");
|
||||
equals(ast_for("{{foo false}}"), "{{ ID:foo [BOOLEAN{false}] }}\n");
|
||||
});
|
||||
|
||||
it('parses mutaches with DATA parameters', function() {
|
||||
equals(ast_for("{{foo @bar}}"), "{{ ID:foo [@ID:bar] }}\n");
|
||||
});
|
||||
|
||||
it('parses mustaches with hash arguments', function() {
|
||||
equals(ast_for("{{foo bar=baz}}"), "{{ ID:foo [] HASH{bar=ID:baz} }}\n");
|
||||
equals(ast_for("{{foo bar=1}}"), "{{ ID:foo [] HASH{bar=NUMBER{1}} }}\n");
|
||||
equals(ast_for("{{foo bar=true}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{true}} }}\n");
|
||||
equals(ast_for("{{foo bar=false}}"), "{{ ID:foo [] HASH{bar=BOOLEAN{false}} }}\n");
|
||||
equals(ast_for("{{foo bar=@baz}}"), "{{ ID:foo [] HASH{bar=@ID:baz} }}\n");
|
||||
|
||||
equals(ast_for("{{foo bar=baz bat=bam}}"), "{{ ID:foo [] HASH{bar=ID:baz, bat=ID:bam} }}\n");
|
||||
equals(ast_for("{{foo bar=baz bat=\"bam\"}}"), '{{ ID:foo [] HASH{bar=ID:baz, bat="bam"} }}\n');
|
||||
|
||||
equals(ast_for("{{foo bat='bam'}}"), '{{ ID:foo [] HASH{bat="bam"} }}\n');
|
||||
|
||||
equals(ast_for("{{foo omg bar=baz bat=\"bam\"}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam"} }}\n');
|
||||
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=1}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=NUMBER{1}} }}\n');
|
||||
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=true}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{true}} }}\n');
|
||||
equals(ast_for("{{foo omg bar=baz bat=\"bam\" baz=false}}"), '{{ ID:foo [ID:omg] HASH{bar=ID:baz, bat="bam", baz=BOOLEAN{false}} }}\n');
|
||||
});
|
||||
|
||||
it('parses contents followed by a mustache', function() {
|
||||
equals(ast_for("foo bar {{baz}}"), "CONTENT[ \'foo bar \' ]\n{{ ID:baz [] }}\n");
|
||||
});
|
||||
|
||||
it('parses a partial', function() {
|
||||
equals(ast_for("{{> foo }}"), "{{> PARTIAL:foo }}\n");
|
||||
});
|
||||
|
||||
it('parses a partial with context', function() {
|
||||
equals(ast_for("{{> foo bar}}"), "{{> PARTIAL:foo ID:bar }}\n");
|
||||
});
|
||||
|
||||
it('parses a partial with hash', function() {
|
||||
equals(ast_for("{{> foo bar=bat}}"), "{{> PARTIAL:foo HASH{bar=ID:bat} }}\n");
|
||||
});
|
||||
|
||||
it('parses a partial with context and hash', function() {
|
||||
equals(ast_for("{{> foo bar bat=baz}}"), "{{> PARTIAL:foo ID:bar HASH{bat=ID:baz} }}\n");
|
||||
});
|
||||
|
||||
it('parses a partial with a complex name', function() {
|
||||
equals(ast_for("{{> shared/partial?.bar}}"), "{{> PARTIAL:shared/partial?.bar }}\n");
|
||||
});
|
||||
|
||||
it('parses a comment', function() {
|
||||
equals(ast_for("{{! this is a comment }}"), "{{! ' this is a comment ' }}\n");
|
||||
});
|
||||
|
||||
it('parses a multi-line comment', function() {
|
||||
equals(ast_for("{{!\nthis is a multi-line comment\n}}"), "{{! \'\nthis is a multi-line comment\n\' }}\n");
|
||||
});
|
||||
|
||||
it('parses an inverse section', function() {
|
||||
equals(ast_for("{{#foo}} bar {{^}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
|
||||
});
|
||||
|
||||
it('parses an inverse (else-style) section', function() {
|
||||
equals(ast_for("{{#foo}} bar {{else}} baz {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n");
|
||||
});
|
||||
|
||||
it('parses empty blocks', function() {
|
||||
equals(ast_for("{{#foo}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
|
||||
});
|
||||
|
||||
it('parses empty blocks with empty inverse section', function() {
|
||||
equals(ast_for("{{#foo}}{{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
|
||||
});
|
||||
|
||||
it('parses empty blocks with empty inverse (else-style) section', function() {
|
||||
equals(ast_for("{{#foo}}{{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n");
|
||||
});
|
||||
|
||||
it('parses non-empty blocks with empty inverse section', function() {
|
||||
equals(ast_for("{{#foo}} bar {{^}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
|
||||
});
|
||||
|
||||
it('parses non-empty blocks with empty inverse (else-style) section', function() {
|
||||
equals(ast_for("{{#foo}} bar {{else}}{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n");
|
||||
});
|
||||
|
||||
it('parses empty blocks with non-empty inverse section', function() {
|
||||
equals(ast_for("{{#foo}}{{^}} bar {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
|
||||
});
|
||||
|
||||
it('parses empty blocks with non-empty inverse (else-style) section', function() {
|
||||
equals(ast_for("{{#foo}}{{else}} bar {{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n");
|
||||
});
|
||||
|
||||
it('parses a standalone inverse section', function() {
|
||||
equals(ast_for("{{^foo}}bar{{/foo}}"), "BLOCK:\n {{ ID:foo [] }}\n {{^}}\n CONTENT[ 'bar' ]\n");
|
||||
});
|
||||
|
||||
it("raises if there's a Parse error", function() {
|
||||
shouldThrow(function() {
|
||||
ast_for("foo{{^}}bar");
|
||||
}, Error, /Parse error on line 1/);
|
||||
shouldThrow(function() {
|
||||
ast_for("{{foo}");
|
||||
}, Error, /Parse error on line 1/);
|
||||
shouldThrow(function() {
|
||||
ast_for("{{foo &}}");
|
||||
}, Error, /Parse error on line 1/);
|
||||
shouldThrow(function() {
|
||||
ast_for("{{#goodbyes}}{{/hellos}}");
|
||||
}, Error, /goodbyes doesn't match hellos/);
|
||||
|
||||
shouldThrow(function() {
|
||||
ast_for("{{{{goodbyes}}}} {{{{/hellos}}}}");
|
||||
}, Error, /goodbyes doesn't match hellos/);
|
||||
});
|
||||
|
||||
it('knows how to report the correct line number in errors', function() {
|
||||
shouldThrow(function() {
|
||||
ast_for("hello\nmy\n{{foo}");
|
||||
}, Error, /Parse error on line 3/);
|
||||
shouldThrow(function() {
|
||||
ast_for("hello\n\nmy\n\n{{foo}");
|
||||
}, Error, /Parse error on line 5/);
|
||||
});
|
||||
|
||||
it('knows how to report the correct line number in errors when the first character is a newline', function() {
|
||||
shouldThrow(function() {
|
||||
ast_for("\n\nhello\n\nmy\n\n{{foo}");
|
||||
}, Error, /Parse error on line 7/);
|
||||
});
|
||||
|
||||
describe('externally compiled AST', function() {
|
||||
it('can pass through an already-compiled AST', function() {
|
||||
equals(ast_for(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")])), "CONTENT[ \'Hello\' ]\n");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,181 +0,0 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe "Parser" do
|
||||
let(:handlebars) { @context["Handlebars"] }
|
||||
|
||||
def program(&block)
|
||||
ASTBuilder.build do
|
||||
program do
|
||||
instance_eval(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ast_for(string)
|
||||
ast = handlebars.parse(string)
|
||||
handlebars.print(ast)
|
||||
end
|
||||
|
||||
class ASTBuilder
|
||||
def self.build(&block)
|
||||
ret = new
|
||||
ret.evaluate(&block)
|
||||
ret.out
|
||||
end
|
||||
|
||||
attr_reader :out
|
||||
|
||||
def initialize
|
||||
@padding = 0
|
||||
@out = ""
|
||||
end
|
||||
|
||||
def evaluate(&block)
|
||||
instance_eval(&block)
|
||||
end
|
||||
|
||||
def pad(string)
|
||||
@out << (" " * @padding) + string + "\n"
|
||||
end
|
||||
|
||||
def with_padding
|
||||
@padding += 1
|
||||
ret = yield
|
||||
@padding -= 1
|
||||
ret
|
||||
end
|
||||
|
||||
def program
|
||||
pad("PROGRAM:")
|
||||
with_padding { yield }
|
||||
end
|
||||
|
||||
def inverse
|
||||
pad("{{^}}")
|
||||
with_padding { yield }
|
||||
end
|
||||
|
||||
def block
|
||||
pad("BLOCK:")
|
||||
with_padding { yield }
|
||||
end
|
||||
|
||||
def inverted_block
|
||||
pad("INVERSE:")
|
||||
with_padding { yield }
|
||||
end
|
||||
|
||||
def mustache(id, *params)
|
||||
pad("{{ #{id} [#{params.join(", ")}] }}")
|
||||
end
|
||||
|
||||
def partial(id, context = nil)
|
||||
content = id.dup
|
||||
content << " #{context}" if context
|
||||
pad("{{> #{content} }}")
|
||||
end
|
||||
|
||||
def comment(comment)
|
||||
pad("{{! '#{comment}' }}")
|
||||
end
|
||||
|
||||
def content(string)
|
||||
pad("CONTENT[ '#{string}' ]")
|
||||
end
|
||||
|
||||
def string(string)
|
||||
string.inspect
|
||||
end
|
||||
|
||||
def id(id)
|
||||
"ID:#{id}"
|
||||
end
|
||||
|
||||
def path(*parts)
|
||||
"PATH:#{parts.join("/")}"
|
||||
end
|
||||
end
|
||||
|
||||
it "parses simple mustaches" do
|
||||
ast_for("{{foo}}").should == program { mustache id("foo") }
|
||||
end
|
||||
|
||||
it "parses mustaches with paths" do
|
||||
ast_for("{{foo/bar}}").should == program { mustache path("foo", "bar") }
|
||||
end
|
||||
|
||||
it "parses mustaches with this/foo" do
|
||||
ast_for("{{this/foo}}").should == program { mustache id("foo") }
|
||||
end
|
||||
|
||||
it "parses mustaches with parameters" do
|
||||
ast_for("{{foo bar}}").should == program { mustache id("foo"), id("bar") }
|
||||
end
|
||||
|
||||
it "parses mustaches with string parameters" do
|
||||
ast_for("{{foo bar \"baz\" }}").should == program { mustache id("foo"), id("bar"), string("baz")}
|
||||
end
|
||||
|
||||
it "parses contents followed by a mustache" do
|
||||
ast_for("foo bar {{baz}}").should == program do
|
||||
content "foo bar "
|
||||
mustache id("baz")
|
||||
end
|
||||
end
|
||||
|
||||
it "parses a partial" do
|
||||
ast_for("{{> foo }}").should == program { partial id("foo") }
|
||||
end
|
||||
|
||||
it "parses a partial with context" do
|
||||
ast_for("{{> foo bar}}").should == program { partial id("foo"), id("bar") }
|
||||
end
|
||||
|
||||
it "parses a comment" do
|
||||
ast_for("{{! this is a comment }}").should == program do
|
||||
comment " this is a comment "
|
||||
end
|
||||
end
|
||||
|
||||
it "parses an inverse section" do
|
||||
ast_for("{{#foo}} bar {{^}} baz {{/foo}}").should == program do
|
||||
block do
|
||||
mustache id("foo")
|
||||
|
||||
program do
|
||||
content " bar "
|
||||
end
|
||||
|
||||
inverse do
|
||||
content " baz "
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "parses a standalone inverse section" do
|
||||
ast_for("{{^foo}}bar{{/foo}}").should == program do
|
||||
inverted_block do
|
||||
mustache id("foo")
|
||||
|
||||
program do
|
||||
content "bar"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "raises if there's a Parse error" do
|
||||
lambda { ast_for("{{foo}") }.should raise_error(V8::JSError, /Parse error on line 1/)
|
||||
lambda { ast_for("{{foo &}}")}.should raise_error(V8::JSError, /Parse error on line 1/)
|
||||
end
|
||||
|
||||
it "knows how to report the correct line number in errors" do
|
||||
lambda { ast_for("hello\nmy\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 3/m)
|
||||
lambda { ast_for("hello\n\nmy\n\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 5/m)
|
||||
end
|
||||
|
||||
it "knows how to report the correct line number in errors when the first character is a newline" do
|
||||
lambda { ast_for("\n\nhello\n\nmy\n\n{{foo}") }.should raise_error(V8::JSError, /Parse error on line 7/m)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,140 @@
|
||||
/*global CompilerContext, shouldCompileTo, shouldCompileToWithPartials */
|
||||
describe('partials', function() {
|
||||
it("basic partials", function() {
|
||||
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
|
||||
var partial = "{{name}} ({{url}}) ";
|
||||
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
"Basic partials output based on current context.");
|
||||
});
|
||||
|
||||
it("partials with context", function() {
|
||||
var string = "Dudes: {{>dude dudes}}";
|
||||
var partial = "{{#this}}{{name}} ({{url}}) {{/this}}";
|
||||
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
"Partials can be passed a context");
|
||||
});
|
||||
|
||||
it("partials with undefined context", function() {
|
||||
var string = "Dudes: {{>dude dudes}}";
|
||||
var partial = "{{foo}} Empty";
|
||||
var hash = {};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: Empty");
|
||||
});
|
||||
|
||||
it("partials with parameters", function() {
|
||||
var string = "Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}";
|
||||
var partial = "{{others.foo}}{{name}} ({{url}}) ";
|
||||
var hash = {foo: 'bar', dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: barYehuda (http://yehuda) barAlan (http://alan) ",
|
||||
"Basic partials output based on current context.");
|
||||
});
|
||||
|
||||
it("partial in a partial", function() {
|
||||
var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}";
|
||||
var dude = "{{name}} {{> url}} ";
|
||||
var url = "<a href='{{url}}'>{{url}}</a>";
|
||||
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {dude: dude, url: url}], true, "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> ", "Partials are rendered inside of other partials");
|
||||
});
|
||||
|
||||
it("rendering undefined partial throws an exception", function() {
|
||||
shouldThrow(function() {
|
||||
var template = CompilerContext.compile("{{> whatever}}");
|
||||
template();
|
||||
}, Handlebars.Exception, 'The partial whatever could not be found');
|
||||
});
|
||||
|
||||
it("rendering template partial in vm mode throws an exception", function() {
|
||||
shouldThrow(function() {
|
||||
var template = CompilerContext.compile("{{> whatever}}");
|
||||
template();
|
||||
}, Handlebars.Exception, 'The partial whatever could not be found');
|
||||
});
|
||||
|
||||
it("rendering function partial in vm mode", function() {
|
||||
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
|
||||
var partial = function(context) {
|
||||
return context.name + ' (' + context.url + ') ';
|
||||
};
|
||||
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
|
||||
shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
"Function partials output based in VM.");
|
||||
});
|
||||
|
||||
it("GH-14: a partial preceding a selector", function() {
|
||||
var string = "Dudes: {{>dude}} {{another_dude}}";
|
||||
var dude = "{{name}}";
|
||||
var hash = {name:"Jeepers", another_dude:"Creepers"};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {dude:dude}], true, "Dudes: Jeepers Creepers", "Regular selectors can follow a partial");
|
||||
});
|
||||
|
||||
it("Partials with slash paths", function() {
|
||||
var string = "Dudes: {{> shared/dude}}";
|
||||
var dude = "{{name}}";
|
||||
var hash = {name:"Jeepers", another_dude:"Creepers"};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
|
||||
});
|
||||
|
||||
it("Partials with slash and point paths", function() {
|
||||
var string = "Dudes: {{> shared/dude.thing}}";
|
||||
var dude = "{{name}}";
|
||||
var hash = {name:"Jeepers", another_dude:"Creepers"};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude.thing':dude}], true, "Dudes: Jeepers", "Partials can use literal with points in paths");
|
||||
});
|
||||
|
||||
it("Global Partials", function() {
|
||||
handlebarsEnv.registerPartial('global_test', '{{another_dude}}');
|
||||
|
||||
var string = "Dudes: {{> shared/dude}} {{> global_test}}";
|
||||
var dude = "{{name}}";
|
||||
var hash = {name:"Jeepers", another_dude:"Creepers"};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {'shared/dude':dude}], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
|
||||
});
|
||||
|
||||
it("Multiple partial registration", function() {
|
||||
handlebarsEnv.registerPartial({
|
||||
'shared/dude': '{{name}}',
|
||||
global_test: '{{another_dude}}'
|
||||
});
|
||||
|
||||
var string = "Dudes: {{> shared/dude}} {{> global_test}}";
|
||||
var hash = {name:"Jeepers", another_dude:"Creepers"};
|
||||
shouldCompileToWithPartials(string, [hash], true, "Dudes: Jeepers Creepers", "Partials can use globals or passed");
|
||||
});
|
||||
|
||||
it("Partials with integer path", function() {
|
||||
var string = "Dudes: {{> 404}}";
|
||||
var dude = "{{name}}";
|
||||
var hash = {name:"Jeepers", another_dude:"Creepers"};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {404:dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
|
||||
});
|
||||
|
||||
it("Partials with complex path", function() {
|
||||
var string = "Dudes: {{> 404/asdf?.bar}}";
|
||||
var dude = "{{name}}";
|
||||
var hash = {name:"Jeepers", another_dude:"Creepers"};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {'404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
|
||||
});
|
||||
|
||||
it("Partials with escaped", function() {
|
||||
var string = "Dudes: {{> [+404/asdf?.bar]}}";
|
||||
var dude = "{{name}}";
|
||||
var hash = {name:"Jeepers", another_dude:"Creepers"};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
|
||||
});
|
||||
|
||||
it("Partials with string", function() {
|
||||
var string = "Dudes: {{> \"+404/asdf?.bar\"}}";
|
||||
var dude = "{{name}}";
|
||||
var hash = {name:"Jeepers", another_dude:"Creepers"};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {'+404/asdf?.bar':dude}], true, "Dudes: Jeepers", "Partials can use literal paths");
|
||||
});
|
||||
|
||||
it('should handle empty partial', function() {
|
||||
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
|
||||
var partial = "";
|
||||
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
|
||||
shouldCompileToWithPartials(string, [hash, {}, {dude: partial}], true, "Dudes: "); });
|
||||
});
|
||||
@@ -1,603 +0,0 @@
|
||||
module("basic context");
|
||||
|
||||
Handlebars.registerHelper('helperMissing', function(helper, context) {
|
||||
if(helper === "link_to") {
|
||||
return new Handlebars.SafeString("<a>" + context + "</a>");
|
||||
}
|
||||
});
|
||||
|
||||
var shouldCompileTo = function(string, hash, expected, message) {
|
||||
var template = Handlebars.compile(string);
|
||||
if(Object.prototype.toString.call(hash) === "[object Array]") {
|
||||
if(hash[1]) {
|
||||
for(var prop in Handlebars.helpers) {
|
||||
hash[1][prop] = Handlebars.helpers[prop];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hash = [hash];
|
||||
}
|
||||
|
||||
result = template.apply(this, hash)
|
||||
equal(result, expected, "'" + expected + "' should === '" + result + "': " + message);
|
||||
};
|
||||
|
||||
var shouldThrow = function(fn, exception, message) {
|
||||
var caught = false;
|
||||
try {
|
||||
fn();
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof exception) {
|
||||
caught = true;
|
||||
}
|
||||
}
|
||||
|
||||
ok(caught, message || null);
|
||||
}
|
||||
|
||||
|
||||
test("compiling with a basic context", function() {
|
||||
shouldCompileTo("Goodbye\n{{cruel}}\n{{world}}!", {cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
|
||||
"It works if all the required keys are provided");
|
||||
});
|
||||
|
||||
test("comments", function() {
|
||||
shouldCompileTo("{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!",
|
||||
{cruel: "cruel", world: "world"}, "Goodbye\ncruel\nworld!",
|
||||
"comments are ignored");
|
||||
});
|
||||
|
||||
test("boolean", function() {
|
||||
var string = "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!";
|
||||
shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
|
||||
"booleans show the contents when true");
|
||||
|
||||
shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
|
||||
"booleans do not show the contents when false");
|
||||
});
|
||||
|
||||
test("zeros", function() {
|
||||
shouldCompileTo("num1: {{num1}}, num2: {{num2}}", {num1: 42, num2: 0},
|
||||
"num1: 42, num2: 0");
|
||||
shouldCompileTo("num: {{.}}", 0, "num: 0");
|
||||
shouldCompileTo("num: {{num1/num2}}", {num1: {num2: 0}}, "num: 0");
|
||||
});
|
||||
|
||||
test("newlines", function() {
|
||||
shouldCompileTo("Alan's\nTest", {}, "Alan's\nTest");
|
||||
shouldCompileTo("Alan's\rTest", {}, "Alan's\rTest");
|
||||
});
|
||||
|
||||
test("escaping text", function() {
|
||||
shouldCompileTo("Awesome's", {}, "Awesome's", "text is escaped so that it doesn't get caught on single quotes");
|
||||
shouldCompileTo("Awesome\\", {}, "Awesome\\", "text is escaped so that the closing quote can't be ignored");
|
||||
shouldCompileTo("Awesome\\ foo", {}, "Awesome\\ foo", "text is escaped so that it doesn't mess up backslashes");
|
||||
shouldCompileTo(' " " ', {}, ' " " ', "double quotes never produce invalid javascript");
|
||||
});
|
||||
|
||||
test("escaping expressions", function() {
|
||||
shouldCompileTo("{{{awesome}}}", {awesome: "&\"\\<>"}, '&\"\\<>',
|
||||
"expressions with 3 handlebars aren't escaped");
|
||||
|
||||
shouldCompileTo("{{awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
|
||||
"by default expressions should be escaped");
|
||||
|
||||
shouldCompileTo("{{&awesome}}", {awesome: "&\"\\<>"}, '&\"\\<>',
|
||||
"expressions with {{& handlebars aren't escaped");
|
||||
|
||||
});
|
||||
|
||||
test("functions returning safestrings shouldn't be escaped", function() {
|
||||
var hash = {awesome: function() { return new Handlebars.SafeString("&\"\\<>"); }};
|
||||
shouldCompileTo("{{awesome}}", hash, '&\"\\<>',
|
||||
"functions returning safestrings aren't escaped");
|
||||
});
|
||||
|
||||
test("functions", function() {
|
||||
shouldCompileTo("{{awesome}}", {awesome: function() { return "Awesome"; }}, "Awesome",
|
||||
"functions are called and render their output");
|
||||
});
|
||||
|
||||
test("functions with context argument", function() {
|
||||
shouldCompileTo("{{awesome frank}}",
|
||||
{awesome: function(context) { return context; },
|
||||
frank: "Frank"},
|
||||
"Frank", "functions are called with context arguments");
|
||||
});
|
||||
|
||||
test("nested paths", function() {
|
||||
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: "beautiful"}},
|
||||
"Goodbye beautiful world!", "Nested paths access nested objects");
|
||||
});
|
||||
|
||||
test("nested paths with empty string value", function() {
|
||||
shouldCompileTo("Goodbye {{alan/expression}} world!", {alan: {expression: ""}},
|
||||
"Goodbye world!", "Nested paths access nested objects with empty string");
|
||||
});
|
||||
|
||||
test("--- TODO --- bad idea nested paths", function() {
|
||||
return;
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
shouldThrow(function() {
|
||||
Handlebars.compile("{{#goodbyes}}{{../name/../name}}{{/goodbyes}}")(hash);
|
||||
}, Handlebars.Exception,
|
||||
"Cannot jump (..) into previous context after moving into a context.");
|
||||
|
||||
var string = "{{#goodbyes}}{{.././world}} {{/goodbyes}}";
|
||||
shouldCompileTo(string, hash, "world world world ", "Same context (.) is ignored in paths");
|
||||
});
|
||||
|
||||
test("that current context path ({{.}}) doesn't hit fallback", function() {
|
||||
shouldCompileTo("test: {{.}}", [null, {helper: "awesome"}], "test: ");
|
||||
});
|
||||
|
||||
test("complex but empty paths", function() {
|
||||
shouldCompileTo("{{person/name}}", {person: {name: null}}, "");
|
||||
shouldCompileTo("{{person/name}}", {person: {}}, "");
|
||||
});
|
||||
|
||||
test("this keyword in paths", function() {
|
||||
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}";
|
||||
var hash = {goodbyes: ["goodbye", "Goodbye", "GOODBYE"]};
|
||||
shouldCompileTo(string, hash, "goodbyeGoodbyeGOODBYE",
|
||||
"This keyword in paths evaluates to current context");
|
||||
|
||||
string = "{{#hellos}}{{this/text}}{{/hellos}}"
|
||||
hash = {hellos: [{text: "hello"}, {text: "Hello"}, {text: "HELLO"}]};
|
||||
shouldCompileTo(string, hash, "helloHelloHELLO", "This keyword evaluates in more complex paths");
|
||||
});
|
||||
|
||||
module("inverted sections");
|
||||
|
||||
test("inverted sections with unset value", function() {
|
||||
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
|
||||
var hash = {};
|
||||
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value isn't set.");
|
||||
});
|
||||
|
||||
test("inverted section with false value", function() {
|
||||
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
|
||||
var hash = {goodbyes: false};
|
||||
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is false.");
|
||||
});
|
||||
|
||||
test("inverted section with empty set", function() {
|
||||
var string = "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
|
||||
var hash = {goodbyes: []};
|
||||
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when value is empty set.");
|
||||
});
|
||||
|
||||
test("inverted section using result of function call", function() {
|
||||
var string = "{{goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}";
|
||||
var hash = {goodbyes: function() { return false; }}
|
||||
shouldCompileTo(string, hash, "Right On!", "Inverted section rendered when result of function in expression is false.");
|
||||
});
|
||||
|
||||
module("blocks");
|
||||
|
||||
test("array", function() {
|
||||
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!"
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
"Arrays iterate over the contents when not empty");
|
||||
|
||||
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
|
||||
"Arrays ignore the contents when empty");
|
||||
|
||||
});
|
||||
|
||||
test("empty block", function() {
|
||||
var string = "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!"
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
shouldCompileTo(string, hash, "cruel world!",
|
||||
"Arrays iterate over the contents when not empty");
|
||||
|
||||
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
|
||||
"Arrays ignore the contents when empty");
|
||||
});
|
||||
|
||||
test("incorrectly matched blocks", function() {
|
||||
var string = "{{#goodbyes}}{{/hellos}}";
|
||||
|
||||
shouldThrow(function() {
|
||||
Handlebars.compile(string);
|
||||
}, Handlebars.Exception, "Incorrectly matched blocks return an exception at compile time.");
|
||||
});
|
||||
|
||||
test("nested iteration", function() {
|
||||
|
||||
});
|
||||
|
||||
test("block with complex lookup", function() {
|
||||
var string = "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}"
|
||||
var hash = {name: "Alan", goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}]};
|
||||
|
||||
shouldCompileTo(string, hash, "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ",
|
||||
"Templates can access variables in contexts up the stack with relative path syntax");
|
||||
});
|
||||
|
||||
test("helper with complex lookup", function() {
|
||||
var string = "{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"
|
||||
var hash = {prefix: "/root", goodbyes: [{text: "Goodbye", url: "goodbye"}]};
|
||||
var fallback = {link: function(prefix) {
|
||||
return "<a href='" + prefix + "/" + this.url + "'>" + this.text + "</a>"
|
||||
}};
|
||||
shouldCompileTo(string, [hash, fallback], "<a href='/root/goodbye'>Goodbye</a>")
|
||||
});
|
||||
|
||||
test("helper block with complex lookup expression", function() {
|
||||
var string = "{{#goodbyes}}{{../name}}{{/goodbyes}}"
|
||||
var hash = {name: "Alan"};
|
||||
var fallback = {goodbyes: function(fn) {
|
||||
var out = "";
|
||||
var byes = ["Goodbye", "goodbye", "GOODBYE"];
|
||||
for (var i = 0,j = byes.length; i < j; i++) {
|
||||
out += byes[i] + " " + fn(this) + "! ";
|
||||
}
|
||||
return out;
|
||||
}};
|
||||
shouldCompileTo(string, [hash, fallback], "Goodbye Alan! goodbye Alan! GOODBYE Alan! ");
|
||||
});
|
||||
|
||||
test("helper with complex lookup and nested template", function() {
|
||||
var string = "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}";
|
||||
var hash = {prefix: '/root', goodbyes: [{text: "Goodbye", url: "goodbye"}]};
|
||||
var fallback = {link: function (prefix, fn) {
|
||||
return "<a href='" + prefix + "/" + this.url + "'>" + fn(this) + "</a>";
|
||||
}};
|
||||
shouldCompileTo(string, [hash, fallback], "<a href='/root/goodbye'>Goodbye</a>")
|
||||
});
|
||||
|
||||
test("block with deep nested complex lookup", function() {
|
||||
var string = "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}";
|
||||
var hash = {omg: "OMG!", outer: [{ inner: [{ text: "goodbye" }] }] };
|
||||
|
||||
shouldCompileTo(string, hash, "Goodbye cruel OMG!");
|
||||
});
|
||||
|
||||
test("block helper", function() {
|
||||
var string = "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!";
|
||||
var template = Handlebars.compile(string);
|
||||
|
||||
result = template({goodbyes: function(fn) { return fn({text: "GOODBYE"}); }, world: "world"});
|
||||
equal(result, "GOODBYE! cruel world!");
|
||||
});
|
||||
|
||||
test("block helper staying in the same context", function() {
|
||||
var string = "{{#form}}<p>{{name}}</p>{{/form}}"
|
||||
var template = Handlebars.compile(string);
|
||||
|
||||
result = template({form: function(fn) { return "<form>" + fn(this) + "</form>" }, name: "Yehuda"});
|
||||
equal(result, "<form><p>Yehuda</p></form>");
|
||||
});
|
||||
|
||||
test("block helper should have context in this", function() {
|
||||
var source = "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>";
|
||||
var link = function(fn) {
|
||||
return '<a href="/people/' + this.id + '">' + fn(this) + '</a>';
|
||||
};
|
||||
var data = { "people": [
|
||||
{ "name": "Alan", "id": 1 },
|
||||
{ "name": "Yehuda", "id": 2 }
|
||||
]};
|
||||
|
||||
shouldCompileTo(source, [data, {link: link}], "<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>");
|
||||
});
|
||||
|
||||
test("block helper for undefined value", function() {
|
||||
shouldCompileTo("{{#empty}}shouldn't render{{/empty}}", {}, "");
|
||||
});
|
||||
|
||||
test("block helper passing a new context", function() {
|
||||
var string = "{{#form yehuda}}<p>{{name}}</p>{{/form}}"
|
||||
var template = Handlebars.compile(string);
|
||||
|
||||
result = template({form: function(context, fn) { return "<form>" + fn(context) + "</form>" }, yehuda: {name: "Yehuda"}});
|
||||
equal(result, "<form><p>Yehuda</p></form>");
|
||||
});
|
||||
|
||||
test("block helper passing a complex path context", function() {
|
||||
var string = "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}"
|
||||
var template = Handlebars.compile(string);
|
||||
|
||||
result = template({form: function(context, fn) { return "<form>" + fn(context) + "</form>" }, yehuda: {name: "Yehuda", cat: {name: "Harold"}}});
|
||||
equal(result, "<form><p>Harold</p></form>");
|
||||
});
|
||||
|
||||
test("nested block helpers", function() {
|
||||
var string = "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}"
|
||||
var template = Handlebars.compile(string);
|
||||
|
||||
result = template({
|
||||
form: function(context, fn) { return "<form>" + fn(context) + "</form>" },
|
||||
yehuda: {name: "Yehuda",
|
||||
link: function(fn) { return "<a href='" + this.name + "'>" + fn(this) + "</a>"; }
|
||||
}
|
||||
});
|
||||
equal(result, "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>");
|
||||
});
|
||||
|
||||
test("block inverted sections", function() {
|
||||
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people"},
|
||||
"No people");
|
||||
});
|
||||
|
||||
test("block inverted sections with empty arrays", function() {
|
||||
shouldCompileTo("{{#people}}{{name}}{{^}}{{none}}{{/people}}", {none: "No people", people: []},
|
||||
"No people");
|
||||
});
|
||||
|
||||
test("block helper inverted sections", function() {
|
||||
var string = "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}"
|
||||
var list = function(context, fn, inverse) {
|
||||
if (context.length > 0) {
|
||||
var out = "<ul>";
|
||||
for(var i = 0,j=context.length; i < j; i++) {
|
||||
out += "<li>";
|
||||
out += fn(context[i]);
|
||||
out += "</li>";
|
||||
}
|
||||
out += "</ul>";
|
||||
return out;
|
||||
} else {
|
||||
return "<p>" + inverse(this) + "</p>";
|
||||
}
|
||||
};
|
||||
|
||||
var hash = {list: list, people: [{name: "Alan"}, {name: "Yehuda"}]};
|
||||
var empty = {list: list, people: []};
|
||||
var rootMessage = {
|
||||
list: function(context, fn, inverse) { if(context.length === 0) { return "<p>" + inverse(this) + "</p>"; } },
|
||||
people: [],
|
||||
message: "Nobody's here"
|
||||
}
|
||||
|
||||
var messageString = "{{#list people}}Hello{{^}}{{message}}{{/list}}";
|
||||
|
||||
// the meaning here may be kind of hard to catch, but list.not is always called,
|
||||
// so we should see the output of both
|
||||
shouldCompileTo(string, hash, "<ul><li>Alan</li><li>Yehuda</li></ul>", "an inverse wrapper is passed in as a new context");
|
||||
shouldCompileTo(string, empty, "<p><em>Nobody's here</em></p>", "an inverse wrapper can be optionally called");
|
||||
shouldCompileTo(messageString, rootMessage, "<p>Nobody's here</p>", "the context of an inverse is the parent of the block");
|
||||
});
|
||||
|
||||
module("fallback hash");
|
||||
|
||||
test("providing a fallback hash", function() {
|
||||
shouldCompileTo("Goodbye {{cruel}} {{world}}!", [{cruel: "cruel"}, {world: "world"}], "Goodbye cruel world!",
|
||||
"Fallback hash is available");
|
||||
|
||||
shouldCompileTo("Goodbye {{#iter}}{{cruel}} {{world}}{{/iter}}!", [{iter: [{cruel: "cruel"}]}, {world: "world"}],
|
||||
"Goodbye cruel world!", "Fallback hash is available inside other blocks");
|
||||
});
|
||||
|
||||
test("in cases of conflict, the explicit hash wins", function() {
|
||||
|
||||
});
|
||||
|
||||
test("the fallback hash is available is nested contexts", function() {
|
||||
|
||||
});
|
||||
|
||||
module("partials");
|
||||
|
||||
test("basic partials", function() {
|
||||
var string = "Dudes: {{#dudes}}{{> dude}}{{/dudes}}";
|
||||
var partial = "{{name}} ({{url}}) ";
|
||||
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
|
||||
shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
"Basic partials output based on current context.");
|
||||
});
|
||||
|
||||
test("partials with context", function() {
|
||||
var string = "Dudes: {{>dude dudes}}";
|
||||
var partial = "{{#this}}{{name}} ({{url}}) {{/this}}";
|
||||
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
|
||||
shouldCompileTo(string, [hash, {}, {dude: partial}], "Dudes: Yehuda (http://yehuda) Alan (http://alan) ",
|
||||
"Partials can be passed a context");
|
||||
});
|
||||
|
||||
test("partial in a partial", function() {
|
||||
var string = "Dudes: {{#dudes}}{{>dude}}{{/dudes}}";
|
||||
var dude = "{{name}} {{> url}} ";
|
||||
var url = "<a href='{{url}}'>{{url}}</a>";
|
||||
var hash = {dudes: [{name: "Yehuda", url: "http://yehuda"}, {name: "Alan", url: "http://alan"}]};
|
||||
shouldCompileTo(string, [hash, {}, {dude: dude, url: url}], "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> ", "Partials are rendered inside of other partials");
|
||||
});
|
||||
|
||||
test("rendering undefined partial throws an exception", function() {
|
||||
shouldThrow(function() {
|
||||
var template = Handlebars.compile("{{> whatever}}");
|
||||
template();
|
||||
}, Handlebars.Exception, "Should throw exception");
|
||||
});
|
||||
|
||||
test("GH-14: a partial preceding a selector", function() {
|
||||
var string = "Dudes: {{>dude}} {{another_dude}}";
|
||||
var dude = "{{name}}";
|
||||
var hash = {name:"Jeepers", another_dude:"Creepers"};
|
||||
shouldCompileTo(string, [hash, {}, {dude:dude}], "Dudes: Jeepers Creepers", "Regular selectors can follow a partial");
|
||||
});
|
||||
|
||||
test("Partial containing complex expression", function() {
|
||||
var template = "Dudes: {{#dudes}}{{> dude}} {{/dudes}}";
|
||||
var dude = "{{../salutation}} {{name}}";
|
||||
var hash = {salutation: "Mr.", dudes: [{name: "Yehuda"}, {name: "Alan"}]};
|
||||
shouldCompileTo(template, [hash, {}, {dude: dude}], "Dudes: Mr. Yehuda Mr. Alan ");
|
||||
});
|
||||
|
||||
module("String literal parameters");
|
||||
|
||||
test("simple literals work", function() {
|
||||
var string = 'Message: {{hello "world"}}';
|
||||
var hash = {};
|
||||
var fallback = {hello: function(param) { return "Hello " + param; }}
|
||||
shouldCompileTo(string, [hash, fallback], "Message: Hello world", "template with a simple String literal");
|
||||
});
|
||||
|
||||
test("using a quote in the middle of a parameter raises an error", function() {
|
||||
shouldThrow(function() {
|
||||
var string = 'Message: {{hello wo"rld"}}';
|
||||
Handlebars.compile(string);
|
||||
}, Error, "should throw exception");
|
||||
});
|
||||
|
||||
test("escaping a String is possible", function(){
|
||||
var string = 'Message: {{hello "\\"world\\""}}';
|
||||
var hash = {}
|
||||
var fallback = {hello: function(param) { return "Hello " + param; }}
|
||||
shouldCompileTo(string, [hash, fallback], "Message: Hello \"world\"", "template with an escaped String literal");
|
||||
});
|
||||
|
||||
test("it works with ' marks", function() {
|
||||
var string = 'Message: {{hello "Alan\'s world"}}';
|
||||
var hash = {}
|
||||
var fallback = {hello: function(param) { return "Hello " + param; }}
|
||||
shouldCompileTo(string, [hash, fallback], "Message: Hello Alan's world", "template with a ' mark");
|
||||
});
|
||||
|
||||
module("multiple parameters");
|
||||
|
||||
test("simple multi-params work", function() {
|
||||
var string = 'Message: {{goodbye cruel world}}';
|
||||
var hash = {cruel: "cruel", world: "world"}
|
||||
var fallback = {goodbye: function(cruel, world) { return "Goodbye " + cruel + " " + world; }}
|
||||
shouldCompileTo(string, [hash, fallback], "Message: Goodbye cruel world", "regular helpers with multiple params");
|
||||
});
|
||||
|
||||
test("block multi-params work", function() {
|
||||
var string = 'Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}';
|
||||
var hash = {cruel: "cruel", world: "world"}
|
||||
var fallback = {goodbye: function(cruel, world, fn) {
|
||||
return fn({greeting: "Goodbye", adj: cruel, noun: world});
|
||||
}}
|
||||
shouldCompileTo(string, [hash, fallback], "Message: Goodbye cruel world", "block helpers with multiple params");
|
||||
})
|
||||
|
||||
module("safestring");
|
||||
|
||||
test("constructing a safestring from a string and checking its type", function() {
|
||||
var safe = new Handlebars.SafeString("testing 1, 2, 3");
|
||||
ok(safe instanceof Handlebars.SafeString, "SafeString is an instance of Handlebars.SafeString");
|
||||
equal(safe, "testing 1, 2, 3", "SafeString is equivalent to its underlying string");
|
||||
});
|
||||
|
||||
module("helperMissing");
|
||||
|
||||
test("if a context is not found, helperMissing is used", function() {
|
||||
var string = "{{hello}} {{link_to world}}"
|
||||
var context = { hello: "Hello", world: "world" };
|
||||
|
||||
shouldCompileTo(string, context, "Hello <a>world</a>")
|
||||
});
|
||||
|
||||
module("built-in helpers");
|
||||
|
||||
test("with", function() {
|
||||
var string = "{{#with person}}{{first}} {{last}}{{/with}}";
|
||||
shouldCompileTo(string, {person: {first: "Alan", last: "Johnson"}}, "Alan Johnson");
|
||||
});
|
||||
|
||||
test("if", function() {
|
||||
var string = "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!";
|
||||
shouldCompileTo(string, {goodbye: true, world: "world"}, "GOODBYE cruel world!",
|
||||
"if with boolean argument shows the contents when true");
|
||||
shouldCompileTo(string, {goodbye: "dummy", world: "world"}, "GOODBYE cruel world!",
|
||||
"if with string argument shows the contents");
|
||||
shouldCompileTo(string, {goodbye: false, world: "world"}, "cruel world!",
|
||||
"if with boolean argument does not show the contents when false");
|
||||
shouldCompileTo(string, {world: "world"}, "cruel world!",
|
||||
"if with undefined does not show the contents");
|
||||
});
|
||||
|
||||
test("each", function() {
|
||||
var string = "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!"
|
||||
var hash = {goodbyes: [{text: "goodbye"}, {text: "Goodbye"}, {text: "GOODBYE"}], world: "world"};
|
||||
shouldCompileTo(string, hash, "goodbye! Goodbye! GOODBYE! cruel world!",
|
||||
"each with array argument iterates over the contents when not empty");
|
||||
shouldCompileTo(string, {goodbyes: [], world: "world"}, "cruel world!",
|
||||
"each with array argument ignores the contents when empty");
|
||||
});
|
||||
|
||||
test("overriding property lookup", function() {
|
||||
|
||||
});
|
||||
|
||||
|
||||
test("passing in data to a compiled function that expects data - works with helpers", function() {
|
||||
var template = Handlebars.compile("{{hello}}", true);
|
||||
|
||||
var helpers = {
|
||||
hello: function(data) {
|
||||
return data.adjective + " " + this.noun;
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({noun: "cat"}, helpers, null, {adjective: "happy"});
|
||||
equals("happy cat", result);
|
||||
});
|
||||
|
||||
test("passing in data to a compiled function that expects data - works with helpers and parameters", function() {
|
||||
var template = Handlebars.compile("{{hello world}}", true);
|
||||
|
||||
var helpers = {
|
||||
hello: function(noun, data) {
|
||||
return data.adjective + " " + noun + (this.exclaim ? "!" : "");
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({exclaim: true, world: "world"}, helpers, null, {adjective: "happy"});
|
||||
equals("happy world!", result);
|
||||
});
|
||||
|
||||
test("passing in data to a compiled function that expects data - works with block helpers", function() {
|
||||
var template = Handlebars.compile("{{#hello}}{{world}}{{/hello}}", true);
|
||||
|
||||
var helpers = {
|
||||
hello: function(fn) {
|
||||
return fn(this);
|
||||
},
|
||||
world: function(data) {
|
||||
return data.adjective + " world" + (this.exclaim ? "!" : "");
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({exclaim: true}, helpers, null, {adjective: "happy"});
|
||||
equals("happy world!", result);
|
||||
});
|
||||
|
||||
test("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
|
||||
var template = Handlebars.compile("{{#hello}}{{world ../zomg}}{{/hello}}", true);
|
||||
|
||||
var helpers = {
|
||||
hello: function(fn) {
|
||||
return fn({exclaim: "?"});
|
||||
},
|
||||
world: function(thing, data) {
|
||||
return data.adjective + " " + thing + (this.exclaim || "");
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({exclaim: true, zomg: "world"}, helpers, null, {adjective: "happy"});
|
||||
equals("happy world?", result);
|
||||
});
|
||||
|
||||
test("passing in data to a compiled function that expects data - works with block helpers that use ..", function() {
|
||||
var template = Handlebars.compile("{{#hello}}{{world ../zomg}}{{/hello}}", true);
|
||||
|
||||
var helpers = {
|
||||
hello: function(fn, inverse, data) {
|
||||
return data.accessData + " " + fn({exclaim: "?"});
|
||||
},
|
||||
world: function(thing, data) {
|
||||
return data.adjective + " " + thing + (this.exclaim || "");
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({exclaim: true, zomg: "world"}, helpers, null, {adjective: "happy", accessData: "#win"});
|
||||
equals("#win happy world?", result);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*global CompilerContext, Handlebars, shouldCompileTo, shouldThrow */
|
||||
describe('Regressions', function() {
|
||||
it("GH-94: Cannot read property of undefined", function() {
|
||||
var data = {"books":[{"title":"The origin of species","author":{"name":"Charles Darwin"}},{"title":"Lazarillo de Tormes"}]};
|
||||
var string = "{{#books}}{{title}}{{author.name}}{{/books}}";
|
||||
shouldCompileTo(string, data, "The origin of speciesCharles DarwinLazarillo de Tormes",
|
||||
"Renders without an undefined property error");
|
||||
});
|
||||
|
||||
it("GH-150: Inverted sections print when they shouldn't", function() {
|
||||
var string = "{{^set}}not set{{/set}} :: {{#set}}set{{/set}}";
|
||||
|
||||
shouldCompileTo(string, {}, "not set :: ", "inverted sections run when property isn't present in context");
|
||||
shouldCompileTo(string, {set: undefined}, "not set :: ", "inverted sections run when property is undefined");
|
||||
shouldCompileTo(string, {set: false}, "not set :: ", "inverted sections run when property is false");
|
||||
shouldCompileTo(string, {set: true}, " :: set", "inverted sections don't run when property is true");
|
||||
});
|
||||
|
||||
it("GH-158: Using array index twice, breaks the template", function() {
|
||||
var string = "{{arr.[0]}}, {{arr.[1]}}";
|
||||
var data = { "arr": [1,2] };
|
||||
|
||||
shouldCompileTo(string, data, "1, 2", "it works as expected");
|
||||
});
|
||||
|
||||
it("bug reported by @fat where lambdas weren't being properly resolved", function() {
|
||||
var string = "<strong>This is a slightly more complicated {{thing}}.</strong>.\n{{! Just ignore this business. }}\nCheck this out:\n{{#hasThings}}\n<ul>\n{{#things}}\n<li class={{className}}>{{word}}</li>\n{{/things}}</ul>.\n{{/hasThings}}\n{{^hasThings}}\n\n<small>Nothing to check out...</small>\n{{/hasThings}}";
|
||||
var data = {
|
||||
thing: function() {
|
||||
return "blah";
|
||||
},
|
||||
things: [
|
||||
{className: "one", word: "@fat"},
|
||||
{className: "two", word: "@dhg"},
|
||||
{className: "three", word:"@sayrer"}
|
||||
],
|
||||
hasThings: function() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
var output = "<strong>This is a slightly more complicated blah.</strong>.\n\nCheck this out:\n\n<ul>\n\n<li class=one>@fat</li>\n\n<li class=two>@dhg</li>\n\n<li class=three>@sayrer</li>\n</ul>.\n\n";
|
||||
shouldCompileTo(string, data, output);
|
||||
});
|
||||
|
||||
it('GH-408: Multiple loops fail', function() {
|
||||
var context = [
|
||||
{ name: "John Doe", location: { city: "Chicago" } },
|
||||
{ name: "Jane Doe", location: { city: "New York"} }
|
||||
];
|
||||
|
||||
var template = CompilerContext.compile('{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}{{#.}}{{name}}{{/.}}');
|
||||
|
||||
var result = template(context);
|
||||
equals(result, "John DoeJane DoeJohn DoeJane DoeJohn DoeJane Doe", 'It should output multiple times');
|
||||
});
|
||||
|
||||
it('GS-428: Nested if else rendering', function() {
|
||||
var succeedingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}';
|
||||
var failingTemplate = '{{#inverse}} {{#blk}} Unexpected {{/blk}} {{else}} {{#blk}} Expected {{/blk}} {{/inverse}}';
|
||||
|
||||
var helpers = {
|
||||
blk: function(block) { return block.fn(''); },
|
||||
inverse: function(block) { return block.inverse(''); }
|
||||
};
|
||||
|
||||
shouldCompileTo(succeedingTemplate, [{}, helpers], ' Expected ');
|
||||
shouldCompileTo(failingTemplate, [{}, helpers], ' Expected ');
|
||||
});
|
||||
|
||||
it('GH-458: Scoped this identifier', function() {
|
||||
shouldCompileTo('{{./foo}}', {foo: 'bar'}, 'bar');
|
||||
});
|
||||
|
||||
it('GH-375: Unicode line terminators', function() {
|
||||
shouldCompileTo('\u2028', {}, '\u2028');
|
||||
});
|
||||
|
||||
it('GH-534: Object prototype aliases', function() {
|
||||
Object.prototype[0xD834] = true;
|
||||
|
||||
shouldCompileTo('{{foo}}', { foo: 'bar' }, 'bar');
|
||||
|
||||
delete Object.prototype[0xD834];
|
||||
});
|
||||
|
||||
it('GH-437: Matching escaping', function() {
|
||||
shouldThrow(function() {
|
||||
CompilerContext.compile('{{{a}}');
|
||||
}, Error);
|
||||
shouldThrow(function() {
|
||||
CompilerContext.compile('{{a}}}');
|
||||
}, Error);
|
||||
});
|
||||
|
||||
it("GH-676: Using array in escaping mustache fails", function() {
|
||||
var string = "{{arr}}";
|
||||
var data = { "arr": [1,2] };
|
||||
|
||||
shouldCompileTo(string, data, data.arr.toString(), "it works as expected");
|
||||
});
|
||||
|
||||
it("Mustache man page", function() {
|
||||
var string = "Hello {{name}}. You have just won ${{value}}!{{#in_ca}} Well, ${{taxed_value}}, after taxes.{{/in_ca}}";
|
||||
var data = {
|
||||
"name": "Chris",
|
||||
"value": 10000,
|
||||
"taxed_value": 10000 - (10000 * 0.4),
|
||||
"in_ca": true
|
||||
};
|
||||
|
||||
shouldCompileTo(string, data, "Hello Chris. You have just won $10000! Well, $6000, after taxes.", "the hello world mustache example works");
|
||||
});
|
||||
|
||||
it("Passing falsy values to Handlebars.compile throws an error", function() {
|
||||
shouldThrow(function() {
|
||||
CompilerContext.compile(null);
|
||||
}, Error, 'You must pass a string or Handlebars AST to Handlebars.precompile. You passed null');
|
||||
});
|
||||
|
||||
if (Handlebars.AST) {
|
||||
it("can pass through an already-compiled AST via compile/precompile", function() {
|
||||
equal(Handlebars.compile(new Handlebars.AST.ProgramNode([ new Handlebars.AST.ContentNode("Hello")]))(), 'Hello');
|
||||
});
|
||||
|
||||
it("can pass through an empty string", function() {
|
||||
equal(Handlebars.compile('')(), '');
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
if (typeof(require) !== 'undefined' && require.extensions[".handlebars"]) {
|
||||
describe('Require', function() {
|
||||
it('Load .handlebars files with require()', function() {
|
||||
var template = require("./artifacts/example_1");
|
||||
equal(template, require("./artifacts/example_1.handlebars"));
|
||||
|
||||
var expected = 'foo\n';
|
||||
var result = template({foo: "foo"});
|
||||
|
||||
equal(result, expected);
|
||||
});
|
||||
|
||||
it('Load .hbs files with require()', function() {
|
||||
var template = require("./artifacts/example_2");
|
||||
equal(template, require("./artifacts/example_2.hbs"));
|
||||
|
||||
var expected = 'Hello, World!\n';
|
||||
var result = template({name: "World"});
|
||||
|
||||
equal(result, expected);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
require "v8"
|
||||
|
||||
# Monkey patches due to bugs in RubyRacer
|
||||
class V8::JSError
|
||||
def initialize(try, to)
|
||||
@to = to
|
||||
begin
|
||||
super(initialize_unsafe(try))
|
||||
rescue Exception => e
|
||||
# Original code does not make an Array here
|
||||
@boundaries = [Boundary.new(:rbframes => e.backtrace)]
|
||||
@value = e
|
||||
super("BUG! please report. JSError#initialize failed!: #{e.message}")
|
||||
end
|
||||
end
|
||||
|
||||
def parse_js_frames(try)
|
||||
raw = @to.rb(try.StackTrace())
|
||||
if raw && !raw.empty?
|
||||
raw.split("\n")[1..-1].tap do |frames|
|
||||
# Original code uses strip!, and the frames are not guaranteed to be strippable
|
||||
frames.each {|frame| frame.strip.chomp!(",")}
|
||||
end
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Handlebars
|
||||
module Spec
|
||||
def self.js_backtrace(context)
|
||||
begin
|
||||
context.eval("throw")
|
||||
rescue V8::JSError => e
|
||||
return e.backtrace(:javascript)
|
||||
end
|
||||
end
|
||||
|
||||
def self.remove_exports(string)
|
||||
match = string.match(%r{\A(.*?)^// BEGIN\(BROWSER\)\n(.*)\n^// END\(BROWSER\)(.*?)\Z}m)
|
||||
prelines = match ? match[1].count("\n") + 1 : 0
|
||||
ret = match ? match[2] : string
|
||||
("\n" * prelines) + ret
|
||||
end
|
||||
|
||||
def self.js_load(file)
|
||||
str = File.read(file)
|
||||
CONTEXT.eval(remove_exports(str), file)
|
||||
end
|
||||
|
||||
CONTEXT = V8::Context.new
|
||||
CONTEXT.instance_eval do |context|
|
||||
context["exports"] = nil
|
||||
|
||||
context["p"] = proc do |val|
|
||||
p val if ENV["DEBUG_JS"]
|
||||
end
|
||||
|
||||
context["puts"] = proc do |val|
|
||||
puts val if ENV["DEBUG_JS"]
|
||||
end
|
||||
|
||||
context["puts_node"] = proc do |val|
|
||||
puts context["Handlebars"]["PrintVisitor"].new.accept(val)
|
||||
puts
|
||||
end
|
||||
|
||||
context["puts_caller"] = proc do
|
||||
puts "BACKTRACE:"
|
||||
puts Handlebars::Spec.js_backtrace(context)
|
||||
puts
|
||||
end
|
||||
|
||||
Handlebars::Spec.js_load('lib/handlebars/parser.js')
|
||||
Handlebars::Spec.js_load('lib/handlebars/base.js');
|
||||
Handlebars::Spec.js_load('lib/handlebars/ast.js');
|
||||
Handlebars::Spec.js_load('lib/handlebars/visitor.js');
|
||||
Handlebars::Spec.js_load('lib/handlebars/printer.js')
|
||||
Handlebars::Spec.js_load('lib/handlebars/runtime.js')
|
||||
Handlebars::Spec.js_load('lib/handlebars/utils.js')
|
||||
Handlebars::Spec.js_load('lib/Handlebars/vm.js')
|
||||
Handlebars::Spec.js_load('lib/handlebars.js')
|
||||
|
||||
context["Handlebars"]["logger"]["level"] = ENV["DEBUG_JS"] ? context["Handlebars"]["logger"][ENV["DEBUG_JS"]] : 4
|
||||
|
||||
context["Handlebars"]["logger"]["log"] = proc do |level, str|
|
||||
logger_level = context["Handlebars"]["logger"]["level"].to_i
|
||||
|
||||
if logger_level <= level
|
||||
puts str
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
require "test/unit/assertions"
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include Test::Unit::Assertions
|
||||
|
||||
config.before(:all) do
|
||||
@context = Handlebars::Spec::CONTEXT
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,176 @@
|
||||
describe('string params mode', function() {
|
||||
it("arguments to helpers can be retrieved from options hash in string form", function() {
|
||||
var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {stringParams: true});
|
||||
|
||||
var helpers = {
|
||||
wycats: function(passiveVoice, noun) {
|
||||
return "HELP ME MY BOSS " + passiveVoice + ' ' + noun;
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, {helpers: helpers});
|
||||
|
||||
equals(result, "HELP ME MY BOSS is.a slave.driver", "String parameters output");
|
||||
});
|
||||
|
||||
it("when using block form, arguments to helpers can be retrieved from options hash in string form", function() {
|
||||
var template = CompilerContext.compile('{{#wycats is.a slave.driver}}help :({{/wycats}}', {stringParams: true});
|
||||
|
||||
var helpers = {
|
||||
wycats: function(passiveVoice, noun, options) {
|
||||
return "HELP ME MY BOSS " + passiveVoice + ' ' +
|
||||
noun + ': ' + options.fn(this);
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, {helpers: helpers});
|
||||
|
||||
equals(result, "HELP ME MY BOSS is.a slave.driver: help :(", "String parameters output");
|
||||
});
|
||||
|
||||
it("when inside a block in String mode, .. passes the appropriate context in the options hash", function() {
|
||||
var template = CompilerContext.compile('{{#with dale}}{{tomdale ../need dad.joke}}{{/with}}', {stringParams: true});
|
||||
|
||||
var helpers = {
|
||||
tomdale: function(desire, noun, options) {
|
||||
return "STOP ME FROM READING HACKER NEWS I " +
|
||||
options.contexts[0][desire] + " " + noun;
|
||||
},
|
||||
|
||||
"with": function(context, options) {
|
||||
return options.fn(options.contexts[0][context]);
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({
|
||||
dale: {},
|
||||
|
||||
need: 'need-a'
|
||||
}, {helpers: helpers});
|
||||
|
||||
equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke", "Proper context variable output");
|
||||
});
|
||||
|
||||
it("information about the types is passed along", function() {
|
||||
var template = CompilerContext.compile('{{tomdale "need" dad.joke true false}}', { stringParams: true });
|
||||
|
||||
var helpers = {
|
||||
tomdale: function(desire, noun, trueBool, falseBool, options) {
|
||||
equal(options.types[0], 'STRING', "the string type is passed");
|
||||
equal(options.types[1], 'ID', "the expression type is passed");
|
||||
equal(options.types[2], 'BOOLEAN', "the expression type is passed");
|
||||
equal(desire, "need", "the string form is passed for strings");
|
||||
equal(noun, "dad.joke", "the string form is passed for expressions");
|
||||
equal(trueBool, true, "raw booleans are passed through");
|
||||
equal(falseBool, false, "raw booleans are passed through");
|
||||
return "Helper called";
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, { helpers: helpers });
|
||||
equal(result, "Helper called");
|
||||
});
|
||||
|
||||
it("hash parameters get type information", function() {
|
||||
var template = CompilerContext.compile('{{tomdale he.says desire="need" noun=dad.joke bool=true}}', { stringParams: true });
|
||||
|
||||
var helpers = {
|
||||
tomdale: function(exclamation, options) {
|
||||
equal(exclamation, "he.says");
|
||||
equal(options.types[0], "ID");
|
||||
|
||||
equal(options.hashTypes.desire, "STRING");
|
||||
equal(options.hashTypes.noun, "ID");
|
||||
equal(options.hashTypes.bool, "BOOLEAN");
|
||||
equal(options.hash.desire, "need");
|
||||
equal(options.hash.noun, "dad.joke");
|
||||
equal(options.hash.bool, true);
|
||||
return "Helper called";
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, { helpers: helpers });
|
||||
equal(result, "Helper called");
|
||||
});
|
||||
|
||||
it("hash parameters get context information", function() {
|
||||
var template = CompilerContext.compile('{{#with dale}}{{tomdale he.says desire="need" noun=../dad/joke bool=true}}{{/with}}', { stringParams: true });
|
||||
|
||||
var context = {dale: {}};
|
||||
|
||||
var helpers = {
|
||||
tomdale: function(exclamation, options) {
|
||||
equal(exclamation, "he.says");
|
||||
equal(options.types[0], "ID");
|
||||
|
||||
equal(options.contexts.length, 1);
|
||||
equal(options.hashContexts.noun, context);
|
||||
equal(options.hash.desire, "need");
|
||||
equal(options.hash.noun, "dad.joke");
|
||||
equal(options.hash.bool, true);
|
||||
return "Helper called";
|
||||
},
|
||||
"with": function(context, options) {
|
||||
return options.fn(options.contexts[0][context]);
|
||||
}
|
||||
};
|
||||
|
||||
var result = template(context, { helpers: helpers });
|
||||
equal(result, "Helper called");
|
||||
});
|
||||
|
||||
it("when inside a block in String mode, .. passes the appropriate context in the options hash to a block helper", function() {
|
||||
var template = CompilerContext.compile('{{#with dale}}{{#tomdale ../need dad.joke}}wot{{/tomdale}}{{/with}}', {stringParams: true});
|
||||
|
||||
var helpers = {
|
||||
tomdale: function(desire, noun, options) {
|
||||
return "STOP ME FROM READING HACKER NEWS I " +
|
||||
options.contexts[0][desire] + " " + noun + " " +
|
||||
options.fn(this);
|
||||
},
|
||||
|
||||
"with": function(context, options) {
|
||||
return options.fn(options.contexts[0][context]);
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({
|
||||
dale: {},
|
||||
|
||||
need: 'need-a'
|
||||
}, {helpers: helpers});
|
||||
|
||||
equals(result, "STOP ME FROM READING HACKER NEWS I need-a dad.joke wot", "Proper context variable output");
|
||||
});
|
||||
|
||||
it("with nested block ambiguous", function() {
|
||||
var template = CompilerContext.compile('{{#with content}}{{#view}}{{firstName}} {{lastName}}{{/view}}{{/with}}', {stringParams: true});
|
||||
|
||||
var helpers = {
|
||||
with: function(options) {
|
||||
return "WITH";
|
||||
},
|
||||
view: function() {
|
||||
return "VIEW";
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, {helpers: helpers});
|
||||
equals(result, "WITH");
|
||||
});
|
||||
|
||||
it('should handle DATA', function() {
|
||||
var template = CompilerContext.compile('{{foo @bar}}', { stringParams: true });
|
||||
|
||||
var helpers = {
|
||||
foo: function(bar, options) {
|
||||
equal(bar, 'bar');
|
||||
equal(options.types[0], 'DATA');
|
||||
return 'Foo!';
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, { helpers: helpers });
|
||||
equal(result, 'Foo!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
/*global CompilerContext, shouldCompileTo */
|
||||
describe('subexpressions', function() {
|
||||
it("arg-less helper", function() {
|
||||
var string = "{{foo (bar)}}!";
|
||||
var context = {};
|
||||
var helpers = {
|
||||
foo: function(val) {
|
||||
return val+val;
|
||||
},
|
||||
bar: function() {
|
||||
return "LOL";
|
||||
}
|
||||
};
|
||||
shouldCompileTo(string, [context, helpers], "LOLLOL!");
|
||||
});
|
||||
|
||||
it("helper w args", function() {
|
||||
var string = '{{blog (equal a b)}}';
|
||||
|
||||
var context = { bar: "LOL" };
|
||||
var helpers = {
|
||||
blog: function(val) {
|
||||
return "val is " + val;
|
||||
},
|
||||
equal: function(x, y) {
|
||||
return x === y;
|
||||
}
|
||||
};
|
||||
shouldCompileTo(string, [context, helpers], "val is true");
|
||||
});
|
||||
|
||||
it("supports much nesting", function() {
|
||||
var string = '{{blog (equal (equal true true) true)}}';
|
||||
|
||||
var context = { bar: "LOL" };
|
||||
var helpers = {
|
||||
blog: function(val) {
|
||||
return "val is " + val;
|
||||
},
|
||||
equal: function(x, y) {
|
||||
return x === y;
|
||||
}
|
||||
};
|
||||
shouldCompileTo(string, [context, helpers], "val is true");
|
||||
});
|
||||
|
||||
it("provides each nested helper invocation its own options hash", function() {
|
||||
var string = '{{equal (equal true true) true}}';
|
||||
|
||||
var lastOptions = null;
|
||||
var helpers = {
|
||||
equal: function(x, y, options) {
|
||||
if (!options || options === lastOptions) {
|
||||
throw new Error("options hash was reused");
|
||||
}
|
||||
lastOptions = options;
|
||||
return x === y;
|
||||
}
|
||||
};
|
||||
shouldCompileTo(string, [{}, helpers], "true");
|
||||
});
|
||||
|
||||
it("with hashes", function() {
|
||||
var string = '{{blog (equal (equal true true) true fun="yes")}}';
|
||||
|
||||
var context = { bar: "LOL" };
|
||||
var helpers = {
|
||||
blog: function(val) {
|
||||
return "val is " + val;
|
||||
},
|
||||
equal: function(x, y) {
|
||||
return x === y;
|
||||
}
|
||||
};
|
||||
shouldCompileTo(string, [context, helpers], "val is true");
|
||||
});
|
||||
|
||||
it("as hashes", function() {
|
||||
var string = '{{blog fun=(equal (blog fun=1) "val is 1")}}';
|
||||
|
||||
var helpers = {
|
||||
blog: function(options) {
|
||||
return "val is " + options.hash.fun;
|
||||
},
|
||||
equal: function(x, y) {
|
||||
return x === y;
|
||||
}
|
||||
};
|
||||
shouldCompileTo(string, [{}, helpers], "val is true");
|
||||
});
|
||||
|
||||
it("in string params mode,", function() {
|
||||
var template = CompilerContext.compile('{{snog (blorg foo x=y) yeah a=b}}', {stringParams: true});
|
||||
|
||||
var helpers = {
|
||||
snog: function(a, b, options) {
|
||||
equals(a, 'foo');
|
||||
equals(options.types.length, 2, "string params for outer helper processed correctly");
|
||||
equals(options.types[0], 'sexpr', "string params for outer helper processed correctly");
|
||||
equals(options.types[1], 'ID', "string params for outer helper processed correctly");
|
||||
return a + b;
|
||||
},
|
||||
|
||||
blorg: function(a, options) {
|
||||
equals(options.types.length, 1, "string params for inner helper processed correctly");
|
||||
equals(options.types[0], 'ID', "string params for inner helper processed correctly");
|
||||
return a;
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({
|
||||
foo: {},
|
||||
yeah: {}
|
||||
}, {helpers: helpers});
|
||||
|
||||
equals(result, "fooyeah");
|
||||
});
|
||||
|
||||
it("as hashes in string params mode", function() {
|
||||
|
||||
var template = CompilerContext.compile('{{blog fun=(bork)}}', {stringParams: true});
|
||||
|
||||
var helpers = {
|
||||
blog: function(options) {
|
||||
equals(options.hashTypes.fun, 'sexpr');
|
||||
return "val is " + options.hash.fun;
|
||||
},
|
||||
bork: function() {
|
||||
return "BORK";
|
||||
}
|
||||
};
|
||||
|
||||
var result = template({}, {helpers: helpers});
|
||||
equals(result, "val is BORK");
|
||||
});
|
||||
|
||||
it("subexpression functions on the context", function() {
|
||||
var string = "{{foo (bar)}}!";
|
||||
var context = {
|
||||
bar: function() {
|
||||
return "LOL";
|
||||
}
|
||||
};
|
||||
var helpers = {
|
||||
foo: function(val) {
|
||||
return val+val;
|
||||
}
|
||||
};
|
||||
shouldCompileTo(string, [context, helpers], "LOLLOL!");
|
||||
});
|
||||
|
||||
it("subexpressions can't just be property lookups", function() {
|
||||
var string = "{{foo (bar)}}!";
|
||||
var context = {
|
||||
bar: "LOL"
|
||||
};
|
||||
var helpers = {
|
||||
foo: function(val) {
|
||||
return val+val;
|
||||
}
|
||||
};
|
||||
shouldThrow(function() {
|
||||
shouldCompileTo(string, [context, helpers], "LOLLOL!");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,402 @@
|
||||
function shouldMatchTokens(result, tokens) {
|
||||
for (var index = 0; index < result.length; index++) {
|
||||
equals(result[index].name, tokens[index]);
|
||||
}
|
||||
}
|
||||
function shouldBeToken(result, name, text) {
|
||||
equals(result.name, name);
|
||||
equals(result.text, text);
|
||||
}
|
||||
|
||||
describe('Tokenizer', function() {
|
||||
if (!Handlebars.Parser) {
|
||||
return;
|
||||
}
|
||||
|
||||
function tokenize(template) {
|
||||
var parser = Handlebars.Parser,
|
||||
lexer = parser.lexer;
|
||||
|
||||
lexer.setInput(template);
|
||||
var out = [],
|
||||
token;
|
||||
|
||||
while (token = lexer.lex()) {
|
||||
var result = parser.terminals_[token] || token;
|
||||
if (!result || result === 'EOF' || result === 'INVALID') {
|
||||
break;
|
||||
}
|
||||
out.push({name: result, text: lexer.yytext});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
it('tokenizes a simple mustache as "OPEN ID CLOSE"', function() {
|
||||
var result = tokenize("{{foo}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "foo");
|
||||
});
|
||||
|
||||
it('supports unescaping with &', function() {
|
||||
var result = tokenize("{{&bar}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
|
||||
|
||||
shouldBeToken(result[0], "OPEN", "{{&");
|
||||
shouldBeToken(result[1], "ID", "bar");
|
||||
});
|
||||
|
||||
it('supports unescaping with {{{', function() {
|
||||
var result = tokenize("{{{bar}}}");
|
||||
shouldMatchTokens(result, ['OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED']);
|
||||
|
||||
shouldBeToken(result[1], "ID", "bar");
|
||||
});
|
||||
|
||||
it('supports escaping delimiters', function() {
|
||||
var result = tokenize("{{foo}} \\{{bar}} {{baz}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
|
||||
|
||||
shouldBeToken(result[3], "CONTENT", " ");
|
||||
shouldBeToken(result[4], "CONTENT", "{{bar}} ");
|
||||
});
|
||||
|
||||
it('supports escaping multiple delimiters', function() {
|
||||
var result = tokenize("{{foo}} \\{{bar}} \\{{baz}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
|
||||
|
||||
shouldBeToken(result[3], "CONTENT", " ");
|
||||
shouldBeToken(result[4], "CONTENT", "{{bar}} ");
|
||||
shouldBeToken(result[5], "CONTENT", "{{baz}}");
|
||||
});
|
||||
|
||||
it('supports escaping a triple stash', function() {
|
||||
var result = tokenize("{{foo}} \\{{{bar}}} {{baz}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
|
||||
|
||||
shouldBeToken(result[4], "CONTENT", "{{{bar}}} ");
|
||||
});
|
||||
|
||||
it('supports escaping escape character', function() {
|
||||
var result = tokenize("{{foo}} \\\\{{bar}} {{baz}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
|
||||
|
||||
shouldBeToken(result[3], "CONTENT", " \\");
|
||||
shouldBeToken(result[5], "ID", "bar");
|
||||
});
|
||||
|
||||
it('supports escaping multiple escape characters', function() {
|
||||
var result = tokenize("{{foo}} \\\\{{bar}} \\\\{{baz}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
|
||||
|
||||
shouldBeToken(result[3], "CONTENT", " \\");
|
||||
shouldBeToken(result[5], "ID", "bar");
|
||||
shouldBeToken(result[7], "CONTENT", " \\");
|
||||
shouldBeToken(result[9], "ID", "baz");
|
||||
});
|
||||
|
||||
it('supports escaped mustaches after escaped escape characters', function() {
|
||||
var result = tokenize("{{foo}} \\\\{{bar}} \\{{baz}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT']);
|
||||
|
||||
shouldBeToken(result[3], "CONTENT", " \\");
|
||||
shouldBeToken(result[4], "OPEN", "{{");
|
||||
shouldBeToken(result[5], "ID", "bar");
|
||||
shouldBeToken(result[7], "CONTENT", " ");
|
||||
shouldBeToken(result[8], "CONTENT", "{{baz}}");
|
||||
});
|
||||
|
||||
it('supports escaped escape characters after escaped mustaches', function() {
|
||||
var result = tokenize("{{foo}} \\{{bar}} \\\\{{baz}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
|
||||
|
||||
shouldBeToken(result[4], "CONTENT", "{{bar}} ");
|
||||
shouldBeToken(result[5], "CONTENT", "\\");
|
||||
shouldBeToken(result[6], "OPEN", "{{");
|
||||
shouldBeToken(result[7], "ID", "baz");
|
||||
});
|
||||
|
||||
it('supports escaped escape character on a triple stash', function() {
|
||||
var result = tokenize("{{foo}} \\\\{{{bar}}} {{baz}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN_UNESCAPED', 'ID', 'CLOSE_UNESCAPED', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
|
||||
|
||||
shouldBeToken(result[3], "CONTENT", " \\");
|
||||
shouldBeToken(result[5], "ID", "bar");
|
||||
});
|
||||
|
||||
it('tokenizes a simple path', function() {
|
||||
var result = tokenize("{{foo/bar}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('allows dot notation', function() {
|
||||
var result = tokenize("{{foo.bar}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
|
||||
|
||||
shouldMatchTokens(tokenize("{{foo.bar.baz}}"), ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('allows path literals with []', function() {
|
||||
var result = tokenize("{{foo.[bar]}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('allows multiple path literals on a line with []', function() {
|
||||
var result = tokenize("{{foo.[bar]}}{{foo.[baz]}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE', 'OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('tokenizes {{.}} as OPEN ID CLOSE', function() {
|
||||
var result = tokenize("{{.}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('tokenizes a path as "OPEN (ID SEP)* ID CLOSE"', function() {
|
||||
var result = tokenize("{{../foo/bar}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "..");
|
||||
});
|
||||
|
||||
it('tokenizes a path with .. as a parent path', function() {
|
||||
var result = tokenize("{{../foo.bar}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "..");
|
||||
});
|
||||
|
||||
it('tokenizes a path with this/foo as OPEN ID SEP ID CLOSE', function() {
|
||||
var result = tokenize("{{this/foo}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'SEP', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "this");
|
||||
shouldBeToken(result[3], "ID", "foo");
|
||||
});
|
||||
|
||||
it('tokenizes a simple mustache with spaces as "OPEN ID CLOSE"', function() {
|
||||
var result = tokenize("{{ foo }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "foo");
|
||||
});
|
||||
|
||||
it('tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"', function() {
|
||||
var result = tokenize("{{ foo \n bar }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "foo");
|
||||
});
|
||||
|
||||
it('tokenizes raw content as "CONTENT"', function() {
|
||||
var result = tokenize("foo {{ bar }} baz");
|
||||
shouldMatchTokens(result, ['CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT']);
|
||||
shouldBeToken(result[0], "CONTENT", "foo ");
|
||||
shouldBeToken(result[4], "CONTENT", " baz");
|
||||
});
|
||||
|
||||
it('tokenizes a partial as "OPEN_PARTIAL ID CLOSE"', function() {
|
||||
var result = tokenize("{{> foo}}");
|
||||
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"', function() {
|
||||
var result = tokenize("{{> foo bar }}");
|
||||
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"', function() {
|
||||
var result = tokenize("{{>foo}}");
|
||||
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() {
|
||||
var result = tokenize("{{>foo }}");
|
||||
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"', function() {
|
||||
var result = tokenize("{{>foo/bar.baz }}");
|
||||
shouldMatchTokens(result, ['OPEN_PARTIAL', 'ID', 'SEP', 'ID', 'SEP', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('tokenizes a comment as "COMMENT"', function() {
|
||||
var result = tokenize("foo {{! this is a comment }} bar {{ baz }}");
|
||||
shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "COMMENT", " this is a comment ");
|
||||
});
|
||||
|
||||
it('tokenizes a block comment as "COMMENT"', function() {
|
||||
var result = tokenize("foo {{!-- this is a {{comment}} --}} bar {{ baz }}");
|
||||
shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "COMMENT", " this is a {{comment}} ");
|
||||
});
|
||||
|
||||
it('tokenizes a block comment with whitespace as "COMMENT"', function() {
|
||||
var result = tokenize("foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}");
|
||||
shouldMatchTokens(result, ['CONTENT', 'COMMENT', 'CONTENT', 'OPEN', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "COMMENT", " this is a\n{{comment}}\n");
|
||||
});
|
||||
|
||||
it('tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE', function() {
|
||||
var result = tokenize("{{#foo}}content{{/foo}}");
|
||||
shouldMatchTokens(result, ['OPEN_BLOCK', 'ID', 'CLOSE', 'CONTENT', 'OPEN_ENDBLOCK', 'ID', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('tokenizes inverse sections as "OPEN_INVERSE CLOSE"', function() {
|
||||
shouldMatchTokens(tokenize("{{^}}"), ['OPEN_INVERSE', 'CLOSE']);
|
||||
shouldMatchTokens(tokenize("{{else}}"), ['OPEN_INVERSE', 'CLOSE']);
|
||||
shouldMatchTokens(tokenize("{{ else }}"), ['OPEN_INVERSE', 'CLOSE']);
|
||||
});
|
||||
|
||||
it('tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"', function() {
|
||||
var result = tokenize("{{^foo}}");
|
||||
shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "foo");
|
||||
});
|
||||
|
||||
it('tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"', function() {
|
||||
var result = tokenize("{{^ foo }}");
|
||||
shouldMatchTokens(result, ['OPEN_INVERSE', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "foo");
|
||||
});
|
||||
|
||||
it('tokenizes mustaches with params as "OPEN ID ID ID CLOSE"', function() {
|
||||
var result = tokenize("{{ foo bar baz }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "foo");
|
||||
shouldBeToken(result[2], "ID", "bar");
|
||||
shouldBeToken(result[3], "ID", "baz");
|
||||
});
|
||||
|
||||
it('tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"', function() {
|
||||
var result = tokenize("{{ foo bar \"baz\" }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
|
||||
shouldBeToken(result[3], "STRING", "baz");
|
||||
});
|
||||
|
||||
it('tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"', function() {
|
||||
var result = tokenize("{{ foo bar \'baz\' }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
|
||||
shouldBeToken(result[3], "STRING", "baz");
|
||||
});
|
||||
|
||||
it('tokenizes String params with spaces inside as "STRING"', function() {
|
||||
var result = tokenize("{{ foo bar \"baz bat\" }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'STRING', 'CLOSE']);
|
||||
shouldBeToken(result[3], "STRING", "baz bat");
|
||||
});
|
||||
|
||||
it('tokenizes String params with escapes quotes as STRING', function() {
|
||||
var result = tokenize('{{ foo "bar\\"baz" }}');
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
|
||||
shouldBeToken(result[2], "STRING", 'bar"baz');
|
||||
});
|
||||
|
||||
it('tokenizes String params using single quotes with escapes quotes as STRING', function() {
|
||||
var result = tokenize("{{ foo 'bar\\'baz' }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'STRING', 'CLOSE']);
|
||||
shouldBeToken(result[2], "STRING", "bar'baz");
|
||||
});
|
||||
|
||||
it('tokenizes numbers', function() {
|
||||
var result = tokenize('{{ foo 1 }}');
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
|
||||
shouldBeToken(result[2], "NUMBER", "1");
|
||||
|
||||
result = tokenize('{{ foo 1.1 }}');
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
|
||||
shouldBeToken(result[2], "NUMBER", "1.1");
|
||||
|
||||
result = tokenize('{{ foo -1 }}');
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
|
||||
shouldBeToken(result[2], "NUMBER", "-1");
|
||||
|
||||
result = tokenize('{{ foo -1.1 }}');
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'NUMBER', 'CLOSE']);
|
||||
shouldBeToken(result[2], "NUMBER", "-1.1");
|
||||
});
|
||||
|
||||
it('tokenizes booleans', function() {
|
||||
var result = tokenize('{{ foo true }}');
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
|
||||
shouldBeToken(result[2], "BOOLEAN", "true");
|
||||
|
||||
result = tokenize('{{ foo false }}');
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'BOOLEAN', 'CLOSE']);
|
||||
shouldBeToken(result[2], "BOOLEAN", "false");
|
||||
});
|
||||
|
||||
it('tokenizes hash arguments', function() {
|
||||
var result = tokenize("{{ foo bar=baz }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
|
||||
|
||||
result = tokenize("{{ foo bar baz=bat }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
|
||||
|
||||
result = tokenize("{{ foo bar baz=1 }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'NUMBER', 'CLOSE']);
|
||||
|
||||
result = tokenize("{{ foo bar baz=true }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
|
||||
|
||||
result = tokenize("{{ foo bar baz=false }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'BOOLEAN', 'CLOSE']);
|
||||
|
||||
result = tokenize("{{ foo bar\n baz=bat }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'CLOSE']);
|
||||
|
||||
result = tokenize("{{ foo bar baz=\"bat\" }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
|
||||
|
||||
result = tokenize("{{ foo bar baz=\"bat\" bam=wot }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'STRING', 'ID', 'EQUALS', 'ID', 'CLOSE']);
|
||||
|
||||
result = tokenize("{{foo omg bar=baz bat=\"bam\"}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'ID', 'EQUALS', 'ID', 'ID', 'EQUALS', 'STRING', 'CLOSE']);
|
||||
shouldBeToken(result[2], "ID", "omg");
|
||||
});
|
||||
|
||||
it('tokenizes special @ identifiers', function() {
|
||||
var result = tokenize("{{ @foo }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'DATA', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[2], "ID", "foo");
|
||||
|
||||
result = tokenize("{{ foo @bar }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'DATA', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[3], "ID", "bar");
|
||||
|
||||
result = tokenize("{{ foo bar=@baz }}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'ID', 'EQUALS', 'DATA', 'ID', 'CLOSE']);
|
||||
shouldBeToken(result[5], "ID", "baz");
|
||||
});
|
||||
|
||||
it('does not time out in a mustache with a single } followed by EOF', function() {
|
||||
shouldMatchTokens(tokenize("{{foo}"), ['OPEN', 'ID']);
|
||||
});
|
||||
|
||||
it('does not time out in a mustache when invalid ID characters are used', function() {
|
||||
shouldMatchTokens(tokenize("{{foo & }}"), ['OPEN', 'ID']);
|
||||
});
|
||||
|
||||
it('tokenizes subexpressions', function() {
|
||||
var result = tokenize("{{foo (bar)}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "foo");
|
||||
shouldBeToken(result[3], "ID", "bar");
|
||||
|
||||
result = tokenize("{{foo (a-x b-y)}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
|
||||
shouldBeToken(result[1], "ID", "foo");
|
||||
shouldBeToken(result[3], "ID", "a-x");
|
||||
shouldBeToken(result[4], "ID", "b-y");
|
||||
});
|
||||
|
||||
it('tokenizes nested subexpressions', function() {
|
||||
var result = tokenize("{{foo (bar (lol rofl)) (baz)}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'OPEN_SEXPR', 'ID', 'ID', 'CLOSE_SEXPR', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'CLOSE_SEXPR', 'CLOSE']);
|
||||
shouldBeToken(result[3], "ID", "bar");
|
||||
shouldBeToken(result[5], "ID", "lol");
|
||||
shouldBeToken(result[6], "ID", "rofl");
|
||||
shouldBeToken(result[10], "ID", "baz");
|
||||
});
|
||||
|
||||
it('tokenizes nested subexpressions: literals', function() {
|
||||
var result = tokenize("{{foo (bar (lol true) false) (baz 1) (blah 'b') (blorg \"c\")}}");
|
||||
shouldMatchTokens(result, ['OPEN', 'ID', 'OPEN_SEXPR', 'ID', 'OPEN_SEXPR', 'ID', 'BOOLEAN', 'CLOSE_SEXPR', 'BOOLEAN', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'NUMBER', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'OPEN_SEXPR', 'ID', 'STRING', 'CLOSE_SEXPR', 'CLOSE']);
|
||||
});
|
||||
});
|
||||
@@ -1,148 +0,0 @@
|
||||
require "spec_helper"
|
||||
require "timeout"
|
||||
|
||||
describe "Tokenizer" do
|
||||
let(:parser) { @context["handlebars"] }
|
||||
let(:lexer) { @context["handlebars"]["lexer"] }
|
||||
|
||||
Token = Struct.new(:name, :text)
|
||||
|
||||
def tokenize(string)
|
||||
lexer.setInput(string)
|
||||
out = []
|
||||
|
||||
while result = parser.terminals_[lexer.lex] and result != "EOF"
|
||||
out << Token.new(result, lexer.yytext)
|
||||
end
|
||||
|
||||
out
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :match_tokens do |tokens|
|
||||
match do |result|
|
||||
result.map(&:name).should == tokens
|
||||
end
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :be_token do |name, string|
|
||||
match do |token|
|
||||
token.name.should == name
|
||||
token.text.should == string
|
||||
end
|
||||
end
|
||||
|
||||
it "tokenizes a simple mustache as 'OPEN ID CLOSE'" do
|
||||
result = tokenize("{{foo}}")
|
||||
result.should match_tokens(%w(OPEN ID CLOSE))
|
||||
result[1].should be_token("ID", "foo")
|
||||
end
|
||||
|
||||
it "tokenizes a path as 'OPEN (ID SEP)* ID CLOSE'" do
|
||||
result = tokenize("{{../foo/bar}}")
|
||||
result.should match_tokens(%w(OPEN ID SEP ID SEP ID CLOSE))
|
||||
result[1].should be_token("ID", "..")
|
||||
end
|
||||
|
||||
it "tokenizes a path with this/foo as OPEN ID SEP ID CLOSE" do
|
||||
result = tokenize("{{this/foo}}")
|
||||
result.should match_tokens(%w(OPEN ID SEP ID CLOSE))
|
||||
result[1].should be_token("ID", "this")
|
||||
result[3].should be_token("ID", "foo")
|
||||
end
|
||||
|
||||
it "tokenizes a simple mustahe with spaces as 'OPEN ID CLOSE'" do
|
||||
result = tokenize("{{ foo }}")
|
||||
result.should match_tokens(%w(OPEN ID CLOSE))
|
||||
result[1].should be_token("ID", "foo")
|
||||
end
|
||||
|
||||
it "tokenizes raw content as 'CONTENT'" do
|
||||
result = tokenize("foo {{ bar }} baz")
|
||||
result.should match_tokens(%w(CONTENT OPEN ID CLOSE CONTENT))
|
||||
result[0].should be_token("CONTENT", "foo ")
|
||||
result[4].should be_token("CONTENT", " baz")
|
||||
end
|
||||
|
||||
it "tokenizes a partial as 'OPEN_PARTIAL ID CLOSE'" do
|
||||
result = tokenize("{{> foo}}")
|
||||
result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
|
||||
end
|
||||
|
||||
it "tokenizes a partial with context as 'OPEN_PARTIAL ID ID CLOSE'" do
|
||||
result = tokenize("{{> foo bar }}")
|
||||
result.should match_tokens(%w(OPEN_PARTIAL ID ID CLOSE))
|
||||
end
|
||||
|
||||
it "tokenizes a partial without spaces as 'OPEN_PARTIAL ID CLOSE'" do
|
||||
result = tokenize("{{>foo}}")
|
||||
result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
|
||||
end
|
||||
|
||||
it "tokenizes a partial space at the end as 'OPEN_PARTIAL ID CLOSE'" do
|
||||
result = tokenize("{{>foo }}")
|
||||
result.should match_tokens(%w(OPEN_PARTIAL ID CLOSE))
|
||||
end
|
||||
|
||||
it "tokenizes a comment as 'COMMENT'" do
|
||||
result = tokenize("foo {{! this is a comment }} bar {{ baz }}")
|
||||
result.should match_tokens(%w(CONTENT COMMENT CONTENT OPEN ID CLOSE))
|
||||
result[1].should be_token("COMMENT", " this is a comment ")
|
||||
end
|
||||
|
||||
it "tokenizes open and closing blocks as 'OPEN_BLOCK ID CLOSE ... OPEN_ENDBLOCK ID CLOSE'" do
|
||||
result = tokenize("{{#foo}}content{{/foo}}")
|
||||
result.should match_tokens(%w(OPEN_BLOCK ID CLOSE CONTENT OPEN_ENDBLOCK ID CLOSE))
|
||||
end
|
||||
|
||||
it "tokenizes inverse sections as 'OPEN_INVERSE CLOSE'" do
|
||||
tokenize("{{^}}").should match_tokens(%w(OPEN_INVERSE CLOSE))
|
||||
tokenize("{{else}}").should match_tokens(%w(OPEN_INVERSE CLOSE))
|
||||
tokenize("{{ else }}").should match_tokens(%w(OPEN_INVERSE CLOSE))
|
||||
end
|
||||
|
||||
it "tokenizes inverse sections with ID as 'OPEN_INVERSE ID CLOSE'" do
|
||||
result = tokenize("{{^foo}}")
|
||||
result.should match_tokens(%w(OPEN_INVERSE ID CLOSE))
|
||||
result[1].should be_token("ID", "foo")
|
||||
end
|
||||
|
||||
it "tokenizes inverse sections with ID and spaces as 'OPEN_INVERSE ID CLOSE'" do
|
||||
result = tokenize("{{^ foo }}")
|
||||
result.should match_tokens(%w(OPEN_INVERSE ID CLOSE))
|
||||
result[1].should be_token("ID", "foo")
|
||||
end
|
||||
|
||||
it "tokenizes mustaches with params as 'OPEN ID ID ID CLOSE'" do
|
||||
result = tokenize("{{ foo bar baz }}")
|
||||
result.should match_tokens(%w(OPEN ID ID ID CLOSE))
|
||||
result[1].should be_token("ID", "foo")
|
||||
result[2].should be_token("ID", "bar")
|
||||
result[3].should be_token("ID", "baz")
|
||||
end
|
||||
|
||||
it "tokenizes mustaches with String params as 'OPEN ID ID STRING CLOSE'" do
|
||||
result = tokenize("{{ foo bar \"baz\" }}")
|
||||
result.should match_tokens(%w(OPEN ID ID STRING CLOSE))
|
||||
result[3].should be_token("STRING", "baz")
|
||||
end
|
||||
|
||||
it "tokenizes String params with spaces inside as 'STRING'" do
|
||||
result = tokenize("{{ foo bar \"baz bat\" }}")
|
||||
result.should match_tokens(%w(OPEN ID ID STRING CLOSE))
|
||||
result[3].should be_token("STRING", "baz bat")
|
||||
end
|
||||
|
||||
it "tokenizes String params with escapes quotes as 'STRING'" do
|
||||
result = tokenize(%|{{ foo "bar\\"baz" }}|)
|
||||
result.should match_tokens(%w(OPEN ID STRING CLOSE))
|
||||
result[2].should be_token("STRING", %{bar"baz})
|
||||
end
|
||||
|
||||
it "does not time out in a mustache with a single } followed by EOF" do
|
||||
Timeout.timeout(1) { tokenize("{{foo}").should match_tokens(%w(OPEN ID)) }
|
||||
end
|
||||
|
||||
it "does not time out in a mustache when invalid ID characters are used" do
|
||||
Timeout.timeout(1) { tokenize("{{foo & }}").should match_tokens(%w(OPEN ID)) }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,163 @@
|
||||
/*global CompilerContext */
|
||||
describe('track ids', function() {
|
||||
var context;
|
||||
beforeEach(function() {
|
||||
context = {is: {a: 'foo'}, slave: {driver: 'bar'}};
|
||||
});
|
||||
|
||||
it('should not include anything without the flag', function() {
|
||||
var template = CompilerContext.compile('{{wycats is.a slave.driver}}');
|
||||
|
||||
var helpers = {
|
||||
wycats: function(passiveVoice, noun, options) {
|
||||
equal(options.ids, undefined);
|
||||
equal(options.hashIds, undefined);
|
||||
|
||||
return 'success';
|
||||
}
|
||||
};
|
||||
|
||||
equals(template({}, {helpers: helpers}), 'success');
|
||||
});
|
||||
it('should include argument ids', function() {
|
||||
var template = CompilerContext.compile('{{wycats is.a slave.driver}}', {trackIds: true});
|
||||
|
||||
var helpers = {
|
||||
wycats: function(passiveVoice, noun, options) {
|
||||
equal(options.ids[0], 'is.a');
|
||||
equal(options.ids[1], 'slave.driver');
|
||||
|
||||
return "HELP ME MY BOSS " + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
|
||||
}
|
||||
};
|
||||
|
||||
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS is.a:foo slave.driver:bar');
|
||||
});
|
||||
it('should include hash ids', function() {
|
||||
var template = CompilerContext.compile('{{wycats bat=is.a baz=slave.driver}}', {trackIds: true});
|
||||
|
||||
var helpers = {
|
||||
wycats: function(options) {
|
||||
equal(options.hashIds.bat, 'is.a');
|
||||
equal(options.hashIds.baz, 'slave.driver');
|
||||
|
||||
return "HELP ME MY BOSS " + options.hashIds.bat + ':' + options.hash.bat + ' ' + options.hashIds.baz + ':' + options.hash.baz;
|
||||
}
|
||||
};
|
||||
|
||||
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS is.a:foo slave.driver:bar');
|
||||
});
|
||||
it('should note ../ and ./ references', function() {
|
||||
var template = CompilerContext.compile('{{wycats ./is.a ../slave.driver}}', {trackIds: true});
|
||||
|
||||
var helpers = {
|
||||
wycats: function(passiveVoice, noun, options) {
|
||||
equal(options.ids[0], 'is.a');
|
||||
equal(options.ids[1], '../slave.driver');
|
||||
|
||||
return "HELP ME MY BOSS " + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
|
||||
}
|
||||
};
|
||||
|
||||
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS is.a:foo ../slave.driver:undefined');
|
||||
});
|
||||
it('should note @data references', function() {
|
||||
var template = CompilerContext.compile('{{wycats @is.a @slave.driver}}', {trackIds: true});
|
||||
|
||||
var helpers = {
|
||||
wycats: function(passiveVoice, noun, options) {
|
||||
equal(options.ids[0], '@is.a');
|
||||
equal(options.ids[1], '@slave.driver');
|
||||
|
||||
return "HELP ME MY BOSS " + options.ids[0] + ':' + passiveVoice + ' ' + options.ids[1] + ':' + noun;
|
||||
}
|
||||
};
|
||||
|
||||
equals(template({}, {helpers: helpers, data:context}), 'HELP ME MY BOSS @is.a:foo @slave.driver:bar');
|
||||
});
|
||||
|
||||
it('should return null for constants', function() {
|
||||
var template = CompilerContext.compile('{{wycats 1 "foo" key=false}}', {trackIds: true});
|
||||
|
||||
var helpers = {
|
||||
wycats: function(passiveVoice, noun, options) {
|
||||
equal(options.ids[0], null);
|
||||
equal(options.ids[1], null);
|
||||
equal(options.hashIds.key, null);
|
||||
|
||||
return "HELP ME MY BOSS " + passiveVoice + ' ' + noun + ' ' + options.hash.key;
|
||||
}
|
||||
};
|
||||
|
||||
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS 1 foo false');
|
||||
});
|
||||
it('should return true for subexpressions', function() {
|
||||
var template = CompilerContext.compile('{{wycats (sub)}}', {trackIds: true});
|
||||
|
||||
var helpers = {
|
||||
sub: function() { return 1; },
|
||||
wycats: function(passiveVoice, options) {
|
||||
equal(options.ids[0], true);
|
||||
|
||||
return "HELP ME MY BOSS " + passiveVoice;
|
||||
}
|
||||
};
|
||||
|
||||
equals(template(context, {helpers: helpers}), 'HELP ME MY BOSS 1');
|
||||
});
|
||||
|
||||
describe('builtin helpers', function() {
|
||||
var helpers = {
|
||||
wycats: function(name, options) {
|
||||
return name + ':' + options.data.contextPath + '\n';
|
||||
}
|
||||
};
|
||||
|
||||
describe('#each', function() {
|
||||
it('should track contextPath for arrays', function() {
|
||||
var template = CompilerContext.compile('{{#each array}}{{wycats name}}{{/each}}', {trackIds: true});
|
||||
|
||||
equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:array.0\nbar:array.1\n');
|
||||
});
|
||||
it('should track contextPath for keys', function() {
|
||||
var template = CompilerContext.compile('{{#each object}}{{wycats name}}{{/each}}', {trackIds: true});
|
||||
|
||||
equals(template({object: {foo: {name: 'foo'}, bar: {name: 'bar'}}}, {helpers: helpers}), 'foo:object.foo\nbar:object.bar\n');
|
||||
});
|
||||
it('should handle nesting', function() {
|
||||
var template = CompilerContext.compile('{{#each .}}{{#each .}}{{wycats name}}{{/each}}{{/each}}', {trackIds: true});
|
||||
|
||||
equals(template({array: [{name: 'foo'}, {name: 'bar'}]}, {helpers: helpers}), 'foo:.array..0\nbar:.array..1\n');
|
||||
});
|
||||
});
|
||||
describe('#with', function() {
|
||||
it('should track contextPath', function() {
|
||||
var template = CompilerContext.compile('{{#with field}}{{wycats name}}{{/with}}', {trackIds: true});
|
||||
|
||||
equals(template({field: {name: 'foo'}}, {helpers: helpers}), 'foo:field\n');
|
||||
});
|
||||
it('should handle nesting', function() {
|
||||
var template = CompilerContext.compile('{{#with bat}}{{#with field}}{{wycats name}}{{/with}}{{/with}}', {trackIds: true});
|
||||
|
||||
equals(template({bat: {field: {name: 'foo'}}}, {helpers: helpers}), 'foo:bat.field\n');
|
||||
});
|
||||
});
|
||||
describe('#blockHelperMissing', function() {
|
||||
it('should track contextPath for arrays', function() {
|
||||
var template = CompilerContext.compile('{{#field}}{{wycats name}}{{/field}}', {trackIds: true});
|
||||
|
||||
equals(template({field: [{name: 'foo'}]}, {helpers: helpers}), 'foo:field.0\n');
|
||||
});
|
||||
it('should track contextPath for keys', function() {
|
||||
var template = CompilerContext.compile('{{#field}}{{wycats name}}{{/field}}', {trackIds: true});
|
||||
|
||||
equals(template({field: {name: 'foo'}}, {helpers: helpers}), 'foo:field\n');
|
||||
});
|
||||
it('should handle nesting', function() {
|
||||
var template = CompilerContext.compile('{{#bat}}{{#field}}{{wycats name}}{{/field}}{{/bat}}', {trackIds: true});
|
||||
|
||||
equals(template({bat: {field: {name: 'foo'}}}, {helpers: helpers}), 'foo:bat.field\n');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/*global Handlebars, shouldCompileTo */
|
||||
|
||||
describe('utils', function() {
|
||||
describe('#SafeString', function() {
|
||||
it("constructing a safestring from a string and checking its type", function() {
|
||||
var safe = new Handlebars.SafeString("testing 1, 2, 3");
|
||||
if (!(safe instanceof Handlebars.SafeString)) {
|
||||
throw new Error('Must be instance of SafeString');
|
||||
}
|
||||
equals(safe == 'testing 1, 2, 3', true, 'SafeString is equivalent to its underlying string');
|
||||
});
|
||||
|
||||
it("it should not escape SafeString properties", function() {
|
||||
var name = new Handlebars.SafeString("<em>Sean O'Malley</em>");
|
||||
|
||||
shouldCompileTo('{{name}}', [{ name: name }], "<em>Sean O'Malley</em>");
|
||||
});
|
||||
});
|
||||
|
||||
describe('#escapeExpression', function() {
|
||||
it('shouhld escape html', function() {
|
||||
equals(Handlebars.Utils.escapeExpression('foo<&"\'>'), 'foo<&"'>');
|
||||
});
|
||||
it('should not escape SafeString', function() {
|
||||
var string = new Handlebars.SafeString('foo<&"\'>');
|
||||
equals(Handlebars.Utils.escapeExpression(string), 'foo<&"\'>');
|
||||
|
||||
});
|
||||
it('should handle falsy', function() {
|
||||
equals(Handlebars.Utils.escapeExpression(''), '');
|
||||
equals(Handlebars.Utils.escapeExpression(undefined), '');
|
||||
equals(Handlebars.Utils.escapeExpression(null), '');
|
||||
equals(Handlebars.Utils.escapeExpression(false), '');
|
||||
|
||||
equals(Handlebars.Utils.escapeExpression(0), '0');
|
||||
});
|
||||
it('should handle empty objects', function() {
|
||||
equals(Handlebars.Utils.escapeExpression({}), {}.toString());
|
||||
equals(Handlebars.Utils.escapeExpression([]), [].toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isEmpty', function() {
|
||||
it('should not be empty', function() {
|
||||
equals(Handlebars.Utils.isEmpty(undefined), true);
|
||||
equals(Handlebars.Utils.isEmpty(null), true);
|
||||
equals(Handlebars.Utils.isEmpty(false), true);
|
||||
equals(Handlebars.Utils.isEmpty(''), true);
|
||||
equals(Handlebars.Utils.isEmpty([]), true);
|
||||
});
|
||||
|
||||
it('should be empty', function() {
|
||||
equals(Handlebars.Utils.isEmpty(0), false);
|
||||
equals(Handlebars.Utils.isEmpty([1]), false);
|
||||
equals(Handlebars.Utils.isEmpty('foo'), false);
|
||||
equals(Handlebars.Utils.isEmpty({bar: 1}), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user