Compare commits
736 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9c3ee5edf | |||
| 41d8516449 | |||
| 8b7ef6201e | |||
| 5fb5a4d73d | |||
| 86971d1902 | |||
| 9071332e1e | |||
| ae62c0c89f | |||
| 027a8d69f5 | |||
| 4c4ef34e8b | |||
| 4d519fd6c9 | |||
| 4ffa5499a4 | |||
| 6e7e7a005e | |||
| 20a0d8ee64 | |||
| 56fbd7a9b2 | |||
| 293617aa22 | |||
| 7ae40e7164 | |||
| 361bc707d3 | |||
| 04dc60f128 | |||
| 70537168a0 | |||
| 6e33697c5c | |||
| 8680e884a1 | |||
| ec693190b5 | |||
| 4dfdaf6010 | |||
| 9540b2c00a | |||
| 9445d04efa | |||
| 7a7ee02905 | |||
| a6605064c1 | |||
| 4438caae92 | |||
| 212f376b77 | |||
| 35310a5980 | |||
| 4ccb76f53c | |||
| 1ac4fb24aa | |||
| 1aef4ef1c9 | |||
| c421cb10b6 | |||
| 739ae86ffe | |||
| 3dc4a78812 | |||
| 97c0691994 | |||
| 79c9b73e0d | |||
| 1e20e498c4 | |||
| 5e6f3c8ba4 | |||
| c15bd7391d | |||
| 66f8ffc4c2 | |||
| 93c2428626 | |||
| c04d0b6631 | |||
| 05fc742588 | |||
| d590984eb6 | |||
| a67ec2948f | |||
| c432f33149 | |||
| 1bc6e63d18 | |||
| d5831e65a1 | |||
| 15d13e2fa2 | |||
| 5e100e88fb | |||
| 18b5d88f07 | |||
| 201fc044ee | |||
| bb28a85d97 | |||
| 414e48c022 | |||
| 7a5957cb27 | |||
| 2ae4ba0a09 | |||
| 40ac799134 | |||
| 990ca04065 | |||
| efa2fa7ade | |||
| 1de86c435a | |||
| df3ddb214e | |||
| fa3467c649 | |||
| bba93dad9f | |||
| d2db781f82 | |||
| 11d1b36fc3 | |||
| 509aa3b52d | |||
| d916a8dece | |||
| ff9fa3954d | |||
| 9b0e15cd78 | |||
| 795bef96ac | |||
| 45f081ba54 | |||
| 308715a495 | |||
| abbbd6bf04 | |||
| 569fce4072 | |||
| 5b88230c32 | |||
| 097854f30f | |||
| 6fc6bcc4b2 | |||
| 0045ebe3fb | |||
| 1cefb68b47 | |||
| 78bab74e11 | |||
| a43f2da4c9 | |||
| 7ff81409c7 | |||
| 0661ada8e6 | |||
| ec1c180173 | |||
| d5969cc2f5 | |||
| b23f6e4e9f | |||
| da1e7ed52b | |||
| e6d3195df0 | |||
| 81117ce07b | |||
| 6dbee54f6a | |||
| 5f9890d157 | |||
| db1201d84e | |||
| f3ab2bc57f | |||
| a4c617f1df | |||
| e1b993a0ae | |||
| 42db525b30 | |||
| 76691cc52b | |||
| 82f3706eff | |||
| a0ac2ba83b | |||
| b89b5f76bb | |||
| 2bfdcb582c | |||
| 604129a10d | |||
| 563141f1c4 | |||
| a0d49f4ee5 | |||
| 23445a82c4 | |||
| 0c51760fc5 | |||
| 2ce0d7985a | |||
| f8eb111f89 | |||
| 9e9ba88ede | |||
| 8ed209bba4 | |||
| 0260014524 | |||
| c6cafd10d4 | |||
| e46f0322ed | |||
| 0ecb96c42a | |||
| 191a34d060 | |||
| 484997e3fc | |||
| 7482220201 | |||
| 6606413eef | |||
| 6dce371011 | |||
| 6b2800ef70 | |||
| 29c187f076 | |||
| 7d7217007a | |||
| e783afc43c | |||
| 4183228262 | |||
| b92b63f94f | |||
| 4ddab3a2d9 | |||
| 3b381ac925 | |||
| 1d803dd1b8 | |||
| 074f3ec723 | |||
| caf6701c7b | |||
| 99d72711be | |||
| 7ed52215cb | |||
| 50e9140ee3 | |||
| 6f81c4dc17 | |||
| 7a1dc0582c | |||
| d9f2431414 | |||
| cec2f13361 | |||
| 6939e10c9b | |||
| 2ee04485d6 | |||
| d98f3ea94f | |||
| 83b6ae9f64 | |||
| 92397aa351 | |||
| 5e64329a8c | |||
| c0e0dec1fb | |||
| 90c781c4e2 | |||
| 74636c4098 | |||
| bc989a5761 | |||
| 586ff43bec | |||
| e856423ee1 | |||
| 8e903ba50e | |||
| 68d898764e | |||
| 9bde3b9982 | |||
| c4c935175e | |||
| c7986b0eab | |||
| a1dfc2f79d | |||
| bda06a8385 | |||
| 0dd0df60f5 | |||
| d4956466e4 | |||
| 0b4c66f88a | |||
| f21b90b061 | |||
| 1df6c44007 | |||
| 53f96a2367 | |||
| f6b4b705e0 | |||
| fc3c62af0a | |||
| fe2f7bde9e | |||
| 070ef1aee3 | |||
| d05e8015b4 | |||
| cb52d62158 | |||
| 232dddf6e5 | |||
| 53d9721f18 | |||
| 981132c08a | |||
| ff1739fe67 | |||
| 3df0b5bab7 | |||
| f9a8791280 | |||
| 3140391c16 | |||
| 6fa2b2c581 | |||
| c099015f6a | |||
| 9e039ee13d | |||
| 8a53a56157 | |||
| ff04dbab24 | |||
| a9ec919118 | |||
| 676f167180 | |||
| 3a277f9295 | |||
| c469780107 | |||
| 539fa58daf | |||
| 3df3eb7c11 | |||
| eebbad7fd1 | |||
| 0cd9dbe150 | |||
| 5b783850e8 | |||
| 0c2fecd1cc | |||
| d1b17204e9 | |||
| 0292acc20c | |||
| f1e820819c | |||
| c28c722263 | |||
| 64ac166485 | |||
| 8cabd87c60 | |||
| 6986531992 | |||
| 42282c2807 | |||
| f2765085c4 | |||
| 8e28a95e4e | |||
| 782d34c3eb | |||
| b2d9b757e2 | |||
| 41cb005ec6 | |||
| d84573038d | |||
| e047bed5b9 | |||
| 3a9528479b | |||
| ba7f990190 | |||
| 0c76be560e | |||
| 01214bb3f6 | |||
| 2f183071b6 | |||
| 8e12db7564 | |||
| e2eb93748d | |||
| c8a2ea8a1e | |||
| 124d309a4d | |||
| f407b40abb | |||
| be09db8218 | |||
| 36e99a7853 | |||
| bfe847e3bd | |||
| 87c1050089 | |||
| f3d7d4b681 | |||
| 2d65d19fc6 | |||
| 498a6a7cbe | |||
| 52bd57395d | |||
| 618ca069fc | |||
| f3a07a3984 | |||
| eaf2c40e1d | |||
| aa0f15b348 | |||
| 9c5900ebc0 | |||
| b9e6611b1b | |||
| dd52b024d0 | |||
| 5ec1fd9499 | |||
| 36d881b601 | |||
| e9019bd6c0 | |||
| 798482c3d1 | |||
| 07641a3842 | |||
| b1b59869ec | |||
| 67e963f498 | |||
| b6974ce17f | |||
| b0bbfb3b9b | |||
| 3d383a956a | |||
| 79d5ac7789 | |||
| 91524988b4 | |||
| 39990739bd | |||
| dde42d8d8a | |||
| c11a9900e8 | |||
| e811f8c05f | |||
| f6e5a3fd80 | |||
| 9958465b34 | |||
| a9d6700ad6 | |||
| de6cc6bbc4 | |||
| 292c5c3dee | |||
| 30cffd6725 | |||
| a9275aecd6 | |||
| e670c491c2 | |||
| 8025fa324e | |||
| 51d2715e0a | |||
| 67ecdb8151 | |||
| 84fa5b6423 | |||
| b3290a1bd4 | |||
| d31f459704 | |||
| c401eb4043 | |||
| 5c0140cd95 | |||
| 66557f247c | |||
| b8aca3fc53 | |||
| e4671348c5 | |||
| ef223d83a8 | |||
| aa64b76214 | |||
| 979dec810e | |||
| 866a44d46f | |||
| 4c4eea9b47 | |||
| 696730244c | |||
| 5065ab4658 | |||
| 4f8474aa66 | |||
| f6a9f63213 | |||
| 352b26320e | |||
| aef4466a03 | |||
| bb8c106b5a | |||
| d399ff88f9 | |||
| 2d5534042b | |||
| 83bbe54273 | |||
| f94c95b38a | |||
| 28491b8268 | |||
| b7e3b98a13 | |||
| 7e69289727 | |||
| aabc163160 | |||
| 099c812e4b | |||
| b98eeadcdd | |||
| 83e125fe36 | |||
| 80ff9679cf | |||
| edda987835 | |||
| f48f5736dd | |||
| 463f63c1cb | |||
| 0c425859b9 | |||
| 27136eff07 | |||
| d32da0126e | |||
| 2a53165151 | |||
| e17fb75f6c | |||
| 93a5209e85 | |||
| e22a447cfe | |||
| b6e31a68a2 | |||
| 8a61f500a8 | |||
| b1ebe70f3d | |||
| 6d1d6d7aa3 | |||
| 05fdc87824 | |||
| 8e1ab7ed00 | |||
| b2ff78a037 | |||
| 96c4c09bd7 | |||
| ef8d638494 | |||
| 538543b0e8 | |||
| ed2d262608 | |||
| bbca3726d3 | |||
| 63c258b106 | |||
| 443570baa7 | |||
| b58ec66203 | |||
| a93afd1ffb | |||
| 71151b265d | |||
| 534c2cfc4d | |||
| c505b9cea5 | |||
| 46bd989252 | |||
| 8511c3f39c | |||
| baa9bd58b4 | |||
| c45b647cdd | |||
| 34b067a18e | |||
| 39ee4dfd34 | |||
| 2f2a19ee7e | |||
| 49e70f68af | |||
| 88149e8dc3 | |||
| 0371028b01 | |||
| 04011260f9 | |||
| 12f869effc | |||
| e03f0ea0d6 | |||
| 49562a8920 | |||
| 7e4cf218ff | |||
| 091a39a900 | |||
| 93753bdcde | |||
| 859eddb9af | |||
| 2e7ef509a1 | |||
| 16865b5dd8 | |||
| 5c36832fd0 | |||
| 879d85ac55 | |||
| e6a2f1b6bc | |||
| 5c69780a87 | |||
| b8706a1dcd | |||
| 55d0817352 | |||
| 4bfacf2a79 | |||
| ac0faab286 | |||
| cdf8c59c81 | |||
| 10ad819efc | |||
| 59a4d471cf | |||
| 4d3c37a353 | |||
| 4696467fc1 | |||
| 87e3e25038 | |||
| 46f414aa91 | |||
| f47ec8a5d7 | |||
| 08a9e156d3 | |||
| 106a7872d0 | |||
| 47b6613601 | |||
| 8011022c5e | |||
| b4d3f21b96 | |||
| cc67654d7a | |||
| 08b76b72ef | |||
| b72fd99871 | |||
| ce5895a932 | |||
| 8bccf4c791 | |||
| eb23363bf0 | |||
| 69a981ac3f | |||
| 2f36f34960 | |||
| 18f0d0463c | |||
| 2932fbac12 | |||
| fec7bd504b | |||
| 04699eb800 | |||
| 37fcb59ee8 | |||
| d6c66b41ca | |||
| c05ede82b3 | |||
| 72f221db59 | |||
| 6d1b29628a | |||
| 7111648548 | |||
| 621d2b7efc | |||
| 9c599e4a92 | |||
| 2608d59885 | |||
| 43d3002b63 | |||
| f7fc52a6d7 | |||
| d842e4fa3b | |||
| 985e9c8de8 | |||
| cb2faea9d9 | |||
| 887655c058 | |||
| dc284f9992 | |||
| 062dab65d4 | |||
| 73d0b94459 | |||
| 75ecb533b2 | |||
| c4f04b12ee | |||
| 385d0a7992 | |||
| 6efd6c4202 | |||
| aa05790c8e | |||
| 2916af4972 | |||
| 1c5bf4375e | |||
| 8810ff2076 | |||
| ea4fba1fdb | |||
| 3f5c8b6cfd | |||
| 3dda004d23 | |||
| ff3e85dda1 | |||
| 05e692b380 | |||
| 9a65557adc | |||
| 188a349947 | |||
| 8497fccdb4 | |||
| 199af87d77 | |||
| 49545fbc00 | |||
| 125b2b280b | |||
| 54cf2673f4 | |||
| 8fa92017c5 | |||
| a053e6a44a | |||
| e3ed3e82fb | |||
| 73755e0c8a | |||
| f951475103 | |||
| 77474771e6 | |||
| 55f9cb42fe | |||
| a8addddbd6 | |||
| ccd4a53cfb | |||
| 3928bf57f0 | |||
| dd6d008b74 | |||
| 45aabac358 | |||
| 55c9dc10ca | |||
| 32c232308f | |||
| 2eaa6e451d | |||
| 03ed6091c2 | |||
| 43c3e26596 | |||
| 9e584bb75b | |||
| e62f150f32 | |||
| 24ccc5200c | |||
| c627e72b03 | |||
| 6eeb634a36 | |||
| 2349b82f0f | |||
| 443714878b | |||
| 7a6f956b49 | |||
| 73e464e357 | |||
| 71d58e686c | |||
| adaba54be0 | |||
| 5be310a6ba | |||
| ee45e0379e | |||
| 848898c8d0 | |||
| deda90698b | |||
| 774b218aff | |||
| 6bb4873d67 | |||
| 54b4245482 | |||
| 39d75c189d | |||
| ae56620f91 | |||
| ee07ca1d7c | |||
| b6ca859f4b | |||
| c4f87d54bf | |||
| b662698e0c | |||
| 0198fb235c | |||
| fc5dce86ea | |||
| d918b1256d | |||
| c65305fbe8 | |||
| 111c4199ff | |||
| 8bc205fc01 | |||
| e69221af7d | |||
| 6ba048e55a | |||
| 8dda6d67e6 | |||
| 7c6793be8c | |||
| 123339519d | |||
| 12374765ed | |||
| 255447b147 | |||
| 32507ad644 | |||
| d06777d2b3 | |||
| b727f3c6e2 | |||
| 140393fcca | |||
| 2d308b7130 | |||
| 6a282416a8 | |||
| 40ebcfc6b7 | |||
| d23e57d215 | |||
| 77ca69e1d3 | |||
| c2ff6dc369 | |||
| af9ea7c886 | |||
| e1eb4f8e6f | |||
| 7303fd0f53 | |||
| 96bf8d7f6c | |||
| 74de507782 | |||
| 0155cd7392 | |||
| 974c8f70ff | |||
| e4cd237779 | |||
| 4dec3b36f4 | |||
| e275cfbf2e | |||
| 4d25127617 | |||
| 8f6deb2f8e | |||
| 3958d9f504 | |||
| 120a8e8cbd | |||
| 99dfafeafd | |||
| 379265414a | |||
| e09a34cca6 | |||
| b49bd14e55 | |||
| febd4f18ca | |||
| a633963b9f | |||
| b0db48f28e | |||
| c893984115 | |||
| a4ef8b7ef8 | |||
| a26a238b09 | |||
| 43c72308fb | |||
| b5579ab340 | |||
| dbf4b26768 | |||
| d6abba96d1 | |||
| 165e18ae38 | |||
| 90f08a4116 | |||
| 0dc8a08b3f | |||
| 6ffd67523d | |||
| bd857350a0 | |||
| d42261ae3e | |||
| 18b764c7ed | |||
| df5318f14c | |||
| 6783efbe91 | |||
| 72df7fce8a | |||
| 0ad4eac649 | |||
| 225b696077 | |||
| 33ed1e44cd | |||
| 44f02e50b4 | |||
| c06dd13717 | |||
| 26edc2e0d9 | |||
| 5992664bed | |||
| 7d2070900b | |||
| 1fcb8d6e37 | |||
| 7d81f7d695 | |||
| f4c2c0ee28 | |||
| 3b513c0e27 | |||
| ed49bd544f | |||
| 3667ab415b | |||
| 7aaf3a3f24 | |||
| d3608c0f72 | |||
| 07341f89c8 | |||
| d2b0da2954 | |||
| f312514e40 | |||
| 1354964bb6 | |||
| a14389c7e1 | |||
| 31d03baf9f | |||
| 833c429e88 | |||
| 6ba8ce45c7 | |||
| 269e3b917b | |||
| b7919d8b7c | |||
| 3fa1be6c33 | |||
| 754e28231d | |||
| 5823d77075 | |||
| f40766049a | |||
| 94cdace6d9 | |||
| b5de1556db | |||
| 5bf0a13277 | |||
| d8636b0302 | |||
| 4e2cb294ed | |||
| 1e52a0943b | |||
| 86cdaf2edf | |||
| 508c49cc44 | |||
| e4e3401573 | |||
| 8e5f242743 | |||
| 0cbe15698e | |||
| a90c8a0e0a | |||
| 5e81bff15c | |||
| 79268096e0 | |||
| 28e2588df3 | |||
| 8261bac95a | |||
| 79a9cd06cc | |||
| ec84d62c60 | |||
| e5fc5ca571 | |||
| dfb9a372dd | |||
| 98e95936b6 | |||
| dc8e7d4214 | |||
| 23f5c21bb6 | |||
| a0d357329a | |||
| ffb6da34a3 | |||
| 05dcd65dd5 | |||
| 7567a6a8b6 | |||
| 09efaea7ac | |||
| 985de99d81 | |||
| 6492af0675 | |||
| b0c327ed26 | |||
| d5292e374e | |||
| dac9cb0688 | |||
| e40c3d50fd | |||
| f2edbfc4cf | |||
| 4505edf8ba | |||
| 7f9c72dab6 | |||
| 00c7a6bbc4 | |||
| fb10729218 | |||
| 0b3fecb4ed | |||
| 11bd54014e | |||
| f25eec680b | |||
| bd4ea55c49 | |||
| 0b13f7d73b | |||
| 5ec09a2046 | |||
| 893739e0a9 | |||
| f82cd599ea | |||
| 5878fdc0f0 | |||
| 9a70f46c8e | |||
| d026a88214 | |||
| 5e25f4eedb | |||
| ce071a8abd | |||
| d8818f9a0c | |||
| 8187072a4e | |||
| c93caa5010 | |||
| 2efb871d3c | |||
| b5f9532717 | |||
| 5b5c4e76b0 | |||
| 2cc37f9dc0 | |||
| d247af70f6 | |||
| b4f9125c42 | |||
| a793ea2d3a | |||
| cfacd9dd62 | |||
| 79c70367dd | |||
| 12aaf1b70f | |||
| fa08da4607 | |||
| d23b9f94e6 | |||
| 9cb08a3f67 | |||
| 61e4632a51 | |||
| 39986b546e | |||
| e2b0a1f0a5 | |||
| c5aa45ef66 | |||
| c4b52a9df6 | |||
| 7400ffc20f | |||
| 410b899820 | |||
| 9a278ce847 | |||
| 74bff2b045 | |||
| 03a3bf0bf9 | |||
| 1987ea4ae8 | |||
| 12b9764407 | |||
| 43c02eac4e | |||
| b9c7b1a592 | |||
| 8e34b1a3be | |||
| ab17f15db6 | |||
| f38bc04f00 | |||
| a28efac134 | |||
| 27192b1923 | |||
| cece34bb97 | |||
| 39d9f4a6cc | |||
| 1990f9292f | |||
| ac9e195259 | |||
| 4e30e24d7e | |||
| cbd267806b | |||
| c184d740fc | |||
| aab7e7686a | |||
| 738097aa3f | |||
| ee4dc2debe | |||
| b24350b479 | |||
| fb750feba3 | |||
| 98aee11520 | |||
| c818482698 | |||
| 05fc563fb7 | |||
| 0f999d9c3d | |||
| 9995e9dbef | |||
| 3e3df2f2bb | |||
| 85880708b7 | |||
| 1c2145b518 | |||
| 151ab46b4a | |||
| 07c2d4616e | |||
| 11cb6c99c3 | |||
| 74ca49a76a | |||
| 00a1289a94 | |||
| c972912a6b | |||
| e0842477e2 | |||
| e20cb424f9 | |||
| e9ce7cf0f4 | |||
| 41c787c5d2 | |||
| 91837dccf7 | |||
| d6d2c9be6c | |||
| d0aae3a060 | |||
| 50a3abd90e | |||
| ee833e8638 | |||
| 96e006404f | |||
| 0404e5bbe7 | |||
| 8b0138a66f | |||
| 840c2353b9 | |||
| 98efc87f04 | |||
| fed6ec8331 | |||
| 2568372923 | |||
| d4cfe0169e | |||
| fe5eb70809 | |||
| 20191d30c9 | |||
| cb3cd43047 | |||
| ae04f8c35c | |||
| 63acb94037 | |||
| b90d92e8aa | |||
| e79445f4e1 | |||
| ca90aff3e5 | |||
| 40e47f1b26 | |||
| eda209430b | |||
| 98d75ba557 | |||
| ba7bc66b30 | |||
| 42825b5c7e | |||
| 1efb48cf41 | |||
| 39066d868f | |||
| aeca930448 | |||
| bb9ab8bd35 | |||
| f5fa07727a | |||
| 49ce0c582e | |||
| f46b2ad419 | |||
| 36b421d4cb | |||
| c6c6562592 | |||
| d0b617b642 | |||
| 3c3b806a2b | |||
| 10ffd48223 | |||
| 70da450a9c | |||
| bf0555e6c2 | |||
| 4172b43558 | |||
| e1954a4609 | |||
| 62fbbe9303 | |||
| 55e23dac3b | |||
| c7146b9846 | |||
| fcab391f81 | |||
| 75614aa0f1 | |||
| bb9b69749e | |||
| d804e93ba2 | |||
| b486bb6846 | |||
| d01233f6ba | |||
| b953b6a05e | |||
| 887b64d213 | |||
| 8bd8c2d498 | |||
| 822b663f1c | |||
| 9d2113f02e | |||
| 9b9820c8b1 | |||
| 1b0b2f0a45 | |||
| 2285cd25fd | |||
| 2d5699728a | |||
| bfb837b422 | |||
| 73629ce126 | |||
| 23471f17f8 | |||
| 32e62092cf | |||
| 56d9a5e072 | |||
| 6311c8ddb2 | |||
| 9361f1db81 | |||
| f79626f9e1 | |||
| caf3f99860 | |||
| 3c1d6e4e21 | |||
| 65ff77c628 | |||
| da46ad1964 | |||
| 5d40ba02ff | |||
| 8004fc4f68 | |||
| b00eef4335 | |||
| 5ef99ac871 |
@@ -1,7 +1,14 @@
|
||||
node_modules/
|
||||
/build/doc.js
|
||||
/build/typescript/typescript.d.ts
|
||||
/build/typescript/*.js
|
||||
|
||||
libjass.js
|
||||
libjass.min.js
|
||||
*.map
|
||||
/dist/
|
||||
|
||||
*.log
|
||||
/lib/
|
||||
!/lib/libjass.css
|
||||
|
||||
/node_modules/
|
||||
|
||||
/npm-debug.log
|
||||
|
||||
/src/version.ts
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
- "0.12"
|
||||
- "4"
|
||||
- "5"
|
||||
- "6"
|
||||
before_script:
|
||||
- "node ./build.js doc"
|
||||
sudo: false
|
||||
@@ -1,40 +0,0 @@
|
||||
1. Install node.js from http://nodejs.org/ or via your package manager
|
||||
|
||||
1. Change to the directory where you cloned this repository.
|
||||
|
||||
1. Run the following command
|
||||
|
||||
npm install
|
||||
|
||||
This will install the dependencies - [Jake](https://github.com/mde/jake), [PEG.js](http://pegjs.majda.cz/), [TypeScript](http://www.typescriptlang.org/) and [UglifyJS2](https://github.com/mishoo/UglifyJS2). It will then run Jake to build libjass.js and use UglifyJS2 to minify it into libjass.min.js
|
||||
|
||||
1. Set the URLs of the video and the ASS file in index.xhtml
|
||||
|
||||
<video id="video" src=" <URL OF VIDEO HERE> " controls="">
|
||||
<track src=" <URL OF SCRIPT HERE> " kind="metadata" data-format="ass" />
|
||||
</video>
|
||||
|
||||
1. Set the URLs of any fonts you want to make available in fonts.css
|
||||
|
||||
@font-face {
|
||||
font-family: " <NAME OF FONT HERE> ";
|
||||
src: url(" <URL OF FONT HERE> ");
|
||||
}
|
||||
for each font. The name of the font is what it's called in the ASS file.
|
||||
|
||||
1. Start your web server and navigate to index.xhtml in a browser.
|
||||
|
||||
***
|
||||
|
||||
### Alternative ways to minify
|
||||
|
||||
* UglifyJS2 via command line
|
||||
|
||||
uglifyjs libjass.js --source-map libjass.min.js.map --in-source-map libjass.js.map --output libjass.min.js --mangle --compress
|
||||
|
||||
* Microsoft AJAX Minifer:
|
||||
|
||||
"C:\Program Files (x86)\Microsoft\Microsoft Ajax Minifier\ajaxmin.exe" libjass.js ass.pegjs.js -enc:in utf-8 -enc:out utf-8 -out libjass.min.js -comments:none -debug:false,console,libjass.debugMode,libjass.verboseMode -esc:true -inline:false -map:V3 libjass.min.js.map -strict:true
|
||||
|
||||
|
||||
These commands will not preserve the license notice header in the minified file. Remember to prepend the license notice to libjass.min.js from libjass.js or any one of the TS files.
|
||||
@@ -0,0 +1,133 @@
|
||||
### v0.11.0 - 2016/01/24
|
||||
- BREAKING CHANGE - WebRenderer.resize(width, height) used to have a broken implementation of letterboxing to move the subs div right or down. Now it's WebRenderer.resize(width, height, left, top) and expects the caller to calculate letterboxing itself and supply left and top accordingly. DefaultRenderer does it using the video resolution and users of WebRenderer can do the same.
|
||||
- BREAKING CHANGE - DefaultRenderer.resize() now ignores its parameters and always resizes to its video element's dimensions. It had already stopped resizing the video element when it was renamed from resizeVideo in v0.6.0, so it doesn't make sense to let it take a completely different width and height.
|
||||
- BREAKING CHANGE - Removed fullscreen support in DefaultRenderer. It started out as a hack using max z-index and works on even fewer browsers now. It probably didn't work for you anyway so it should be no big loss.
|
||||
- Implemented experimental support for \t
|
||||
- Added RendererSettings.fallbackFonts to set the fallback fonts for all styles. Defaults to 'Arial, Helvetica, sans-serif, "Segoe UI Symbol"'.
|
||||
- Better compatibility with loose ASS scripts - assume unnamed first section is Script Info, fall back to Default style for missing styles, recognize arbitrary-case property names, normalize asterisks in style names, etc.
|
||||
- Various font size improvements - faster calculation, fix for incorrect size when line-height is overridden by site CSS, fix for incorrect scaled sizes for letterboxed subs, fix for incorrect metrics for web fonts, etc. The last one requires that all web fonts be specified in RendererSettings.fontMap to be rendered accurately.
|
||||
- WebRenderer now supports using local() URLs in addition to url() in CSS font-face rules.
|
||||
- Added RendererSettings.useAttachedFonts. If true, TTF fonts attached to the script will be used in addition to fonts specified in RendererSettings.fontMap. This setting is false by default, and should only be enabled on trusted fonts since it uses a very naive base64 and TTF parser to extract the font names from the attachment. It also requires ES6 typed arrays - ArrayBuffer, DataView, Uint8Array, etc. in the environment.
|
||||
- Various pre-render, SVG filter and DOM perf improvements.
|
||||
- Fixed \fscx and \fscy to not scale shadows.
|
||||
- Fixed \fscx and \fscy to have optional values.
|
||||
- Fixed \fs+ and \fs- to have required values.
|
||||
- Fixed \r<target_style> to use the target style's alpha values instead of 1.
|
||||
- Fixed \fad subs to not flash after the fade-out ends with low-resolution clocks.
|
||||
- Fixed outlines to not be darker than they should be.
|
||||
- Fixed styles to not ignore the ScaleX and ScaleY properties in the script.
|
||||
- Fixed lack of sufficient space between normal and italic text.
|
||||
- Fixed SVG filters to interpolate in sRGB space instead of RGB.
|
||||
- Fixed ASS parser to complain if a script doesn't have a Script Info section at all.
|
||||
- The promise returned from ASS.from*() is now properly rejected due to errors from loading the script, instead of just remaining unresolved forever.
|
||||
- Fixed SRT parser to swallow UTF-8 BOM just like the ASS parser.
|
||||
- Fixed all clocks to suppress redundant ticks if the current timestamp hasn't change from the last tick.
|
||||
- Fixed {AutoClock, VideoClock}.{setEnabled, toggle} methods to actually enable / disable the high-resolution timer.
|
||||
|
||||
|
||||
### v0.10.0 - 2015/05/05
|
||||
- Implemented libjass.renderers.AutoClock, a clock that automatically ticks and generates clock events according to the state of an external driver.
|
||||
- Implemented \k
|
||||
- libjass.{Set, Map, Promise} can now be set to null to force the use of the polyfills, even if it defaulted to a runtime-provided implementation.
|
||||
- Added ASS.fromReadableStream(), a function that can be used to parse ASS from a readable stream such as the response of window.fetch().
|
||||
- ASS.fromUrl() now tries to use window.fetch() if available instead of XMLHttpRequest.
|
||||
- Fixed constant pausing and playing on Firefox triggered by how slowly it updates video.currentTime (wasn't noticeable but still undesirable).
|
||||
- Fixed a dialogue's animation state not getting updated while seeking if the start and end times of the seek were within its start and end times.
|
||||
- Fixed wrapping mode 1 (end-of-line wrapping) to actually wrap.
|
||||
- Fixed parser to parse the time components of karaoke tags as centiseconds instead of seconds.
|
||||
- Fixed parser to swallow leading BOM, if any.
|
||||
- Fixed errors reported by webworker API were empty objects without message and stack properties.
|
||||
|
||||
|
||||
### v0.9.0 - 2014/11/27
|
||||
- BREAKING CHANGE - ASS.fromString() now returns a Promise of an ASS object, not an ASS object directly. The synchronous ASS parser used by ASS.fromString() is no more.
|
||||
- BACKWARD-COMPATIBLE CHANGE - WebRenderer constructor parameters order has changed from (ass, clock, settings, libjassSubsWrapper) to (ass, clock, libjassSubsWrapper, settings). The constructor will detect the old order and reorder accordingly.
|
||||
- Added ASS.fromStream and ASS.fromXhr that read a stream and an XMLHttpRequest object's response respectively and return (a Promise of) an ASS object. Both of these parse the script asynchronously.
|
||||
- Added RendererSettings.enableSvg that can be used to toggle the use of SVG filter effects for outlines and blur.
|
||||
- libjass.js now has an AMD wrapper so that it can be used with RequireJS, etc.
|
||||
- Settings parameter is now optional for WebRenderer and DefaultRenderer.
|
||||
- Added support for clock rates apart from 1 to clocks and renderers.
|
||||
- Added a parameter to libjass.createWorker to specify the path to libjass.js that will run in the worker.
|
||||
- Fixed Style and Dialogue constructors not setting defaults for missing properties.
|
||||
- Fixed color and alpha parser to support more formats.
|
||||
- Fixed SRT parser to replace all HTML tags it finds, instead of just the first one.
|
||||
- Fixed font size calculation to use the vertical scale instead of horizontal.
|
||||
- Fixed line-height on newlines.
|
||||
- Fixed missing perspective on X and Y rotations.
|
||||
|
||||
|
||||
### v0.8.0 - 2014/08/16
|
||||
- Added web worker support. libjass.parse can now be offloaded to a web worker.
|
||||
- Implemented \fs+ and \fs-
|
||||
- Added ASS.addEvent() to add dialogue lines to an ASS object.
|
||||
- Renamed ClockEvent.TimeUpdate to ClockEvent.Tick, and added ClockEvent.Stop
|
||||
- Clock.enable() and .disable() now return a boolean to indicate whether the function had any effect.
|
||||
- Added Clock.setEnabled() to force the enabled-state to the given value.
|
||||
- Renamed ManualClock.timeUpdate() to ManualClock.tick()
|
||||
- Moved WebRenderer.enable(), .disable() and .enabled to NullRenderer
|
||||
- Fixed not being able to parse tags with default values.
|
||||
- Fixed font preloader downloading the same font multiple times because it didn't filter for duplicates.
|
||||
- Fixed min-width value not taking separate left and right margins into account.
|
||||
- Fixed absolutely positioned subs were always left-aligned even if they had an alignment tag.
|
||||
- Fixed blur and outlines getting truncated.
|
||||
|
||||
|
||||
### v0.7.0 - 2014/05/15
|
||||
- Implemented \be
|
||||
- Split a new renderer, WebRenderer, off DefaultRenderer that doesn't rely on a video element.
|
||||
- All renderers now require a Clock to generate time events. VideoClock is a Clock backed by a video element, while ManualClock is a clock that can be used to generate arbitrary time events.
|
||||
|
||||
|
||||
### v0.6.0 - 2014/03/24
|
||||
- All script properties and style properties are now parsed and stored in the ASS and Style objects.
|
||||
- Basic SRT support, by passing in a libjass.Format argument to ASS.fromString()
|
||||
- \clip and \iclip now have their drawing instructions parsed as an array of libjass.parts.drawing.Instruction's instead of just a string.
|
||||
- Added DefaultRenderer.enable(), DefaultRenderer.disable() and DefaultRenderer.toggle() to change whether the renderer is displaying subtitles or not.
|
||||
- DefaultRenderer.resizeVideo is now called DefaultRenderer.resize. Now it only resizes the subtitle wrapper div, not the video element.
|
||||
- Replaced the 41ms setInterval-bsed timer with a requestAnimationFrame-based timer to reduce load on minimized or hidden browser tabs.
|
||||
- DefaultRenderer now renders dialogues in the correct order according to the script.
|
||||
- Fixed incorrect font sizes.
|
||||
- Replaced jake with gulp.
|
||||
|
||||
|
||||
### v0.5.0 - 2014/01/26
|
||||
- Removed preLoadFonts renderer setting. It was redundant with the actual fontMap setting since the presence or absence of that setting is enough to signal whether the user wants to preload fonts or not.
|
||||
- Multiple renderers can now be used on the same page without conflicting with each other.
|
||||
- Implemented \shad, \xshad, \yshad
|
||||
- Fixed ASS draw scale being used incorrectly.
|
||||
- ASS.resolutionX and ASS.resolutionY are now properties of ASS.properties, a ScriptProperties object.
|
||||
|
||||
|
||||
### v0.4.0 - 2013/12/27
|
||||
- All parts moved from the libjass.tags namespace to the libjass.parts namespace.
|
||||
- Replaced PEG.js parser with a hand-written one. This allows for parsing lines that are strictly invalid grammar but are parsed successfully by VSFilter or libass.
|
||||
- All ASS tags are now supported by the parser.
|
||||
- Removed the useHighResolutionTimer setting for DefaultRenderer. DefaultRenderer always uses the 41ms timer now.
|
||||
- Implemented \move
|
||||
- Implemented ASS draw
|
||||
- Fixed subs overflowing the video dimensions still being visible.
|
||||
- SVG filters are now used for outlines and blur.
|
||||
- Delay parsing of dialogue lines till they need to be pre-rendered. As a side-effect, all fonts in the font map are preloaded now, not just the ones used in the current script.
|
||||
|
||||
|
||||
### v0.3.0 - 2013/10/28
|
||||
- Moved libjass.DefaultRenderer to libjass.renderers.DefaultRenderer
|
||||
- Added libjass.renderers.NullRenderer, a renderer that doesn't render anything.
|
||||
- DefaultRenderer's fontMap setting is now a Map instead of an Object. It now supports more than one URL for each font name.
|
||||
- DefaultRenderer now generates the subtitle wrapper div itself.
|
||||
- DefaultRenderer now takes video letterboxing into account when resizing the subtitles.
|
||||
- DefaultRenderer has a new setting useHighResolutionTimer that makes it use a 41ms timer instead of video.timeUpdate's 250ms timer.
|
||||
- div IDs and CSS class names are now prefixed with "libjass-" to avoid collisions with other elements on the page.
|
||||
- All numeric CSS property values are now truncated to three decimal places.
|
||||
- Added ```jake watch``` that rebuilds and runs tests on changes to the source.
|
||||
- Added ```jake doc``` that builds API documentation.
|
||||
- Added Travis CI build.
|
||||
|
||||
|
||||
### v0.2.0 - 2013/09/11
|
||||
- Added libjass.DefaultRenderer, a class that handles initializing the layer div's, preloading fonts, and drawing Dialogues based on the current video time.
|
||||
- libjass.js can now be loaded in node. Only the parser can be used.
|
||||
- Tests can now be run with ```jake test``` or ```npm test``` using Mocha.
|
||||
|
||||
### v0.1.0 - 2013/08/29
|
||||
- First npm release.
|
||||
@@ -1,455 +0,0 @@
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var UglifyJS = require("uglify-js");
|
||||
|
||||
namespace("_default", function () {
|
||||
var tscJsResolvedPath = path.resolve("./node_modules/typescript/bin/tsc.js");
|
||||
|
||||
task("tscRequire", [], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
var vm = require("vm");
|
||||
|
||||
var data = fs.readFileSync(tscJsResolvedPath, { encoding: "utf8" });
|
||||
|
||||
data =
|
||||
data.substr(0, data.lastIndexOf("})(TypeScript || (TypeScript = {}));") + "})(TypeScript || (TypeScript = {}));".length) +
|
||||
"module.exports = TypeScript;";
|
||||
|
||||
var TypeScript = {};
|
||||
vm.runInNewContext(data, {
|
||||
module: Object.defineProperty(Object.create(null), "exports", {
|
||||
get: function () { return TypeScript; },
|
||||
set: function (value) { TypeScript = value; }
|
||||
}),
|
||||
require: require,
|
||||
process: process,
|
||||
__filename: tscJsResolvedPath,
|
||||
__dirname: path.dirname(tscJsResolvedPath)
|
||||
});
|
||||
|
||||
return TypeScript;
|
||||
});
|
||||
|
||||
task("tscCreate", ["_default:tscRequire"], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
var TypeScript = jake.Task["_default:tscRequire"].value;
|
||||
|
||||
var compiler = new TypeScript.BatchCompiler({
|
||||
arguments: [],
|
||||
directoryExists: function (path) {
|
||||
return fs.existsSync(path) && fs.statSync(path).isDirectory();
|
||||
},
|
||||
dirName: function (file) {
|
||||
var result = path.dirname(file);
|
||||
if (result === file) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
fileExists: fs.existsSync.bind(fs),
|
||||
getExecutingFilePath: function () {
|
||||
return tscJsResolvedPath;
|
||||
},
|
||||
quit: function (code) {
|
||||
if (code !== 0) {
|
||||
throw new Error("TypeScript compiler exited with code " + code);
|
||||
}
|
||||
},
|
||||
readFile: function (file, codepage) {
|
||||
return {
|
||||
contents: fs.readFileSync(file, { encoding: "utf8" }),
|
||||
byteOrderMark: 0
|
||||
};
|
||||
},
|
||||
resolvePath: path.resolve.bind(path),
|
||||
stdout: {
|
||||
Write: process.stdout.write.bind(process.stderr),
|
||||
WriteLine: console.log.bind(console)
|
||||
},
|
||||
stderr: {
|
||||
Write: process.stderr.write.bind(process.stderr),
|
||||
WriteLine: console.error.bind(console)
|
||||
},
|
||||
writeFile: function (file, contents, writeByteOrderMark) {
|
||||
file = path.relative(".", file);
|
||||
|
||||
output[file] = contents;
|
||||
}
|
||||
});
|
||||
|
||||
var output;
|
||||
|
||||
compiler.compilationSettings.codeGenTarget = TypeScript.LanguageVersion.EcmaScript5;
|
||||
compiler.compilationSettings.mapSourceFiles = true;
|
||||
compiler.compilationSettings.noImplicitAny = true;
|
||||
|
||||
return function (inputFilenames, outputFilename) {
|
||||
output = Object.create(null);
|
||||
|
||||
compiler.inputFiles = inputFilenames;
|
||||
compiler.compilationSettings.outFileOption = outputFilename;
|
||||
|
||||
try {
|
||||
compiler.batchCompile();
|
||||
}
|
||||
catch (ex) {
|
||||
if (ex instanceof Error) {
|
||||
throw ex;
|
||||
}
|
||||
else {
|
||||
throw new Error("Internal compiler error: " + ex.stack + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
});
|
||||
|
||||
task("tscCompile", ["_default:tscCreate"], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
var compile = jake.Task["_default:tscCreate"].value;
|
||||
|
||||
var output = compile(["libjass.ts"], "libjass.js");
|
||||
|
||||
return { code: output["libjass.js"], sourceMap: JSON.parse(output["libjass.js.map"]) };
|
||||
});
|
||||
|
||||
task("pegjs", [], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
var PEG = require("pegjs");
|
||||
|
||||
var data = fs.readFileSync("ass.pegjs", { encoding: "utf8" });
|
||||
|
||||
var parser = PEG.buildParser(data);
|
||||
|
||||
return "libjass.parser = " + parser.toSource();
|
||||
});
|
||||
|
||||
task("combine", ["_default:tscCompile", "_default:pegjs"], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
var compiled = jake.Task["_default:tscCompile"].value;
|
||||
var parserSource = jake.Task["_default:pegjs"].value;
|
||||
|
||||
UglifyJS.base54.reset();
|
||||
|
||||
|
||||
// Parse
|
||||
var root = null;
|
||||
root = UglifyJS.parse(compiled.code, {
|
||||
filename: "libjass.js",
|
||||
toplevel: root
|
||||
});
|
||||
root = UglifyJS.parse(parserSource, {
|
||||
filename: "ass.pegjs.js",
|
||||
toplevel: root
|
||||
});
|
||||
|
||||
root.figure_out_scope();
|
||||
|
||||
|
||||
// Remove some things from the AST
|
||||
var nodesToRemove;
|
||||
|
||||
nodesToRemove = [];
|
||||
|
||||
// 1. All but the first top-level "var libjass;"
|
||||
var firstVarLibjassFound = false;
|
||||
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (node instanceof UglifyJS.AST_Var && node.definitions[0].name.name === "libjass") {
|
||||
if (firstVarLibjassFound === false) {
|
||||
firstVarLibjassFound = true;
|
||||
}
|
||||
else {
|
||||
nodesToRemove.push({ node: node, parent: root.body });
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Repeat because removing some declarations may make others unreferenced
|
||||
for (;;) {
|
||||
// 2. Unreferenced variable and function declarations, and unreferenced terminal function arguments
|
||||
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (node instanceof UglifyJS.AST_SymbolDeclaration && node.unreferenced()) {
|
||||
if (node instanceof UglifyJS.AST_SymbolFunarg) {
|
||||
if (this.parent().argnames.indexOf(node) === this.parent().argnames.length - 1) {
|
||||
nodesToRemove.push({ node: node, parent: this.parent().argnames });
|
||||
}
|
||||
}
|
||||
else if (node instanceof UglifyJS.AST_SymbolVar) {
|
||||
nodesToRemove.push({ node: this.parent(), parent: this.parent(1).definitions });
|
||||
if (this.parent(1).definitions.length === 1) {
|
||||
nodesToRemove.push({ node: this.parent(1), parent: this.parent(2).body });
|
||||
}
|
||||
}
|
||||
else if (node instanceof UglifyJS.AST_SymbolDefun) {
|
||||
nodesToRemove.push({ node: this.parent(), parent: this.parent(1).body });
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
if (nodesToRemove.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
nodesToRemove.forEach(function (node) {
|
||||
node.parent.splice(node.parent.indexOf(node.node), 1);
|
||||
});
|
||||
|
||||
nodesToRemove = [];
|
||||
|
||||
root.figure_out_scope();
|
||||
}
|
||||
|
||||
|
||||
// Output
|
||||
var firstLicenseHeaderFound = false; // To detect and preserve the first license header
|
||||
|
||||
var output = {
|
||||
source_map: UglifyJS.SourceMap({
|
||||
file: "libjass.js.map",
|
||||
orig: compiled.sourceMap,
|
||||
root: ""
|
||||
}),
|
||||
beautify: true,
|
||||
comments: {
|
||||
test: function (comment) {
|
||||
if (comment.indexOf("Copyright") !== -1) {
|
||||
if (!firstLicenseHeaderFound) {
|
||||
firstLicenseHeaderFound = true;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var stream = UglifyJS.OutputStream(output);
|
||||
root.print(stream);
|
||||
|
||||
return {
|
||||
code: stream.toString() + "\n//# sourceMappingURL=libjass.js.map",
|
||||
sourceMap: JSON.parse(output.source_map.toString())
|
||||
};
|
||||
});
|
||||
|
||||
task("writeCode", ["_default:combine"], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
var combined = jake.Task["_default:combine"].value;
|
||||
|
||||
fs.writeFileSync("libjass.js", combined.code);
|
||||
});
|
||||
|
||||
task("writeSourceMap", ["_default:combine"], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
var combined = jake.Task["_default:combine"].value;
|
||||
|
||||
fs.writeFileSync("libjass.js.map", JSON.stringify(combined.sourceMap));
|
||||
});
|
||||
|
||||
task("minify", ["_default:combine"], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
var combined = jake.Task["_default:combine"].value;
|
||||
|
||||
UglifyJS.base54.reset();
|
||||
|
||||
|
||||
// Parse
|
||||
var root = null;
|
||||
root = UglifyJS.parse(combined.code, {
|
||||
filename: "libjass.js",
|
||||
toplevel: root
|
||||
});
|
||||
|
||||
root.figure_out_scope();
|
||||
|
||||
|
||||
// Suppress some warnings
|
||||
var originalWarn = UglifyJS.AST_Node.warn;
|
||||
UglifyJS.AST_Node.warn = function (text, properties) {
|
||||
if (
|
||||
(text === "{type} {name} is declared but not referenced [{file}:{line},{col}]" && properties.type === "Symbol" && properties.name === "offset" && properties.col === 39) ||
|
||||
(text === "Eval is used [{file}:{line},{col}]" && properties.file === "libjass.js" && properties.line === 23)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
originalWarn.call(UglifyJS.AST_Node, text, properties);
|
||||
};
|
||||
|
||||
// Warnings
|
||||
root.scope_warnings({
|
||||
func_arguments: false
|
||||
});
|
||||
|
||||
|
||||
// Compress
|
||||
var compressor = UglifyJS.Compressor({
|
||||
warnings: true
|
||||
});
|
||||
root = root.transform(compressor);
|
||||
|
||||
|
||||
// Mangle
|
||||
root.figure_out_scope();
|
||||
root.compute_char_frequency();
|
||||
root.mangle_names();
|
||||
|
||||
|
||||
// Mangle private members
|
||||
var occurrences = Object.create(null);
|
||||
|
||||
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (
|
||||
node instanceof UglifyJS.AST_PropAccess &&
|
||||
typeof node.property === "string" &&
|
||||
node.property.indexOf("_") === 0 &&
|
||||
node.property !== "__iterator__"
|
||||
) {
|
||||
var occurrence = occurrences[node.property];
|
||||
if (occurrence === undefined) {
|
||||
occurrences[node.property] = 1;
|
||||
}
|
||||
else {
|
||||
occurrences[node.property]++;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
var identifiers = Object.keys(occurrences);
|
||||
identifiers.sort(function (first, second) { return occurrences[second] - occurrences[first]; });
|
||||
|
||||
var generatedIdentifiers = occurrences;
|
||||
|
||||
var validIdentifierCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_0123456789";
|
||||
var toIdentifier = function (index) {
|
||||
var result = validIdentifierCharacters[(index % validIdentifierCharacters.length)];
|
||||
index = (index / validIdentifierCharacters.length) | 0;
|
||||
|
||||
while (index > 0) {
|
||||
index--;
|
||||
result = validIdentifierCharacters[index % validIdentifierCharacters.length] + result;
|
||||
index = (index / validIdentifierCharacters.length) | 0;
|
||||
}
|
||||
|
||||
return "_" + result;
|
||||
};
|
||||
|
||||
identifiers.forEach(function (identifier, index) {
|
||||
generatedIdentifiers[identifier] = toIdentifier(index);
|
||||
});
|
||||
|
||||
root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (
|
||||
node instanceof UglifyJS.AST_PropAccess &&
|
||||
typeof node.property === "string" &&
|
||||
node.property in generatedIdentifiers
|
||||
) {
|
||||
node.property = generatedIdentifiers[node.property];
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// Output
|
||||
var firstLicenseHeaderFound = false; // To detect and preserve the first license header
|
||||
|
||||
var output = {
|
||||
source_map: UglifyJS.SourceMap({
|
||||
file: "libjass.min.js.map",
|
||||
orig: combined.sourceMap,
|
||||
root: ""
|
||||
}),
|
||||
comments: {
|
||||
test: function (comment) {
|
||||
if (!firstLicenseHeaderFound && comment.indexOf("Copyright") !== -1) {
|
||||
firstLicenseHeaderFound = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var stream = UglifyJS.OutputStream(output);
|
||||
root.print(stream);
|
||||
|
||||
return {
|
||||
code: stream.toString() + "\n//# sourceMappingURL=libjass.min.js.map",
|
||||
sourceMap: JSON.parse(output.source_map.toString())
|
||||
};
|
||||
});
|
||||
|
||||
task("writeMinifiedCode", ["_default:minify"], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
var minified = jake.Task["_default:minify"].value;
|
||||
|
||||
fs.writeFileSync("libjass.min.js", minified.code);
|
||||
});
|
||||
|
||||
task("writeMinifiedSourceMap", ["_default:minify"], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
var minified = jake.Task["_default:minify"].value;
|
||||
|
||||
fs.writeFileSync("libjass.min.js.map", JSON.stringify(minified.sourceMap));
|
||||
});
|
||||
});
|
||||
|
||||
desc("Build libjass.js, libjass.min.js and their sourcemaps");
|
||||
task("default", ["_default:writeCode", "_default:writeSourceMap", "_default:writeMinifiedCode", "_default:writeMinifiedSourceMap"], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
});
|
||||
|
||||
desc("Clean");
|
||||
task("clean", [], function () {
|
||||
console.log("[" + this.fullName + "]");
|
||||
|
||||
["libjass.js", "libjass.js.map", "libjass.min.js", "libjass.min.js.map"].forEach(function (file) {
|
||||
try {
|
||||
fs.unlinkSync(file);
|
||||
}
|
||||
catch (ex) {
|
||||
if (ex.code !== "ENOENT") {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
desc("Test");
|
||||
task("test", [], function () {
|
||||
var Mocha = require("mocha");
|
||||
|
||||
var mocha = new Mocha({
|
||||
ui: "tdd"
|
||||
});
|
||||
fs.readdirSync("./tests/").filter(function (filename) {
|
||||
if (filename.indexOf("test-") === 0) {
|
||||
var extensionIndex = filename.lastIndexOf(".js");
|
||||
if (extensionIndex !== -1 && extensionIndex === filename.length - ".js".length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}).forEach(function (filename) {
|
||||
mocha.addFile("./tests/" + filename);
|
||||
});
|
||||
|
||||
mocha.run();
|
||||
// .\node_modules\.bin\mocha -u tdd tests/test*.js
|
||||
});
|
||||
@@ -1,91 +1,154 @@
|
||||
libjass is a JavaScript library written in TypeScript to render ASS subs on HTML5 video in the browser.
|
||||
[](https://travis-ci.org/Arnavion/libjass)
|
||||
|
||||
libjass is a JavaScript library written in TypeScript to render ASS subs in the browser. [Check out the demo.](http://arnavion.github.io/libjass/demo/index.xhtml)
|
||||
|
||||
|
||||
### What's special about libjass?
|
||||
|
||||
* libjass requires no tweaks to the ASS file from the original video.
|
||||
|
||||
* It's easy to deploy. There is no server-side support required. A static hosting is all that's needed.
|
||||
|
||||
* libjass uses the browser's native CSS engine by converting the components of each line in the ASS script into a series of styled <div> and <span> elements. This allows all the layout and rendering to be handled by the browser instead of requiring complex and costly drawing and animation code. For example, libjass uses CSS3 animations to simulate tags such as \fad. While a canvas-drawing library would have to re-draw such a subtitle on the canvas for every frame of the video, libjass only renders it once and lets the browser render the fade effect.
|
||||
|
||||
As a result, libjass is able to render subtitles with very low CPU usage. The downside to libjass's aproach is that it is hard (and potentially impossible) to map all effects possible in ASS (using \t, ASS draw) etc. into DOM elements. As of now, the subset of tags supported by libjass has no such problems.
|
||||
|
||||
|
||||
### What are all these files?
|
||||
|
||||
* The .ts files are the source of libjass. They are TypeScript files and must be compiled into JavaScript for the browser using the TypeScript compiler.
|
||||
|
||||
* The ass.pegjs file is the source of a parser for the ASS format. It is compiled to JavaScript along with the .ts files.
|
||||
|
||||
* The rest of the files - index.xhtml, index.js, index.css and fonts.css - are a sample implementation of how to use libjass on a web page. They demonstrate the API to call, how to place <div> elements to render the subs, etc.
|
||||
|
||||
|
||||
### I want to use libjass for my website. What do I need to do?
|
||||
|
||||
1. You need to build libjass.js using the instructions in BUILD.md
|
||||
1. You need to load libjass.js on the page with your video.
|
||||
1. You need to call the libjass API.
|
||||
You can install the latest release of libjass
|
||||
|
||||
Only libjass.js is needed to use libjass on your website. The other files are only used during the build process and you don't need to deploy them to your website.
|
||||
* using npm with `npm install libjass` and load with `var libjass = require("libjass");`
|
||||
* using bower with `bower install https://github.com/Arnavion/libjass/releases/download/<release name>/libjass.zip`
|
||||
* using jspm with `jspm install github:Arnavion/libjass` and load with `import libjass from "Arnavion/libjass";`
|
||||
|
||||
Inside the package, you will find libjass.js and libjass.css, which you need to load on your website with your video.
|
||||
|
||||
Alternatively, you can build libjass from source by cloning this repository and running `npm install`. This will install the dependencies and run the build. libjass.js and libjass.css will be found in the lib/ directory.
|
||||
|
||||
Only libjass.js and libjass.css are needed to use libjass on your website. The other files are only used during the build process and you don't need to deploy them to your website.
|
||||
|
||||
|
||||
### Where's the API documentation? What API do I need to call to use libjass?
|
||||
### What are all these files?
|
||||
|
||||
Formal documentation is coming soon. In the meantime, here's an overview:
|
||||
* The src/ directory contains the source of libjass. They are TypeScript files and get compiled into JavaScript for the browser using the TypeScript compiler.
|
||||
|
||||
* The constructor ASS() takes in the raw ASS string and returns an object representing the script information, the line styles and dialogue lines in it. The example index.js uses XHR to get this data using the URL specified in a track tag.
|
||||
* build.js is the build script. The build command will use this script to build libjass.js. The build/ directory contains other files used for the build.
|
||||
|
||||
* ASS.dialogues is an array of Dialogue objects, each corresponding to a dialogue in the ASS file. These objects have a draw() method that returns a <div> containing the rendered subtitle line.
|
||||
* The tests/ directory contains unit and functional tests.
|
||||
|
||||
* index.js initializes a default renderer that libjass ships with, the DefaultRenderer. This renderer uses information from the ASS object to build up a series of div elements around the video tag. There is a wrapper (#subs) containing div's corresponding to the 9 alignment directions, 9 for each layer in the ASS script. index.css contains styles for these div's to render them at the correct location.
|
||||
* The lib/ directory contains libjass.js and libjass.css. You will need to deploy these to your website.
|
||||
|
||||
* The renderer listens for the video element's "timeupdate" event. In the event handler, it determines the set of dialogues to be shown, calls draw() on each of them, and appendChild's the result into the appropriate layer+alignment div. It only does this if the dialogue has not already been drawn by a previous timeupdate.
|
||||
|
||||
* The default renderer handles resizing the video and subs when the user clicks the browser's native fullscreen-video button. index.js also contains code to change the size of the video based on user input.
|
||||
### How do I use libjass?
|
||||
|
||||
* Lastly, the renderer contains an implementation of preloading all the fonts used in the ASS file. It uses a map of font names to URLs provided by index.js - this map is contained in fonts.css in the form of @font-family rules.
|
||||
The API documentation is linked in the Links section below. Here's an overview:
|
||||
|
||||
* The [ASS.fromUrl()](http://arnavion.github.io/libjass/api.xhtml#libjass.ASS.fromUrl) function takes in a URL to an ASS script and returns a promise that resolves to an [ASS](http://arnavion.github.io/libjass/api.xhtml#libjass.ASS) object. This ASS object represents the script properties, the line styles and dialogue lines in it. Alternatively, you can use [ASS.fromString()](http://arnavion.github.io/libjass/api.xhtml#libjass.ASS.fromString) to convert a string of the script contents into an ASS object.
|
||||
|
||||
* Next, you initialize a renderer to render the subtitles. libjass ships with an easy-to-use renderer, the [DefaultRenderer](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.DefaultRenderer). It uses information from the ASS object to build up a series of div elements around the video tag. There is a wrapper (.libjass-subs) containing div's corresponding to the layers in the ASS script, and each layer has div's corresponding to the 9 alignment directions. libjass.css contains styles for these div's to render them at the correct location.
|
||||
|
||||
* The renderer uses [window.requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame) as a source of timer ticks. In each tick, it determines the set of dialogues to be shown at the current video time, renders each of them as a div, and appendChild's the div into the appropriate layer+alignment div.
|
||||
|
||||
* The renderer can be told to dynamically change the size of the subtitles based on user input by calling [WebRenderer.resize()](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.WebRenderer.resize)
|
||||
|
||||
* Lastly, the renderer contains an implementation of preloading fonts before playing the video. It uses a map of font names to URLs - this map can be conveniently created from a CSS file containing @font-face rules using [RendererSettings.makeFontMapFromStyleElement()](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.RendererSettings.makeFontMapFromStyleElement)
|
||||
|
||||
* For an example of using libjass, check out [the demo.](http://arnavion.github.io/libjass/demo/index.xhtml) It has comments explaining basic usage and pointers to some advanced usage.
|
||||
|
||||
|
||||
### What browser and JavaScript features does libjass need?
|
||||
|
||||
* libjass uses some ES5 features like getters and setters (via Object.defineProperty), and assumptions like the behavior of parseInt with leading zeros. It cannot be used with an ES3 environment.
|
||||
|
||||
* libjass will use ES6 Set, Map and Promise if they're available on the global object. If they're not present, it will use its own minimal internal implementations. If you have implementations of these that you would like libjass to use but don't want to register them on the global object, you can provide them to libjass specifically by setting the [libjass.Set](http://arnavion.github.io/libjass/api.xhtml#libjass.Set), [libjass.Map](http://arnavion.github.io/libjass/api.xhtml#libjass.Map) and [libjass.Promise](http://arnavion.github.io/libjass/api.xhtml#libjass.Promise) properties.
|
||||
|
||||
* [AutoClock](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.AutoClock) and [VideoClock](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.VideoClock) use [window.requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) to generate clock ticks.
|
||||
|
||||
* [WebRenderer](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.WebRenderer) and [DefaultRenderer](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.DefaultRenderer) use [SVG filter effects for HTML](http://caniuse.com/#feat=svg-html) to render outlines and blur. This feature is not available on all browsers, so you can tell them to fall back to more widely available CSS methods by setting the [RendererSettings.enableSvg](http://arnavion.github.io/libjass/api.xhtml#libjass.renderers.RendererSettings.enableSvg) property to false.
|
||||
|
||||
* WebRenderer and DefaultRenderer use [CSS3 animations](http://caniuse.com/#feat=css-animation) for effects like \mov and \fad.
|
||||
|
||||
* Using fonts attached to the script requires [ES6 typed arrays](http://caniuse.com/#feat=typedarrays) (ArrayBuffer, DataView, Uint8Array, etc).
|
||||
|
||||
|
||||
### Can I use libjass in node?
|
||||
|
||||
libjass's parser works in node. Entire scripts can be parsed via [ASS.fromString()](http://arnavion.github.io/libjass/api.xhtml#libjass.ASS.fromString)
|
||||
|
||||
```javascript
|
||||
> var libjass = require("libjass")
|
||||
undefined
|
||||
> var ass; libjass.ASS.fromString(fs.readFileSync("mysubs.ass", "utf8")).then(function (result) { ass = result; })
|
||||
{}
|
||||
> ass.properties.resolutionX
|
||||
1280
|
||||
> ass.dialogues.length
|
||||
9
|
||||
> ass.dialogues[0].toString()
|
||||
'#0 [646.460-652.130] {\\fad(200,0)}Sapien rhoncus, suscipit posuere in nunc pellentesque'
|
||||
> var parts = ass.dialogues[0].parts
|
||||
undefined
|
||||
> parts.length
|
||||
2
|
||||
> parts[0] instanceof libjass.parts.Fade
|
||||
true
|
||||
> parts[0].toString()
|
||||
'Fade { start: 0.2, end: 0 }'
|
||||
```
|
||||
|
||||
[libjass.parser.parse](http://arnavion.github.io/libjass/api.xhtml#libjass.parser.parse) parses the first parameter using the second parameter as the rule name. For example, the [dialogueParts](http://arnavion.github.io/libjass/api.xhtml#./parser/parse.ParserRun.parse_dialogueParts) rule can be used to get an array of [libjass.parts](http://arnavion.github.io/libjass/api.xhtml#libjass.parts) objects that represent the parts of an ASS dialogue line.
|
||||
|
||||
```javascript
|
||||
> var parts = libjass.parser.parse("{\\an8}Are {\\i1}you{\\i0} the one who stole the clock?!", "dialogueParts")
|
||||
undefined
|
||||
> parts.join(" ")
|
||||
'Alignment { value: 8 } Text { value: Are } Italic { value: true } Text { value: you } Italic { value: false } Text { value: the one who stole the clock?! }'
|
||||
> parts.length
|
||||
6
|
||||
> parts[0].toString()
|
||||
'Alignment { value: 8 }'
|
||||
> parts[0] instanceof libjass.parts.Alignment
|
||||
true
|
||||
> parts[0].value
|
||||
8
|
||||
```
|
||||
|
||||
The rule names are derived from the methods on the [ParserRun class](http://arnavion.github.io/libjass/api.xhtml#./parser/parse.ParserRun).
|
||||
|
||||
See the tests, particularly the ones in tests/unit/miscellaneous.js, for examples.
|
||||
|
||||
|
||||
### Can I contribute?
|
||||
|
||||
Yes! Feature requests, suggestions, bug reports and pull requests are welcome! I'm especially looking for details and edge-cases of the ASS syntax that libjass doesn't support.
|
||||
|
||||
You can also hop by the IRC channel below and ask any questions.
|
||||
You can also join the IRC channel in the links section below and ask any questions.
|
||||
|
||||
|
||||
## Links
|
||||
### Supported features
|
||||
|
||||
* [GitHub](https://github.com/Arnavion/libjass/)
|
||||
* IRC channel - #libjass on irc.rizon.net
|
||||
* [Aegisub's documentation on ASS](http://docs.aegisub.org/3.0/ASS_Tags/)
|
||||
|
||||
|
||||
## Supported features
|
||||
|
||||
* Styles: Italic, Bold, Underline, StrikeOut, FontName, FontSize, ScaleX, ScaleY, Spacing, PrimaryColor, OutlineColor, Outline, Alignment, MarginL, MarginR, MarginV
|
||||
* Tags: \i, \b, \u, \s, \bord, \xbord, \ybord, \blur, \fn, \fs, \fscx, \fscy, \fsp, \frx, \fry, \frz, \fax, \fay, \c, \1c, \3c, \alpha, \1a, \3a, \an, \r, \pos, \fad
|
||||
* Styles: Italic, Bold, Underline, StrikeOut, FontName, FontSize, ScaleX, ScaleY, Spacing, PrimaryColor, OutlineColor, BackColor, Outline, Shadow, Alignment, MarginL, MarginR, MarginV
|
||||
* Tags: \i, \b, \u, \s, \bord, \xbord, \ybord, \shad, \xshad, \yshad, \be, \blur, \fn, \fs, \fscx, \fscy, \fsp, \frx, \fry, \frz, \fr, \fax, \fay, \c, \1c, \3c, \4c, \alpha, \1a, \3a, \4a, \an, \a, \k, \q, \r, \pos, \move, \fad, \fade, \t (experimental), \p
|
||||
* Custom fonts, using CSS web fonts.
|
||||
|
||||
|
||||
## Known bugs
|
||||
### Known issues
|
||||
|
||||
* Unsupported tags: \fe, \2c, \2a, \K, \kf, \ko, \org, \clip, \iclip
|
||||
* \an4, \an5, \an6 aren't positioned correctly.
|
||||
* Unsupported tags: Everything else, notably \t.
|
||||
* Font sizes aren't pixel perfect.
|
||||
* \blur uses an approximation instead of Gaussian blur.
|
||||
* ASS draw is unsupported.
|
||||
* Smart line wrapping is not supported. Such lines are rendered as [wrapping style 1 (end-of-line wrapping).](http://docs.aegisub.org/3.0/ASS_Tags/#wrapstyle)
|
||||
* Lines with multiple rotations aren't rotated the same as VSFilter or libass. See [#14](https://github.com/Arnavion/libjass/issues/14)
|
||||
- Desktop renderers include borders when calculating space between adjacent lines. libjass doesn't.
|
||||
|
||||
|
||||
## Planned improvements
|
||||
### Links
|
||||
|
||||
* Document browser compatibility. Currently libjass is tested on IE11 (Windows 7), Firefox Nightly and Google Chrome (Dev channel).
|
||||
* Write API documentation. Add more explanatory comments to the code.
|
||||
* Write more parser tests. Also figure out a way to test layout.
|
||||
* Evaluate (document, benchmark) the benefits and drawbacks of DOM+CSS-based drawing over canvas.
|
||||
* [GitHub](https://github.com/Arnavion/libjass/)
|
||||
* IRC channel - #libjass on irc.rizon.net
|
||||
* [API documentation](http://arnavion.github.io/libjass/api.xhtml)
|
||||
* [Aegisub's documentation on ASS](http://docs.aegisub.org/3.0/ASS_Tags/)
|
||||
|
||||
|
||||
# License
|
||||
### License
|
||||
|
||||
```
|
||||
libjass
|
||||
|
||||
@@ -1,438 +0,0 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
{
|
||||
var scriptTemplate = Object.create(null);
|
||||
var currentSectionContents;
|
||||
var currentFormatSpecifier;
|
||||
}
|
||||
|
||||
script
|
||||
= sections:(script_section eol*)* {
|
||||
return scriptTemplate;
|
||||
}
|
||||
|
||||
script_section
|
||||
= name:script_sectionHeader (
|
||||
script_comment /
|
||||
script_property /
|
||||
eol
|
||||
)* {
|
||||
scriptTemplate[name] = currentSectionContents;
|
||||
}
|
||||
|
||||
script_sectionHeader
|
||||
= "[" name:[^\]\n]+ "]" eol? {
|
||||
currentSectionContents = null;
|
||||
currentFormatSpecifier = null;
|
||||
|
||||
return name.join("");
|
||||
}
|
||||
|
||||
script_comment
|
||||
= ";" [^\n]+ eol?
|
||||
|
||||
script_property
|
||||
= key:[^:\n]+ ":" " "* value:[^\n]+ eol? {
|
||||
key = key.join("");
|
||||
value = value.join("");
|
||||
|
||||
if (key === "Format") {
|
||||
currentFormatSpecifier = value.split(",").map(function (formatPart) { return formatPart.trim(); });
|
||||
}
|
||||
|
||||
else if (currentFormatSpecifier !== null) {
|
||||
if (currentSectionContents === null) {
|
||||
currentSectionContents = [];
|
||||
}
|
||||
|
||||
var template = Object.create(null);
|
||||
value = value.split(",");
|
||||
|
||||
if (value.length > currentFormatSpecifier.length) {
|
||||
value[currentFormatSpecifier.length - 1] = value.slice(currentFormatSpecifier.length - 1).join(",");
|
||||
}
|
||||
|
||||
currentFormatSpecifier.forEach(function (formatKey, index) {
|
||||
template[formatKey] = value[index];
|
||||
});
|
||||
|
||||
currentSectionContents.push({ type: key, template: template });
|
||||
}
|
||||
|
||||
else {
|
||||
if (currentSectionContents === null) {
|
||||
currentSectionContents = Object.create(null);
|
||||
}
|
||||
|
||||
currentSectionContents[key] = value;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
eol
|
||||
= "\n"
|
||||
|
||||
dialogueParts
|
||||
= parts:(enclosedTags / comment / newline / hardspace / text)* {
|
||||
// Flatten parts
|
||||
parts = parts.reduce(function (previous, current) {
|
||||
return previous.concat(current);
|
||||
}, []);
|
||||
|
||||
// Merge consecutive text and comment parts into one part
|
||||
parts = parts.reduce(function (previous, current) {
|
||||
if (current instanceof libjass.tags.Text && previous[previous.length - 1] instanceof libjass.tags.Text) {
|
||||
previous[previous.length - 1] = new libjass.tags.Text(previous[previous.length - 1].value + current.value);
|
||||
}
|
||||
else if (current instanceof libjass.tags.Comment && previous[previous.length - 1] instanceof libjass.tags.Comment) {
|
||||
previous[previous.length - 1] = new libjass.tags.Comment(previous[previous.length - 1].value + current.value);
|
||||
}
|
||||
else {
|
||||
previous.push(current);
|
||||
}
|
||||
|
||||
return previous;
|
||||
}, []);
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
enclosedTags
|
||||
= "{" tagsWithSlashes:(
|
||||
"\\" tag_alpha /
|
||||
"\\" tag_xbord /
|
||||
"\\" tag_ybord /
|
||||
|
||||
"\\" tag_bord /
|
||||
"\\" tag_blur /
|
||||
"\\" tag_fscx /
|
||||
"\\" tag_fscy /
|
||||
|
||||
"\\" tag_fsp /
|
||||
"\\" tag_frx /
|
||||
"\\" tag_fry /
|
||||
"\\" tag_frz /
|
||||
"\\" tag_fax /
|
||||
"\\" tag_fay /
|
||||
"\\" tag_pos /
|
||||
"\\" tag_fad /
|
||||
|
||||
"\\" tag_fn /
|
||||
"\\" tag_fs /
|
||||
"\\" tag_1c /
|
||||
"\\" tag_3c /
|
||||
"\\" tag_1a /
|
||||
"\\" tag_3a /
|
||||
"\\" tag_an /
|
||||
|
||||
"\\" tag_i /
|
||||
"\\" tag_b /
|
||||
"\\" tag_u /
|
||||
"\\" tag_s /
|
||||
"\\" tag_c /
|
||||
"\\" tag_r
|
||||
)+
|
||||
"}" {
|
||||
return tagsWithSlashes.map(function (tagWithSlash) { return tagWithSlash[1]; });
|
||||
}
|
||||
|
||||
comment
|
||||
= "{" value:[^}]* "}" {
|
||||
return new libjass.tags.Comment(
|
||||
value.join("")
|
||||
);
|
||||
}
|
||||
|
||||
newline
|
||||
= "\\N" {
|
||||
return new libjass.tags.NewLine();
|
||||
}
|
||||
|
||||
hardspace
|
||||
= "\\h" {
|
||||
return new libjass.tags.HardSpace();
|
||||
}
|
||||
|
||||
text
|
||||
= value:. {
|
||||
return new libjass.tags.Text(
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
tag_i
|
||||
= "i" value:enableDisable? {
|
||||
return new libjass.tags.Italic(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_b
|
||||
= "b" value:(([1-9] "0" "0") / "1" / "0")? {
|
||||
if (Array.isArray(value)) {
|
||||
value = value.join("");
|
||||
}
|
||||
switch (value) {
|
||||
case "1":
|
||||
return new libjass.tags.Bold(true);
|
||||
case "0":
|
||||
return new libjass.tags.Bold(false);
|
||||
case "":
|
||||
return new libjass.tags.Bold(null);
|
||||
default:
|
||||
return new libjass.tags.Bold(parseInt(value));
|
||||
}
|
||||
}
|
||||
|
||||
tag_u
|
||||
= "u" value:enableDisable? {
|
||||
return new libjass.tags.Underline(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_s
|
||||
= "s" value:enableDisable? {
|
||||
return new libjass.tags.StrikeThrough(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_bord
|
||||
= "bord" value:decimal? {
|
||||
return new libjass.tags.Border(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_xbord
|
||||
= "bord" value:decimal? {
|
||||
return new libjass.tags.BorderX(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_ybord
|
||||
= "bord" value:decimal? {
|
||||
return new libjass.tags.BorderY(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_blur
|
||||
= "blur" value:decimal? {
|
||||
return new libjass.tags.Blur(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_fn
|
||||
= "fn" value:[^\\}]* {
|
||||
return new libjass.tags.FontName(
|
||||
(value.length > 0) ? value.join("") : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_fs
|
||||
= "fs" value:decimal? {
|
||||
return new libjass.tags.FontSize(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_fscx
|
||||
= "fscx" value:decimal? {
|
||||
return new libjass.tags.FontScaleX(
|
||||
(value !== "") ? (value / 100) : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_fscy
|
||||
= "fscy" value:decimal? {
|
||||
return new libjass.tags.FontScaleY(
|
||||
(value !== "") ? (value / 100) : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_fsp
|
||||
= "fsp" value:decimal? {
|
||||
return new libjass.tags.LetterSpacing(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_frx
|
||||
= "frx" value:decimal? {
|
||||
return new libjass.tags.RotateX(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_fry
|
||||
= "fry" value:decimal? {
|
||||
return new libjass.tags.RotateY(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_frz
|
||||
= "frz" value:decimal? {
|
||||
return new libjass.tags.RotateZ(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_fax
|
||||
= "fax" value:decimal? {
|
||||
return new libjass.tags.SkewX(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_fay
|
||||
= "fay" value:decimal? {
|
||||
return new libjass.tags.SkewY(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_1c
|
||||
= "1c" value:color? {
|
||||
return new libjass.tags.PrimaryColor(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_c
|
||||
= "c" value:color? {
|
||||
return new libjass.tags.PrimaryColor(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_3c
|
||||
= "3c" value:color? {
|
||||
return new libjass.tags.OutlineColor(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_alpha
|
||||
= "alpha" value:alpha? {
|
||||
return new libjass.tags.Alpha(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_1a
|
||||
= "1a" value:alpha? {
|
||||
return new libjass.tags.PrimaryAlpha(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_3a
|
||||
= "3a" value:alpha? {
|
||||
return new libjass.tags.OutlineAlpha(
|
||||
(value !== "") ? value : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_an
|
||||
= "an" value:[1-9] {
|
||||
return new libjass.tags.Alignment(
|
||||
parseInt(value)
|
||||
);
|
||||
}
|
||||
|
||||
tag_r
|
||||
= "r" value:[^\\}]* {
|
||||
return new libjass.tags.Reset(
|
||||
(value.length > 0) ? value.join("") : null
|
||||
);
|
||||
}
|
||||
|
||||
tag_pos
|
||||
= "pos(" x:decimal "," y:decimal ")" {
|
||||
return new libjass.tags.Pos(
|
||||
x,
|
||||
y
|
||||
);
|
||||
}
|
||||
|
||||
tag_fad
|
||||
= "fad(" start:decimal "," end:decimal ")" {
|
||||
return new libjass.tags.Fade(
|
||||
parseFloat(start) / 1000,
|
||||
parseFloat(end) / 1000
|
||||
);
|
||||
}
|
||||
|
||||
decimal
|
||||
= sign:"-"? unsignedDecimal:unsignedDecimal {
|
||||
return (sign === "") ? unsignedDecimal : -unsignedDecimal;
|
||||
}
|
||||
|
||||
unsignedDecimal
|
||||
= characteristic:[0-9]+ mantissa:("." [0-9]+)? {
|
||||
return parseFloat(
|
||||
characteristic.join("") +
|
||||
(mantissa[0] || "") + (mantissa[1] && mantissa[1].join("") || "")
|
||||
);
|
||||
}
|
||||
|
||||
enableDisable
|
||||
= value:("0" / "1") {
|
||||
switch (value) {
|
||||
case "0":
|
||||
return false;
|
||||
case "1":
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
hex
|
||||
= [0-9a-fA-F]
|
||||
|
||||
color
|
||||
= "&H" blue:(hex hex) green:(hex hex) red:(hex hex) "&" {
|
||||
return new libjass.tags.Color(
|
||||
parseInt(red.join(""), 16),
|
||||
parseInt(green.join(""), 16),
|
||||
parseInt(blue.join(""), 16)
|
||||
);
|
||||
}
|
||||
|
||||
alpha
|
||||
= "&H" value:(hex hex) "&" {
|
||||
return 1 - parseInt(value.join(""), 16) / 255;
|
||||
}
|
||||
|
||||
colorWithAlpha
|
||||
= "&H" alpha:(hex hex) blue:(hex hex) green:(hex hex) red:(hex hex) {
|
||||
return new libjass.tags.Color(
|
||||
parseInt(red.join(""), 16),
|
||||
parseInt(green.join(""), 16),
|
||||
parseInt(blue.join(""), 16),
|
||||
1 - parseInt(alpha.join(""), 16) / 255
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var stream = require("stream");
|
||||
|
||||
var async = require("async");
|
||||
|
||||
var task = require("async-build");
|
||||
|
||||
(function () {
|
||||
var _TypeScript = null;
|
||||
var _UglifyJS = null;
|
||||
var _npm = null;
|
||||
|
||||
Object.defineProperties(global, {
|
||||
TypeScript: { get: function () { return _TypeScript || (_TypeScript = require("./build/typescript/index.js")); } },
|
||||
UglifyJS: { get: function () { return _UglifyJS || (_UglifyJS = require("./build/uglify.js")); } },
|
||||
npm: { get: function () { return _npm || (_npm = require("npm")); } },
|
||||
});
|
||||
})();
|
||||
|
||||
task("build-tools", function (callback) {
|
||||
async.every(["./build/typescript/typescript.d.ts", "./build/typescript/index.js", "./build/doc.js"], fs.exists.bind(fs), function (allExist) {
|
||||
if (allExist) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var typescriptPath = path.join(require.resolve("typescript"), "..", "..");
|
||||
|
||||
async.waterfall([
|
||||
async.parallel.bind(async, [
|
||||
fs.readFile.bind(fs, path.join(typescriptPath, "lib", "typescript.d.ts"), "utf8"),
|
||||
fs.readFile.bind(fs, "./build/typescript/extras.d.ts", "utf8")
|
||||
]),
|
||||
function (results, callback) {
|
||||
var newDts = results[0] + "\n\n" + results[1];
|
||||
fs.writeFile("./build/typescript/typescript.d.ts", newDts, "utf8", callback);
|
||||
},
|
||||
function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["build"], callback);
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
|
||||
task("default", ["libjass.js", "libjass.min.js"]);
|
||||
|
||||
task("version.ts", function (callback) {
|
||||
fs.exists("./src/version.ts", function (exists) {
|
||||
if (exists) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
fs.readFile.bind(fs, "./package.json"),
|
||||
function (buffer, callback) {
|
||||
try {
|
||||
var packageJson = JSON.parse(buffer);
|
||||
var versionString = packageJson.version;
|
||||
var versionParts = versionString.split(".").map(function (num) { return parseInt(num); });
|
||||
var versionFileContents =
|
||||
"/**\n" +
|
||||
" * The version of libjass. An array like\n" +
|
||||
" *\n" +
|
||||
" * [\"0.12.0\", 0, 12, 0]\n" +
|
||||
" *\n" +
|
||||
" * @type {!Array.<string|number>}\n" +
|
||||
" */\n" +
|
||||
"export const version = " + JSON.stringify([versionString].concat(versionParts)) + ";\n";
|
||||
callback(null, versionFileContents);
|
||||
}
|
||||
catch (ex) {
|
||||
callback(ex);
|
||||
}
|
||||
},
|
||||
function (contents, callback) {
|
||||
fs.writeFile("./src/version.ts", contents, "utf8", callback);
|
||||
}
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
|
||||
task("libjass.js", ["build-tools", "version.ts"], function (callback) {
|
||||
fs.exists("./lib/libjass.js", function (exists) {
|
||||
if (exists) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, task.src("./src/tsconfig.json")
|
||||
.pipe(TypeScript.build("./src/index.ts", "libjass"))
|
||||
.pipe(UglifyJS.build("libjass", ["AttachmentType", "BorderStyle", "ClockEvent", "Format", "WorkerCommands", "WrappingStyle"]))
|
||||
.pipe(task.dest("./lib")));
|
||||
});
|
||||
});
|
||||
|
||||
task("libjass.min.js", ["libjass.js"], function (callback) {
|
||||
fs.exists("./lib/libjass.min.js", function (exists) {
|
||||
if (exists) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, task.src(["./lib/libjass.js", "./lib/libjass.js.map"], { relativeTo: "./lib" })
|
||||
.pipe(UglifyJS.minify())
|
||||
.pipe(task.dest("./lib")));
|
||||
});
|
||||
});
|
||||
|
||||
task("clean", task.clean(["./src/version.ts", "./lib/libjass.js", "./lib/libjass.js.map", "./lib/libjass.min.js", "./lib/libjass.min.js.map"]));
|
||||
|
||||
task("watch", ["build-tools", "version.ts"], function (callback) {
|
||||
npm.load(function () {
|
||||
var testRunning = false;
|
||||
var rerunTest = false;
|
||||
|
||||
var startTest = function () {
|
||||
npm.commands["run-script"](["test-lib"], function () {
|
||||
testRunning = false;
|
||||
|
||||
if (rerunTest) {
|
||||
startTest();
|
||||
rerunTest = false;
|
||||
}
|
||||
});
|
||||
|
||||
testRunning = true;
|
||||
};
|
||||
|
||||
var runTest = function () {
|
||||
if (!testRunning) {
|
||||
startTest();
|
||||
}
|
||||
else {
|
||||
rerunTest = true;
|
||||
}
|
||||
};
|
||||
|
||||
task.src("./src/tsconfig.json")
|
||||
.pipe(TypeScript.watch("./src/index.ts", "libjass"))
|
||||
.pipe(UglifyJS.watch("libjass", ["BorderStyle", "ClockEvent", "Format", "WorkerCommands", "WrappingStyle"]))
|
||||
.pipe(task.dest("./lib"))
|
||||
.pipe(new task.FileTransform(function (file) {
|
||||
if (file.path === "libjass.js") {
|
||||
runTest();
|
||||
}
|
||||
}));
|
||||
|
||||
task.watch("./tests/unit/", runTest);
|
||||
});
|
||||
});
|
||||
|
||||
task("test-lib", ["libjass.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-lib"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
task("test-minified", ["libjass.min.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-minified"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
// Start Selenium server with
|
||||
// java.exe -jar .\selenium-server-standalone-2.48.2.jar -Dwebdriver.ie.driver=IEDriverServer.exe -Dwebdriver.chrome.driver=chromedriver.exe
|
||||
task("test-browser", ["libjass.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-browser"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
task("test", ["test-lib", "test-minified"]);
|
||||
|
||||
task("demo", ["libjass.js"], function () {
|
||||
return task.src(["./lib/libjass.js", "./lib/libjass.js.map", "./lib/libjass.css"], { relativeTo: "./lib" }).pipe(task.dest("../libjass-gh-pages/demo/"));
|
||||
});
|
||||
|
||||
task("doc", ["make-doc", "test-doc"]);
|
||||
|
||||
task("make-doc", ["build-tools", "version.ts"], function () {
|
||||
var Doc = require("./build/doc.js");
|
||||
|
||||
return task.src("./src/tsconfig.json")
|
||||
.pipe(Doc.build("./api.xhtml", "./src/index.ts", "libjass"))
|
||||
.pipe(task.dest("../libjass-gh-pages/"));
|
||||
});
|
||||
|
||||
task("test-doc", ["make-doc", "libjass.js"], function (callback) {
|
||||
npm.load(function () {
|
||||
npm.commands["run-script"](["test-doc"], callback);
|
||||
});
|
||||
});
|
||||
|
||||
task("dist", ["libjass.js", "libjass.min.js", "test", "test-browser", "demo", "doc"], function (callback) {
|
||||
var inputFiles = [
|
||||
"./README.md", "./CHANGELOG.md", "./LICENSE",
|
||||
"./lib/libjass.js", "./lib/libjass.js.map",
|
||||
"./lib/libjass.min.js", "./lib/libjass.min.js.map",
|
||||
"./lib/libjass.css"
|
||||
];
|
||||
|
||||
var files = Object.create(null);
|
||||
inputFiles.forEach(function (filename) {
|
||||
files["./dist/" + path.basename(filename)] = filename;
|
||||
});
|
||||
|
||||
async.series([
|
||||
// Clean dist/
|
||||
task.clean(Object.keys(files).concat(["./dist/package.json"])),
|
||||
|
||||
// Create dist/ if necessary
|
||||
function (callback) {
|
||||
fs.mkdir("./dist", function (err) {
|
||||
if (err && err.code !== "EEXIST") {
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
// Copy all files except package.json
|
||||
async.forEachOf.bind(async, files, function (inputFilename, outputFilename, callback) {
|
||||
async.waterfall([fs.readFile.bind(fs, inputFilename), fs.writeFile.bind(fs, outputFilename)], callback);
|
||||
}),
|
||||
|
||||
// Copy package.json
|
||||
async.waterfall.bind(async, [
|
||||
fs.readFile.bind(fs, "./package.json"),
|
||||
function (data, callback) {
|
||||
try {
|
||||
var packageJson = JSON.parse(data);
|
||||
packageJson.devDependencies = undefined;
|
||||
packageJson.private = undefined;
|
||||
packageJson.scripts = undefined;
|
||||
packageJson.main = "libjass.js";
|
||||
}
|
||||
catch (ex) {
|
||||
callback(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
callback(null, new Buffer(JSON.stringify(packageJson, null, "\t")));
|
||||
},
|
||||
fs.writeFile.bind(fs, "./dist/package.json")
|
||||
])
|
||||
], callback);
|
||||
});
|
||||
|
||||
task.runArgv(function (err) {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,884 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FileTransform } from "async-build";
|
||||
|
||||
import * as AST from "./typescript/ast";
|
||||
import { Compiler } from "./typescript/compiler";
|
||||
import { walk } from "./typescript/walker";
|
||||
|
||||
function flatten<T>(arr: T[][]): T[] {
|
||||
let result: T[] = [];
|
||||
|
||||
for (const a of arr) {
|
||||
result = result.concat(a);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const sorter = (() => {
|
||||
function visibilitySorter(value1: { isPrivate?: boolean; isProtected?: boolean; }, value2: { isPrivate?: boolean; isProtected?: boolean; }) {
|
||||
if (value1.isPrivate === value2.isPrivate && value1.isProtected === value2.isProtected) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (value1.isPrivate) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (value2.isPrivate) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (value1.isProtected) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (value2.isProtected) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const types = [AST.Property, AST.Function, AST.Interface, AST.Class, AST.Enum];
|
||||
function typeSorter(value1: AST.ModuleMember | AST.NamespaceMember, value2: AST.ModuleMember | AST.NamespaceMember) {
|
||||
let type1Index = -1;
|
||||
let type2Index = -1;
|
||||
|
||||
types.every((type, index) => {
|
||||
if (value1 instanceof type) {
|
||||
type1Index = index;
|
||||
}
|
||||
if (value2 instanceof type) {
|
||||
type2Index = index;
|
||||
}
|
||||
return (type1Index === -1) || (type2Index === -1);
|
||||
});
|
||||
|
||||
return type1Index - type2Index;
|
||||
}
|
||||
|
||||
function nameSorter(value1: { name: string }, value2: { name: string }) {
|
||||
return value1.name.localeCompare(value2.name);
|
||||
}
|
||||
|
||||
const sorters: ((value1: AST.ModuleMember, value2: AST.ModuleMember) => number)[] = [visibilitySorter, typeSorter, nameSorter];
|
||||
|
||||
return (value1: AST.ModuleMember, value2: AST.ModuleMember) => {
|
||||
for (const sorter of sorters) {
|
||||
const result = sorter(value1, value2);
|
||||
|
||||
if (result !== 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
})();
|
||||
|
||||
function indenter(indent: number) {
|
||||
return (line: string) => ((line === "") ? line : (Array(indent + 1).join("\t") + line));
|
||||
}
|
||||
|
||||
function sanitize(str: string) {
|
||||
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function toVariableName(item: { name: string }) {
|
||||
// TODO: Handle non-letters (are both their toLowerCase() and toUpperCase())
|
||||
|
||||
const name = item.name;
|
||||
let result = "";
|
||||
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
if (name[i] === name[i].toLowerCase()) {
|
||||
// This is lower case. Write it as lower case.
|
||||
result += name[i];
|
||||
}
|
||||
|
||||
else {
|
||||
// This is upper case.
|
||||
|
||||
if (i === 0) {
|
||||
// This is the first character. Write it as lower case.
|
||||
result += name[i].toLowerCase();
|
||||
}
|
||||
|
||||
else if (name[i - 1] === name[i - 1].toUpperCase()) {
|
||||
// The previous character was upper case.
|
||||
|
||||
if (i === name.length - 1) {
|
||||
// This is the last character. Write it as lower case.
|
||||
result += name[i].toLowerCase();
|
||||
}
|
||||
else if (name[i + 1] === name[i + 1].toLowerCase()) {
|
||||
// The next character is lower case so this is the start of a new word. Write this one as upper case.
|
||||
result += name[i];
|
||||
}
|
||||
else {
|
||||
// The next character is upper case. Write this one as lower case.
|
||||
result += name[i].toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
// Previous character was lower case so this is the start of a new word. Write this one as upper case.
|
||||
result += name[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function toUsageName(item: AST.Class | AST.Interface | AST.Function | AST.Property | AST.Enum): string {
|
||||
if (item.parent instanceof AST.Module) {
|
||||
return item.name;
|
||||
}
|
||||
|
||||
if (item instanceof AST.Class || item instanceof AST.Interface || item instanceof AST.Enum) {
|
||||
if (item.isPrivate) {
|
||||
return item.name;
|
||||
}
|
||||
|
||||
return item.fullName;
|
||||
}
|
||||
|
||||
if (item.parent instanceof AST.Namespace) {
|
||||
if ((item as AST.CanBePrivate).isPrivate) {
|
||||
return item.name;
|
||||
}
|
||||
|
||||
return item.fullName;
|
||||
}
|
||||
|
||||
if ((item as AST.CanBeStatic).isStatic) {
|
||||
return toUsageName(item.parent as AST.Class | AST.Interface) + '.' + item.name;
|
||||
}
|
||||
|
||||
return toVariableName(item.parent) + '.' + item.name;
|
||||
}
|
||||
|
||||
function toId(item: { fullName?: string; name: string; }): string {
|
||||
return sanitize((item.fullName === undefined) ? item.name : item.fullName);
|
||||
}
|
||||
|
||||
function toLink(item: AST.ModuleMember | AST.EnumMember | AST.TypeReference): string {
|
||||
let result = `<a href="#${ toId(item) }">${ sanitize(item.name) }`;
|
||||
|
||||
if (AST.hasGenerics(item) && item.generics.length > 0) {
|
||||
const generics = item.generics as (string | AST.TypeReference | AST.IntrinsicTypeReference)[];
|
||||
result += sanitize(`.<${ generics.map(generic =>
|
||||
(generic instanceof AST.TypeReference || generic instanceof AST.IntrinsicTypeReference) ? generic.name : generic
|
||||
).join(', ') }>`);
|
||||
}
|
||||
|
||||
result += '</a>';
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function writeDescription(text: string): string {
|
||||
let result = sanitize(text).replace(/\{@link ([^} ]+)\}/g, (substring, linkTarget) => `<a href="#${ linkTarget }">${ linkTarget }</a>`);
|
||||
|
||||
let inCodeBlock = false;
|
||||
result = result.split("\n").map(line => {
|
||||
if (line.substr(0, " ".length) === " ") {
|
||||
line = line.substr(" ".length);
|
||||
|
||||
if (!inCodeBlock) {
|
||||
inCodeBlock = true;
|
||||
line = `<pre class="code"><code>${ line }`;
|
||||
}
|
||||
}
|
||||
else if (line.length > 0 && inCodeBlock) {
|
||||
inCodeBlock = false;
|
||||
line = `</code></pre>${ line }`;
|
||||
}
|
||||
else if (line.length === 0 && !inCodeBlock) {
|
||||
line = "</p><p>";
|
||||
}
|
||||
|
||||
return line;
|
||||
}).join("\n");
|
||||
|
||||
if (inCodeBlock) {
|
||||
result += '</code></pre>';
|
||||
}
|
||||
|
||||
result = `<p>${ result }</p>`.replace(/<p>\s*<\/p>/g, "").replace(/<p>\s+/g, "<p>").replace(/\s+<\/p>/g, "</p>");
|
||||
|
||||
if (result.substr("<p>".length).indexOf("<p>") === -1) {
|
||||
result = result.substring("<p>".length, result.length - "</p>".length);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function writeParameters(parameters: AST.Parameter[]): string[] {
|
||||
if (parameters.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'<dd class="parameters">',
|
||||
' <dl>'
|
||||
].concat(flatten(parameters.map(parameter => {
|
||||
return [
|
||||
` <dt class="parameter name">${ sanitize(parameter.name) }</dt>`,
|
||||
` <dd class="parameter type">${ sanitize(parameter.type) }</dd>`,
|
||||
` <dd class="parameter description">${ writeDescription(parameter.description) }</dd>`
|
||||
].concat(writeParameters(parameter.subParameters).map(indenter(2)));
|
||||
}))).concat([
|
||||
' </dl>',
|
||||
'</dd>'
|
||||
]);
|
||||
}
|
||||
|
||||
function functionToHtml(func: AST.Function): string[] {
|
||||
return [
|
||||
`<dl id="${ toId(func) }" class="function${
|
||||
func.isAbstract ? ' abstract' : '' }${
|
||||
func.isPrivate ? ' private' : ''}${
|
||||
func.isProtected ? ' protected' : ''}${
|
||||
func.isStatic ? ' static' : ''}">`,
|
||||
` <dt class="name">${ toLink(func) }</dt>`,
|
||||
' <dd class="description">',
|
||||
` ${ writeDescription(func.description) }`,
|
||||
' </dd>',
|
||||
` <dd class="usage"><fieldset><legend /><pre><code>${
|
||||
sanitize(
|
||||
`${ (func.returnType !== null) ? 'var result = ' : '' }${ toUsageName(func) }(${
|
||||
func.parameters.map(parameter => parameter.name).join(', ') });`
|
||||
) }</code></pre></fieldset></dd>`
|
||||
].concat(writeParameters(func.parameters).map(indenter(1))).concat(
|
||||
(func.returnType === null) ? [] : [
|
||||
' <dt>Returns</dt>',
|
||||
` <dd class="return type">${ sanitize(func.returnType.type) }</dd>`,
|
||||
` <dd class="return description">${ writeDescription(func.returnType.description) }</dd>`
|
||||
]
|
||||
).concat([
|
||||
'</dl>',
|
||||
''
|
||||
]);
|
||||
}
|
||||
|
||||
function interfaceToHtml(interfase: AST.Interface): string[] {
|
||||
const members: AST.InterfaceMember[] = [];
|
||||
Object.keys(interfase.members).forEach(memberName => members.push(interfase.members[memberName]));
|
||||
|
||||
members.sort(sorter);
|
||||
|
||||
return [
|
||||
`<dl id="${ toId(interfase) }" class="interface${ interfase.isPrivate ? ' private' : '' }">`,
|
||||
` <dt class="name">interface ${ toLink(interfase) }${ (interfase.baseTypes.length > 0) ? ` extends ${
|
||||
interfase.baseTypes.map(baseType => baseType instanceof AST.TypeReference ? toLink(baseType) : baseType.name).join(', ')
|
||||
}` : '' }</dt>`,
|
||||
' <dd class="description">',
|
||||
` ${ writeDescription(interfase.description) }`,
|
||||
' </dd>',
|
||||
' <dd class="members">'
|
||||
].concat(flatten(members.map(member => {
|
||||
if (member instanceof AST.Property) {
|
||||
return propertyToHtml(member).map(indenter(2));
|
||||
}
|
||||
else if (member instanceof AST.Function) {
|
||||
return functionToHtml(member).map(indenter(2));
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unrecognized member type: ${ (member as any).constructor.name }`);
|
||||
}
|
||||
}))).concat([
|
||||
' </dd>',
|
||||
'</dl>',
|
||||
''
|
||||
]);
|
||||
}
|
||||
|
||||
function classToHtml(clazz: AST.Class): string[] {
|
||||
const members: AST.InterfaceMember[] = [];
|
||||
Object.keys(clazz.members).forEach(memberName => members.push(clazz.members[memberName]));
|
||||
|
||||
members.sort(sorter);
|
||||
|
||||
return [
|
||||
`<dl id="${ toId(clazz) }" class="clazz${
|
||||
clazz.isAbstract ? ' abstract' : ''}${
|
||||
clazz.isPrivate ? ' private' : ''}">`,
|
||||
` <dt class="name">class ${ toLink(clazz) }${
|
||||
(clazz.baseType !== null) ? ` extends ${ (clazz.baseType instanceof AST.TypeReference) ? toLink(clazz.baseType) : clazz.baseType.name }` : '' }${
|
||||
(clazz.interfaces.length > 0) ? ` implements ${ clazz.interfaces.map(interfase => interfase instanceof AST.TypeReference ? toLink(interfase) : interfase.name).join(', ') }` : ''}</dt>`,
|
||||
' <dd class="description">',
|
||||
` ${ writeDescription(clazz.description) }`,
|
||||
' </dd>',
|
||||
` <dd class="usage"><fieldset><legend /><pre><code>${
|
||||
sanitize(
|
||||
`var ${ toVariableName(clazz) } = new ${ toUsageName(clazz) }(${
|
||||
clazz.parameters.map(parameter => parameter.name).join(', ') });`
|
||||
) }</code></pre></fieldset></dd>`
|
||||
].concat(writeParameters(clazz.parameters).map(indenter(1))).concat([
|
||||
' <dd class="members">'
|
||||
]).concat(flatten(members.map(member => {
|
||||
if (member instanceof AST.Property) {
|
||||
return propertyToHtml(member).map(indenter(2));
|
||||
}
|
||||
else if (member instanceof AST.Function) {
|
||||
return functionToHtml(member).map(indenter(2));
|
||||
}
|
||||
else {
|
||||
throw new Error(`Unrecognized member type: ${ (member as any).constructor.name }`);
|
||||
}
|
||||
}))).concat([
|
||||
' </dd>',
|
||||
'</dl>',
|
||||
''
|
||||
]);
|
||||
}
|
||||
|
||||
function enumToHtml(enumType: AST.Enum): string[] {
|
||||
return [
|
||||
`<dl id="${ toId(enumType) }" class="enum${ enumType.isPrivate ? ' private' : '' }">`,
|
||||
` <dt class="name">enum <a href="#${ sanitize(enumType.fullName) }">${ sanitize(enumType.name) }</a></dt>`,
|
||||
' <dd class="description">',
|
||||
` ${ writeDescription(enumType.description) }`,
|
||||
' </dd>'
|
||||
].concat([
|
||||
' <dd class="members">'
|
||||
]).concat(flatten(enumType.members.map(member => [
|
||||
` <dl id="${ toId(member) }" class="member">`,
|
||||
` <dt class="name">${ toLink(member) } = ${ member.value }</dt>`,
|
||||
' <dd class="description">',
|
||||
` ${ writeDescription(member.description) }`,
|
||||
' </dd>',
|
||||
' </dl>'
|
||||
]))).concat([
|
||||
' </dd>',
|
||||
'</dl>',
|
||||
''
|
||||
]);
|
||||
}
|
||||
|
||||
function propertyToHtml(property: AST.Property): string[] {
|
||||
return [
|
||||
`<dl id="${ toId(property) }" class="property">`,
|
||||
` <dt class="name">${ toLink(property) }</dt>`
|
||||
].concat((property.getter === null) ? [] : [
|
||||
` <dt class="getter${ property.getter.isPrivate ? ' private' : '' }">Getter</dt>`,
|
||||
' <dd class="description">',
|
||||
` ${ writeDescription(property.getter.description) }`,
|
||||
' </dd>',
|
||||
` <dd class="usage"><fieldset><legend /><pre><code>${ sanitize(`var result = ${ toUsageName(property) };`) }</code></pre></fieldset></dd>`,
|
||||
` <dd class="return type">${ sanitize(property.getter.type) }</dd>`
|
||||
]).concat((property.setter === null) ? [] : [
|
||||
` <dt class="setter${ property.setter.isPrivate ? ' private' : '' }">Setter</dt>`,
|
||||
' <dd class="description">',
|
||||
` ${ writeDescription(property.setter.description) }`,
|
||||
' </dd>',
|
||||
` <dd class="usage"><fieldset><legend /><pre><code>${ sanitize(`${ toUsageName(property) } = value;`) }</code></pre></fieldset></dd>`
|
||||
].concat(writeParameters([new AST.Parameter("value", "", property.setter.type)]).map(indenter(1)))).concat([
|
||||
'</dl>',
|
||||
''
|
||||
]);
|
||||
}
|
||||
|
||||
export function build(outputFilePath: string, root: string, rootNamespaceName: string): FileTransform {
|
||||
const compiler = new Compiler();
|
||||
|
||||
return new FileTransform(function (file): void {
|
||||
// Compile
|
||||
compiler.compile(file);
|
||||
|
||||
// Walk
|
||||
const walkResult = walk(compiler, root, rootNamespaceName);
|
||||
const namespaces = walkResult.namespaces;
|
||||
const modules = walkResult.modules;
|
||||
|
||||
// Make HTML
|
||||
|
||||
const namespaceNames = Object.keys(namespaces)
|
||||
.filter(namespaceName => namespaceName.substr(0, rootNamespaceName.length) === rootNamespaceName)
|
||||
.sort((ns1, ns2) => ns1.localeCompare(ns2));
|
||||
|
||||
const moduleNames = Object.keys(modules).sort((ns1, ns2) => ns1.localeCompare(ns2)).filter(moduleName => Object.keys(modules[moduleName].members).length > 0);
|
||||
|
||||
this.push({
|
||||
path: outputFilePath,
|
||||
contents: Buffer.concat([new Buffer(
|
||||
`<?xml version="1.0" encoding="utf-8" ?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
||||
<head>
|
||||
<title>${ rootNamespaceName } API Documentation</title>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
html, body, .navigation, .content {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navigation, .content {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
float: left;
|
||||
background-color: white;
|
||||
padding: 0 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.navigation .namespace, .navigation .module {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.navigation .elements {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content > section:not(:last-child) {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.clazz, .enum, .function, .interface, .property {
|
||||
margin-left: 30px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.getter, .setter {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
section > .clazz:nth-child(2n), section > .enum:nth-child(2n), section > .function:nth-child(2n), section > .interface:nth-child(2n), section > .property:nth-child(2n) {
|
||||
background-color: rgb(221, 250, 238);
|
||||
}
|
||||
|
||||
section > .clazz:nth-child(2n + 1), section > .enum:nth-child(2n + 1), section > .function:nth-child(2n + 1), section > .interface:nth-child(2n + 1), section > .property:nth-child(2n + 1) {
|
||||
background-color: rgb(244, 250, 221);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
.usage {
|
||||
font-size: large;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.usage legend:before {
|
||||
content: "Usage";
|
||||
}
|
||||
|
||||
.usage fieldset {
|
||||
min-width: initial;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.usage pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clazz .function, .clazz .property, .interface .function, .interface .property, .enum .member {
|
||||
background-color: rgb(250, 241, 221);
|
||||
}
|
||||
|
||||
.parameter.name {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.type {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.type:before {
|
||||
content: "Type: ";
|
||||
}
|
||||
|
||||
.abstract > .name:before {
|
||||
content: "abstract ";
|
||||
}
|
||||
|
||||
.clazz .private > .name:before {
|
||||
content: "private ";
|
||||
}
|
||||
|
||||
.clazz .protected > .name:before {
|
||||
content: "protected ";
|
||||
}
|
||||
|
||||
.static > .name:before {
|
||||
content: "static ";
|
||||
}
|
||||
|
||||
.abstract.private > .name:before {
|
||||
content: "abstract private ";
|
||||
}
|
||||
|
||||
.private.static > .name:before {
|
||||
content: "static private ";
|
||||
}
|
||||
|
||||
.abstract.protected > .name:before {
|
||||
content: "abstract protected ";
|
||||
}
|
||||
|
||||
.protected.static > .name:before {
|
||||
content: "static protected ";
|
||||
}
|
||||
|
||||
body:not(.show-private) .clazz .private, body:not(.show-private) .clazz .protected, body:not(.show-private) .module {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.description .code {
|
||||
margin-left: 30px;
|
||||
}
|
||||
]]>
|
||||
</style>
|
||||
<script>
|
||||
<![CDATA[
|
||||
addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelector("#show-private").addEventListener("change", function (event) {
|
||||
document.body.className = (event.target.checked ? "show-private" : "");
|
||||
}, false);
|
||||
|
||||
showPrivateIfNecessary();
|
||||
}, false);
|
||||
|
||||
function showPrivateIfNecessary() {
|
||||
var jumpToElement = document.querySelector("[id=\\"" + location.hash.substr(1) + "\\"]");
|
||||
if (jumpToElement !== null && jumpToElement.offsetHeight === 0) {
|
||||
document.querySelector("#show-private").click()
|
||||
jumpToElement.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener("hashchange", showPrivateIfNecessary, false);
|
||||
]]>
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navigation">
|
||||
<label><input type="checkbox" id="show-private" />Show private</label>
|
||||
`
|
||||
)].concat(namespaceNames.map(namespaceName => {
|
||||
const namespace = namespaces[namespaceName];
|
||||
|
||||
const namespaceMembers: AST.NamespaceMember[] = [];
|
||||
for (const memberName of Object.keys(namespace.members)) {
|
||||
namespaceMembers.push(namespace.members[memberName]);
|
||||
}
|
||||
|
||||
namespaceMembers.sort(sorter);
|
||||
|
||||
return Buffer.concat([new Buffer(
|
||||
` <fieldset class="namespace">
|
||||
<legend><a href="#${ sanitize(namespaceName) }">${ sanitize(namespaceName) }</a></legend>
|
||||
<ul class="elements">
|
||||
`
|
||||
)].concat(namespaceMembers.map(member => new Buffer(
|
||||
` <li><a href="#${ sanitize(member.fullName) }">${ sanitize(member.name) }</a></li>
|
||||
`
|
||||
))).concat([new Buffer(
|
||||
` </ul>
|
||||
</fieldset>
|
||||
|
||||
`
|
||||
)]));
|
||||
})).concat(moduleNames.map(moduleName => {
|
||||
const module = modules[moduleName];
|
||||
|
||||
const moduleMembers: AST.ModuleMemberWithoutReference[] = [];
|
||||
for (const memberName of Object.keys(module.members)) {
|
||||
const member = module.members[memberName];
|
||||
if ((member as AST.HasParent).parent === module) {
|
||||
moduleMembers.push(member as AST.ModuleMemberWithoutReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleMembers.length === 0) {
|
||||
return new Buffer("");
|
||||
}
|
||||
|
||||
moduleMembers.sort(sorter);
|
||||
|
||||
return Buffer.concat([new Buffer(
|
||||
` <fieldset class="module">
|
||||
<legend><a href="#${ sanitize(moduleName) }">${ sanitize(moduleName) }</a></legend>
|
||||
<ul class="elements">
|
||||
`
|
||||
)].concat(moduleMembers.map(member => new Buffer(
|
||||
` <li><a href="#${ sanitize(member.fullName) }">${ sanitize(member.name) }</a></li>
|
||||
`
|
||||
))).concat([new Buffer(
|
||||
` </ul>
|
||||
</fieldset>
|
||||
|
||||
`
|
||||
)]));
|
||||
})).concat([new Buffer(
|
||||
` </nav>
|
||||
|
||||
<div class="content">
|
||||
`
|
||||
)]).concat(flatten(namespaceNames.map(namespaceName => {
|
||||
const namespace = namespaces[namespaceName];
|
||||
|
||||
const namespaceMembers: AST.NamespaceMember[] = [];
|
||||
for (const memberName of Object.keys(namespace.members)) {
|
||||
namespaceMembers.push(namespace.members[memberName]);
|
||||
}
|
||||
|
||||
namespaceMembers.sort(sorter);
|
||||
|
||||
const properties = namespaceMembers.filter(member => member instanceof AST.Property) as AST.Property[];
|
||||
const functions = namespaceMembers.filter(member => member instanceof AST.Function) as AST.Function[];
|
||||
const interfaces = namespaceMembers.filter(member => member instanceof AST.Interface) as AST.Interface[];
|
||||
const classes = namespaceMembers.filter(member => member instanceof AST.Class) as AST.Class[];
|
||||
const enums = namespaceMembers.filter(member => member instanceof AST.Enum) as AST.Enum[];
|
||||
|
||||
const result = [new Buffer(
|
||||
` <section class="namespace">
|
||||
<h1 id="${ sanitize(namespaceName) }">Namespace ${ sanitize(namespaceName) }</h1>
|
||||
`
|
||||
)];
|
||||
|
||||
if (properties.length > 0) {
|
||||
result.push(new Buffer(
|
||||
` <section>
|
||||
<h2>Properties</h2>
|
||||
`
|
||||
));
|
||||
|
||||
for (const property of properties) {
|
||||
result.push(new Buffer(propertyToHtml(property).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
}
|
||||
|
||||
if (functions.length > 0) {
|
||||
result.push(new Buffer(
|
||||
` <section>
|
||||
<h2>Free functions</h2>
|
||||
`
|
||||
));
|
||||
|
||||
for (const func of functions) {
|
||||
result.push(new Buffer(functionToHtml(func).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
}
|
||||
|
||||
if (interfaces.length > 0) {
|
||||
result.push(new Buffer(
|
||||
` <section>
|
||||
<h2>Interfaces</h2>
|
||||
`
|
||||
));
|
||||
|
||||
for (const interfase of interfaces) {
|
||||
result.push(new Buffer(interfaceToHtml(interfase).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
}
|
||||
|
||||
if (classes.length > 0) {
|
||||
result.push(new Buffer(
|
||||
` <section>
|
||||
<h2>Classes</h2>
|
||||
`
|
||||
));
|
||||
|
||||
for (const clazz of classes) {
|
||||
result.push(new Buffer(classToHtml(clazz).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
}
|
||||
|
||||
if (enums.length > 0) {
|
||||
result.push(new Buffer(
|
||||
` <section>
|
||||
<h2>Enums</h2>
|
||||
`
|
||||
));
|
||||
|
||||
for (const enumType of enums) {
|
||||
result.push(new Buffer(enumToHtml(enumType).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
|
||||
return result;
|
||||
}))).concat(flatten(moduleNames.map(moduleName => {
|
||||
const module = modules[moduleName];
|
||||
|
||||
const moduleMembers: AST.ModuleMember[] = [];
|
||||
for (const memberName of Object.keys(module.members)) {
|
||||
const member = module.members[memberName];
|
||||
if ((member as AST.HasParent).parent === module) {
|
||||
moduleMembers.push(member);
|
||||
}
|
||||
}
|
||||
|
||||
if (moduleMembers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
moduleMembers.sort(sorter);
|
||||
|
||||
const properties = moduleMembers.filter(member => member instanceof AST.Property) as AST.Property[];
|
||||
const functions = moduleMembers.filter(member => member instanceof AST.Function) as AST.Function[];
|
||||
const interfaces = moduleMembers.filter(member => member instanceof AST.Interface) as AST.Interface[];
|
||||
const classes = moduleMembers.filter(member => member instanceof AST.Class) as AST.Class[];
|
||||
const enums = moduleMembers.filter(member => member instanceof AST.Enum) as AST.Enum[];
|
||||
|
||||
const result = [new Buffer(
|
||||
` <section class="module">
|
||||
<h1 id="${ sanitize(moduleName) }">Module ${ sanitize(moduleName) }</h1>
|
||||
`
|
||||
)];
|
||||
|
||||
if (properties.length > 0) {
|
||||
result.push(new Buffer(
|
||||
` <section>
|
||||
<h2>Properties</h2>
|
||||
`
|
||||
));
|
||||
|
||||
for (const property of properties) {
|
||||
result.push(new Buffer(propertyToHtml(property).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
}
|
||||
|
||||
if (functions.length > 0) {
|
||||
result.push(new Buffer(
|
||||
` <section>
|
||||
<h2>Free functions</h2>
|
||||
`
|
||||
));
|
||||
|
||||
for (const func of functions) {
|
||||
result.push(new Buffer(functionToHtml(func).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
}
|
||||
|
||||
if (interfaces.length > 0) {
|
||||
result.push(new Buffer(
|
||||
` <section>
|
||||
<h2>Interfaces</h2>
|
||||
`
|
||||
));
|
||||
|
||||
for (const interfase of interfaces) {
|
||||
result.push(new Buffer(interfaceToHtml(interfase).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
}
|
||||
|
||||
if (classes.length > 0) {
|
||||
result.push(new Buffer(
|
||||
` <section>
|
||||
<h2>Classes</h2>
|
||||
`
|
||||
));
|
||||
|
||||
for (const clazz of classes) {
|
||||
result.push(new Buffer(classToHtml(clazz).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
}
|
||||
|
||||
if (enums.length > 0) {
|
||||
result.push(new Buffer(
|
||||
` <section>
|
||||
<h2>Enums</h2>
|
||||
`
|
||||
));
|
||||
|
||||
for (const enumType of enums) {
|
||||
result.push(new Buffer(enumToHtml(enumType).map(indenter(5)).join("\n")));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
}
|
||||
|
||||
result.push(new Buffer(
|
||||
` </section>
|
||||
`
|
||||
));
|
||||
|
||||
return result;
|
||||
}))).concat([new Buffer(
|
||||
` </div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
)]))
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Type definitions for Node.js v0.12.0
|
||||
// Project: http://nodejs.org/
|
||||
// Definitions by: Microsoft TypeScript <http://typescriptlang.org>, DefinitelyTyped <https://github.com/borisyankov/DefinitelyTyped>
|
||||
// Definitions: https://github.com/borisyankov/DefinitelyTyped
|
||||
|
||||
declare var require: {
|
||||
resolve(id: string): string;
|
||||
};
|
||||
|
||||
interface BufferConstructor {
|
||||
new (str: string): Buffer;
|
||||
prototype: Buffer;
|
||||
concat(list: Buffer[]): Buffer;
|
||||
}
|
||||
|
||||
declare module "fs" {
|
||||
export function existsSync(filename: string): boolean;
|
||||
export function readFileSync(filename: string, options: { encoding: string }): string;
|
||||
}
|
||||
|
||||
declare module "path" {
|
||||
export function basename(p: string, ext?: string): string;
|
||||
export function extname(p: string): string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": false,
|
||||
"strictNullChecks": false,
|
||||
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "classic",
|
||||
"noImplicitUseStrict": false
|
||||
},
|
||||
|
||||
"files": [
|
||||
"./typescript/index.ts",
|
||||
"./doc.ts",
|
||||
"./node.d.ts",
|
||||
"./typescript/typescript.d.ts",
|
||||
"../node_modules/async-build/typings.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ts = require("typescript");
|
||||
|
||||
export class HasParent {
|
||||
public parent: HasParent = null;
|
||||
|
||||
constructor(public name: string) { }
|
||||
|
||||
get fullName(): string {
|
||||
if (this.parent === null) {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
const parent = this.parent;
|
||||
if (parent instanceof Namespace) {
|
||||
return parent.getMemberFullName(this);
|
||||
}
|
||||
|
||||
return `${ this.parent.fullName }.${ this.name }`;
|
||||
}
|
||||
}
|
||||
|
||||
export class Module extends HasParent {
|
||||
public parent: Module = null;
|
||||
|
||||
public members: { [name: string]: ModuleMember } = Object.create(null);
|
||||
|
||||
constructor(public fileName: string) {
|
||||
super(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
export type ModuleMember = Class | Interface | Function | Property | Enum | Reference;
|
||||
export type ModuleMemberWithoutReference = Class | Interface | Function | Property | Enum;
|
||||
|
||||
export class Reference {
|
||||
constructor(public moduleName: string, public name: string, public isPrivate: boolean) { }
|
||||
}
|
||||
|
||||
export class Namespace extends HasParent {
|
||||
public parent: Namespace = null;
|
||||
|
||||
public members: { [name: string]: NamespaceMember } = Object.create(null);
|
||||
|
||||
constructor(name: string) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
getMemberFullName(member: HasParent) {
|
||||
return `${ this.fullName }.${ member.name }`;
|
||||
}
|
||||
}
|
||||
|
||||
export type NamespaceMember = Class | Interface | Function | Property | Enum;
|
||||
|
||||
export class Class extends HasParent {
|
||||
public parent: Module | Namespace = null;
|
||||
|
||||
public unresolvedBaseType: UnresolvedType | TypeReference | IntrinsicTypeReference;
|
||||
public baseType: TypeReference | IntrinsicTypeReference = null;
|
||||
|
||||
public unresolvedInterfaces: (UnresolvedType | TypeReference | IntrinsicTypeReference)[];
|
||||
public interfaces: (TypeReference | IntrinsicTypeReference)[] = [];
|
||||
|
||||
public members: { [name: string]: InterfaceMember } = Object.create(null);
|
||||
|
||||
constructor(name: string, public astNode: ts.Node, public description: string, public generics: string[], public parameters: Parameter[], baseType: UnresolvedType | TypeReference | IntrinsicTypeReference, interfaces: (UnresolvedType | TypeReference | IntrinsicTypeReference)[], public isAbstract: boolean, public isPrivate: boolean) {
|
||||
super(name);
|
||||
|
||||
this.unresolvedBaseType = baseType;
|
||||
this.unresolvedInterfaces = interfaces;
|
||||
}
|
||||
}
|
||||
|
||||
export type InterfaceMember = Property | Function;
|
||||
|
||||
export class Interface extends HasParent {
|
||||
public parent: Module | Namespace = null;
|
||||
|
||||
public unresolvedBaseTypes: (UnresolvedType | TypeReference | IntrinsicTypeReference)[];
|
||||
public baseTypes: (TypeReference | IntrinsicTypeReference)[] = [];
|
||||
|
||||
public members: { [name: string]: InterfaceMember } = Object.create(null);
|
||||
|
||||
constructor(name: string, public astNode: ts.Node, public description: string, public generics: string[], baseTypes: (UnresolvedType | TypeReference | IntrinsicTypeReference)[], public isPrivate: boolean) {
|
||||
super(name);
|
||||
|
||||
this.unresolvedBaseTypes = baseTypes;
|
||||
}
|
||||
}
|
||||
|
||||
export class Function extends HasParent {
|
||||
public parent: Module | Namespace | Class | Interface = null;
|
||||
|
||||
constructor(name: string, public astNode: ts.Node, public description: string, public generics: string[], public parameters: Parameter[], public returnType: ReturnType, public isAbstract: boolean, public isPrivate: boolean, public isProtected: boolean, public isStatic: boolean) {
|
||||
super(name);
|
||||
}
|
||||
}
|
||||
|
||||
export class Property extends HasParent {
|
||||
public parent: Module | Namespace | Class | Interface = null;
|
||||
|
||||
public getter: Getter = null;
|
||||
public setter: Setter = null;
|
||||
|
||||
constructor(name: string) {
|
||||
super(name);
|
||||
}
|
||||
}
|
||||
|
||||
export class Getter {
|
||||
constructor(public astNode: ts.Node, public description: string, public type: string, public isPrivate: boolean) { }
|
||||
}
|
||||
|
||||
export class Setter {
|
||||
constructor(public astNode: ts.Node, public description: string, public type: string, public isPrivate: boolean) { }
|
||||
}
|
||||
|
||||
export class Parameter {
|
||||
public subParameters: Parameter[] = [];
|
||||
|
||||
constructor(public name: string, public description: string, public type: string) { }
|
||||
}
|
||||
|
||||
export class ReturnType {
|
||||
constructor(public description: string, public type: string) { }
|
||||
}
|
||||
|
||||
export class Enum extends HasParent {
|
||||
public parent: Module | Namespace = null;
|
||||
|
||||
public members: EnumMember[] = [];
|
||||
|
||||
constructor(name: string, public astNode: ts.Node, public description: string, public isPrivate: boolean) {
|
||||
super(name);
|
||||
}
|
||||
}
|
||||
|
||||
export class EnumMember extends HasParent {
|
||||
public parent: Enum = null;
|
||||
|
||||
constructor(name: string, public description: string, public value: number) {
|
||||
super(name);
|
||||
}
|
||||
}
|
||||
|
||||
export class TypeReference {
|
||||
constructor(public type: NamespaceMember, public generics: (TypeReference | IntrinsicTypeReference)[]) { }
|
||||
|
||||
get name(): string {
|
||||
return this.type.name;
|
||||
}
|
||||
|
||||
get fullName(): string {
|
||||
return this.type.fullName;
|
||||
}
|
||||
}
|
||||
|
||||
export class IntrinsicTypeReference {
|
||||
public fullName: string;
|
||||
|
||||
constructor(public name: string) {
|
||||
this.fullName = name;
|
||||
}
|
||||
}
|
||||
|
||||
export class UnresolvedType {
|
||||
constructor(public symbol: ts.Symbol, public generics: (UnresolvedType | IntrinsicTypeReference)[]) { }
|
||||
}
|
||||
|
||||
export type HasStringGenerics = Class | Interface | Function;
|
||||
|
||||
export function hasStringGenerics(item: NamespaceMember): item is HasStringGenerics {
|
||||
return (item as HasGenerics).generics !== undefined;
|
||||
}
|
||||
|
||||
export type HasGenerics = HasStringGenerics | TypeReference;
|
||||
|
||||
export function hasGenerics(item: ModuleMember | EnumMember | TypeReference): item is HasGenerics {
|
||||
return (item as HasGenerics).generics !== undefined;
|
||||
}
|
||||
|
||||
export type CanBePrivate = Class | Interface | Function | Getter | Setter | Enum | Reference;
|
||||
export type CanBeProtected = Function;
|
||||
export type CanBeStatic = Function;
|
||||
@@ -0,0 +1,312 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path = require("path");
|
||||
import ts = require("typescript");
|
||||
|
||||
import { File, FileTransform } from "async-build";
|
||||
|
||||
import * as AST from "./ast";
|
||||
import { walk } from "./walker";
|
||||
|
||||
export interface StreamingCompilerHost extends ts.CompilerHost {
|
||||
setOutputStream(outputStream: FileTransform): void;
|
||||
}
|
||||
|
||||
function createCompilerHost(options: ts.CompilerOptions): StreamingCompilerHost {
|
||||
const host = ts.createCompilerHost(options) as StreamingCompilerHost;
|
||||
|
||||
let _outputStream: FileTransform = null;
|
||||
host.setOutputStream = outputStream => _outputStream = outputStream;
|
||||
|
||||
host.writeFile = (fileName, data, writeByteOrderMark, onError?, sourceFiles?): void => {
|
||||
_outputStream.push({
|
||||
path: fileName,
|
||||
contents: new Buffer(data)
|
||||
});
|
||||
};
|
||||
|
||||
host.useCaseSensitiveFileNames = () => true;
|
||||
|
||||
host.getNewLine = () => "\n";
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
export class Compiler {
|
||||
private _projectRoot: string = null;
|
||||
private _host: StreamingCompilerHost;
|
||||
private _program: ts.Program = null;
|
||||
|
||||
compile(projectConfigFile: File) {
|
||||
this._projectRoot = path.dirname(projectConfigFile.path);
|
||||
|
||||
const projectConfig = ts.parseJsonConfigFileContent(JSON.parse(projectConfigFile.contents.toString()), ts.sys, this._projectRoot);
|
||||
|
||||
this._host = createCompilerHost(projectConfig.options);
|
||||
this._program = ts.createProgram(projectConfig.fileNames, projectConfig.options, this._host);
|
||||
|
||||
const syntacticDiagnostics = this._program.getSyntacticDiagnostics();
|
||||
if (syntacticDiagnostics.length > 0) {
|
||||
this._reportDiagnostics(syntacticDiagnostics);
|
||||
throw new Error("There were one or more syntactic diagnostics.");
|
||||
}
|
||||
|
||||
const optionsDiagnostics = this._program.getOptionsDiagnostics();
|
||||
if (optionsDiagnostics.length > 0) {
|
||||
this._reportDiagnostics(optionsDiagnostics);
|
||||
throw new Error("There were one or more options diagnostics.");
|
||||
}
|
||||
|
||||
const globalDiagnostics = this._program.getGlobalDiagnostics();
|
||||
if (globalDiagnostics.length > 0) {
|
||||
this._reportDiagnostics(globalDiagnostics);
|
||||
throw new Error("There were one or more global diagnostics.");
|
||||
}
|
||||
|
||||
const semanticDiagnostics = this._program.getSemanticDiagnostics();
|
||||
if (semanticDiagnostics.length > 0) {
|
||||
this._reportDiagnostics(semanticDiagnostics);
|
||||
throw new Error("There were one or more semantic diagnostics.");
|
||||
}
|
||||
};
|
||||
|
||||
writeFiles(outputStream: FileTransform) {
|
||||
this._host.setOutputStream(outputStream);
|
||||
|
||||
const emitDiagnostics = this._program.emit().diagnostics;
|
||||
if (emitDiagnostics.length > 0) {
|
||||
this._reportDiagnostics(emitDiagnostics);
|
||||
throw new Error("There were one or more emit diagnostics.");
|
||||
}
|
||||
};
|
||||
|
||||
get projectRoot(): string {
|
||||
return this._projectRoot;
|
||||
}
|
||||
|
||||
get typeChecker(): ts.TypeChecker {
|
||||
return this._program.getTypeChecker();
|
||||
}
|
||||
|
||||
get sourceFiles(): ts.SourceFile[] {
|
||||
return this._program.getSourceFiles();
|
||||
}
|
||||
|
||||
private _reportDiagnostics(diagnostics: ts.Diagnostic[]) {
|
||||
for (const diagnostic of diagnostics) {
|
||||
let message = "";
|
||||
|
||||
if (diagnostic.file) {
|
||||
const location = ts.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
|
||||
message = `${ diagnostic.file.fileName }(${ location.line + 1 },${ location.character }): `;
|
||||
}
|
||||
|
||||
message +=
|
||||
ts.DiagnosticCategory[diagnostic.category].toLowerCase() +
|
||||
` TS${ diagnostic.code }: ` +
|
||||
ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
||||
|
||||
console.error(message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function build(root: string, rootNamespaceName: string): FileTransform {
|
||||
const compiler = new Compiler();
|
||||
|
||||
return new FileTransform(function (projectConfigFile): void {
|
||||
console.log("Compiling " + projectConfigFile.path + "...");
|
||||
|
||||
compiler.compile(projectConfigFile);
|
||||
|
||||
const walkResult = walk(compiler, root, rootNamespaceName);
|
||||
addJSDocComments(walkResult.modules);
|
||||
|
||||
compiler.writeFiles(this);
|
||||
|
||||
console.log("Compile succeeded.");
|
||||
});
|
||||
}
|
||||
|
||||
function addJSDocComments(modules: { [name: string]: AST.Module }): void {
|
||||
function visitor(current: AST.Module | AST.ModuleMember | AST.InterfaceMember) {
|
||||
if (current instanceof AST.Module) {
|
||||
for (const memberName of Object.keys(current.members)) {
|
||||
visitor(current.members[memberName]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newComments: string[] = [];
|
||||
|
||||
if (current instanceof AST.Class) {
|
||||
newComments.push("@constructor");
|
||||
|
||||
if (current.baseType !== null) {
|
||||
const baseType = current.baseType;
|
||||
newComments.push(
|
||||
"@extends {" +
|
||||
baseType.fullName + (
|
||||
(baseType instanceof AST.TypeReference && baseType.generics.length) > 0 ?
|
||||
(".<" + (baseType as AST.TypeReference).generics.map(generic => generic.fullName).join(", ") + ">") :
|
||||
""
|
||||
) +
|
||||
"}"
|
||||
);
|
||||
}
|
||||
|
||||
if (current.interfaces.length > 0) {
|
||||
current.interfaces.forEach(interfase => {
|
||||
newComments.push(
|
||||
"@implements {" +
|
||||
interfase.fullName + (
|
||||
(interfase instanceof AST.TypeReference && interfase.generics.length > 0) ?
|
||||
(".<" + interfase.generics.map(generic => generic.fullName).join(", ") + ">") :
|
||||
""
|
||||
) +
|
||||
"}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(current.members).forEach(memberName => visitor(current.members[memberName]));
|
||||
}
|
||||
else if (current instanceof AST.Enum) {
|
||||
newComments.push("@enum");
|
||||
}
|
||||
else if (current instanceof AST.Reference) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.parent instanceof AST.Namespace) {
|
||||
newComments.push("@memberOf " + current.parent.fullName);
|
||||
}
|
||||
|
||||
if (AST.hasStringGenerics(current) && current.generics.length > 0) {
|
||||
newComments.push("@template " + current.generics.join(", "));
|
||||
}
|
||||
|
||||
if ((current as AST.CanBePrivate).isPrivate) {
|
||||
newComments.push("@private");
|
||||
}
|
||||
|
||||
if ((current as AST.CanBeProtected).isProtected) {
|
||||
newComments.push("@protected");
|
||||
}
|
||||
|
||||
if ((current as AST.CanBeStatic).isStatic) {
|
||||
newComments.push("@static");
|
||||
}
|
||||
|
||||
if (newComments.length > 0) {
|
||||
if (current instanceof AST.Property) {
|
||||
const nodes: ts.Node[] = [];
|
||||
if (current.getter !== null) { nodes.push(current.getter.astNode); }
|
||||
if (current.setter !== null && nodes[0] !== current.setter.astNode) { nodes.push(current.setter.astNode); }
|
||||
for (const node of nodes) {
|
||||
(node as any)["typescript-new-comment"] = newComments;
|
||||
}
|
||||
}
|
||||
else {
|
||||
(current.astNode as any)["typescript-new-comment"] = newComments;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const moduleName of Object.keys(modules)) {
|
||||
visitor(modules[moduleName]);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class FakeSourceFile {
|
||||
public text: string;
|
||||
public lineMap: number[];
|
||||
|
||||
constructor(originalSourceFile: ts.SourceFile) {
|
||||
this.text = originalSourceFile.text;
|
||||
this.lineMap = ts.getLineStarts(originalSourceFile).slice();
|
||||
}
|
||||
|
||||
addComment(originalComment: ts.CommentRange, newComments: string[]): ts.CommentRange & { sourceFile: FakeSourceFile } {
|
||||
var pos = this.text.length;
|
||||
|
||||
this.text += "/**\n";
|
||||
this.lineMap.push(this.text.length);
|
||||
|
||||
var originalCommentLines = this.text.substring(originalComment.pos, originalComment.end).split("\n");
|
||||
originalCommentLines.shift();
|
||||
|
||||
originalCommentLines = originalCommentLines.map(line => line.replace(/^\s+/, " "));
|
||||
|
||||
if (originalCommentLines.length > 1) {
|
||||
originalCommentLines.splice(originalCommentLines.length - 1, 0, " *");
|
||||
}
|
||||
|
||||
for (const newComment of newComments) {
|
||||
originalCommentLines.splice(originalCommentLines.length - 1, 0, " * " + newComment);
|
||||
}
|
||||
|
||||
for (const newCommentLine of originalCommentLines) {
|
||||
this.text += newCommentLine + "\n";
|
||||
this.lineMap.push(this.text.length);
|
||||
}
|
||||
|
||||
var end = this.text.length;
|
||||
|
||||
return { pos, end, hasTrailingNewLine: originalComment.hasTrailingNewLine, kind: ts.SyntaxKind.MultiLineCommentTrivia, sourceFile: this };
|
||||
}
|
||||
}
|
||||
|
||||
var fakeSourceFiles: { [name: string]: FakeSourceFile } = Object.create(null);
|
||||
*/
|
||||
|
||||
export const oldGetLeadingCommentRangesOfNodeFromText: typeof ts.getLeadingCommentRangesOfNodeFromText = ts.getLeadingCommentRangesOfNodeFromText.bind(ts);
|
||||
|
||||
/*
|
||||
ts.getLeadingCommentRangesOfNodeFromText = (node: ts.Node, text: string) => {
|
||||
const originalComments = oldGetLeadingCommentRangesOfNodeFromText(node, text);
|
||||
|
||||
if (originalComments !== undefined && (<any>node)["typescript-new-comment"] !== undefined) {
|
||||
const sourceFileOfNode = ts.getSourceFileOfNode(node);
|
||||
let fakeSourceFile = fakeSourceFiles[sourceFileOfNode.fileName];
|
||||
if (fakeSourceFile === undefined) {
|
||||
fakeSourceFile = fakeSourceFiles[sourceFileOfNode.fileName] = new FakeSourceFile(sourceFileOfNode);
|
||||
}
|
||||
|
||||
originalComments[originalComments.length - 1] = fakeSourceFile.addComment(originalComments[originalComments.length - 1], (<any>node)["typescript-new-comment"]);
|
||||
}
|
||||
|
||||
return originalComments;
|
||||
};
|
||||
|
||||
var oldWriteCommentRange: typeof ts.writeCommentRange = ts.writeCommentRange.bind(ts);
|
||||
ts.writeCommentRange = (text: string, lineMap: number[], writer: ts.EmitTextWriter, commentPos: number, commentEnd: number, newLine: string) => {
|
||||
if ((<{ sourceFile: ts.SourceFile }><any>comment).sourceFile) {
|
||||
const currentSourceFile = (<{ sourceFile: ts.SourceFile }><any>comment).sourceFile;
|
||||
text = currentSourceFile.text;
|
||||
lineMap = currentSourceFile.lineMap;
|
||||
}
|
||||
|
||||
return oldWriteCommentRange(text, lineMap, writer, commentPos, commentEnd, newLine);
|
||||
};
|
||||
*/
|
||||
@@ -0,0 +1,23 @@
|
||||
declare namespace ts {
|
||||
interface EmitTextWriter { }
|
||||
|
||||
interface IntrinsicType extends Type {
|
||||
intrinsicName: string;
|
||||
}
|
||||
|
||||
interface SourceFile {
|
||||
lineMap: number[];
|
||||
}
|
||||
|
||||
function forEachProperty<T, U>(map: Map<T>, callback: (value: T, key: string) => U): U;
|
||||
function getClassExtendsHeritageClauseElement(node: ClassLikeDeclaration | InterfaceDeclaration): ExpressionWithTypeArguments;
|
||||
function getClassImplementsHeritageClauseElements(node: ClassLikeDeclaration): ExpressionWithTypeArguments[];
|
||||
function getInterfaceBaseTypeNodes(node: InterfaceDeclaration): ExpressionWithTypeArguments[];
|
||||
function getLeadingCommentRangesOfNodeFromText(node: Node, text: string): CommentRange[];
|
||||
function getLineStarts(sourceFile: SourceFile): number[];
|
||||
function getSourceFileOfNode(node: Node): SourceFile;
|
||||
function getTextOfNode(node: Node, includeTrivia?: boolean): string;
|
||||
function normalizeSlashes(path: string): string;
|
||||
function writeCommentRange(text: string, lineMap: number[], writer: EmitTextWriter, comment: CommentRange, newLine: string): void;
|
||||
function hasModifier(node: Node, flags: ModifierFlags): boolean;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { build } from "./compiler";
|
||||
@@ -0,0 +1,879 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path = require("path");
|
||||
import ts = require("typescript");
|
||||
|
||||
import { Compiler, oldGetLeadingCommentRangesOfNodeFromText } from "./compiler";
|
||||
|
||||
import * as AST from "./ast";
|
||||
|
||||
interface JSDoc {
|
||||
description: string;
|
||||
isAbstract: boolean;
|
||||
parameters: { [name: string]: AST.Parameter };
|
||||
returnType: AST.ReturnType;
|
||||
typeAnnotation: string;
|
||||
}
|
||||
|
||||
class WalkerScope {
|
||||
private _scopes: AST.HasParent[] = [];
|
||||
|
||||
get current(): AST.HasParent {
|
||||
return (this._scopes.length > 0) ? this._scopes[this._scopes.length - 1] : null;
|
||||
}
|
||||
|
||||
enter<T extends AST.HasParent>(scope: T): T {
|
||||
scope.parent = this.current;
|
||||
this._scopes.push(scope);
|
||||
return scope;
|
||||
}
|
||||
|
||||
leave(): void {
|
||||
this._scopes.pop();
|
||||
}
|
||||
}
|
||||
|
||||
class Walker {
|
||||
private _typeChecker: ts.TypeChecker;
|
||||
private _globalNS: AST.Namespace = new AST.Namespace("Global");
|
||||
private _scope: WalkerScope = new WalkerScope();
|
||||
|
||||
private _currentSourceFile: ts.SourceFile;
|
||||
|
||||
public modules: { [name: string]: AST.Module } = Object.create(null);
|
||||
public namespaces: { [name: string]: AST.Namespace } = Object.create(null);
|
||||
|
||||
constructor(private _compiler: Compiler) {
|
||||
this._typeChecker = _compiler.typeChecker;
|
||||
|
||||
this._globalNS.getMemberFullName = member => member.name;
|
||||
}
|
||||
|
||||
walk(sourceFile: ts.SourceFile): void {
|
||||
const moduleName = this._moduleNameFromFileName(sourceFile.fileName);
|
||||
|
||||
if (!(moduleName in this.modules)) {
|
||||
this.modules[moduleName] = new AST.Module(moduleName);
|
||||
}
|
||||
|
||||
const module = this._scope.enter(this.modules[moduleName]);
|
||||
this._currentSourceFile = sourceFile;
|
||||
|
||||
for (const statement of sourceFile.statements) {
|
||||
this._walk(statement, module);
|
||||
}
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
private _walk(node: ts.Node, parent: AST.Module): void {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.VariableStatement:
|
||||
this._visitVariableStatement(node as ts.VariableStatement, parent);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.FunctionDeclaration:
|
||||
this._visitFunctionDeclaration(node as ts.FunctionDeclaration, parent);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
this._visitClassDeclaration(node as ts.ClassDeclaration, parent);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.InterfaceDeclaration:
|
||||
this._visitInterfaceDeclaration(node as ts.InterfaceDeclaration, parent);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.EnumDeclaration:
|
||||
this._visitEnumDeclaration(node as ts.EnumDeclaration, parent);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.ImportDeclaration:
|
||||
this._visitImportDeclaration(node as ts.ImportDeclaration, parent);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.ExportDeclaration:
|
||||
this._visitExportDeclaration(node as ts.ExportDeclaration, parent);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.ExpressionStatement:
|
||||
case ts.SyntaxKind.ForOfStatement:
|
||||
case ts.SyntaxKind.IfStatement:
|
||||
case ts.SyntaxKind.TypeAliasDeclaration:
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(node.kind, ts.SyntaxKind[node.kind], node);
|
||||
throw new Error("Unrecognized node.");
|
||||
}
|
||||
}
|
||||
|
||||
private _walkClassMember(node: ts.Node, clazz: AST.Class): void {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.PropertySignature:
|
||||
case ts.SyntaxKind.PropertyDeclaration:
|
||||
this._visitProperty(node as ts.PropertyDeclaration, clazz);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.MethodSignature:
|
||||
case ts.SyntaxKind.MethodDeclaration:
|
||||
this._visitMethod(node as ts.MethodDeclaration, clazz);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.GetAccessor:
|
||||
this._visitGetAccessor(node as ts.AccessorDeclaration, clazz);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.SetAccessor:
|
||||
this._visitSetAccessor(node as ts.AccessorDeclaration, clazz);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.TypeParameter:
|
||||
case ts.SyntaxKind.Parameter:
|
||||
case ts.SyntaxKind.Constructor:
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(node.kind, ts.SyntaxKind[node.kind], node);
|
||||
throw new Error("Unrecognized node.");
|
||||
}
|
||||
}
|
||||
|
||||
private _walkInterfaceMember(node: ts.Node, interfase: AST.Interface): void {
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.PropertySignature:
|
||||
case ts.SyntaxKind.PropertyDeclaration:
|
||||
this._visitProperty(node as ts.PropertyDeclaration, interfase);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.MethodSignature:
|
||||
case ts.SyntaxKind.MethodDeclaration:
|
||||
this._visitMethod(node as ts.MethodDeclaration, interfase);
|
||||
break;
|
||||
|
||||
case ts.SyntaxKind.TypeParameter:
|
||||
case ts.SyntaxKind.CallSignature:
|
||||
case ts.SyntaxKind.ConstructSignature:
|
||||
case ts.SyntaxKind.IndexSignature:
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(node.kind, ts.SyntaxKind[node.kind], node);
|
||||
throw new Error("Unrecognized node.");
|
||||
}
|
||||
}
|
||||
|
||||
private _visitProperty(node: ts.PropertyDeclaration, parent: AST.Class | AST.Interface) {
|
||||
if (ts.hasModifier(node, ts.ModifierFlags.Private)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jsDoc = this._parseJSDoc(node);
|
||||
|
||||
if (jsDoc.typeAnnotation === null) {
|
||||
this._notifyIncorrectJsDoc(`Field ${ ts.getTextOfNode(node.name) } has no @type annotation.`);
|
||||
jsDoc.typeAnnotation = "*";
|
||||
}
|
||||
|
||||
const property = this._scope.enter(new AST.Property(ts.getTextOfNode(node.name)));
|
||||
parent.members[property.name] = property;
|
||||
property.getter = new AST.Getter(node, jsDoc.description, jsDoc.typeAnnotation, false);
|
||||
property.setter = new AST.Setter(node, jsDoc.description, jsDoc.typeAnnotation, false);
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
private _visitMethod(node: ts.MethodDeclaration, parent: AST.Class | AST.Interface) {
|
||||
const jsDoc = this._parseJSDoc(node);
|
||||
|
||||
const parameters = this._connectParameters(node.parameters, jsDoc.parameters,
|
||||
parameterName => `Could not find @param annotation for ${ parameterName } on method ${ ts.getTextOfNode(node.name) }`
|
||||
);
|
||||
|
||||
if (jsDoc.returnType === null && (node.type === undefined || node.type.kind !== ts.SyntaxKind.VoidKeyword)) {
|
||||
this._notifyIncorrectJsDoc(`Missing @return annotation for method ${ ts.getTextOfNode(node.name) }`);
|
||||
jsDoc.returnType = new AST.ReturnType("", "*");
|
||||
}
|
||||
|
||||
const isPrivate = ts.hasModifier(node, ts.ModifierFlags.Private);
|
||||
const isProtected = ts.hasModifier(node, ts.ModifierFlags.Protected);
|
||||
const isStatic = ts.hasModifier(node, ts.ModifierFlags.Static);
|
||||
|
||||
const generics = this._getGenericsOfSignatureDeclaration(node);
|
||||
|
||||
const method = this._scope.enter(new AST.Function(ts.getTextOfNode(node.name), node, jsDoc.description, generics, parameters, jsDoc.returnType, jsDoc.isAbstract, isPrivate, isProtected, isStatic));
|
||||
parent.members[method.name] = method;
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
private _visitGetAccessor(node: ts.AccessorDeclaration, clazz: AST.Class): void {
|
||||
const jsDoc = this._parseJSDoc(node);
|
||||
|
||||
const name = ts.getTextOfNode(node.name);
|
||||
|
||||
const isPrivate = ts.hasModifier(node, ts.ModifierFlags.Private);
|
||||
|
||||
let property = clazz.members[name] as AST.Property;
|
||||
if (property === undefined) {
|
||||
this._scope.enter(property = new AST.Property(name));
|
||||
|
||||
clazz.members[property.name] = property;
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
if (jsDoc.typeAnnotation === null) {
|
||||
this._notifyIncorrectJsDoc(`Getter ${ name } has no @type annotation.`);
|
||||
}
|
||||
|
||||
property.getter = new AST.Getter(node, jsDoc.description, jsDoc.typeAnnotation, isPrivate);
|
||||
}
|
||||
|
||||
private _visitSetAccessor(node: ts.AccessorDeclaration, clazz: AST.Class): void {
|
||||
const jsDoc = this._parseJSDoc(node);
|
||||
|
||||
const name = ts.getTextOfNode(node.name);
|
||||
|
||||
const isPrivate = ts.hasModifier(node, ts.ModifierFlags.Private);
|
||||
|
||||
let property = clazz.members[name] as AST.Property;
|
||||
if (property === undefined) {
|
||||
this._scope.enter(property = new AST.Property(name));
|
||||
|
||||
clazz.members[property.name] = property;
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
if (jsDoc.typeAnnotation === null) {
|
||||
this._notifyIncorrectJsDoc(`Setter ${ name } has no @type annotation.`);
|
||||
}
|
||||
|
||||
property.setter = new AST.Setter(node, jsDoc.description, jsDoc.typeAnnotation, isPrivate);
|
||||
}
|
||||
|
||||
private _visitVariableStatement(node: ts.VariableStatement, parent: AST.Module): void {
|
||||
if (node.declarationList.declarations.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const declaration = node.declarationList.declarations[0];
|
||||
if (ts.hasModifier(declaration, ts.ModifierFlags.Ambient)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jsDoc = this._parseJSDoc(node);
|
||||
if (jsDoc.typeAnnotation === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const property = this._scope.enter(new AST.Property(ts.getTextOfNode(declaration.name)));
|
||||
property.getter = new AST.Getter(node, jsDoc.description, jsDoc.typeAnnotation, false);
|
||||
|
||||
parent.members[property.name] = property;
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
private _visitFunctionDeclaration(node: ts.FunctionDeclaration, parent: AST.Module): void {
|
||||
const jsDoc = this._parseJSDoc(node);
|
||||
|
||||
const isPrivate = !ts.hasModifier(node, ts.ModifierFlags.Export);
|
||||
|
||||
const generics = this._getGenericsOfSignatureDeclaration(node);
|
||||
|
||||
const parameters = this._connectParameters(node.parameters, jsDoc.parameters,
|
||||
parameterName => `Could not find @param annotation for ${ parameterName } on function ${ node.name.text }`
|
||||
);
|
||||
|
||||
if (node.type === undefined) {
|
||||
this._notifyIncorrectJsDoc(`Missing return type annotation for function ${ node.name.text }`);
|
||||
jsDoc.returnType = new AST.ReturnType("", "*");
|
||||
}
|
||||
else if (jsDoc.returnType === null && node.type.kind !== ts.SyntaxKind.VoidKeyword) {
|
||||
this._notifyIncorrectJsDoc(`Missing @return annotation for function ${ node.name.text }`);
|
||||
jsDoc.returnType = new AST.ReturnType("", "*");
|
||||
}
|
||||
|
||||
const freeFunction = this._scope.enter(new AST.Function(node.name.text, node, jsDoc.description, generics, parameters, jsDoc.returnType, jsDoc.isAbstract, isPrivate, false, false));
|
||||
|
||||
parent.members[freeFunction.name] = freeFunction;
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
private _visitClassDeclaration(node: ts.ClassDeclaration, parent: AST.Module): void {
|
||||
const jsDoc = this._parseJSDoc(node);
|
||||
|
||||
const type = this._typeChecker.getTypeAtLocation(node) as ts.InterfaceType;
|
||||
|
||||
const generics = this._getGenericsOfInterfaceType(type);
|
||||
|
||||
const baseTypeHeritageClauseElement = ts.getClassExtendsHeritageClauseElement(node) || null;
|
||||
let baseType: AST.UnresolvedType = null;
|
||||
if (baseTypeHeritageClauseElement !== null) {
|
||||
baseType = new AST.UnresolvedType(
|
||||
this._typeChecker.getTypeAtLocation(baseTypeHeritageClauseElement).symbol,
|
||||
this._getGenericsOfTypeReferenceNode(baseTypeHeritageClauseElement, generics)
|
||||
);
|
||||
}
|
||||
|
||||
const interfaces = (ts.getClassImplementsHeritageClauseElements(node) || []).map(type => new AST.UnresolvedType(
|
||||
this._typeChecker.getTypeAtLocation(type).symbol,
|
||||
this._getGenericsOfTypeReferenceNode(type, generics)
|
||||
));
|
||||
|
||||
const isPrivate = !ts.hasModifier(node, ts.ModifierFlags.Export);
|
||||
|
||||
let parameters: AST.Parameter[] = [];
|
||||
|
||||
if (type.symbol.members["__constructor"] !== undefined) {
|
||||
parameters = this._connectParameters((type.symbol.members["__constructor"].declarations[0] as ts.ConstructorDeclaration).parameters, jsDoc.parameters,
|
||||
parameterName => `Could not find @param annotation for ${ parameterName } on constructor in class ${ node.name.text }`
|
||||
);
|
||||
}
|
||||
else if (Object.keys(jsDoc.parameters).length > 0) {
|
||||
this._notifyIncorrectJsDoc("There are @param annotations on this class but it has no constructors.");
|
||||
}
|
||||
|
||||
const clazz = this._scope.enter(new AST.Class(node.name.text, node, jsDoc.description, generics, parameters, baseType, interfaces, jsDoc.isAbstract, isPrivate));
|
||||
|
||||
parent.members[clazz.name] = clazz;
|
||||
|
||||
ts.forEachProperty(type.symbol.exports, symbol => {
|
||||
if (symbol.name === "prototype") {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const declaration of symbol.declarations) {
|
||||
this._walkClassMember(declaration, clazz);
|
||||
}
|
||||
});
|
||||
|
||||
ts.forEachProperty(type.symbol.members, symbol => {
|
||||
for (const declaration of symbol.declarations) {
|
||||
this._walkClassMember(declaration, clazz);
|
||||
}
|
||||
});
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
private _visitInterfaceDeclaration(node: ts.InterfaceDeclaration, parent: AST.Module): void {
|
||||
const jsDoc = this._parseJSDoc(node);
|
||||
|
||||
const type = this._typeChecker.getTypeAtLocation(node) as ts.InterfaceType;
|
||||
|
||||
const generics = this._getGenericsOfInterfaceType(type);
|
||||
|
||||
const baseTypes = (ts.getInterfaceBaseTypeNodes(node) || []).map(type => new AST.UnresolvedType(
|
||||
this._typeChecker.getTypeAtLocation(type).symbol,
|
||||
this._getGenericsOfTypeReferenceNode(type, generics)
|
||||
));
|
||||
|
||||
const existingInterfaceType = parent.members[node.name.text];
|
||||
if (existingInterfaceType !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isPrivate = !ts.hasModifier(node, ts.ModifierFlags.Export);
|
||||
|
||||
const interfase = this._scope.enter(new AST.Interface(node.name.text, node, jsDoc.description, generics, baseTypes, isPrivate));
|
||||
parent.members[interfase.name] = interfase;
|
||||
|
||||
ts.forEachProperty(type.symbol.members, symbol => {
|
||||
for (const declaration of symbol.declarations) {
|
||||
this._walkInterfaceMember(declaration, interfase);
|
||||
}
|
||||
});
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
private _visitEnumDeclaration(node: ts.EnumDeclaration, parent: AST.Module): void {
|
||||
const jsDoc = this._parseJSDoc(node);
|
||||
|
||||
const existingEnumType = parent.members[node.name.text];
|
||||
if (existingEnumType !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isPrivate = !ts.hasModifier(node, ts.ModifierFlags.Export);
|
||||
|
||||
const type = this._typeChecker.getTypeAtLocation(node);
|
||||
|
||||
const enumType = this._scope.enter(new AST.Enum(node.name.text, node, jsDoc.description, isPrivate));
|
||||
parent.members[enumType.name] = enumType;
|
||||
|
||||
ts.forEachProperty(type.symbol.exports, symbol => {
|
||||
this._visitEnumMember(symbol.declarations[0] as ts.EnumMember, enumType);
|
||||
});
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
private _visitEnumMember(node: ts.EnumMember, parent: AST.Enum): void {
|
||||
const jsDoc = this._parseJSDoc(node);
|
||||
|
||||
const value = (node.initializer === undefined) ? null : parseInt((node.initializer as ts.LiteralExpression).text);
|
||||
|
||||
const enumMember = this._scope.enter(new AST.EnumMember(ts.getTextOfNode(node.name), (jsDoc === null) ? "" : jsDoc.description, value));
|
||||
|
||||
parent.members.push(enumMember);
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
private _visitImportDeclaration(node: ts.ImportDeclaration, parent: AST.Module): void {
|
||||
if (node.importClause === undefined) {
|
||||
// import "foo";
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.importClause.namedBindings === undefined) {
|
||||
throw new Error("Default import is not supported.");
|
||||
}
|
||||
|
||||
const moduleName = this._resolve((node.moduleSpecifier as ts.LiteralExpression).text, parent);
|
||||
|
||||
if ((node.importClause.namedBindings as ts.NamespaceImport).name !== undefined) {
|
||||
// import * as foo from "baz";
|
||||
parent.members[(node.importClause.namedBindings as ts.NamespaceImport).name.text] = new AST.Reference(moduleName, "*", true);
|
||||
}
|
||||
else if ((node.importClause.namedBindings as ts.NamedImports).elements !== undefined) {
|
||||
// import { foo, bar } from "baz";
|
||||
for (const element of (node.importClause.namedBindings as ts.NamedImports).elements) {
|
||||
const importedName = element.propertyName && element.propertyName.text || element.name.text;
|
||||
parent.members[element.name.text] = new AST.Reference(moduleName, importedName, true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error("Unrecognized import declaration syntax.");
|
||||
}
|
||||
}
|
||||
|
||||
private _visitExportDeclaration(node: ts.ExportDeclaration, parent: AST.Module): void {
|
||||
if (node.moduleSpecifier !== undefined) {
|
||||
// export { foo } from "bar";
|
||||
const moduleName = this._resolve((node.moduleSpecifier as ts.LiteralExpression).text, parent);
|
||||
for (const element of node.exportClause.elements) {
|
||||
const importedName = element.propertyName && element.propertyName.text || element.name.text;
|
||||
parent.members[element.name.text] = new AST.Reference(moduleName, importedName, false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// export { foo };
|
||||
for (const element of node.exportClause.elements) {
|
||||
(parent.members[element.name.text] as AST.CanBePrivate).isPrivate = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _resolve(relativeModuleName: string, currentModule: AST.Module): string {
|
||||
let result = ts.normalizeSlashes(path.join(currentModule.name, `../${ relativeModuleName }`));
|
||||
|
||||
if (result[0] !== ".") {
|
||||
result = `./${ result }`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _parseJSDoc(node: ts.Node): JSDoc {
|
||||
let comments = oldGetLeadingCommentRangesOfNodeFromText(node, this._currentSourceFile.text);
|
||||
|
||||
if (comments === undefined) {
|
||||
comments = [];
|
||||
}
|
||||
|
||||
if (comments.length > 1) {
|
||||
comments = [comments[comments.length - 1]];
|
||||
}
|
||||
|
||||
const comment =
|
||||
(comments.length === 0) ?
|
||||
"" :
|
||||
this._currentSourceFile.text.substring(comments[0].pos, comments[0].end);
|
||||
|
||||
const commentStartIndex = comment.indexOf("/**");
|
||||
const commentEndIndex = comment.lastIndexOf("*/");
|
||||
|
||||
const lines =
|
||||
(commentStartIndex === -1 || commentEndIndex === -1) ?
|
||||
[] :
|
||||
comment.substring(commentStartIndex + 2, commentEndIndex).split("\n").map(line => {
|
||||
const match = line.match(/^[ \t]*\* (.*)/);
|
||||
if (match === null) {
|
||||
return "";
|
||||
}
|
||||
return match[1];
|
||||
});
|
||||
|
||||
let rootDescription = "";
|
||||
|
||||
const parameters: { [name: string]: AST.Parameter } = Object.create(null);
|
||||
|
||||
let typeAnnotation: string = null;
|
||||
|
||||
let returnType: AST.ReturnType = null;
|
||||
|
||||
let isAbstract = false;
|
||||
|
||||
let lastRead: { description: string } = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const firstWordMatch = line.match(/^\s*(\S+)(\s*)/);
|
||||
const firstWord = (firstWordMatch !== null) ? firstWordMatch[1] : "";
|
||||
let remainingLine = (firstWordMatch !== null) ? line.substring(firstWordMatch[0].length) : "";
|
||||
|
||||
if (firstWord[0] === "@") {
|
||||
lastRead = null;
|
||||
}
|
||||
|
||||
switch (firstWord) {
|
||||
case "@abstract":
|
||||
isAbstract = true;
|
||||
break;
|
||||
|
||||
case "@param": {
|
||||
let type: string;
|
||||
[type, remainingLine] = this._readType(remainingLine);
|
||||
|
||||
const [, name, description] = remainingLine.match(/(\S+)\s*(.*)/);
|
||||
|
||||
const subParameterMatch = name.match(/^(?:(.+)\.([^\.]+))|(?:(.+)\[("[^\[\]"]+")\])$/);
|
||||
if (subParameterMatch === null) {
|
||||
parameters[name] = lastRead = new AST.Parameter(name, description, type);
|
||||
}
|
||||
else {
|
||||
const parentName = subParameterMatch[1] || subParameterMatch[3];
|
||||
const childName = subParameterMatch[2] || subParameterMatch[4];
|
||||
const parentParameter = parameters[parentName];
|
||||
parentParameter.subParameters.push(lastRead = new AST.Parameter(childName, description, type));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "@return": {
|
||||
const [type, description] = this._readType(remainingLine);
|
||||
|
||||
returnType = lastRead = new AST.ReturnType(description, type);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "@type":
|
||||
[typeAnnotation] = this._readType(remainingLine);
|
||||
break;
|
||||
|
||||
default:
|
||||
if (lastRead !== null) {
|
||||
lastRead.description += "\n" + line;
|
||||
}
|
||||
else {
|
||||
rootDescription += ((rootDescription.length > 0) ? "\n" : "") + line;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
description: rootDescription,
|
||||
isAbstract: isAbstract,
|
||||
parameters: parameters,
|
||||
returnType: returnType,
|
||||
typeAnnotation: typeAnnotation,
|
||||
};
|
||||
}
|
||||
|
||||
private _readType(remainingLine: string): [string, string] {
|
||||
if (remainingLine[0] !== "{") {
|
||||
return ["*", remainingLine];
|
||||
}
|
||||
|
||||
let index = -1;
|
||||
let numberOfUnterminatedBraces = 0;
|
||||
for (let i = 0; i < remainingLine.length; i++) {
|
||||
if (remainingLine[i] === "{") {
|
||||
numberOfUnterminatedBraces++;
|
||||
}
|
||||
else if (remainingLine[i] === "}") {
|
||||
numberOfUnterminatedBraces--;
|
||||
|
||||
if (numberOfUnterminatedBraces === 0) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (index === -1) {
|
||||
throw new Error("Unterminated type specifier.");
|
||||
}
|
||||
|
||||
const type = remainingLine.substr(1, index - 1);
|
||||
remainingLine = remainingLine.substr(index + 1).replace(/^\s+/, "");
|
||||
|
||||
return [type, remainingLine];
|
||||
}
|
||||
|
||||
private _getGenericsOfSignatureDeclaration(signatureDeclaration: ts.SignatureDeclaration): string[] {
|
||||
if (signatureDeclaration.typeParameters === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return signatureDeclaration.typeParameters.map(typeParameter => typeParameter.name.text);
|
||||
}
|
||||
|
||||
private _getGenericsOfTypeReferenceNode(typeReferenceNode: ts.ExpressionWithTypeArguments, intrinsicGenerics: string[]): (AST.UnresolvedType | AST.IntrinsicTypeReference)[] {
|
||||
if (typeReferenceNode.typeArguments === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const typeReference = this._typeChecker.getTypeAtLocation(typeReferenceNode) as ts.TypeReference;
|
||||
|
||||
return typeReference.typeArguments.map(typeArgument => {
|
||||
if ((typeArgument as ts.IntrinsicType).intrinsicName !== undefined) {
|
||||
return new AST.IntrinsicTypeReference((typeArgument as ts.IntrinsicType).intrinsicName);
|
||||
}
|
||||
|
||||
if (typeArgument.flags & ts.TypeFlags.TypeParameter) {
|
||||
if (intrinsicGenerics.indexOf(typeArgument.symbol.name) !== -1) {
|
||||
return new AST.IntrinsicTypeReference(typeArgument.symbol.name);
|
||||
}
|
||||
|
||||
throw new Error(`Unbound type parameter ${ typeArgument.symbol.name }`);
|
||||
}
|
||||
|
||||
return new AST.UnresolvedType(typeArgument.symbol, []);
|
||||
});
|
||||
}
|
||||
|
||||
private _getGenericsOfInterfaceType(interfaceType: ts.InterfaceType): string[] {
|
||||
if (interfaceType.typeParameters === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return interfaceType.typeParameters.map(typeParameter => {
|
||||
return typeParameter.symbol.name;
|
||||
});
|
||||
}
|
||||
|
||||
private _connectParameters(astParameters: ts.ParameterDeclaration[], jsDocParameters: { [name: string]: AST.Parameter }, onMissingMessageCallback: (parameterName: string) => string) {
|
||||
return astParameters.map(parameter => {
|
||||
let parameterName = (parameter.name as ts.Identifier).text;
|
||||
if (parameterName[0] === "_") {
|
||||
parameterName = parameterName.substr(1);
|
||||
}
|
||||
|
||||
let jsDocParameter = jsDocParameters[parameterName];
|
||||
|
||||
if (jsDocParameter === undefined) {
|
||||
this._notifyIncorrectJsDoc(onMissingMessageCallback.call(this, parameterName));
|
||||
jsDocParameter = new AST.Parameter(parameterName, "*", "");
|
||||
}
|
||||
|
||||
return jsDocParameter;
|
||||
});
|
||||
}
|
||||
|
||||
private _notifyIncorrectJsDoc(message: string): void {
|
||||
const fileName = path.basename(this._currentSourceFile.fileName);
|
||||
if (fileName === "lib.es5.d.ts" || fileName === "lib.dom.d.ts") {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`${ fileName }: ${ this._scope.current.fullName }: ${ message }`);
|
||||
}
|
||||
|
||||
link(rootNamespaceName: string): void {
|
||||
for (const moduleName of Object.keys(this.modules)) {
|
||||
const module = this.modules[moduleName];
|
||||
|
||||
for (const memberName of Object.keys(module.members)) {
|
||||
const member = module.members[memberName];
|
||||
|
||||
if (member instanceof AST.Class) {
|
||||
if (member.unresolvedBaseType instanceof AST.UnresolvedType) {
|
||||
member.baseType = this._resolveTypeReference(member.unresolvedBaseType);
|
||||
}
|
||||
else {
|
||||
member.baseType = member.unresolvedBaseType;
|
||||
}
|
||||
|
||||
member.interfaces = member.unresolvedInterfaces.map(interfase => {
|
||||
if (interfase instanceof AST.UnresolvedType) {
|
||||
return this._resolveTypeReference(interfase);
|
||||
}
|
||||
|
||||
return interfase;
|
||||
});
|
||||
}
|
||||
|
||||
else if (member instanceof AST.Interface) {
|
||||
member.baseTypes = member.unresolvedBaseTypes.map(baseType => {
|
||||
if (baseType instanceof AST.UnresolvedType) {
|
||||
return this._resolveTypeReference(baseType);
|
||||
}
|
||||
|
||||
return baseType;
|
||||
});
|
||||
}
|
||||
|
||||
else if (member instanceof AST.Enum) {
|
||||
let value = 0;
|
||||
for (const enumMember of member.members) {
|
||||
if (enumMember.value === null) {
|
||||
enumMember.value = value;
|
||||
}
|
||||
else {
|
||||
value = enumMember.value;
|
||||
}
|
||||
|
||||
value++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.namespaces[rootNamespaceName] = this._scope.enter(new AST.Namespace(rootNamespaceName));
|
||||
this._moduleToNamespace(this.modules["./index"]);
|
||||
this._scope.leave();
|
||||
}
|
||||
|
||||
private _moduleToNamespace(module: AST.Module): void {
|
||||
for (const memberName of Object.keys(module.members)) {
|
||||
let member = module.members[memberName];
|
||||
|
||||
if (member instanceof AST.Reference) {
|
||||
if (member.isPrivate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (member.name === "*") {
|
||||
const newNamespace = this._scope.enter(new AST.Namespace(memberName));
|
||||
|
||||
const existingNamespace = this.namespaces[newNamespace.fullName];
|
||||
if (existingNamespace !== undefined) {
|
||||
this._scope.leave();
|
||||
this._scope.enter(existingNamespace);
|
||||
}
|
||||
else {
|
||||
this.namespaces[newNamespace.fullName] = newNamespace;
|
||||
}
|
||||
|
||||
let referencedModuleName = member.moduleName;
|
||||
let referencedModule = this.modules[referencedModuleName];
|
||||
if (referencedModule === undefined && ((referencedModuleName + "/index") in this.modules)) {
|
||||
member.moduleName = referencedModuleName = referencedModuleName + "/index";
|
||||
referencedModule = this.modules[referencedModuleName];
|
||||
}
|
||||
this._moduleToNamespace(referencedModule);
|
||||
|
||||
this._scope.leave();
|
||||
}
|
||||
else {
|
||||
while (member instanceof AST.Reference) {
|
||||
member = this.modules[member.moduleName].members[member.name];
|
||||
}
|
||||
|
||||
this._scope.enter(member);
|
||||
this._scope.leave();
|
||||
(this._scope.current as AST.Namespace).members[member.name] = member;
|
||||
}
|
||||
}
|
||||
else if (!(member as AST.CanBePrivate).isPrivate) {
|
||||
this._scope.enter(member);
|
||||
this._scope.leave();
|
||||
(this._scope.current as AST.Namespace).members[member.name] = member;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveTypeReference(unresolvedType: AST.UnresolvedType): AST.TypeReference {
|
||||
let node: ts.Node = unresolvedType.symbol.declarations[0];
|
||||
while (node.kind !== ts.SyntaxKind.SourceFile) {
|
||||
node = node.parent;
|
||||
}
|
||||
|
||||
const sourceFile = node as ts.SourceFile;
|
||||
|
||||
const moduleName = this._moduleNameFromFileName(sourceFile.fileName);
|
||||
const module = this.modules[moduleName];
|
||||
|
||||
let result = module.members[unresolvedType.symbol.name];
|
||||
|
||||
if (result === undefined) {
|
||||
throw new Error(`Type ${ unresolvedType.symbol.name } could not be resolved.`);
|
||||
}
|
||||
|
||||
while (result instanceof AST.Reference) {
|
||||
result = this.modules[result.moduleName].members[result.name];
|
||||
}
|
||||
|
||||
const resultGenerics = unresolvedType.generics.map(generic => {
|
||||
if (generic instanceof AST.UnresolvedType) {
|
||||
return this._resolveTypeReference(generic);
|
||||
}
|
||||
|
||||
return generic;
|
||||
});
|
||||
|
||||
return new AST.TypeReference(result, resultGenerics);
|
||||
}
|
||||
|
||||
private _moduleNameFromFileName(fileName: string): string {
|
||||
let result = ts.normalizeSlashes(path.relative(this._compiler.projectRoot, fileName));
|
||||
|
||||
result = result.substr(0, result.length - ".ts".length);
|
||||
|
||||
if (result[0] !== ".") {
|
||||
result = `./${ result }`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function walk(compiler: Compiler, root: string, rootNamespaceName: string) {
|
||||
const sourceFiles = compiler.sourceFiles;
|
||||
|
||||
const walker = new Walker(compiler);
|
||||
|
||||
// Walk
|
||||
for (const sourceFile of sourceFiles) {
|
||||
if (
|
||||
path.basename(sourceFile.fileName) === "lib.es5.d.ts" ||
|
||||
path.basename(sourceFile.fileName) === "lib.dom.d.ts" ||
|
||||
sourceFile.fileName.substr(-"references.d.ts".length) === "references.d.ts"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
walker.walk(sourceFile);
|
||||
}
|
||||
|
||||
// Link base types and set enum member values if unspecified.
|
||||
walker.link(rootNamespaceName);
|
||||
|
||||
// Return types
|
||||
return { namespaces: walker.namespaces, modules: walker.modules };
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
|
||||
var UglifyJS = require("uglify-js");
|
||||
|
||||
var FileTransform = require("async-build").FileTransform;
|
||||
|
||||
var Run = (function () {
|
||||
function Run(outputLibraryName, unusedVarsToIgnore) {
|
||||
this._outputLibraryName = outputLibraryName;
|
||||
this._unusedVarsToIgnore = unusedVarsToIgnore;
|
||||
|
||||
this._root = UglifyJS.parse(fs.readFileSync(path.resolve(__filename, "..", "umd-wrapper.js"), "utf8"));
|
||||
|
||||
this._root.figure_out_scope({ screw_ie8: true });
|
||||
|
||||
this._toInsert = null;
|
||||
|
||||
this._rootSourceMap = null;
|
||||
}
|
||||
|
||||
Run.prototype.addFile = function (file) {
|
||||
switch (path.extname(file.path)) {
|
||||
case ".js":
|
||||
try {
|
||||
this._toInsert = UglifyJS.parse(file.contents.toString(), {
|
||||
filename: path.basename(file.path),
|
||||
toplevel: null,
|
||||
}).body;
|
||||
}
|
||||
catch (ex) {
|
||||
if (ex instanceof UglifyJS.JS_Parse_Error) {
|
||||
throw new Error("UglifyJS parse error: " + ex.toString() + "\n");
|
||||
}
|
||||
|
||||
throw ex;
|
||||
}
|
||||
break;
|
||||
|
||||
case ".map":
|
||||
var rawSourceMap = JSON.parse(file.contents.toString());
|
||||
|
||||
this._rootSourceMap = UglifyJS.SourceMap({
|
||||
file: this._outputLibraryName + ".js",
|
||||
root: "",
|
||||
orig: rawSourceMap,
|
||||
});
|
||||
|
||||
var generator = this._rootSourceMap.get();
|
||||
|
||||
rawSourceMap.sources.forEach(function (sourceRelativePath, index) {
|
||||
generator.setSourceContent(sourceRelativePath, rawSourceMap.sourcesContent[index]);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Run.prototype.build = function (outputStream) {
|
||||
var _this = this;
|
||||
|
||||
|
||||
// Splice in the TS output into the UMD wrapper.
|
||||
var insertionParent = this._root.body[0].body.args[1].body;
|
||||
|
||||
this._toInsert.reverse();
|
||||
for (var i = this._toInsert.length - 1; i >= 0; i--) {
|
||||
var node = this._toInsert[i];
|
||||
if (node instanceof UglifyJS.AST_Var) {
|
||||
for (var j = 0; j < node.definitions.length; j++) {
|
||||
var definition = node.definitions[j];
|
||||
if (definition.name.name === "__extends" || definition.name.name === "__decorate") {
|
||||
definition.value = definition.value.right;
|
||||
}
|
||||
}
|
||||
|
||||
insertionParent.splice(-1, 0, node);
|
||||
this._toInsert.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
insertionParent.splice.apply(insertionParent, [-1, 0].concat(this._toInsert));
|
||||
|
||||
|
||||
// Fixups
|
||||
for (var i = 0; i < this._toInsert.length; i++) {
|
||||
var node = this._toInsert[i];
|
||||
if (node instanceof UglifyJS.AST_Statement && node.body instanceof UglifyJS.AST_Call && node.body.expression.name === "define") {
|
||||
var defineCall = node.body;
|
||||
|
||||
defineCall.expression.name = "def";
|
||||
|
||||
if (defineCall.args[1].elements[0].value !== "require") {
|
||||
throw new Error("Expected first dep to be require");
|
||||
}
|
||||
defineCall.args[1].elements.shift();
|
||||
defineCall.args[2].argnames.shift();
|
||||
|
||||
if (defineCall.args[1].elements[0].value !== "exports") {
|
||||
throw new Error("Expected second dep to be exports");
|
||||
}
|
||||
defineCall.args[1].elements.shift();
|
||||
defineCall.args[2].argnames.push(defineCall.args[2].argnames.shift());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remove all license headers except the one from the UMD wrapper
|
||||
var firstLicenseHeader = null;
|
||||
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (node.start) {
|
||||
(node.start.comments_before || []).some(function (comment, i) {
|
||||
if (comment.value.indexOf("Copyright") !== -1) {
|
||||
if (firstLicenseHeader === null) {
|
||||
firstLicenseHeader = comment;
|
||||
}
|
||||
else if (comment !== firstLicenseHeader) {
|
||||
node.start.comments_before.splice(i, 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// Fixup anonymous functions to print a space after "function"
|
||||
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (node instanceof UglifyJS.AST_Lambda && !node.name) {
|
||||
node.name = Object.create(UglifyJS.AST_Node.prototype);
|
||||
node.name.print = function () { };
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// Fix alignment of multi-line block comments
|
||||
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (node.start && node.start.comments_before) {
|
||||
node.start.comments_before.forEach(function (comment) {
|
||||
if (comment.value.indexOf("Copyright") !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lines = comment.value.split("\n");
|
||||
if (lines.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
var indent = " "; // 5 spaces
|
||||
for (var i = 0; i < comment.col; i++) {
|
||||
indent += " ";
|
||||
}
|
||||
lines[lines.length - 1] = lines[lines.length - 1].replace(/\s+$/, indent);
|
||||
comment.value = [lines[0]].concat(lines.slice(1).map(function (line) { return line.replace(/^\s+/, indent); })).join("\n");
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
this._root.figure_out_scope({ screw_ie8: true });
|
||||
|
||||
|
||||
// Remove some things from the AST
|
||||
var nodesToRemove = [];
|
||||
|
||||
// Set if there are any unused variables apart from the ones in unusedVarsToIgnore
|
||||
var haveUnusedVars = false;
|
||||
|
||||
// Repeat because removing some declarations may make others unreferenced
|
||||
for (;;) {
|
||||
this._root.figure_out_scope({ screw_ie8: true });
|
||||
|
||||
// Unreferenced variable and function declarations, and unreferenced terminal function arguments
|
||||
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (node instanceof UglifyJS.AST_SymbolDeclaration && node.unreferenced()) {
|
||||
if (node instanceof UglifyJS.AST_SymbolFunarg) {
|
||||
if (this.parent().argnames.indexOf(node) === this.parent().argnames.length - 1) {
|
||||
nodesToRemove.push({ node: node, parent: this.parent().argnames });
|
||||
}
|
||||
}
|
||||
else if (node instanceof UglifyJS.AST_SymbolVar) {
|
||||
if (_this._unusedVarsToIgnore.indexOf(node.name) !== -1) {
|
||||
nodesToRemove.push({ node: this.parent(), parent: this.parent(1).definitions });
|
||||
if (this.parent(1).definitions.length === 1) {
|
||||
nodesToRemove.push({ node: this.parent(1), parent: this.parent(2).body });
|
||||
}
|
||||
}
|
||||
else {
|
||||
haveUnusedVars = true;
|
||||
}
|
||||
}
|
||||
else if (node instanceof UglifyJS.AST_SymbolDefun) {
|
||||
nodesToRemove.push({ node: this.parent(), parent: this.parent(1).body });
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
if (nodesToRemove.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
nodesToRemove.forEach(function (tuple) {
|
||||
tuple.parent.splice(tuple.parent.indexOf(tuple.node), 1);
|
||||
});
|
||||
|
||||
nodesToRemove = [];
|
||||
}
|
||||
|
||||
|
||||
// Move var statements at the end of blocks (generated by TS for rest parameters) to the start of the block.
|
||||
// This is needed to prevent unreachable-code warnings from UJS
|
||||
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (
|
||||
node instanceof UglifyJS.AST_Block &&
|
||||
node.body[node.body.length - 1] instanceof UglifyJS.AST_Var
|
||||
) {
|
||||
node.body.unshift(node.body.pop());
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// Split multiple vars per declaration into one var per declaration
|
||||
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (
|
||||
node instanceof UglifyJS.AST_Var &&
|
||||
node.definitions.length > 1 &&
|
||||
this.parent() instanceof UglifyJS.AST_Block
|
||||
) {
|
||||
var parent = this.parent().body;
|
||||
parent.splice.apply(parent, [parent.indexOf(node), 1].concat(node.definitions.map(function (definition) {
|
||||
return new UglifyJS.AST_Var({ start: node.start, end: node.end, definitions: [definition] });
|
||||
})));
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// Rename all function arguments that begin with _ to not have the _.
|
||||
// This converts the TypeScript syntax of declaring private members in the constructor declaration `function Color(private _red: number, ...)` to `function Color(red, ...)`
|
||||
// so that it matches the JSDoc (and looks nicer).
|
||||
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (
|
||||
node instanceof UglifyJS.AST_SymbolFunarg &&
|
||||
node.thedef.name[0] === "_" &&
|
||||
node.thedef.name[1] !== "_" &&
|
||||
node.thedef.name !== "_super" // Don't rename _super (used in TypeScript's inheritance shim) to super. super is a reserved word.
|
||||
) {
|
||||
node.thedef.name = node.thedef.name.slice(1);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// Output
|
||||
var output = {
|
||||
source_map: this._rootSourceMap,
|
||||
ascii_only: true,
|
||||
beautify: true,
|
||||
comments: true,
|
||||
};
|
||||
|
||||
var stream = UglifyJS.OutputStream(output);
|
||||
this._root.print(stream);
|
||||
|
||||
outputStream.push({
|
||||
path: this._outputLibraryName + ".js",
|
||||
contents: Buffer.concat([new Buffer(stream.toString()), new Buffer("\n//# sourceMappingURL=" + this._outputLibraryName + ".js.map")])
|
||||
});
|
||||
|
||||
outputStream.push({
|
||||
path: this._outputLibraryName + ".js.map",
|
||||
contents: new Buffer(this._rootSourceMap.get().toString())
|
||||
});
|
||||
|
||||
// Print unused variables
|
||||
if (haveUnusedVars) {
|
||||
this._root.walk(new UglifyJS.TreeWalker(function (node, descend) {
|
||||
if (node instanceof UglifyJS.AST_SymbolVar && node.unreferenced()) {
|
||||
if (_this._unusedVarsToIgnore.indexOf(node.name) === -1) {
|
||||
console.warn("Unused variable %s at %s:%s:%s", node.name, node.start.file, node.start.line, node.start.col);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return Run;
|
||||
})();
|
||||
|
||||
module.exports = {
|
||||
build: function (outputLibraryName, unusedVarsToIgnore) {
|
||||
var run = new Run(outputLibraryName, unusedVarsToIgnore);
|
||||
|
||||
return new FileTransform(function (file) {
|
||||
run.addFile(file);
|
||||
}, function () {
|
||||
run.build(this);
|
||||
});
|
||||
},
|
||||
|
||||
watch: function (outputLibraryName, unusedVarsToIgnore) {
|
||||
var files = Object.create(null);
|
||||
|
||||
return new FileTransform(function (file) {
|
||||
if (file.path !== "END") {
|
||||
files[file.path] = file;
|
||||
}
|
||||
else {
|
||||
var run = new Run(outputLibraryName, unusedVarsToIgnore);
|
||||
Object.keys(files).forEach(function (filename) {
|
||||
run.addFile(files[filename]);
|
||||
});
|
||||
run.build(this);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
minify: function () {
|
||||
var codeFile = null;
|
||||
var sourceMapFile = null;
|
||||
|
||||
return new FileTransform(function (file) {
|
||||
switch (path.extname(file.path)) {
|
||||
case ".js":
|
||||
codeFile = file;
|
||||
break;
|
||||
case ".map":
|
||||
sourceMapFile = file;
|
||||
break;
|
||||
}
|
||||
|
||||
if (codeFile !== null && sourceMapFile !== null) {
|
||||
UglifyJS.base54.reset();
|
||||
|
||||
|
||||
// Parse
|
||||
var root = null;
|
||||
root = UglifyJS.parse(codeFile.contents.toString(), {
|
||||
filename: path.basename(codeFile.path),
|
||||
toplevel: root
|
||||
});
|
||||
|
||||
root.figure_out_scope({ screw_ie8: true });
|
||||
|
||||
|
||||
// Warnings
|
||||
root.scope_warnings({
|
||||
func_arguments: false
|
||||
});
|
||||
|
||||
|
||||
// Compress
|
||||
var compressor = UglifyJS.Compressor({
|
||||
warnings: true,
|
||||
screw_ie8: true
|
||||
});
|
||||
root = root.transform(compressor);
|
||||
|
||||
|
||||
// Mangle
|
||||
root.figure_out_scope({ screw_ie8: true });
|
||||
root.compute_char_frequency();
|
||||
root.mangle_names({ screw_ie8: true });
|
||||
root = UglifyJS.mangle_properties(root, {
|
||||
regex: /^_/
|
||||
});
|
||||
|
||||
|
||||
// Output
|
||||
var firstLicenseHeaderFound = false; // To detect and preserve the first license header
|
||||
|
||||
var output = {
|
||||
source_map: UglifyJS.SourceMap({
|
||||
file: path.basename(sourceMapFile.path),
|
||||
orig: sourceMapFile.contents.toString()
|
||||
}),
|
||||
ascii_only: true,
|
||||
comments: function (node, comment) {
|
||||
if (!firstLicenseHeaderFound && comment.value.indexOf("Copyright") !== -1) {
|
||||
firstLicenseHeaderFound = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
screw_ie8: true
|
||||
};
|
||||
|
||||
var stream = UglifyJS.OutputStream(output);
|
||||
root.print(stream);
|
||||
|
||||
codeFile.path = codeFile.path.replace(/\.js$/, ".min.js");
|
||||
sourceMapFile.path = sourceMapFile.path.replace(/\.js\.map$/, ".min.js.map");
|
||||
|
||||
codeFile.contents = Buffer.concat([new Buffer(stream.toString()), new Buffer("\n//# sourceMappingURL="), new Buffer(sourceMapFile.path)]);
|
||||
this.push(codeFile);
|
||||
|
||||
var inputSourceMapObject = JSON.parse(sourceMapFile.contents.toString());
|
||||
var outputSourceMapObject = output.source_map.get();
|
||||
outputSourceMapObject._sources.toArray().forEach(function (filename, i) {
|
||||
outputSourceMapObject.setSourceContent(filename, inputSourceMapObject.sourcesContent[i]);
|
||||
});
|
||||
|
||||
sourceMapFile.contents = new Buffer(output.source_map.toString());
|
||||
this.push(sourceMapFile);
|
||||
|
||||
codeFile = null;
|
||||
sourceMapFile = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var originalSymbolUnreferenced = UglifyJS.AST_Symbol.prototype.unreferenced;
|
||||
|
||||
// Workaround for https://github.com/mishoo/UglifyJS2/issues/789 - Nodes explicitly marked with ujs:unreferenced will not be warned for.
|
||||
UglifyJS.AST_Symbol.prototype.unreferenced = function () {
|
||||
if (this.start.comments_before.length > 0 && this.start.comments_before[0].value.trim() === "ujs:unreferenced") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return originalSymbolUnreferenced.call(this);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @license
|
||||
*/
|
||||
|
||||
(function (root, factory) {
|
||||
var global = this;
|
||||
|
||||
if (typeof define === "function" && define.amd) {
|
||||
define([], function() {
|
||||
return factory(global);
|
||||
});
|
||||
}
|
||||
else if (typeof exports === "object" && typeof module === "object") {
|
||||
module.exports = factory(global);
|
||||
}
|
||||
else if (typeof exports === "object") {
|
||||
exports.libjass = factory(global);
|
||||
}
|
||||
else {
|
||||
root.libjass = factory(global);
|
||||
}
|
||||
})(this, function (global) {
|
||||
"use strict";
|
||||
|
||||
var registeredModules = Object.create(null);
|
||||
var installedModules = Object.create(null);
|
||||
|
||||
function def(moduleId, deps, body) {
|
||||
installedModules[moduleId] = { deps: deps, body: body, };
|
||||
}
|
||||
|
||||
function req(moduleId) {
|
||||
if (moduleId in registeredModules) {
|
||||
return registeredModules[moduleId];
|
||||
}
|
||||
|
||||
var exports = registeredModules[moduleId] = Object.create(null);
|
||||
var deps = installedModules[moduleId].deps.map(req);
|
||||
deps.push(exports);
|
||||
|
||||
installedModules[moduleId].body.apply(null, deps);
|
||||
|
||||
return exports;
|
||||
}
|
||||
|
||||
return req("index");
|
||||
});
|
||||
@@ -1,662 +0,0 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
///<reference path="libjass.ts" />
|
||||
|
||||
module libjass {
|
||||
export class Dialogue {
|
||||
private static _lastDialogueId = -1;
|
||||
private static _animationStyleElement: HTMLStyleElement = null;
|
||||
|
||||
private _id: number;
|
||||
|
||||
private _style: Style;
|
||||
|
||||
private _start: number;
|
||||
private _end: number;
|
||||
|
||||
private _layer: number;
|
||||
private _alignment: number;
|
||||
private _transformOrigin: string;
|
||||
|
||||
private _parts: tags.Tag[];
|
||||
|
||||
private _sub: HTMLDivElement = null;
|
||||
|
||||
constructor(template: Object, private _ass: ASS) {
|
||||
this._id = ++Dialogue._lastDialogueId;
|
||||
|
||||
this._style = this._ass.styles.filter(aStyle => aStyle.name === template["Style"])[0];
|
||||
|
||||
this._start = Dialogue._toTime(template["Start"]);
|
||||
this._end = Dialogue._toTime(template["End"]);
|
||||
|
||||
this._layer = Math.max(parseInt(template["Layer"]), 0);
|
||||
this._alignment = this._style.alignment;
|
||||
this._setTransformOrigin();
|
||||
|
||||
this._parts = <tags.Tag[]>parser.parse(template["Text"], "dialogueParts");
|
||||
|
||||
if (libjass.debugMode) {
|
||||
if (this._parts.some(part => part instanceof tags.Comment && (<tags.Comment>part).value.indexOf("\\") !== -1)) {
|
||||
console.warn("Possible incorrect parse: " + this.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get id(): number {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get start(): number {
|
||||
return this._start;
|
||||
}
|
||||
|
||||
get end(): number {
|
||||
return this._end;
|
||||
}
|
||||
|
||||
get alignment(): number {
|
||||
return this._alignment;
|
||||
}
|
||||
|
||||
get layer(): number {
|
||||
return this._layer;
|
||||
}
|
||||
|
||||
get parts(): tags.Tag[] {
|
||||
return this._parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* The magic happens here. The subtitle div is rendered and stored. Call draw() to get a clone of the div to display.
|
||||
*/
|
||||
preRender(): void {
|
||||
if (this._sub === null) {
|
||||
this._preRender();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the pre-rendered subtitle div created from an earlier call to preRender().
|
||||
*/
|
||||
unPreRender(): void {
|
||||
this._sub = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subtitle div for display. The currentTime is used to shift the animations appropriately, so that at the time the
|
||||
* div is inserted into the DOM and the animations begin, they are in sync with the video time.
|
||||
*
|
||||
* @param {number} currentTime
|
||||
* @return {!HTMLDivElement}
|
||||
*/
|
||||
draw(currentTime: number): HTMLDivElement {
|
||||
if (this._sub === null) {
|
||||
if (libjass.debugMode) {
|
||||
console.warn("This dialogue was not pre-rendered. Call preRender() before calling draw() so that draw() is faster.");
|
||||
}
|
||||
|
||||
this._preRender();
|
||||
}
|
||||
|
||||
var sub = <HTMLDivElement>this._sub.cloneNode(true);
|
||||
|
||||
var animationEndCallback: () => void = () => removeElement(sub);
|
||||
|
||||
sub.style.webkitAnimationDelay = (this._start - currentTime) + "s";
|
||||
sub.addEventListener("webkitAnimationEnd", animationEndCallback, false);
|
||||
|
||||
sub.style.animationDelay = (this._start - currentTime) + "s";
|
||||
sub.addEventListener("animationend", animationEndCallback, false);
|
||||
|
||||
return sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
toString(): string {
|
||||
return "#" + this._id + " [" + this._start.toFixed(3) + "-" + this._end.toFixed(3) + "] " + this._parts.join(", ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this string into the number of seconds it represents. This string must be in the form of hh:mm:ss.MMM
|
||||
*
|
||||
* @param {string} string
|
||||
* @return {number}
|
||||
*/
|
||||
private static _toTime(str: string): number {
|
||||
return str.split(":").reduce((previousValue, currentValue) => previousValue * 60 + parseFloat(currentValue), 0);
|
||||
}
|
||||
|
||||
private static _valueOrDefault = <T>(newValue: T, defaultValue: T): T => ((newValue !== null) ? newValue : defaultValue);
|
||||
|
||||
private _preRender(): void {
|
||||
var sub = document.createElement("div");
|
||||
|
||||
// Create an animation if there is a part that requires it
|
||||
var keyframes = new KeyframeCollection(this._id, this._start, this._end);
|
||||
|
||||
this._parts.forEach(part => {
|
||||
if (part instanceof tags.Alignment) {
|
||||
this._alignment = (<tags.Alignment>part).value;
|
||||
this._setTransformOrigin();
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Fade) {
|
||||
var fadePart = <tags.Fade>part;
|
||||
if (fadePart.start !== 0) {
|
||||
keyframes.add(this._start, "opacity", "0");
|
||||
keyframes.add(this._start + fadePart.start, "opacity", "1");
|
||||
}
|
||||
if (fadePart.end !== 0) {
|
||||
keyframes.add(this._end - fadePart.end, "opacity", "1");
|
||||
keyframes.add(this._end, "opacity", "0");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Dialogue._animationStyleElement === null) {
|
||||
Dialogue._animationStyleElement = <HTMLStyleElement>document.querySelector("#animation-styles");
|
||||
}
|
||||
Dialogue._animationStyleElement.appendChild(document.createTextNode(keyframes.toString()));
|
||||
|
||||
var scaleX = this._ass.scaleX;
|
||||
var scaleY = this._ass.scaleY;
|
||||
var dpi = this._ass.dpi;
|
||||
|
||||
sub.style.webkitAnimationName = "dialogue-" + this._id;
|
||||
sub.style.webkitAnimationDuration = (this._end - this._start) + "s";
|
||||
|
||||
sub.style.animationName = "dialogue-" + this._id;
|
||||
sub.style.animationDuration = (this._end - this._start) + "s";
|
||||
|
||||
sub.style.marginLeft = (scaleX * this._style.marginLeft) + "px";
|
||||
sub.style.marginRight = (scaleX * this._style.marginRight) + "px";
|
||||
sub.style.marginTop = sub.style.marginBottom = (scaleY * this._style.marginVertical) + "px";
|
||||
|
||||
var divTransformStyle = "";
|
||||
|
||||
var currentSpan: HTMLSpanElement = null;
|
||||
var currentSpanStyles = new SpanStyles(this._style, this._transformOrigin, scaleX, scaleY, dpi);
|
||||
|
||||
var startNewSpan = (): void => {
|
||||
if (currentSpan !== null) {
|
||||
currentSpanStyles.setStylesOnSpan(currentSpan);
|
||||
sub.appendChild(currentSpan);
|
||||
}
|
||||
|
||||
currentSpan = document.createElement("span");
|
||||
};
|
||||
startNewSpan();
|
||||
|
||||
this._parts.forEach(part => {
|
||||
if (part instanceof tags.Italic) {
|
||||
currentSpanStyles.italic = (<tags.Italic>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Bold) {
|
||||
currentSpanStyles.bold = (<tags.Bold>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Underline) {
|
||||
currentSpanStyles.underline = (<tags.Underline>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.StrikeThrough) {
|
||||
currentSpanStyles.strikeThrough = (<tags.StrikeThrough>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Border) {
|
||||
currentSpanStyles.outlineWidthX = (<tags.Border>part).value;
|
||||
currentSpanStyles.outlineWidthY = (<tags.Border>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.BorderX) {
|
||||
currentSpanStyles.outlineWidthX = (<tags.BorderX>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.BorderY) {
|
||||
currentSpanStyles.outlineWidthY = (<tags.BorderY>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Blur) {
|
||||
currentSpanStyles.blur = (<tags.Blur>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.FontName) {
|
||||
currentSpanStyles.fontName = (<tags.FontName>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.FontSize) {
|
||||
currentSpanStyles.fontSize = (<tags.FontSize>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.FontScaleX) {
|
||||
currentSpanStyles.fontScaleX = (<tags.FontScaleX>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.FontScaleY) {
|
||||
currentSpanStyles.fontScaleY = (<tags.FontScaleY>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.LetterSpacing) {
|
||||
currentSpanStyles.letterSpacing = (<tags.LetterSpacing>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.RotateX) {
|
||||
divTransformStyle += " rotateX(" + (<tags.RotateX>part).value + "deg)";
|
||||
}
|
||||
|
||||
else if (part instanceof tags.RotateY) {
|
||||
divTransformStyle += " rotateY(" + (<tags.RotateY>part).value + "deg)";
|
||||
}
|
||||
|
||||
else if (part instanceof tags.RotateZ) {
|
||||
divTransformStyle += " rotateZ(" + (-1 * (<tags.RotateZ>part).value) + "deg)";
|
||||
}
|
||||
|
||||
else if (part instanceof tags.SkewX) {
|
||||
divTransformStyle += " skewX(" + (45 * (<tags.SkewX>part).value) + "deg)";
|
||||
}
|
||||
|
||||
else if (part instanceof tags.SkewY) {
|
||||
divTransformStyle += " skewY(" + (45 * (<tags.SkewY>part).value) + "deg)";
|
||||
}
|
||||
|
||||
else if (part instanceof tags.PrimaryColor) {
|
||||
currentSpanStyles.primaryColor = (<tags.PrimaryColor>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.OutlineColor) {
|
||||
currentSpanStyles.outlineColor = (<tags.OutlineColor>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Alpha) {
|
||||
currentSpanStyles.primaryAlpha = (<tags.Alpha>part).value;
|
||||
currentSpanStyles.outlineAlpha = (<tags.Alpha>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.PrimaryAlpha) {
|
||||
currentSpanStyles.primaryAlpha = (<tags.PrimaryAlpha>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.OutlineAlpha) {
|
||||
currentSpanStyles.outlineAlpha = (<tags.OutlineAlpha>part).value;
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Alignment) {
|
||||
// Already handled at the beginning of draw()
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Reset) {
|
||||
var newStyleName = (<tags.Reset>part).value;
|
||||
var newStyle: Style = null;
|
||||
if (newStyleName !== null) {
|
||||
newStyle = this._ass.styles.filter(style => style.name === newStyleName)[0];
|
||||
}
|
||||
currentSpanStyles.reset(newStyle);
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Pos) {
|
||||
// Will be handled at the end of draw()
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Fade) {
|
||||
// Already handled at the beginning of draw()
|
||||
}
|
||||
|
||||
else if (part instanceof tags.NewLine) {
|
||||
sub.appendChild(document.createElement("br"));
|
||||
}
|
||||
|
||||
else if (part instanceof tags.HardSpace) {
|
||||
currentSpan.appendChild(document.createTextNode("\u00A0"));
|
||||
startNewSpan();
|
||||
}
|
||||
|
||||
else if (part instanceof tags.Text || (libjass.debugMode && part instanceof tags.Comment)) {
|
||||
currentSpan.appendChild(document.createTextNode((<tags.Text>part).value));
|
||||
startNewSpan();
|
||||
}
|
||||
});
|
||||
|
||||
if (divTransformStyle) {
|
||||
sub.style.webkitTransform = divTransformStyle;
|
||||
sub.style.webkitTransformOrigin = this._transformOrigin;
|
||||
|
||||
sub.style.transform = divTransformStyle;
|
||||
sub.style.transformOrigin = this._transformOrigin;
|
||||
}
|
||||
|
||||
this._parts.some(part => {
|
||||
if (part instanceof tags.Pos) {
|
||||
var posPart = <tags.Pos>part;
|
||||
|
||||
var absoluteWrapper = document.createElement("div");
|
||||
absoluteWrapper.style.position = "absolute";
|
||||
absoluteWrapper.style.left = (scaleX * posPart.x) + "px";
|
||||
absoluteWrapper.style.top = (scaleY * posPart.y) + "px";
|
||||
|
||||
sub.style.position = "relative";
|
||||
|
||||
var relativeTop: number;
|
||||
var relativeLeft: number;
|
||||
switch (this._alignment) {
|
||||
case 1: relativeLeft = 0; relativeTop = -100; break;
|
||||
case 2: relativeLeft = -50; relativeTop = -100; break;
|
||||
case 3: relativeLeft = -100; relativeTop = -100; break;
|
||||
case 4: relativeLeft = 0; relativeTop = -50; break;
|
||||
case 5: relativeLeft = -50; relativeTop = -50; break;
|
||||
case 6: relativeLeft = -100; relativeTop = -50; break;
|
||||
case 7: relativeLeft = 0; relativeTop = 0; break;
|
||||
case 8: relativeLeft = -50; relativeTop = 0; break;
|
||||
case 9: relativeLeft = -100; relativeTop = 0; break;
|
||||
}
|
||||
sub.style.left = relativeLeft + "%";
|
||||
sub.style.top = relativeTop + "%";
|
||||
|
||||
absoluteWrapper.appendChild(sub);
|
||||
|
||||
sub = absoluteWrapper;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
sub.setAttribute("data-dialogue-id", String(this._id));
|
||||
|
||||
this._sub = sub;
|
||||
}
|
||||
|
||||
private _setTransformOrigin(): void {
|
||||
var transformOriginX: number;
|
||||
var transformOriginY: number;
|
||||
|
||||
switch (this._alignment) {
|
||||
case 1: transformOriginX = 0; transformOriginY = 100; break;
|
||||
case 2: transformOriginX = 50; transformOriginY = 100; break;
|
||||
case 3: transformOriginX = 100; transformOriginY = 100; break;
|
||||
case 4: transformOriginX = 0; transformOriginY = 50; break;
|
||||
case 5: transformOriginX = 50; transformOriginY = 50; break;
|
||||
case 6: transformOriginX = 100; transformOriginY = 50; break;
|
||||
case 7: transformOriginX = 0; transformOriginY = 0; break;
|
||||
case 8: transformOriginX = 50; transformOriginY = 0; break;
|
||||
case 9: transformOriginX = 100; transformOriginY = 0; break;
|
||||
}
|
||||
|
||||
this._transformOrigin = transformOriginX + "% " + transformOriginY + "%";
|
||||
}
|
||||
}
|
||||
|
||||
class KeyframeCollection {
|
||||
/** @type {!Object.<string, !Object.<string, string>>} */
|
||||
private _keyframes: Object = Object.create(null);
|
||||
|
||||
constructor(private _id: number, private _start: number, private _end: number) { }
|
||||
|
||||
/**
|
||||
* @param {number} time
|
||||
* @param {string} property
|
||||
* @param {string} value
|
||||
*/
|
||||
add(time: number, property: string, value: string) {
|
||||
var step = (100 * (time - this._start) / (this._end - this._start)) + "%";
|
||||
this._keyframes[step] = this._keyframes[step] || {};
|
||||
this._keyframes[step][property] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CSS3 animations representation of this keyframe collection.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
toString(): string {
|
||||
var result = "";
|
||||
|
||||
var steps = Object.keys(this._keyframes);
|
||||
if (steps.length > 0) {
|
||||
var cssText = "";
|
||||
|
||||
steps.forEach(step => {
|
||||
cssText += "\t" + step + " {\n";
|
||||
var properties: Object = this._keyframes[step];
|
||||
Object.keys(properties).forEach(property => {
|
||||
cssText += "\t\t" + property + ": " + properties[property] + ";\n";
|
||||
});
|
||||
cssText += "\t}\n";
|
||||
});
|
||||
|
||||
result =
|
||||
"@-webkit-keyframes dialogue-" + this._id + " {\n" + cssText + "}\n\n" +
|
||||
"@keyframes dialogue-" + this._id + " {\n" + cssText + "}\n\n";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class SpanStyles {
|
||||
private _italic: boolean;
|
||||
private _bold: Object;
|
||||
private _underline: boolean;
|
||||
private _strikeThrough: boolean;
|
||||
|
||||
private _outlineWidthX: number;
|
||||
private _outlineWidthY: number;
|
||||
|
||||
private _fontName: string;
|
||||
private _fontSize: number;
|
||||
|
||||
private _fontScaleX: number;
|
||||
private _fontScaleY: number;
|
||||
|
||||
private _letterSpacing: number;
|
||||
|
||||
private _primaryColor: tags.Color;
|
||||
private _outlineColor: tags.Color;
|
||||
|
||||
private _primaryAlpha: number;
|
||||
private _outlineAlpha: number;
|
||||
|
||||
private _blur: number;
|
||||
|
||||
constructor(private _style: Style, private _transformOrigin: string, private _scaleX: number, private _scaleY: number, private _dpi: number) {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset(newStyle: Style = this._style): void {
|
||||
this.italic = newStyle.italic;
|
||||
this.bold = newStyle.bold;
|
||||
this.underline = newStyle.underline;
|
||||
this.strikeThrough = newStyle.strikeThrough;
|
||||
|
||||
this.outlineWidthX = newStyle.outlineWidth;
|
||||
this.outlineWidthY = newStyle.outlineWidth;
|
||||
|
||||
this.fontName = newStyle.fontName;
|
||||
this.fontSize = newStyle.fontSize;
|
||||
|
||||
this.fontScaleX = newStyle.fontScaleX;
|
||||
this.fontScaleY = newStyle.fontScaleY;
|
||||
|
||||
this.letterSpacing = newStyle.letterSpacing;
|
||||
|
||||
this.primaryColor = newStyle.primaryColor;
|
||||
this.outlineColor = newStyle.outlineColor;
|
||||
|
||||
this.primaryAlpha = null;
|
||||
this.outlineAlpha = null;
|
||||
|
||||
this.blur = null;
|
||||
}
|
||||
|
||||
setStylesOnSpan(span: HTMLSpanElement): void {
|
||||
if (this._italic) {
|
||||
span.style.fontStyle = "italic";
|
||||
}
|
||||
|
||||
if (this._bold === true) {
|
||||
span.style.fontWeight = "bold";
|
||||
}
|
||||
else if (this._bold !== false) {
|
||||
span.style.fontWeight = <string>this._bold;
|
||||
}
|
||||
|
||||
var textDecoration = "";
|
||||
if (this._underline) {
|
||||
textDecoration = "underline";
|
||||
}
|
||||
if (this._strikeThrough) {
|
||||
textDecoration += " line-through";
|
||||
}
|
||||
span.style.textDecoration = textDecoration.trim();
|
||||
|
||||
span.style.fontFamily = this._fontName;
|
||||
span.style.fontSize = span.style.lineHeight = ((72 / this._dpi) * this._scaleY * this._fontSize) + "px";
|
||||
|
||||
span.style.webkitTransform = "scaleX(" + this._fontScaleX + ") scaleY(" + this._fontScaleY + ")";
|
||||
span.style.webkitTransformOrigin = this._transformOrigin;
|
||||
span.style.transform = "scaleX(" + this._fontScaleX + ") scaleY(" + this._fontScaleY + ")";
|
||||
span.style.transformOrigin = this._transformOrigin;
|
||||
|
||||
span.style.letterSpacing = (this._scaleX * this._letterSpacing) + "px";
|
||||
|
||||
span.style.color = this._primaryColor.withAlpha(this._primaryAlpha).toString();
|
||||
|
||||
if (this._outlineWidthX > 0 || this._outlineWidthY > 0) {
|
||||
var textShadowColor = this._outlineColor.withAlpha(this._outlineAlpha).toString();
|
||||
var textShadowParts: number[][] = [];
|
||||
|
||||
/* Lay out text-shadows in an ellipse with horizontal radius = this._scaleX * this._outlineWidthX
|
||||
* and vertical radius = this._scaleY * this._outlineWidthY
|
||||
* Shadows are laid inside the region of the ellipse, separated by 0.5px
|
||||
*
|
||||
* The below loop is an unrolled version of the above algorithm that only roams over one quadrant and adds
|
||||
* four shadows at a time.
|
||||
*/
|
||||
|
||||
var a = this._scaleX * this._outlineWidthX;
|
||||
var b = this._scaleY * this._outlineWidthY;
|
||||
|
||||
for (var x = 0; x < a; x += 0.5) {
|
||||
for (var y = 0; y < b && ((x / a) * (x / a) + (y / b) * (y / b)) <= 1; y += 0.5) {
|
||||
textShadowParts.push([x, y, this._scaleX * this._blur]);
|
||||
if (x !== 0) {
|
||||
textShadowParts.push([-x, y, this._scaleX * this._blur]);
|
||||
}
|
||||
if (x !== 0 && y !== 0) {
|
||||
textShadowParts.push([-x, -y, this._scaleY * this._blur]);
|
||||
}
|
||||
if (y !== 0) {
|
||||
textShadowParts.push([x, -y, this._scaleY * this._blur]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the four corner shadows exist
|
||||
textShadowParts.push(
|
||||
[a, 0, this._scaleX * this._blur],
|
||||
[0, b, this._scaleX * this._blur],
|
||||
[-a, 0, this._scaleY * this._blur],
|
||||
[0, -b, this._scaleY * this._blur]
|
||||
);
|
||||
|
||||
span.style.textShadow =
|
||||
textShadowParts
|
||||
.map(triple => triple[0] + "px " + triple[1] + "px " + triple[2] + "px " + textShadowColor)
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
else if (this._blur > 0) {
|
||||
// TODO: Blur text
|
||||
}
|
||||
}
|
||||
|
||||
set italic(value: boolean) {
|
||||
this._italic = SpanStyles._valueOrDefault(value, this._style.italic);
|
||||
}
|
||||
|
||||
set bold(value: Object) {
|
||||
this._bold = SpanStyles._valueOrDefault(value, this._style.bold);
|
||||
}
|
||||
|
||||
set underline(value: boolean) {
|
||||
this._underline = SpanStyles._valueOrDefault(value, this._style.underline);
|
||||
}
|
||||
|
||||
set strikeThrough(value: boolean) {
|
||||
this._strikeThrough = SpanStyles._valueOrDefault(value, this._style.strikeThrough);
|
||||
}
|
||||
|
||||
set outlineWidthX(value: number) {
|
||||
this._outlineWidthX = SpanStyles._valueOrDefault(value, this._style.outlineWidth);
|
||||
}
|
||||
|
||||
set outlineWidthY(value: number) {
|
||||
this._outlineWidthY = SpanStyles._valueOrDefault(value, this._style.outlineWidth);
|
||||
}
|
||||
|
||||
set blur(value: number) {
|
||||
this._blur = SpanStyles._valueOrDefault(value, 0);
|
||||
}
|
||||
|
||||
set fontName(value: string) {
|
||||
this._fontName = SpanStyles._valueOrDefault(value, this._style.fontName);
|
||||
}
|
||||
|
||||
set fontSize(value: number) {
|
||||
this._fontSize = SpanStyles._valueOrDefault(value, this._style.fontSize);
|
||||
}
|
||||
|
||||
set fontScaleX(value: number) {
|
||||
this._fontScaleX = SpanStyles._valueOrDefault(value, this._style.fontScaleX);
|
||||
}
|
||||
|
||||
set fontScaleY(value: number) {
|
||||
this._fontScaleY = SpanStyles._valueOrDefault(value, this._style.fontScaleY);
|
||||
}
|
||||
|
||||
set letterSpacing(value: number) {
|
||||
this._letterSpacing = SpanStyles._valueOrDefault(value, this._style.letterSpacing);
|
||||
}
|
||||
|
||||
set primaryColor(value: tags.Color) {
|
||||
this._primaryColor = SpanStyles._valueOrDefault(value, this._style.primaryColor);
|
||||
}
|
||||
|
||||
set outlineColor(value: tags.Color) {
|
||||
this._outlineColor = SpanStyles._valueOrDefault(value, this._style.outlineColor);
|
||||
}
|
||||
|
||||
set primaryAlpha(value: number) {
|
||||
this._primaryAlpha = value;
|
||||
}
|
||||
|
||||
set outlineAlpha(value: number) {
|
||||
this._outlineAlpha = value;
|
||||
}
|
||||
|
||||
private static _valueOrDefault = <T>(newValue: T, defaultValue: T): T => ((newValue !== null) ? newValue : defaultValue);
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
@font-face {
|
||||
font-family: "Amienne";
|
||||
src: url("/fonts/Amienne.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "ArtificeSSK";
|
||||
src: url("/fonts/ArtificeSSK.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Baar Sophia";
|
||||
src: url("/fonts/BAARS.TTF");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "CAC Moose";
|
||||
src: url("/fonts/CAC%20Moose.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Charme";
|
||||
src: url("/fonts/CHARME.TTF");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Chinacat";
|
||||
src: url("/fonts/chinrg.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Complete in Him";
|
||||
src: url("/fonts/Complete%20in%20Him.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Consolas";
|
||||
src: url("/fonts/consola.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Consolas";
|
||||
src: url("/fonts/consolab.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Continuum Bold";
|
||||
src: url("/fonts/contb.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Elephant";
|
||||
src: url("/fonts/ELEPHNT.TTF");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "EngraversGothic BT";
|
||||
src: url("/fonts/Engrvgot.TTF");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Essai";
|
||||
src: url("/fonts/Essai.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Forte";
|
||||
src: url("/fonts/FORTE.TTF");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "HandelGotDBol";
|
||||
src: url("/fonts/HANDGOTB.TTF");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "HandelGothic BT";
|
||||
src: url("/fonts/HANDGOTN.TTF");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "HZHandwrite";
|
||||
src: url("/fonts/HZHandwrite.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "LTFinnegan Medium";
|
||||
src: url("/fonts/LT.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "LTFinnegan Medium";
|
||||
src: url("/fonts/LT_3italic.ttf");
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Macron Finnetier Medium";
|
||||
src: url("/fonts/macron%20finnetier%200.3.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Old Block";
|
||||
src: url("/fonts/OBGB.TTF");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "PaintyPaint";
|
||||
src: url("/fonts/PAINP.TTF");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "PaintyPaint";
|
||||
src: url("/fonts/PAINP_0.TTF");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Profile";
|
||||
src: url("/fonts/Profile-Medium.otf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Profile";
|
||||
src: url("/fonts/Profile-Regular.otf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Prototype";
|
||||
src: url("/fonts/Prototype.ttf");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Swis721 Md BT";
|
||||
src: url("/fonts/SWZ721M.TTF");
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
libjass.debugMode = (location.search === "?debug") || (location.search === "?verbose");
|
||||
libjass.verboseMode = (location.search === "?verbose");
|
||||
|
||||
addEventListener("DOMContentLoaded", function () {
|
||||
var debug = function () {
|
||||
if (libjass.debugMode) {
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
var video = document.querySelector("#video");
|
||||
|
||||
var videoMetadataLoaded = false;
|
||||
var ass = null;
|
||||
|
||||
var testVideoAndASSLoaded = function () {
|
||||
if (videoMetadataLoaded && ass) {
|
||||
ass.dpi = parseFloat(getComputedStyle(document.querySelector("#dpi-div")).height.match(/(\d+)px/)[1]);
|
||||
|
||||
var renderer = new libjass.DefaultRenderer(video, document.querySelector("#subs"), ass, {
|
||||
preloadFonts: true,
|
||||
fontMap: libjass.FontMap.fromStyleElement(document.querySelector("#font-map"))
|
||||
});
|
||||
|
||||
renderer.addEventListener("ready", function () {
|
||||
debug("All fonts have been preloaded. Beginning autoplay.");
|
||||
video.play();
|
||||
});
|
||||
|
||||
var changeVideoSizeSelection = function (id) {
|
||||
if (typeof id === "undefined") {
|
||||
[].some.call(videoSizeSelector.querySelectorAll("input[name='video-size']"), function (option) {
|
||||
if (option.checked) {
|
||||
id = option.id;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (id === "video-size-video-radio") {
|
||||
renderer.resizeVideo(video.videoWidth, video.videoHeight);
|
||||
}
|
||||
|
||||
else if (id === "video-size-script-radio") {
|
||||
renderer.resizeVideo(ass.resolutionX, ass.resolutionY);
|
||||
}
|
||||
};
|
||||
|
||||
var videoSizeSelector = document.querySelector("#video-size-selector");
|
||||
videoSizeSelector.addEventListener("change", function (event) { changeVideoSizeSelection(event.target.id); }, false);
|
||||
|
||||
renderer.addEventListener("fullScreenChange", function (newState) {
|
||||
if (newState === false) {
|
||||
changeVideoSizeSelection();
|
||||
}
|
||||
});
|
||||
|
||||
changeVideoSizeSelection();
|
||||
};
|
||||
}
|
||||
|
||||
if (video.readyState < HTMLMediaElement.HAVE_METADATA) {
|
||||
video.addEventListener("loadedmetadata", function () {
|
||||
debug("Video metadata loaded.");
|
||||
videoMetadataLoaded = true;
|
||||
|
||||
document.querySelector("#video-resolution-label-width").appendChild(document.createTextNode(video.videoWidth));
|
||||
document.querySelector("#video-resolution-label-height").appendChild(document.createTextNode(video.videoHeight));
|
||||
|
||||
testVideoAndASSLoaded();
|
||||
}, false);
|
||||
}
|
||||
else {
|
||||
debug("Video metadata loaded.");
|
||||
videoMetadataLoaded = true;
|
||||
testVideoAndASSLoaded();
|
||||
}
|
||||
|
||||
var track = document.querySelector("#video > track[data-format='ass']");
|
||||
var subsRequest = new XMLHttpRequest();
|
||||
subsRequest.open("GET", track.src || track.getAttribute("src"), true);
|
||||
subsRequest.addEventListener("readystatechange", function () {
|
||||
if (subsRequest.readyState === XMLHttpRequest.DONE) {
|
||||
debug("ASS script received.");
|
||||
|
||||
ass = new libjass.ASS(subsRequest.responseText);
|
||||
if (libjass.debugMode) {
|
||||
window.ass = ass;
|
||||
}
|
||||
|
||||
document.querySelector("#script-resolution-label-width").appendChild(document.createTextNode(ass.resolutionX));
|
||||
document.querySelector("#script-resolution-label-height").appendChild(document.createTextNode(ass.resolutionY));
|
||||
|
||||
testVideoAndASSLoaded();
|
||||
}
|
||||
}, false);
|
||||
subsRequest.send(null);
|
||||
}, false);
|
||||
@@ -1,56 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
libjass
|
||||
|
||||
https://github.com/Arnavion/libjass
|
||||
|
||||
Copyright 2013 Arnav Singh
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
|
||||
<head>
|
||||
<title>>2012 >Streaman Animu</title>
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
<link rel="stylesheet" href="fonts.css" id="font-map" />
|
||||
|
||||
<!-- Unminified file -->
|
||||
<script src="libjass.js" />
|
||||
|
||||
<!-- Minified file -->
|
||||
<!--
|
||||
<script src="libjass.min.js" />
|
||||
-->
|
||||
|
||||
<script src="index.js" />
|
||||
<style id="animation-styles" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<video id="video" src="/video/gc-01.webm" controls="">
|
||||
<track src="/video/gc-01.ass" kind="metadata" data-format="ass" />
|
||||
</video>
|
||||
<div id="subs" />
|
||||
</div>
|
||||
<div id="dpi-div" />
|
||||
<form class="settings-form">
|
||||
<fieldset>
|
||||
<legend>Video size</legend>
|
||||
<div id="video-size-selector">
|
||||
<input type="radio" name="video-size" id="video-size-video-radio" checked="checked" /><label for="video-size-video-radio">Video resolution <span id="video-resolution-label-width" />x<span id="video-resolution-label-height" /></label>
|
||||
<input type="radio" name="video-size" id="video-size-script-radio" /><label for="video-size-script-radio">Script resolution <span id="script-resolution-label-width" />x<span id="script-resolution-label-height" /></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,404 +0,0 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
///<reference path="libjass.ts" />
|
||||
|
||||
interface Iterator {
|
||||
next(): any
|
||||
}
|
||||
|
||||
declare function Iterator(collection: any, keysOnly?: boolean): Iterator
|
||||
declare var StopIteration: any
|
||||
|
||||
module libjass {
|
||||
export class LazySequence<T> {
|
||||
/**
|
||||
* The base class of all lazy sequences.
|
||||
*
|
||||
* @constructor
|
||||
* @template T
|
||||
*/
|
||||
constructor() { }
|
||||
|
||||
/**
|
||||
* @template U
|
||||
* @param transform {function(T): U} A function (element) -> (transformedElement)
|
||||
* @return {!LazySequence.<U>} A new LazySequence with the given transform applied
|
||||
*/
|
||||
map<U>(transform: (element: T) => U): LazySequence<U> {
|
||||
return new LazySequenceMap<T, U>(this, transform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(T): boolean} filter A function (element) -> (Boolean). Returns true if element should remain in the enumeration.
|
||||
* @return {!LazySequence.<T>} A new LazySequence with the given filter applied
|
||||
*/
|
||||
filter(filter: (element: T) => boolean): LazySequence<T> {
|
||||
return new LazySequenceFilter<T>(this, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(T): boolean} filter A function (element) -> (Boolean). Returns false for an element if enumeration of this LazySequence should stop at that element.
|
||||
* @return {!LazySequence.<T>} A new LazySequence which returns new elements from the underlying sequence as long as the predicate returns true for all of them
|
||||
*/
|
||||
takeWhile(filter: (element: T) => boolean): LazySequence<T> {
|
||||
return new LazySequenceTakeWhile<T>(this, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(T): boolean} filter A function (element) -> (Boolean). Returns true for an element if enumeration of this LazySequence should skip all elements upto that element.
|
||||
* @return {!LazySequence.<T>} A new LazySequence which returns new elements from the underlying sequence as soon as the predicate returns true for one of them
|
||||
*/
|
||||
skipWhile(filter: (element: T) => boolean): LazySequence<T> {
|
||||
return new LazySequenceSkipWhile<T>(this, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array.<T>}
|
||||
*/
|
||||
toArray(): Array<T> {
|
||||
var result: Array = [];
|
||||
|
||||
var iterator = Iterator(this);
|
||||
|
||||
try {
|
||||
for (; ;) {
|
||||
result.push(iterator.next());
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
if (ex !== StopIteration) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array.<T>} array
|
||||
* @return {!LazySequence.<T>} A LazySequence backed by this Array
|
||||
*/
|
||||
export function Lazy<T>(array: Array<T>): LazySequence<T> {
|
||||
return new LazyArray<T>(array);
|
||||
}
|
||||
|
||||
// If this browser does not have an implementation of StopIteration, mock it
|
||||
if (typeof StopIteration === "undefined") {
|
||||
global.StopIteration = Object.create(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A default Iterator for arrays in case Iterator(Array) is not defined by the browser.
|
||||
*
|
||||
* @constructor
|
||||
* @param {!Array} array
|
||||
*/
|
||||
class ArrayIterator implements Iterator {
|
||||
// The index of the element which will be returned in the next call to next()
|
||||
private _currentIndex = 0;
|
||||
|
||||
constructor(private _array: Array) { }
|
||||
|
||||
/**
|
||||
* @return {!Array} Returns a tuple [index, element]
|
||||
*/
|
||||
next(): any[] {
|
||||
// Loop through the array looking for an element to return
|
||||
while (this._currentIndex < this._array.length) {
|
||||
// If the index is less than the array's length and an element is in the array at that index
|
||||
if (this._currentIndex in this._array) {
|
||||
var oldCurrentIndex = this._currentIndex;
|
||||
this._currentIndex++;
|
||||
// ... return it
|
||||
return [oldCurrentIndex, this._array[oldCurrentIndex]];
|
||||
}
|
||||
// Else advance to the next index
|
||||
else {
|
||||
this._currentIndex++;
|
||||
}
|
||||
}
|
||||
// If there are no more elements in the array, throw StopIteration
|
||||
throw StopIteration;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof Iterator === "undefined") {
|
||||
/**
|
||||
* A default function for creating iterators in case it is not defined by the browser.
|
||||
*
|
||||
* @param {!*} collection
|
||||
* @param {boolean=} keysOnly
|
||||
* @return {!{next: function(): *}}
|
||||
*/
|
||||
global.Iterator = (collection: any, keysOnly?: boolean): Iterator => {
|
||||
if (keysOnly) {
|
||||
throw new Error("This Iterator implementation doesn't support keysOnly = true.");
|
||||
}
|
||||
|
||||
if (typeof collection.__iterator__ === "function") {
|
||||
return <Iterator>collection.__iterator__();
|
||||
}
|
||||
else if (Array.isArray(collection)) {
|
||||
return new ArrayIterator(<Array>collection);
|
||||
}
|
||||
else {
|
||||
throw new Error("This Iterator implementation doesn't support iterating arbitrary objects.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is a LazySequence returned by Lazy(Array) and represents a LazySequence backed by the elements of that array.
|
||||
*
|
||||
* @constructor
|
||||
* @template T
|
||||
* @extends {LazySequence.<T>}
|
||||
* @param {!Array} array
|
||||
*/
|
||||
class LazyArray<T> extends LazySequence<T> {
|
||||
constructor(private _array: Array<T>) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {{next: function(): T}}
|
||||
*/
|
||||
__iterator__(): LazyArrayIterator<T> {
|
||||
return new LazyArrayIterator<T>(Iterator(this._array));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @template T
|
||||
* @param {!{next: function(): *}} previous
|
||||
*/
|
||||
class LazyArrayIterator<T> implements Iterator {
|
||||
constructor(private _previous: Iterator) { }
|
||||
|
||||
/**
|
||||
* @return {T}
|
||||
*/
|
||||
next(): T {
|
||||
var result: Array<any> = this._previous.next();
|
||||
|
||||
return <T>result[1];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A LazySequence returned from LazySequence.map()
|
||||
*
|
||||
* @constructor
|
||||
* @template T, U
|
||||
* @extends {LazySequence.<U>}
|
||||
* @param {!*} previous The underlying lazy sequence
|
||||
* @param {function(T): U} transform The transform function (element) -> (transformedElement)
|
||||
*/
|
||||
class LazySequenceMap<T, U> extends LazySequence<U> {
|
||||
constructor(private _previous: any, private _transform: (element: T) => U) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!LazySequenceMapIterator.<T, U>}
|
||||
*/
|
||||
__iterator__(): LazySequenceMapIterator<T, U> {
|
||||
return new LazySequenceMapIterator<T, U>(Iterator(this._previous), this._transform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @template T, U
|
||||
* @param {!{next: function(): T}} previous
|
||||
* @param {function(T): U} transform
|
||||
*/
|
||||
class LazySequenceMapIterator<T, U> implements Iterator {
|
||||
constructor(private _previous: Iterator, private _transform: (element: T) => U) { }
|
||||
|
||||
/**
|
||||
* @return {U}
|
||||
*/
|
||||
next(): U {
|
||||
// Apply the transform function and return the transformed value
|
||||
return this._transform(<T>this._previous.next());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A LazySequence returned from LazySequence.filter()
|
||||
*
|
||||
* @constructor
|
||||
* @template T
|
||||
* @extends {LazySequence.<T>}
|
||||
* @param {!*} previous The underlying lazy sequence
|
||||
* @param {function(T): boolean} filter The filter function (element) -> (Boolean)
|
||||
*/
|
||||
class LazySequenceFilter<T> extends LazySequence<T> {
|
||||
constructor(private _previous: any, private _filter: (element: T) => boolean) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!LazySequenceFilterIterator.<T>}
|
||||
*/
|
||||
__iterator__(): LazySequenceFilterIterator<T> {
|
||||
return new LazySequenceFilterIterator(Iterator(this._previous), this._filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @template T
|
||||
* @param {!{next: function(): T}} previous
|
||||
* @param {function(T): boolean} filter
|
||||
*/
|
||||
class LazySequenceFilterIterator<T> implements Iterator {
|
||||
constructor(private _previous: Iterator, private _filter: (element: T) => boolean) { }
|
||||
|
||||
/**
|
||||
* @return {T}
|
||||
*/
|
||||
next(): T {
|
||||
// Loop to find the next element from the underlying lazy sequence which passes the filter and return it
|
||||
var result: T;
|
||||
|
||||
do {
|
||||
result = <T>this._previous.next();
|
||||
} while (!this._filter(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A LazySequence returned from LazySequence.takeWhile()
|
||||
*
|
||||
* @constructor
|
||||
* @template T
|
||||
* @extends {LazySequence.<T>}
|
||||
* @param {!*} previous The underlying lazy sequence
|
||||
* @param {function(T): boolean} predicate The predicate function (element) -> (Boolean)
|
||||
*/
|
||||
class LazySequenceTakeWhile<T> extends LazySequence<T> {
|
||||
constructor(private _previous: any, private _predicate: (element: T) => boolean) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!LazySequenceTakeWhileIterator.<T>}
|
||||
*/
|
||||
__iterator__(): LazySequenceTakeWhileIterator<T> {
|
||||
return new LazySequenceTakeWhileIterator<T>(Iterator(this._previous), this._predicate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @template T
|
||||
* @param {!{next: function(): T}} previous
|
||||
* @param {function(T): boolean} predicate
|
||||
*/
|
||||
class LazySequenceTakeWhileIterator<T> implements Iterator {
|
||||
// Set to true when an element not matching the predicate is found
|
||||
private _foundEnd = false;
|
||||
|
||||
constructor(private _previous: Iterator, private _predicate: (element: T) => boolean) { }
|
||||
|
||||
/**
|
||||
* @return {T}
|
||||
*/
|
||||
next(): T {
|
||||
var result: T;
|
||||
|
||||
// If we haven't already found the end in a previous call to next()
|
||||
if (!this._foundEnd) {
|
||||
// Get the next element from the underlying lazy sequence and see if we've found the end now
|
||||
result = <T>this._previous.next();
|
||||
this._foundEnd = !this._predicate(result);
|
||||
}
|
||||
|
||||
// If we haven't found the end, return the element
|
||||
if (!this._foundEnd) {
|
||||
return result;
|
||||
}
|
||||
// Else throw StopIteration
|
||||
else {
|
||||
throw StopIteration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A LazySequence returned from LazySequence.skipWhile()
|
||||
*
|
||||
* @constructor
|
||||
* @template T
|
||||
* @extends {LazySequence.<T>}
|
||||
* @param {!*} previous The underlying lazy sequence
|
||||
* @param {function(T): boolean} predicate The predicate function (element) -> (Boolean)
|
||||
*/
|
||||
class LazySequenceSkipWhile<T> extends LazySequence<T> {
|
||||
constructor(private _previous: any, private _predicate: (element: T) => boolean) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!LazySequenceSkipWhileIterator.<T>}
|
||||
*/
|
||||
__iterator__(): LazySequenceSkipWhileIterator<T> {
|
||||
return new LazySequenceSkipWhileIterator<T>(Iterator(this._previous), this._predicate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @template T
|
||||
* @param {!{next: function(): T}} previous
|
||||
* @param {function(T): boolean} predicate
|
||||
*/
|
||||
class LazySequenceSkipWhileIterator<T> implements Iterator {
|
||||
// Set to true when an element not matching the predicate is found
|
||||
private _foundStart = false;
|
||||
|
||||
constructor(private _previous: Iterator, private _predicate: (element: T) => boolean) { }
|
||||
|
||||
/**
|
||||
* @return {T}
|
||||
*/
|
||||
next(): T {
|
||||
var result: T;
|
||||
|
||||
do {
|
||||
// Get the next element
|
||||
result = <T>this._previous.next();
|
||||
// and see if we've already found the start, or if we've found it now
|
||||
this._foundStart = this._foundStart || !this._predicate(result);
|
||||
} while (!this._foundStart); // Keep looping till we find the start
|
||||
|
||||
// We've found the start, so return the element
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,83 +18,87 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
.libjass-wrapper {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.zoomed, .zoomed * {
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
#subs {
|
||||
.libjass-subs {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#subs, #subs * {
|
||||
.libjass-subs, .libjass-subs * {
|
||||
pointer-events: none;
|
||||
z-index: 2147483647;
|
||||
-webkit-animation-fill-mode: both !important;
|
||||
animation-fill-mode: both !important;
|
||||
}
|
||||
|
||||
#subs > * {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#subs.paused * {
|
||||
.libjass-subs.paused * {
|
||||
-webkit-animation-play-state: paused !important;
|
||||
animation-play-state: paused !important;
|
||||
}
|
||||
|
||||
.an1, .an2, .an3 {
|
||||
.libjass-subs .an {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.libjass-subs .an1, .libjass-subs .an2, .libjass-subs .an3 {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.an4, .an5, .an6 {
|
||||
.libjass-subs .an4, .libjass-subs .an5, .libjass-subs .an6 {
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.an4 > *, .an5 > *, .an6 > * {
|
||||
.libjass-subs .an4 > *, .libjass-subs .an5 > *, .libjass-subs .an6 > * {
|
||||
display: table-cell;
|
||||
top: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.an7, .an8, .an9 {
|
||||
.libjass-subs .an7, .libjass-subs .an8, .libjass-subs .an9 {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.an1, .an4, .an7 {
|
||||
.libjass-subs .an1, .libjass-subs .an4, .libjass-subs .an7 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.an2, .an5, .an8 {
|
||||
.libjass-subs .an2, .libjass-subs .an5, .libjass-subs .an8 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.an3, .an6, .an9 {
|
||||
.libjass-subs .an3, .libjass-subs .an6, .libjass-subs .an9 {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#subs span {
|
||||
white-space: pre-wrap;
|
||||
.libjass-subs {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
#dpi-div {
|
||||
/* Filter wrapper span */
|
||||
.libjass-subs div[data-dialogue-id] > span {
|
||||
-webkit-perspective-origin: center;
|
||||
-webkit-perspective: 400px;
|
||||
perspective-origin: center;
|
||||
perspective: 400px;
|
||||
}
|
||||
|
||||
.libjass-font-measure {
|
||||
position: absolute;
|
||||
width: 1in;
|
||||
height: 1in;
|
||||
visibility: hidden;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.settings-form > fieldset {
|
||||
display: inline-block;
|
||||
.libjass-filters {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.libjass-filters * {
|
||||
color-interpolation-filters: sRGB;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
///<reference path="utility.ts" />
|
||||
///<reference path="dialogue.ts" />
|
||||
///<reference path="iterators.ts" />
|
||||
///<reference path="parser.ts" />
|
||||
///<reference path="renderer.ts" />
|
||||
///<reference path="tags.ts" />
|
||||
@@ -1,17 +1,12 @@
|
||||
{
|
||||
"name": "libjass",
|
||||
"version": "0.2.0",
|
||||
"version": "0.12.0",
|
||||
"description": "A library to render ASS subtitles on HTML5 video in the browser.",
|
||||
"keywords": ["browser", "html5", "subtitles"],
|
||||
"homepage": "https://github.com/Arnavion/libjass",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Arnavion/libjass/issues"
|
||||
},
|
||||
"licenses": [{
|
||||
"type": "Apache 2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}],
|
||||
"maintainers": [{
|
||||
"bugs": "https://github.com/Arnavion/libjass/issues",
|
||||
"license": "Apache-2.0",
|
||||
"contributors": [{
|
||||
"name": "Arnav Singh",
|
||||
"email": "arnavion@gmail.com"
|
||||
}],
|
||||
@@ -19,15 +14,25 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/Arnavion/libjass"
|
||||
},
|
||||
"main": "lib/libjass.js",
|
||||
"scripts": {
|
||||
"postinstall": "jake clean && jake --trace",
|
||||
"test": "jake test"
|
||||
"prepublish": "node ./build.js clean default",
|
||||
"build": "tsc -p ./build/tsconfig.json",
|
||||
"test": "node ./build.js test",
|
||||
"test-lib": "intern-client config=tests/intern reporters=Pretty",
|
||||
"test-minified": "intern-client config=tests/intern reporters=Pretty minified=true",
|
||||
"test-browser": "intern-runner config=tests/intern",
|
||||
"test-doc": "intern-client config=tests/intern-doc reporters=Pretty"
|
||||
},
|
||||
"dependencies": {
|
||||
"jake": "latest",
|
||||
"mocha": "~1.12",
|
||||
"pegjs": "0.7.0",
|
||||
"typescript": "0.9.1-1",
|
||||
"uglify-js": "~2"
|
||||
}
|
||||
"devDependencies": {
|
||||
"async": "1.x >=1.4",
|
||||
"async-build": "0.3.1",
|
||||
"intern": "3.x >=3.2.0",
|
||||
"npm": "3.x",
|
||||
"pngjs": "3.x",
|
||||
"sax": "1.x",
|
||||
"typescript": "next",
|
||||
"uglify-js": "2.x >=2.4.24"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
///<reference path="libjass.ts" />
|
||||
|
||||
module libjass {
|
||||
export interface Parser {
|
||||
parse(input: string, startRule?: string): any
|
||||
}
|
||||
|
||||
export var parser: Parser;
|
||||
|
||||
export class ASS {
|
||||
private _resolutionX: number;
|
||||
private _resolutionY: number;
|
||||
|
||||
private _scaleX: number;
|
||||
private _scaleY: number;
|
||||
|
||||
private _dpi: number;
|
||||
|
||||
private _styles: Style[] = [];
|
||||
private _dialogues: Dialogue[] = [];
|
||||
|
||||
/**
|
||||
* This class represents an ASS script. It contains information about the script, an array of Styles, and an array of Dialogues.
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} rawASS
|
||||
*/
|
||||
constructor(rawASS: string) {
|
||||
rawASS = rawASS.replace(/\r$/gm, "");
|
||||
|
||||
var script = libjass.parser.parse(rawASS, "script");
|
||||
|
||||
// Get the script info template
|
||||
var infoTemplate: Object = script["Script Info"];
|
||||
|
||||
if (libjass.verboseMode) {
|
||||
console.log("Read script info: " + JSON.stringify(infoTemplate), infoTemplate);
|
||||
}
|
||||
|
||||
// Parse the horizontal script resolution
|
||||
this._resolutionX = parseInt(infoTemplate["PlayResX"]);
|
||||
|
||||
// Parse the vertical script resolution
|
||||
this._resolutionY = parseInt(infoTemplate["PlayResY"]);
|
||||
|
||||
|
||||
// Get styles from the styles section
|
||||
script["V4+ Styles"].forEach((line: any) => {
|
||||
if (line.type === "Style") {
|
||||
var styleTemplate: Object = line.template;
|
||||
|
||||
if (libjass.verboseMode) {
|
||||
console.log("Read style: " + JSON.stringify(styleTemplate), styleTemplate);
|
||||
}
|
||||
|
||||
// Create the style and add it to the styles array
|
||||
this._styles.push(new Style(styleTemplate));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get dialogues from the events section
|
||||
script["Events"].forEach((line: any) => {
|
||||
if (line.type === "Dialogue") {
|
||||
var dialogueTemplate: Object = line.template;
|
||||
|
||||
if (libjass.verboseMode) {
|
||||
console.log("Read dialogue: " + JSON.stringify(dialogueTemplate), dialogueTemplate);
|
||||
}
|
||||
|
||||
// Create the dialogue and add it to the dialogues array
|
||||
this._dialogues.push(new Dialogue(dialogueTemplate, this));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get resolutionX(): number {
|
||||
return this._resolutionX;
|
||||
}
|
||||
|
||||
get resolutionY(): number {
|
||||
return this._resolutionY;
|
||||
}
|
||||
|
||||
get scaleX(): number {
|
||||
return this._scaleX;
|
||||
}
|
||||
|
||||
get scaleY(): number {
|
||||
return this._scaleY;
|
||||
}
|
||||
|
||||
get dpi(): number {
|
||||
return this._dpi;
|
||||
}
|
||||
set dpi(value: number) {
|
||||
this._dpi = value;
|
||||
}
|
||||
|
||||
get styles(): Style[] {
|
||||
return this._styles;
|
||||
}
|
||||
|
||||
get dialogues(): Dialogue[] {
|
||||
return this._dialogues;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method takes in the actual video height and width and prepares the scaleX and scaleY
|
||||
* properties according to the script resolution.
|
||||
*
|
||||
* @param {number} videoWidth The width of the video, in pixels
|
||||
* @param {number} videoHeight The height of the video, in pixels
|
||||
*/
|
||||
scaleTo(videoWidth: number, videoHeight: number): void {
|
||||
this._scaleX = videoWidth / this._resolutionX;
|
||||
this._scaleY = videoHeight / this._resolutionY;
|
||||
|
||||
// Any dialogues which have been rendered need to be re-rendered.
|
||||
this._dialogues.forEach(dialogue => {
|
||||
dialogue.unPreRender();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents a single global style declaration in an ASS script. The styles can be obtained via the ASS.styles property.
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} name The name of the style
|
||||
* @param {boolean} italic true if the style is italicized
|
||||
* @param {(boolean|number)} bold true if the style is bolded, false if it isn't, or a numerical weight
|
||||
* @param {boolean} underline true if the style is underlined
|
||||
* @param {boolean} strikethrough true if the style is struck-through
|
||||
* @param {number} outlineWidth The outline width, in pixels
|
||||
* @param {string} fontName The name of the font
|
||||
* @param {number} fontSize The size of the font, in pixels
|
||||
* @param {string} primaryColor The primary color, as a CSS rgba string
|
||||
* @param {string} outlineColor The outline color, as a CSS rgba string
|
||||
* @param {number} alignment The alignment, as an integer
|
||||
* @param {number} marginLeft The left margin
|
||||
* @param {number} marginRight The right margin
|
||||
* @param {number} marginVertical The vertical margin
|
||||
*/
|
||||
export class Style {
|
||||
private _name: string;
|
||||
|
||||
private _italic: boolean;
|
||||
private _bold: Object;
|
||||
private _underline: boolean;
|
||||
private _strikeThrough: boolean;
|
||||
|
||||
private _fontName: string;
|
||||
private _fontSize: number;
|
||||
|
||||
private _fontScaleX: number;
|
||||
private _fontScaleY: number;
|
||||
|
||||
private _letterSpacing: number;
|
||||
|
||||
private _primaryColor: tags.Color;
|
||||
private _outlineColor: tags.Color;
|
||||
|
||||
private _outlineWidth: number;
|
||||
|
||||
private _alignment: number;
|
||||
|
||||
private _marginLeft: number;
|
||||
private _marginRight: number;
|
||||
private _marginVertical: number;
|
||||
|
||||
constructor(template: Object) {
|
||||
this._name = template["Name"];
|
||||
|
||||
this._italic = template["Italic"] === "-1";
|
||||
this._bold = template["Bold"] === "-1";
|
||||
this._underline = template["Underline"] === "-1";
|
||||
this._strikeThrough = template["StrikeOut"] === "-1";
|
||||
|
||||
this._fontName = template["Fontname"];
|
||||
this._fontSize = parseFloat(template["Fontsize"]);
|
||||
|
||||
this._fontScaleX = parseFloat(template["ScaleX"]) / 100;
|
||||
this._fontScaleY = parseFloat(template["ScaleY"]) / 100;
|
||||
|
||||
this._letterSpacing = parseFloat(template["Spacing"]);
|
||||
|
||||
this._primaryColor = <tags.Color>parser.parse(template["PrimaryColour"], "colorWithAlpha");
|
||||
this._outlineColor = <tags.Color>parser.parse(template["OutlineColour"], "colorWithAlpha");
|
||||
|
||||
this._outlineWidth = parseFloat(template["Outline"]);
|
||||
|
||||
this._alignment = parseInt(template["Alignment"]);
|
||||
|
||||
this._marginLeft = parseFloat(template["MarginL"]);
|
||||
this._marginRight = parseFloat(template["MarginR"]);
|
||||
this._marginVertical = parseFloat(template["MarginV"]);
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
get italic(): boolean {
|
||||
return this._italic;
|
||||
}
|
||||
|
||||
get bold(): Object {
|
||||
return this._bold;
|
||||
}
|
||||
|
||||
get underline(): boolean {
|
||||
return this._underline;
|
||||
}
|
||||
|
||||
get strikeThrough(): boolean {
|
||||
return this._strikeThrough;
|
||||
}
|
||||
|
||||
get fontName(): string {
|
||||
return this._fontName;
|
||||
}
|
||||
|
||||
get fontSize(): number {
|
||||
return this._fontSize;
|
||||
}
|
||||
|
||||
get fontScaleX(): number {
|
||||
return this._fontScaleX;
|
||||
}
|
||||
|
||||
get fontScaleY(): number {
|
||||
return this._fontScaleY;
|
||||
}
|
||||
|
||||
get letterSpacing(): number {
|
||||
return this._letterSpacing;
|
||||
}
|
||||
|
||||
get primaryColor(): tags.Color {
|
||||
return this._primaryColor;
|
||||
}
|
||||
|
||||
get outlineColor(): tags.Color {
|
||||
return this._outlineColor;
|
||||
}
|
||||
|
||||
get outlineWidth(): number {
|
||||
return this._outlineWidth;
|
||||
}
|
||||
|
||||
get alignment(): number {
|
||||
return this._alignment;
|
||||
}
|
||||
|
||||
get marginLeft(): number {
|
||||
return this._marginLeft;
|
||||
}
|
||||
|
||||
get marginRight(): number {
|
||||
return this._marginRight;
|
||||
}
|
||||
|
||||
get marginVertical(): number {
|
||||
return this._marginVertical;
|
||||
}
|
||||
};
|
||||
|
||||
export var debugMode: boolean = false;
|
||||
export var verboseMode: boolean = false;
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
///<reference path="libjass.ts" />
|
||||
|
||||
module libjass {
|
||||
export class DefaultRenderer {
|
||||
private _dialogues: Dialogue[];
|
||||
private _wrappers: HTMLDivElement[][] = [];
|
||||
|
||||
private _currentTime: number;
|
||||
private _currentSubs: HTMLDivElement[] = [];
|
||||
|
||||
// Iterable of subtitle div's that are also to be displayed
|
||||
private _newSubs: LazySequence<Node>;
|
||||
|
||||
private _videoIsFullScreen: boolean = false;
|
||||
|
||||
private _eventListeners: Object = Object.create(null);
|
||||
|
||||
constructor(private _video: HTMLVideoElement, private _subsWrapper: HTMLDivElement, private _ass: ASS, private _settings: RendererSettings) {
|
||||
RendererSettings.prototype.initializeUnsetProperties.call(_settings);
|
||||
|
||||
// Sort the dialogues array by start time and then by their original position in the script (id)
|
||||
this._dialogues = this._ass.dialogues.slice(0);
|
||||
this._dialogues.sort((dialogue1: Dialogue, dialogue2: Dialogue) => {
|
||||
var result = dialogue1.start - dialogue2.start;
|
||||
|
||||
if (result === 0) {
|
||||
result = dialogue1.id - dialogue2.id;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
this._newSubs =
|
||||
Lazy(this._dialogues)
|
||||
// Skip until dialogues which end at a time later than currentTime
|
||||
.skipWhile((dialogue: Dialogue) => dialogue.end < this._currentTime)
|
||||
// Take until dialogue which starts later than currentTime + settings.preRenderTime
|
||||
.takeWhile((dialogue: Dialogue) => dialogue.start <= (this._currentTime + this._settings.preRenderTime))
|
||||
.filter((dialogue: Dialogue) => {
|
||||
// Ignore dialogues which end at a time less than currentTime
|
||||
if (dialogue.end < this._currentTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All these dialogues are visible at atleast one time in the range [currentTime, currentTime + settings.preRenderTime]
|
||||
|
||||
// Ignore those dialogues which have already been displayed
|
||||
if (this._currentSubs.some((sub: HTMLDivElement) => parseInt(sub.getAttribute("data-dialogue-id")) === dialogue.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the dialogue is to be displayed, keep it to be drawn...
|
||||
if (dialogue.start <= this._currentTime) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ... otherwise pre-render it and forget it
|
||||
else {
|
||||
dialogue.preRender();
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.map((dialogue: Dialogue) => {
|
||||
if (libjass.debugMode) {
|
||||
console.log(dialogue.toString());
|
||||
}
|
||||
|
||||
// Display the dialogue and return the drawn subtitle div
|
||||
return this._wrappers[dialogue.layer][dialogue.alignment].appendChild(dialogue.draw(this._currentTime));
|
||||
});
|
||||
|
||||
|
||||
// Create layer wrapper div's and the alignment div's inside each layer div
|
||||
var layers = new Set<number>();
|
||||
this._dialogues.forEach((dialogue: Dialogue) => {
|
||||
layers.add(dialogue.layer);
|
||||
});
|
||||
var layersArray: number[] = [];
|
||||
layers.forEach((layer: number) => { layersArray.push(layer); });
|
||||
layersArray.sort().forEach((layer: number) => {
|
||||
this._wrappers[layer] = new Array<HTMLDivElement>(9 + 1); // + 1 because alignments are 1-indexed (1 to 9)
|
||||
|
||||
for (var alignment = 1; alignment <= 9; alignment++) {
|
||||
var wrapperDiv = document.createElement("div");
|
||||
wrapperDiv.className = "an" + alignment + " layer" + layer;
|
||||
this._subsWrapper.appendChild(wrapperDiv);
|
||||
this._wrappers[layer][alignment] = wrapperDiv;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!this._settings.preLoadFonts) {
|
||||
setTimeout(() => { this._ready(); }, 0);
|
||||
}
|
||||
// Preload fonts
|
||||
else {
|
||||
var allFonts = new Set<string>();
|
||||
|
||||
this._ass.styles.forEach((style: Style) => {
|
||||
allFonts.add(style.fontName);
|
||||
});
|
||||
|
||||
this._dialogues.forEach((dialogue: Dialogue) => {
|
||||
dialogue.parts.forEach((part: tags.Tag) => {
|
||||
if (part instanceof tags.FontName) {
|
||||
allFonts.add((<tags.FontName>part).value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var urlsToPreload =
|
||||
Object.keys(this._settings.fontMap)
|
||||
.filter((name: string) => allFonts.has(name))
|
||||
.map((name: string) => <string>this._settings.fontMap[name]);
|
||||
|
||||
var urlsLeftToPreload = urlsToPreload.length;
|
||||
|
||||
if (libjass.debugMode) {
|
||||
console.log("Preloading fonts...");
|
||||
}
|
||||
|
||||
urlsToPreload.forEach((url: string) => {
|
||||
var xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open("GET", url, true);
|
||||
xhr.addEventListener("readystatechange", () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (libjass.debugMode) {
|
||||
console.log("Preloaded " + url + ".");
|
||||
}
|
||||
|
||||
--urlsLeftToPreload;
|
||||
|
||||
if (libjass.debugMode) {
|
||||
console.log(urlsLeftToPreload + " fonts left to preload.");
|
||||
}
|
||||
|
||||
if (urlsLeftToPreload === 0) {
|
||||
if (libjass.debugMode) {
|
||||
console.log("All fonts have been preloaded.");
|
||||
}
|
||||
|
||||
this._ready();
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
xhr.send(null);
|
||||
return xhr;
|
||||
});
|
||||
|
||||
if (libjass.debugMode) {
|
||||
console.log(urlsLeftToPreload + " fonts left to preload.");
|
||||
}
|
||||
|
||||
if (urlsLeftToPreload === 0) {
|
||||
if (libjass.debugMode) {
|
||||
console.log("All fonts have been preloaded.");
|
||||
}
|
||||
|
||||
this._ready();
|
||||
}
|
||||
}
|
||||
|
||||
this._eventListeners["ready"] = [];
|
||||
this._eventListeners["fullScreenChange"] = [];
|
||||
}
|
||||
|
||||
public addEventListener(type: string, listener: Object): void {
|
||||
var listeners = <Array>this._eventListeners[type];
|
||||
if (listeners) {
|
||||
listeners.push(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public resizeVideo(width: number, height: number): void {
|
||||
this._currentSubs.forEach(removeElement);
|
||||
|
||||
this._currentSubs = [];
|
||||
|
||||
this._video.style.width = this._subsWrapper.style.width = width + "px";
|
||||
this._video.style.height = this._subsWrapper.style.height = height + "px";
|
||||
|
||||
this._ass.scaleTo(width, height);
|
||||
|
||||
this._video.dispatchEvent(new (<any>Event)("timeupdate"));
|
||||
}
|
||||
|
||||
private _ready(): void {
|
||||
this._video.addEventListener("timeupdate", this._onVideoTimeUpdate.bind(this), false);
|
||||
this._video.addEventListener("seeking", this._onVideoSeeking.bind(this), false);
|
||||
this._video.addEventListener("pause", this._onVideoPause.bind(this), false);
|
||||
this._video.addEventListener("playing", this._onVideoPlaying.bind(this), false);
|
||||
|
||||
document.addEventListener("webkitfullscreenchange", this._onFullScreenChange.bind(this), false);
|
||||
document.addEventListener("mozfullscreenchange", this._onFullScreenChange.bind(this), false);
|
||||
document.addEventListener("fullscreenchange", this._onFullScreenChange.bind(this), false);
|
||||
|
||||
this._dispatchEvent("ready");
|
||||
}
|
||||
|
||||
private _onVideoTimeUpdate(): void {
|
||||
this._currentTime = this._video.currentTime;
|
||||
|
||||
this._currentSubs = this._currentSubs.filter((sub: HTMLDivElement) => {
|
||||
var subDialogue = this._ass.dialogues[parseInt(sub.getAttribute("data-dialogue-id"))];
|
||||
|
||||
// If the sub should still be displayed at currentTime, keep it...
|
||||
if (subDialogue.start <= this._currentTime && this._currentTime < subDialogue.end) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ... otherwise remove it from the DOM and from this array...
|
||||
else {
|
||||
removeElement(sub);
|
||||
return false;
|
||||
}
|
||||
}).concat(this._newSubs.toArray()); // ... and add the new subs that are to be displayed.
|
||||
|
||||
if (libjass.debugMode) {
|
||||
console.log("video.timeupdate: " + this._getVideoStateLogString());
|
||||
}
|
||||
}
|
||||
|
||||
private _onVideoSeeking(): void {
|
||||
this._currentSubs.forEach(removeElement);
|
||||
|
||||
this._currentSubs = [];
|
||||
|
||||
if (libjass.debugMode) {
|
||||
console.log("video.seeking: " + this._getVideoStateLogString());
|
||||
}
|
||||
}
|
||||
|
||||
private _onVideoPause(): void {
|
||||
this._subsWrapper.className = "paused";
|
||||
|
||||
if (libjass.debugMode) {
|
||||
console.log("video.pause: " + this._getVideoStateLogString());
|
||||
}
|
||||
}
|
||||
|
||||
private _onVideoPlaying(): void {
|
||||
this._subsWrapper.className = "";
|
||||
|
||||
if (libjass.debugMode) {
|
||||
console.log("video.playing: " + this._getVideoStateLogString());
|
||||
}
|
||||
}
|
||||
|
||||
private _onFullScreenChange() {
|
||||
var fullScreenElement = document.fullscreenElement;
|
||||
if (fullScreenElement === undefined) {
|
||||
fullScreenElement = document.mozFullScreenElement;
|
||||
}
|
||||
if (fullScreenElement === undefined) {
|
||||
fullScreenElement = document.msFullscreenElement;
|
||||
}
|
||||
if (fullScreenElement === undefined) {
|
||||
fullScreenElement = document.webkitFullscreenElement;
|
||||
}
|
||||
|
||||
if (fullScreenElement === this._video) {
|
||||
this.resizeVideo(screen.width, screen.height);
|
||||
this._videoIsFullScreen = true;
|
||||
}
|
||||
else if (fullScreenElement === null && this._videoIsFullScreen) {
|
||||
this._videoIsFullScreen = false;
|
||||
|
||||
this._dispatchEvent("fullScreenChange", this._videoIsFullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
private _getVideoStateLogString(): string {
|
||||
return "video.currentTime = " + this._video.currentTime + ", video.paused = " + this._video.paused + ", video.seeking = " + this._video.seeking;
|
||||
}
|
||||
|
||||
private _dispatchEvent(type: string, ...args: Object[]): void {
|
||||
var listeners = <Array>this._eventListeners[type];
|
||||
if (listeners) {
|
||||
listeners.forEach((listener: Function) => {
|
||||
listener.apply(this, args);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RendererSettings {
|
||||
public preLoadFonts: boolean;
|
||||
public fontMap: FontMap;
|
||||
|
||||
/**
|
||||
* Subtitles will be pre-rendered for this amount of time (seconds)
|
||||
*/
|
||||
public preRenderTime: number;
|
||||
|
||||
public initializeUnsetProperties(): void {
|
||||
if (this.preLoadFonts === undefined) {
|
||||
this.preLoadFonts = false;
|
||||
}
|
||||
|
||||
if (this.fontMap === undefined) {
|
||||
this.fontMap = null;
|
||||
}
|
||||
|
||||
if (this.preRenderTime === undefined) {
|
||||
this.preRenderTime = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FontMap {
|
||||
constructor(private _map: Object) { }
|
||||
|
||||
public static fromStyleElement(styleElement: HTMLStyleElement): FontMap {
|
||||
var map: Object = Object.create(null);
|
||||
|
||||
var styleSheet = <CSSStyleSheet>styleElement.sheet;
|
||||
var rules: CSSFontFaceRule[] = Array.prototype.filter.call(styleSheet.cssRules, (rule: CSSRule) => rule.type === CSSRule.FONT_FACE_RULE);
|
||||
rules.forEach((rule: CSSFontFaceRule) => {
|
||||
var src =
|
||||
rule.style.getPropertyValue("src") ||
|
||||
rule.cssText.split("\n")
|
||||
.map((line: string) => line.match(/\s*src: url\((.+)\);/))
|
||||
.filter((matches: string[]) => matches !== null)
|
||||
.map((matches: string[]) => FontMap._stripQuotes(matches[1]))[0];
|
||||
|
||||
if (src) {
|
||||
var name = FontMap._stripQuotes(rule.style.getPropertyValue("font-family"));
|
||||
map[name] = src;
|
||||
}
|
||||
});
|
||||
|
||||
return new FontMap(map);
|
||||
}
|
||||
|
||||
private static _stripQuotes(str: string): string {
|
||||
return str.match(/^["']?(.*?)["']?/)[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as settings from "./settings";
|
||||
export { debugMode, verboseMode } from "./settings";
|
||||
|
||||
import * as set from "./utility/set";
|
||||
export { Set } from "./utility/set";
|
||||
|
||||
import * as map from "./utility/map";
|
||||
export { Map } from "./utility/map";
|
||||
|
||||
import * as promise from "./utility/promise";
|
||||
export { Promise, DeferredPromise } from "./utility/promise";
|
||||
|
||||
import * as webworker from "./webworker";
|
||||
export { webworker };
|
||||
|
||||
import * as parts from "./parts";
|
||||
export { parts };
|
||||
|
||||
import * as parser from "./parser";
|
||||
export { parser };
|
||||
|
||||
import * as renderers from "./renderers";
|
||||
export { renderers };
|
||||
|
||||
export { serialize, deserialize } from "./serialization";
|
||||
|
||||
export { ASS } from "./types/ass";
|
||||
export { Attachment, AttachmentType } from "./types/attachment";
|
||||
export { Dialogue } from "./types/dialogue";
|
||||
export { ScriptProperties } from "./types/script-properties";
|
||||
export { Style } from "./types/style";
|
||||
|
||||
export { BorderStyle, Format, WrappingStyle } from "./types/misc";
|
||||
|
||||
export { version } from "./version";
|
||||
|
||||
/**
|
||||
* Configures libjass with the given properties.
|
||||
*
|
||||
* @param {!*} newConfig
|
||||
* @param {?boolean} newConfig["debugMode"] When true, libjass logs some debug messages.
|
||||
* @param {?boolean} newConfig["verboseMode"] When true, libjass logs some more debug messages. This setting is independent of {@link libjass.debugMode}
|
||||
* @param {?function(new:Set, !Array.<T>=)} newConfig["Set"] Sets the Set implementation used by libjass to the provided one. If null, {@link ./utility/set.SimpleSet} is used.
|
||||
* @param {?function(new:Map, !Array.<!Array.<*>>=)} newConfig["Map"] Sets the Map implementation used by libjass to the provided one. If null, {@link ./utility/map.SimpleMap} is used.
|
||||
* @param {?function(new:Promise)} newConfig["Promise"] Sets the Promise implementation used by libjass to the provided one. If null, {@link ./utility/promise.SimplePromise} is used.
|
||||
*/
|
||||
export function configure(newConfig: {
|
||||
debugMode?: boolean,
|
||||
verboseMode?: boolean,
|
||||
Set?: typeof set.Set | null,
|
||||
Map?: typeof map.Map | null,
|
||||
Promise?: typeof promise.Promise | null,
|
||||
}): void {
|
||||
if (typeof newConfig.debugMode === "boolean") {
|
||||
settings.setDebugMode(newConfig.debugMode);
|
||||
}
|
||||
|
||||
if (typeof newConfig.verboseMode === "boolean") {
|
||||
settings.setVerboseMode(newConfig.verboseMode);
|
||||
}
|
||||
|
||||
if (typeof newConfig.Set === "function" || newConfig.Set === null) {
|
||||
set.setImplementation(newConfig.Set);
|
||||
}
|
||||
|
||||
if (typeof newConfig.Map === "function" || newConfig.Map === null) {
|
||||
map.setImplementation(newConfig.Map);
|
||||
}
|
||||
|
||||
if (typeof newConfig.Promise === "function" || newConfig.Promise === null) {
|
||||
promise.setImplementation(newConfig.Promise);
|
||||
}
|
||||
}
|
||||
|
||||
declare const exports: any;
|
||||
|
||||
// Getters below are to work around https://github.com/Microsoft/TypeScript/issues/6366
|
||||
|
||||
Object.defineProperties(exports, {
|
||||
debugMode: {
|
||||
get: () => settings.debugMode,
|
||||
set: value => {
|
||||
console.warn("Setter `libjass.debugMode = value` has been deprecated. Use `libjass.configure({ debugMode: value })` instead.");
|
||||
settings.setDebugMode(value);
|
||||
},
|
||||
},
|
||||
|
||||
verboseMode: {
|
||||
get: () => settings.verboseMode,
|
||||
set: value => {
|
||||
console.warn("Setter `libjass.verboseMode = value` has been deprecated. Use `libjass.configure({ verboseMode: value })` instead.");
|
||||
settings.setVerboseMode(value);
|
||||
},
|
||||
},
|
||||
|
||||
Set: {
|
||||
get: () => set.Set,
|
||||
set: value => {
|
||||
console.warn("Setter `libjass.Set = value` has been deprecated. Use `libjass.configure({ Set: value })` instead.");
|
||||
set.setImplementation(value);
|
||||
},
|
||||
},
|
||||
|
||||
Map: {
|
||||
get: () => map.Map,
|
||||
set: value => {
|
||||
console.warn("Setter `libjass.Map = value` has been deprecated. Use `libjass.configure({ Map: value })` instead.");
|
||||
map.setImplementation(value);
|
||||
},
|
||||
},
|
||||
|
||||
Promise: {
|
||||
get: () => promise.Promise,
|
||||
set: value => {
|
||||
console.warn("Setter `libjass.Promise = value` has been deprecated. Use `libjass.configure({ Promise: value })` instead.");
|
||||
promise.setImplementation(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { parse } from "./parse";
|
||||
export { BrowserReadableStream, Stream, StringStream, XhrStream } from "./streams";
|
||||
export { StreamParser, SrtStreamParser } from "./stream-parsers";
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Property, TypedTemplate } from "../types/misc";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
|
||||
/**
|
||||
* Parses a line into a {@link ./types/misc.Property}.
|
||||
*
|
||||
* @param {string} line
|
||||
* @return {Property}
|
||||
*/
|
||||
export function parseLineIntoProperty(line: string): Property | null {
|
||||
const colonPos = line.indexOf(":");
|
||||
if (colonPos === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = line.substr(0, colonPos);
|
||||
const value = line.substr(colonPos + 1).replace(/^\s+/, "");
|
||||
|
||||
return { name, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a line into a {@link ./types/misc.TypedTemplate} according to the given format specifier.
|
||||
*
|
||||
* @param {string} line
|
||||
* @param {!Array.<string>} formatSpecifier
|
||||
* @return {TypedTemplate}
|
||||
*/
|
||||
export function parseLineIntoTypedTemplate(line: string, formatSpecifier: string[]): TypedTemplate | null {
|
||||
const property = parseLineIntoProperty(line);
|
||||
if (property === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = property.value.split(",");
|
||||
|
||||
if (value.length > formatSpecifier.length) {
|
||||
value[formatSpecifier.length - 1] = value.slice(formatSpecifier.length - 1).join(",");
|
||||
}
|
||||
|
||||
const template = new Map<string, string>();
|
||||
formatSpecifier.forEach((formatKey, index) => {
|
||||
template.set(formatKey, value[index]);
|
||||
});
|
||||
|
||||
return { type: property.name, template };
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { debugMode } from "../settings";
|
||||
|
||||
import { ASS } from "../types/ass";
|
||||
import { Style } from "../types/style";
|
||||
import { Dialogue } from "../types/dialogue";
|
||||
import { Attachment, AttachmentType } from "../types/attachment";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
|
||||
import { Promise, DeferredPromise } from "../utility/promise";
|
||||
|
||||
import { parseLineIntoProperty } from "./misc";
|
||||
import { Stream } from "./streams";
|
||||
|
||||
enum Section {
|
||||
ScriptInfo,
|
||||
Styles,
|
||||
Events,
|
||||
Fonts,
|
||||
Graphics,
|
||||
Other,
|
||||
EOF,
|
||||
}
|
||||
|
||||
/**
|
||||
* A parser that parses an {@link libjass.ASS} object from a {@link libjass.parser.Stream}.
|
||||
*
|
||||
* @param {!libjass.parser.Stream} stream The {@link libjass.parser.Stream} to parse
|
||||
*/
|
||||
export class StreamParser {
|
||||
private _ass: ASS = new ASS();
|
||||
private _minimalDeferred: DeferredPromise<ASS> = new DeferredPromise<ASS>();
|
||||
private _deferred: DeferredPromise<ASS> = new DeferredPromise<ASS>();
|
||||
|
||||
private _shouldSwallowBom: boolean = true;
|
||||
private _currentSection: Section = Section.ScriptInfo;
|
||||
private _currentAttachment: Attachment | null = null;
|
||||
|
||||
constructor(private _stream: Stream) {
|
||||
this._stream.nextLine().then(line => this._onNextLine(line), reason => {
|
||||
this._minimalDeferred.reject(reason);
|
||||
this._deferred.reject(reason);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {!Promise.<!libjass.ASS>} A promise that will be resolved when the script properties of the ASS script have been parsed from the stream. Styles and events have not necessarily been
|
||||
* parsed at the point this promise becomes resolved.
|
||||
*/
|
||||
get minimalASS(): Promise<ASS> {
|
||||
return this._minimalDeferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {!Promise.<!libjass.ASS>} A promise that will be resolved when the entire stream has been parsed.
|
||||
*/
|
||||
get ass(): Promise<ASS> {
|
||||
return this._deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
private get currentSection(): Section {
|
||||
return this._currentSection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
private set currentSection(value: Section) {
|
||||
if (this._currentAttachment !== null) {
|
||||
this._ass.addAttachment(this._currentAttachment);
|
||||
this._currentAttachment = null;
|
||||
}
|
||||
|
||||
if (this._currentSection === Section.ScriptInfo && value !== Section.ScriptInfo) {
|
||||
// Exiting script info section
|
||||
this._minimalDeferred.resolve(this._ass);
|
||||
}
|
||||
|
||||
if (value === Section.EOF) {
|
||||
const scriptProperties = this._ass.properties;
|
||||
if (scriptProperties.resolutionX === undefined || scriptProperties.resolutionY === undefined) {
|
||||
// Malformed script.
|
||||
this._minimalDeferred.reject("Malformed ASS script.");
|
||||
this._deferred.reject("Malformed ASS script.");
|
||||
}
|
||||
else {
|
||||
this._minimalDeferred.resolve(this._ass);
|
||||
this._deferred.resolve(this._ass);
|
||||
}
|
||||
}
|
||||
|
||||
this._currentSection = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
*/
|
||||
private _onNextLine(line: string | null): void {
|
||||
if (line === null) {
|
||||
this.currentSection = Section.EOF;
|
||||
return;
|
||||
}
|
||||
|
||||
if (line[line.length - 1] === "\r") {
|
||||
line = line.substr(0, line.length - 1);
|
||||
}
|
||||
|
||||
if (line.charCodeAt(0) === 0xfeff && this._shouldSwallowBom) {
|
||||
line = line.substr(1);
|
||||
}
|
||||
|
||||
this._shouldSwallowBom = false;
|
||||
|
||||
if (line === "") {
|
||||
// Ignore empty lines.
|
||||
}
|
||||
|
||||
else if (line[0] === ";" && this._currentAttachment === null) {
|
||||
// Lines starting with ; are comments, unless reading an attachment.
|
||||
}
|
||||
|
||||
else if (line === "[Script Info]") {
|
||||
this.currentSection = Section.ScriptInfo;
|
||||
}
|
||||
else if (line === "[V4+ Styles]" || line === "[V4 Styles]") {
|
||||
this.currentSection = Section.Styles;
|
||||
}
|
||||
else if (line === "[Events]") {
|
||||
this.currentSection = Section.Events;
|
||||
}
|
||||
else if (line === "[Fonts]") {
|
||||
this.currentSection = Section.Fonts;
|
||||
}
|
||||
else if (line === "[Graphics]") {
|
||||
this.currentSection = Section.Graphics;
|
||||
}
|
||||
else {
|
||||
if (this._currentAttachment === null && line[0] === "[" && line[line.length - 1] === "]") {
|
||||
/* This looks like the start of a new section. The section name is unrecognized if it is.
|
||||
* Since there's no current attachment being parsed it's definitely the start of a new section.
|
||||
* If an attachment is being parsed, this might be part of the attachment.
|
||||
*/
|
||||
this.currentSection = Section.Other;
|
||||
}
|
||||
|
||||
switch (this.currentSection) {
|
||||
case Section.ScriptInfo:
|
||||
const property = parseLineIntoProperty(line);
|
||||
if (property !== null) {
|
||||
switch (property.name) {
|
||||
case "PlayResX":
|
||||
this._ass.properties.resolutionX = parseInt(property.value);
|
||||
break;
|
||||
case "PlayResY":
|
||||
this._ass.properties.resolutionY = parseInt(property.value);
|
||||
break;
|
||||
case "WrapStyle":
|
||||
this._ass.properties.wrappingStyle = parseInt(property.value);
|
||||
break;
|
||||
case "ScaledBorderAndShadow":
|
||||
this._ass.properties.scaleBorderAndShadow = (property.value === "yes");
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Section.Styles:
|
||||
if (this._ass.stylesFormatSpecifier === null) {
|
||||
const property = parseLineIntoProperty(line);
|
||||
if (property !== null && property.name === "Format") {
|
||||
this._ass.stylesFormatSpecifier = property.value.split(",").map(str => str.trim());
|
||||
}
|
||||
else {
|
||||
// Ignore any non-format lines
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
this._ass.addStyle(line);
|
||||
}
|
||||
catch (ex) {
|
||||
if (debugMode) {
|
||||
console.error(`Could not parse style from line ${ line } - ${ ex.stack || ex }`);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Section.Events:
|
||||
if (this._ass.dialoguesFormatSpecifier === null) {
|
||||
const property = parseLineIntoProperty(line);
|
||||
if (property !== null && property.name === "Format") {
|
||||
this._ass.dialoguesFormatSpecifier = property.value.split(",").map(str => str.trim());
|
||||
}
|
||||
else {
|
||||
// Ignore any non-format lines
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
this._ass.addEvent(line);
|
||||
}
|
||||
catch (ex) {
|
||||
if (debugMode) {
|
||||
console.error(`Could not parse event from line ${ line } - ${ ex.stack || ex }`);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Section.Fonts:
|
||||
case Section.Graphics:
|
||||
const startOfNewAttachmentRegex = (this.currentSection === Section.Fonts) ? /^fontname:(.+)/ : /^filename:(.+)/;
|
||||
const startOfNewAttachment = startOfNewAttachmentRegex.exec(line);
|
||||
|
||||
if (startOfNewAttachment !== null) {
|
||||
// Start of new attachment
|
||||
|
||||
if (this._currentAttachment !== null) {
|
||||
this._ass.addAttachment(this._currentAttachment);
|
||||
this._currentAttachment = null;
|
||||
}
|
||||
|
||||
this._currentAttachment = new Attachment(startOfNewAttachment[1].trim(), (this.currentSection === Section.Fonts) ? AttachmentType.Font : AttachmentType.Graphic);
|
||||
}
|
||||
else if (this._currentAttachment !== null) {
|
||||
try {
|
||||
this._currentAttachment.contents += uuencodedToBase64(line);
|
||||
}
|
||||
catch (ex) {
|
||||
if (debugMode) {
|
||||
console.error(`Encountered error while reading font ${ this._currentAttachment.filename }: %o`, ex);
|
||||
}
|
||||
|
||||
this._currentAttachment = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Ignore.
|
||||
}
|
||||
break;
|
||||
|
||||
case Section.Other:
|
||||
// Ignore other sections.
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unhandled state ${ this.currentSection }`);
|
||||
}
|
||||
}
|
||||
|
||||
this._stream.nextLine().then(line => this._onNextLine(line), reason => {
|
||||
this._minimalDeferred.reject(reason);
|
||||
this._deferred.reject(reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A parser that parses an {@link libjass.ASS} object from a {@link libjass.parser.Stream} of an SRT script.
|
||||
*
|
||||
* @param {!libjass.parser.Stream} stream The {@link libjass.parser.Stream} to parse
|
||||
*/
|
||||
export class SrtStreamParser {
|
||||
private _ass: ASS = new ASS();
|
||||
private _deferred: DeferredPromise<ASS> = new DeferredPromise<ASS>();
|
||||
|
||||
private _shouldSwallowBom: boolean = true;
|
||||
|
||||
private _currentDialogueNumber: string | null = null;
|
||||
private _currentDialogueStart: string | null = null;
|
||||
private _currentDialogueEnd: string | null = null;
|
||||
private _currentDialogueText: string | null = null;
|
||||
|
||||
constructor(private _stream: Stream) {
|
||||
this._stream.nextLine().then(line => this._onNextLine(line), reason => {
|
||||
this._deferred.reject(reason);
|
||||
});
|
||||
|
||||
this._ass.properties.resolutionX = 1280;
|
||||
this._ass.properties.resolutionY = 720;
|
||||
this._ass.properties.wrappingStyle = 1;
|
||||
this._ass.properties.scaleBorderAndShadow = true;
|
||||
|
||||
const newStyle = new Style(new Map([["Name", "Default"], ["FontSize", "36"]]));
|
||||
this._ass.styles.set(newStyle.name, newStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {!Promise.<!libjass.ASS>} A promise that will be resolved when the entire stream has been parsed.
|
||||
*/
|
||||
get ass(): Promise<ASS> {
|
||||
return this._deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} line
|
||||
*/
|
||||
private _onNextLine(line: string | null): void {
|
||||
if (line === null) {
|
||||
if (this._currentDialogueNumber !== null && this._currentDialogueStart !== null && this._currentDialogueEnd !== null && this._currentDialogueText !== null) {
|
||||
this._ass.dialogues.push(new Dialogue(new Map([
|
||||
["Style", "Default"],
|
||||
["Start", this._currentDialogueStart],
|
||||
["End", this._currentDialogueEnd],
|
||||
["Text", this._currentDialogueText],
|
||||
]), this._ass));
|
||||
}
|
||||
|
||||
this._deferred.resolve(this._ass);
|
||||
return;
|
||||
}
|
||||
|
||||
if (line[line.length - 1] === "\r") {
|
||||
line = line.substr(0, line.length - 1);
|
||||
}
|
||||
|
||||
if (line.charCodeAt(0) === 0xfeff && this._shouldSwallowBom) {
|
||||
line = line.substr(1);
|
||||
}
|
||||
|
||||
this._shouldSwallowBom = false;
|
||||
|
||||
if (line === "") {
|
||||
if (this._currentDialogueNumber !== null && this._currentDialogueStart !== null && this._currentDialogueEnd !== null && this._currentDialogueText !== null) {
|
||||
this._ass.dialogues.push(new Dialogue(new Map([
|
||||
["Style", "Default"],
|
||||
["Start", this._currentDialogueStart],
|
||||
["End", this._currentDialogueEnd],
|
||||
["Text", this._currentDialogueText],
|
||||
]), this._ass));
|
||||
}
|
||||
|
||||
this._currentDialogueNumber = this._currentDialogueStart = this._currentDialogueEnd = this._currentDialogueText = null;
|
||||
}
|
||||
else {
|
||||
if (this._currentDialogueNumber === null) {
|
||||
if (/^\d+$/.test(line)) {
|
||||
this._currentDialogueNumber = line;
|
||||
}
|
||||
}
|
||||
else if (this._currentDialogueStart === null && this._currentDialogueEnd === null) {
|
||||
const match = /^(\d\d:\d\d:\d\d,\d\d\d) --> (\d\d:\d\d:\d\d,\d\d\d)/.exec(line);
|
||||
if (match !== null) {
|
||||
this._currentDialogueStart = match[1].replace(",", ".");
|
||||
this._currentDialogueEnd = match[2].replace(",", ".");
|
||||
}
|
||||
}
|
||||
else {
|
||||
line = line
|
||||
.replace(/<b>/g, "{\\b1}").replace(/\{b\}/g, "{\\b1}")
|
||||
.replace(/<\/b>/g, "{\\b0}").replace(/\{\/b\}/g, "{\\b0}")
|
||||
.replace(/<i>/g, "{\\i1}").replace(/\{i\}/g, "{\\i1}")
|
||||
.replace(/<\/i>/g, "{\\i0}").replace(/\{\/i\}/g, "{\\i0}")
|
||||
.replace(/<u>/g, "{\\u1}").replace(/\{u\}/g, "{\\u1}")
|
||||
.replace(/<\/u>/g, "{\\u0}").replace(/\{\/u\}/g, "{\\u0}")
|
||||
.replace(
|
||||
/<font color="#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})">/g,
|
||||
(/* ujs:unreferenced */ substring: string, red: string, green: string, blue: string) => `{\c&H${ blue }${ green }${ red }&}`
|
||||
).replace(/<\/font>/g, "{\\c}");
|
||||
|
||||
if (this._currentDialogueText !== null) {
|
||||
this._currentDialogueText += "\\N" + line;
|
||||
}
|
||||
else {
|
||||
this._currentDialogueText = line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._stream.nextLine().then(line => this._onNextLine(line), reason => {
|
||||
this._deferred.reject(reason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a uuencoded string to a base64 string.
|
||||
*
|
||||
* @param {string} str
|
||||
* @return {string}
|
||||
*/
|
||||
function uuencodedToBase64(str: string): string {
|
||||
let result = "";
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const charCode = str.charCodeAt(i) - 33;
|
||||
|
||||
if (charCode < 0 || charCode > 63) {
|
||||
throw new Error(`Out-of-range character code ${ charCode } at index ${ i } in string ${ str }`);
|
||||
}
|
||||
if (charCode < 26) {
|
||||
result += String.fromCharCode("A".charCodeAt(0) + charCode);
|
||||
}
|
||||
else if (charCode < 52) {
|
||||
result += String.fromCharCode("a".charCodeAt(0) + charCode - 26);
|
||||
}
|
||||
else if (charCode < 62) {
|
||||
result += String.fromCharCode("0".charCodeAt(0) + charCode - 52);
|
||||
}
|
||||
else if (charCode === 62) {
|
||||
result += "+";
|
||||
}
|
||||
else {
|
||||
result += "/";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Promise, DeferredPromise } from "../utility/promise";
|
||||
|
||||
export interface ReadableStream {
|
||||
/**
|
||||
* @return {!ReadableStreamReader}
|
||||
*/
|
||||
getReader(): ReadableStreamReader;
|
||||
}
|
||||
|
||||
export interface ReadableStreamReader {
|
||||
/**
|
||||
* @return {!Promise.<{ value?: Uint8Array, done: boolean }>}
|
||||
*/
|
||||
read(): Promise<{ value: Uint8Array; done: boolean; }>;
|
||||
}
|
||||
|
||||
export interface TextDecoder {
|
||||
/**
|
||||
* @param {!ArrayBuffer|!ArrayBufferView} input
|
||||
* @param {{ stream: boolean }} options
|
||||
* @return {string}
|
||||
*/
|
||||
decode(input: ArrayBuffer | ArrayBufferView, options: { stream: boolean }): string;
|
||||
}
|
||||
export interface TextDecoderConstructor {
|
||||
new (encoding: string, options: { ignoreBOM: boolean }): TextDecoder;
|
||||
|
||||
/**
|
||||
* @type {!TextDecoder}
|
||||
*/
|
||||
prototype: TextDecoder;
|
||||
}
|
||||
|
||||
declare const global: {
|
||||
/**
|
||||
* @type {!TextDecoderConstructor}
|
||||
*/
|
||||
TextDecoder?: TextDecoderConstructor;
|
||||
};
|
||||
|
||||
/**
|
||||
* An interface for a stream.
|
||||
*/
|
||||
export interface Stream {
|
||||
/**
|
||||
* @return {!Promise.<?string>} A promise that will be resolved with the next line, or null if the stream is exhausted.
|
||||
*/
|
||||
nextLine(): Promise<string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link libjass.parser.Stream} that reads from a string in memory.
|
||||
*
|
||||
* @param {string} str The string
|
||||
*/
|
||||
export class StringStream implements Stream {
|
||||
private _readTill: number = 0;
|
||||
|
||||
constructor(private _str: string) { }
|
||||
|
||||
/**
|
||||
* @return {!Promise.<?string>} A promise that will be resolved with the next line, or null if the string has been completely read.
|
||||
*/
|
||||
nextLine(): Promise<string | null> {
|
||||
let result: Promise<string | null>;
|
||||
|
||||
if (this._readTill < this._str.length) {
|
||||
const nextNewLinePos = this._str.indexOf("\n", this._readTill);
|
||||
if (nextNewLinePos !== -1) {
|
||||
result = Promise.resolve(this._str.substring(this._readTill, nextNewLinePos));
|
||||
this._readTill = nextNewLinePos + 1;
|
||||
}
|
||||
else {
|
||||
result = Promise.resolve(this._str.substr(this._readTill));
|
||||
this._readTill = this._str.length;
|
||||
}
|
||||
}
|
||||
else {
|
||||
result = Promise.resolve<string | null>(null);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link libjass.parser.Stream} that reads from an XMLHttpRequest object.
|
||||
*
|
||||
* @param {!XMLHttpRequest} xhr The XMLHttpRequest object. Make sure to not call .open() on this object before passing it in here,
|
||||
* since event handlers cannot be registered after open() has been called.
|
||||
*/
|
||||
export class XhrStream implements Stream {
|
||||
private _readTill: number = 0;
|
||||
private _pendingDeferred: DeferredPromise<string | null> | null = null;
|
||||
private _failedError: ErrorEvent | null = null;
|
||||
|
||||
constructor(private _xhr: XMLHttpRequest) {
|
||||
_xhr.addEventListener("progress", () => this._onXhrProgress(), false);
|
||||
_xhr.addEventListener("load", () => this._onXhrLoad(), false);
|
||||
_xhr.addEventListener("error", event => this._onXhrError(event), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise.<?string>} A promise that will be resolved with the next line, or null if the stream is exhausted.
|
||||
*/
|
||||
nextLine(): Promise<string> {
|
||||
if (this._pendingDeferred !== null) {
|
||||
throw new Error("XhrStream only supports one pending unfulfilled read at a time.");
|
||||
}
|
||||
|
||||
const deferred = this._pendingDeferred = new DeferredPromise<string>();
|
||||
|
||||
this._tryResolveNextLine();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
private _onXhrProgress(): void {
|
||||
if (this._pendingDeferred === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._xhr.readyState === XMLHttpRequest.DONE) {
|
||||
/* Suppress resolving next line here. Let the "load" or "error" event handlers do it.
|
||||
*
|
||||
* This is required because a failed XHR fires the progress event with readyState === DONE before it fires the error event.
|
||||
* This would confuse _tryResolveNextLine() into thinking the request succeeded with no data if it was called here.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
this._tryResolveNextLine();
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
private _onXhrLoad(): void {
|
||||
if (this._pendingDeferred === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._tryResolveNextLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!ErrorEvent} event
|
||||
*/
|
||||
private _onXhrError(event: ErrorEvent): void {
|
||||
this._failedError = event;
|
||||
|
||||
if (this._pendingDeferred === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._tryResolveNextLine();
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
private _tryResolveNextLine(): void {
|
||||
if (this._failedError !== null) {
|
||||
this._pendingDeferred!.reject(this._failedError);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = this._xhr.responseText;
|
||||
|
||||
const nextNewLinePos = response.indexOf("\n", this._readTill);
|
||||
if (nextNewLinePos !== -1) {
|
||||
this._pendingDeferred!.resolve(response.substring(this._readTill, nextNewLinePos));
|
||||
this._readTill = nextNewLinePos + 1;
|
||||
this._pendingDeferred = null;
|
||||
}
|
||||
|
||||
else if (this._xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (this._failedError !== null) {
|
||||
this._pendingDeferred!.reject(this._failedError);
|
||||
}
|
||||
|
||||
// No more data. This is the last line.
|
||||
else if (this._readTill < response.length) {
|
||||
this._pendingDeferred!.resolve(response.substr(this._readTill));
|
||||
this._readTill = response.length;
|
||||
}
|
||||
else {
|
||||
this._pendingDeferred!.resolve(null);
|
||||
}
|
||||
|
||||
this._pendingDeferred = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link libjass.parser.Stream} that reads from a ReadableStream object.
|
||||
*
|
||||
* @param {!ReadableStream} stream
|
||||
* @param {string} encoding
|
||||
*/
|
||||
export class BrowserReadableStream implements Stream {
|
||||
private _reader: ReadableStreamReader;
|
||||
private _decoder: TextDecoder;
|
||||
private _buffer: string = "";
|
||||
private _pendingDeferred: DeferredPromise<string | null> | null = null;
|
||||
|
||||
constructor(stream: ReadableStream, encoding: string) {
|
||||
this._reader = stream.getReader();
|
||||
this._decoder = new global.TextDecoder!(encoding, { ignoreBOM: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {!Promise.<?string>} A promise that will be resolved with the next line, or null if the stream is exhausted.
|
||||
*/
|
||||
nextLine(): Promise<string> {
|
||||
if (this._pendingDeferred !== null) {
|
||||
throw new Error("BrowserReadableStream only supports one pending unfulfilled read at a time.");
|
||||
}
|
||||
|
||||
const deferred = this._pendingDeferred = new DeferredPromise<string>();
|
||||
|
||||
this._tryResolveNextLine();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
private _tryResolveNextLine(): void {
|
||||
const nextNewLinePos = this._buffer.indexOf("\n");
|
||||
if (nextNewLinePos !== -1) {
|
||||
this._pendingDeferred!.resolve(this._buffer.substr(0, nextNewLinePos));
|
||||
this._buffer = this._buffer.substr(nextNewLinePos + 1);
|
||||
this._pendingDeferred = null;
|
||||
}
|
||||
|
||||
else {
|
||||
this._reader.read().then(next => {
|
||||
const { value, done } = next;
|
||||
|
||||
if (!done) {
|
||||
this._buffer += this._decoder.decode(value, { stream: true });
|
||||
this._tryResolveNextLine();
|
||||
}
|
||||
else {
|
||||
// No more data.
|
||||
if (this._buffer.length === 0) {
|
||||
this._pendingDeferred!.resolve(null);
|
||||
}
|
||||
else {
|
||||
this._pendingDeferred!.resolve(this._buffer);
|
||||
this._buffer = "";
|
||||
}
|
||||
|
||||
this._pendingDeferred = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Attachment } from "../types/attachment";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
import { Set } from "../utility/set";
|
||||
|
||||
type DataReader = { dataView: DataView; position: number; };
|
||||
|
||||
enum DataType {
|
||||
Char,
|
||||
Uint16,
|
||||
Uint32,
|
||||
}
|
||||
|
||||
type StructMemberDefinition = { type: DataType; field: string; };
|
||||
|
||||
const fieldDecorators = new Map<DataType, (proto: any, field: string) => void>();
|
||||
|
||||
@struct
|
||||
class OffsetTable {
|
||||
/** @type {number} */ @field(DataType.Uint16) majorVersion: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) minorVersion: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) numTables: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) searchRange: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) entrySelector: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) rangeShift: number;
|
||||
|
||||
/** @type {function(!{ dataView: DataView, position: number }): OffsetTable} */
|
||||
static read: (reader: DataReader) => OffsetTable;
|
||||
}
|
||||
|
||||
@struct
|
||||
class TableRecord {
|
||||
/** @type {string} */ @field(DataType.Char) c1: string;
|
||||
/** @type {string} */ @field(DataType.Char) c2: string;
|
||||
/** @type {string} */ @field(DataType.Char) c3: string;
|
||||
/** @type {string} */ @field(DataType.Char) c4: string;
|
||||
/** @type {number} */ @field(DataType.Uint32) checksum: number;
|
||||
/** @type {number} */ @field(DataType.Uint32) offset: number;
|
||||
/** @type {number} */ @field(DataType.Uint32) length: number;
|
||||
|
||||
/** @type {function(!{ dataView: DataView, position: number }): TableRecord} */
|
||||
static read: (reader: DataReader) => TableRecord;
|
||||
}
|
||||
|
||||
@struct
|
||||
class NameTableHeader {
|
||||
/** @type {number} */ @field(DataType.Uint16) formatSelector: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) count: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) stringOffset: number;
|
||||
|
||||
/** @type {function(!{ dataView: DataView, position: number }): NameTableHeader} */
|
||||
static read: (reader: DataReader) => NameTableHeader;
|
||||
}
|
||||
|
||||
@struct
|
||||
class NameRecord {
|
||||
/** @type {number} */ @field(DataType.Uint16) platformId: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) encodingId: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) languageId: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) nameId: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) length: number;
|
||||
/** @type {number} */ @field(DataType.Uint16) offset: number;
|
||||
|
||||
/** @type {function(!{ dataView: DataView, position: number }): NameRecord} */
|
||||
static read: (reader: DataReader) => NameRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the font names from the given font attachment.
|
||||
*
|
||||
* @param {!libjass.Attachment} attachment
|
||||
* @return {!libjass.Set.<string>}
|
||||
*/
|
||||
export function getTtfNames(attachment: Attachment): Set<string> {
|
||||
const decoded = atob(attachment.contents);
|
||||
|
||||
const bytes = new Uint8Array(new ArrayBuffer(decoded.length));
|
||||
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
bytes[i] = decoded.charCodeAt(i);
|
||||
}
|
||||
|
||||
const reader = { dataView: new DataView(bytes.buffer), position: 0 };
|
||||
|
||||
const offsetTable = OffsetTable.read(reader);
|
||||
let nameTableRecord: TableRecord | null = null;
|
||||
for (let i = 0; i < offsetTable.numTables; i++) {
|
||||
const tableRecord = TableRecord.read(reader);
|
||||
if (tableRecord.c1 + tableRecord.c2 + tableRecord.c3 + tableRecord.c4 === "name") {
|
||||
nameTableRecord = tableRecord;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nameTableRecord === null) {
|
||||
throw new Error('Could not find "name" table record.');
|
||||
}
|
||||
|
||||
reader.position = nameTableRecord.offset;
|
||||
const nameTableHeader = NameTableHeader.read(reader);
|
||||
|
||||
const result = new Set<string>();
|
||||
for (let i = 0; i < nameTableHeader.count; i++) {
|
||||
const nameRecord = NameRecord.read(reader);
|
||||
|
||||
switch (nameRecord.nameId) {
|
||||
case 1:
|
||||
case 4:
|
||||
case 6:
|
||||
const recordOffset = nameTableRecord.offset + nameTableHeader.stringOffset + nameRecord.offset;
|
||||
const nameBytes = bytes.subarray(recordOffset, recordOffset + nameRecord.length);
|
||||
|
||||
switch (nameRecord.platformId) {
|
||||
case 1: {
|
||||
let name = "";
|
||||
|
||||
for (let j = 0; j < nameBytes.length; j++) {
|
||||
name += String.fromCharCode(nameBytes[j]);
|
||||
}
|
||||
|
||||
result.add(name);
|
||||
}
|
||||
break;
|
||||
|
||||
case 3: {
|
||||
let name = "";
|
||||
|
||||
for (let j = 0; j < nameBytes.length; j += 2) {
|
||||
name += String.fromCharCode((nameBytes[j] << 8) + nameBytes[j + 1]);
|
||||
}
|
||||
|
||||
result.add(name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!function(new(): T)} clazz
|
||||
* @return {!function(new(): T)}
|
||||
*/
|
||||
function struct<T>(clazz: { new (): T; read(reader: DataReader): T; }): { new (): T; read(reader: DataReader): T; } {
|
||||
const fields: StructMemberDefinition[] = (clazz as any).__fields;
|
||||
|
||||
clazz.read = (reader: DataReader) => {
|
||||
const result: any = new clazz();
|
||||
|
||||
for (const field of fields) {
|
||||
let value: any;
|
||||
switch (field.type) {
|
||||
case DataType.Char:
|
||||
value = String.fromCharCode(reader.dataView.getInt8(reader.position));
|
||||
reader.position += 1;
|
||||
break;
|
||||
|
||||
case DataType.Uint16:
|
||||
value = reader.dataView.getUint16(reader.position);
|
||||
reader.position += 2;
|
||||
break;
|
||||
|
||||
case DataType.Uint32:
|
||||
value = reader.dataView.getUint32(reader.position);
|
||||
reader.position += 4;
|
||||
break;
|
||||
}
|
||||
|
||||
result[field.field] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return clazz;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} type
|
||||
* @return {function(T, string)}
|
||||
*/
|
||||
function field<T>(type: DataType): (proto: T, field: string) => void {
|
||||
let existingDecorator = fieldDecorators.get(type);
|
||||
if (existingDecorator === undefined) {
|
||||
existingDecorator = (proto: T, field: string) => {
|
||||
const ctor: { __fields?: StructMemberDefinition[] } = proto.constructor;
|
||||
if (ctor.__fields === undefined) {
|
||||
ctor.__fields = [];
|
||||
}
|
||||
|
||||
ctor.__fields.push({ type, field });
|
||||
};
|
||||
|
||||
fieldDecorators.set(type, existingDecorator);
|
||||
}
|
||||
|
||||
return existingDecorator;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The base interface of the drawing instructions.
|
||||
*/
|
||||
export interface Instruction { }
|
||||
|
||||
/**
|
||||
* An instruction to move to a particular position.
|
||||
*
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
export class MoveInstruction implements Instruction {
|
||||
constructor(private _x: number, private _y: number) { }
|
||||
|
||||
/**
|
||||
* The X position of this move instruction.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get x(): number {
|
||||
return this._x;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Y position of this move instruction.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get y(): number {
|
||||
return this._y;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An instruction to draw a line to a particular position.
|
||||
*
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
export class LineInstruction implements Instruction {
|
||||
constructor(private _x: number, private _y: number) { }
|
||||
|
||||
/**
|
||||
* The X position of this line instruction.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get x(): number {
|
||||
return this._x;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Y position of this line instruction.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get y(): number {
|
||||
return this._y;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An instruction to draw a cubic bezier curve to a particular position, with two given control points.
|
||||
*
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {number} x2
|
||||
* @param {number} y2
|
||||
* @param {number} x3
|
||||
* @param {number} y3
|
||||
*/
|
||||
export class CubicBezierCurveInstruction implements Instruction {
|
||||
constructor(private _x1: number, private _y1: number, private _x2: number, private _y2: number, private _x3: number, private _y3: number) { }
|
||||
|
||||
/**
|
||||
* The X position of the first control point of this cubic bezier curve instruction.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get x1(): number {
|
||||
return this._x1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Y position of the first control point of this cubic bezier curve instruction.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get y1(): number {
|
||||
return this._y1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The X position of the second control point of this cubic bezier curve instruction.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get x2(): number {
|
||||
return this._x2;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Y position of the second control point of this cubic bezier curve instruction.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get y2(): number {
|
||||
return this._y2;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ending X position of this cubic bezier curve instruction.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get x3(): number {
|
||||
return this._x3;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ending Y position of this cubic bezier curve instruction.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get y3(): number {
|
||||
return this._y3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { debugMode } from "../../settings";
|
||||
|
||||
import { Clock, ClockEvent } from "./base";
|
||||
import { ManualClock } from "./manual";
|
||||
|
||||
/**
|
||||
* An implementation of {@link libjass.renderers.Clock} that automatically ticks and generates {@link libjass.renderers.ClockEvent}s according to the state of an external driver.
|
||||
*
|
||||
* For example, if you're using libjass to render subtitles on a canvas with your own video controls, these video controls will function as the driver to this AutoClock.
|
||||
* It would call {@link libjass.renderers.AutoClock.play}, {@link libjass.renderers.AutoClock.pause}, etc. when the user pressed the corresponding video controls.
|
||||
*
|
||||
* The difference from ManualClock is that AutoClock does not require the driver to call something like {@link libjass.renderers.ManualClock.tick}. Instead it keeps its
|
||||
* own time with a high-resolution requestAnimationFrame-based timer.
|
||||
*
|
||||
* If using libjass with a <video> element, consider using {@link libjass.renderers.VideoClock} that uses the video element as a driver.
|
||||
*
|
||||
* @param {function():number} getCurrentTime A callback that will be invoked to get the current time of the external driver.
|
||||
* @param {number} autoPauseAfter If two calls to getCurrentTime are more than autoPauseAfter milliseconds apart but return the same time, then the external driver will be
|
||||
* considered to have paused.
|
||||
*/
|
||||
export class AutoClock implements Clock {
|
||||
private _manualClock: ManualClock = new ManualClock();
|
||||
|
||||
private _nextAnimationFrameRequestId: number | null = null;
|
||||
|
||||
private _lastKnownExternalTime: number | null = null;
|
||||
private _lastKnownExternalTimeObtainedAt: number | null = null;
|
||||
|
||||
constructor(private _getCurrentTime: () => number, private _autoPauseAfter: number) { }
|
||||
|
||||
/**
|
||||
* Tells the clock to start generating ticks.
|
||||
*/
|
||||
play(): void {
|
||||
if (!this._manualClock.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._startTicking();
|
||||
|
||||
this._manualClock.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the clock to pause.
|
||||
*/
|
||||
pause(): void {
|
||||
if (!this._manualClock.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._nextAnimationFrameRequestId === null) {
|
||||
if (debugMode) {
|
||||
console.warn("AutoClock.pause: Abnormal state detected. AutoClock._nextAnimationFrameRequestId should not have been null.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._stopTicking();
|
||||
|
||||
this._manualClock.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the clock that the external driver is seeking.
|
||||
*/
|
||||
seeking(): void {
|
||||
this._manualClock.seek(this._getCurrentTime());
|
||||
}
|
||||
|
||||
// Clock members
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get currentTime(): number {
|
||||
return this._manualClock.currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get enabled(): boolean {
|
||||
return this._manualClock.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get paused(): boolean {
|
||||
return this._manualClock.paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rate of the clock - how fast the clock ticks compared to real time.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get rate(): number {
|
||||
return this._manualClock.rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the rate of the clock - how fast the clock ticks compared to real time.
|
||||
*
|
||||
* @param {number} rate The new rate of the clock.
|
||||
*/
|
||||
setRate(rate: number): void {
|
||||
this._manualClock.setRate(rate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the clock.
|
||||
*
|
||||
* @return {boolean} True if the clock is now enabled, false if it was already enabled.
|
||||
*/
|
||||
enable(): boolean {
|
||||
if (!this._manualClock.enable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._startTicking();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the clock.
|
||||
*
|
||||
* @return {boolean} True if the clock is now disabled, false if it was already disabled.
|
||||
*/
|
||||
disable(): boolean {
|
||||
if (!this._manualClock.disable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._stopTicking();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the clock.
|
||||
*/
|
||||
toggle(): void {
|
||||
if (this._manualClock.enabled) {
|
||||
this.disable();
|
||||
}
|
||||
else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the clock.
|
||||
*
|
||||
* @param {boolean} enabled If true, the clock is enabled, otherwise it's disabled.
|
||||
* @return {boolean} True if the clock is now in the given state, false if it was already in that state.
|
||||
*/
|
||||
setEnabled(enabled: boolean): boolean {
|
||||
if (enabled) {
|
||||
return this.enable();
|
||||
}
|
||||
else {
|
||||
return this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} type
|
||||
* @param {!Function} listener
|
||||
*/
|
||||
addEventListener(type: ClockEvent, listener: Function): void {
|
||||
this._manualClock.addEventListener(type, listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {number} timeStamp
|
||||
*/
|
||||
private _onTimerTick(timeStamp: number): void {
|
||||
if (!this._manualClock.enabled) {
|
||||
if (debugMode) {
|
||||
console.warn("AutoClock._onTimerTick: Called when disabled.");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = this._manualClock.currentTime;
|
||||
const currentExternalTime = this._getCurrentTime();
|
||||
|
||||
if (!this._manualClock.paused) {
|
||||
if (this._lastKnownExternalTime !== null && currentExternalTime === this._lastKnownExternalTime) {
|
||||
if (timeStamp - this._lastKnownExternalTimeObtainedAt > this._autoPauseAfter) {
|
||||
this._lastKnownExternalTimeObtainedAt = null;
|
||||
this._manualClock.seek(currentExternalTime);
|
||||
}
|
||||
else {
|
||||
this._manualClock.tick((timeStamp - this._lastKnownExternalTimeObtainedAt) / 1000 * this._manualClock.rate + this._lastKnownExternalTime);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this._lastKnownExternalTime = currentExternalTime;
|
||||
this._lastKnownExternalTimeObtainedAt = timeStamp;
|
||||
this._manualClock.tick(currentExternalTime);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (currentTime !== currentExternalTime) {
|
||||
this._lastKnownExternalTime = currentExternalTime;
|
||||
this._lastKnownExternalTimeObtainedAt = timeStamp;
|
||||
this._manualClock.tick(currentExternalTime);
|
||||
}
|
||||
}
|
||||
|
||||
this._nextAnimationFrameRequestId = requestAnimationFrame(timeStamp => this._onTimerTick(timeStamp));
|
||||
}
|
||||
|
||||
private _startTicking(): void {
|
||||
if (this._nextAnimationFrameRequestId === null) {
|
||||
this._nextAnimationFrameRequestId = requestAnimationFrame(timeStamp => this._onTimerTick(timeStamp));
|
||||
}
|
||||
}
|
||||
|
||||
private _stopTicking(): void {
|
||||
if (this._nextAnimationFrameRequestId !== null) {
|
||||
cancelAnimationFrame(this._nextAnimationFrameRequestId);
|
||||
this._nextAnimationFrameRequestId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Map } from "../../utility/map";
|
||||
|
||||
/**
|
||||
* A mixin class that represents an event source.
|
||||
*/
|
||||
export class EventSource<T> {
|
||||
/**
|
||||
* A map from event type to an array of all the listeners registered for that event type.
|
||||
*
|
||||
* @type {!Map.<T, !Array.<Function>>}
|
||||
*/
|
||||
_eventListeners: Map<T, Function[]>;
|
||||
|
||||
/**
|
||||
* Add a listener for the given event.
|
||||
*
|
||||
* @param {!T} type The type of event to attach the listener for
|
||||
* @param {!Function} listener The listener
|
||||
*/
|
||||
addEventListener(type: T, listener: Function): void {
|
||||
let listeners = this._eventListeners.get(type);
|
||||
|
||||
if (listeners === undefined) {
|
||||
this._eventListeners.set(type, listeners = []);
|
||||
}
|
||||
|
||||
listeners.push(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls all listeners registered for the given event type.
|
||||
*
|
||||
* @param {!T} type The type of event to dispatch
|
||||
* @param {!Array.<*>} args Arguments for the listeners of the event
|
||||
*/
|
||||
_dispatchEvent(type: T, args: Object[]): void {
|
||||
const listeners = this._eventListeners.get(type);
|
||||
if (listeners !== undefined) {
|
||||
for (const listener of listeners) {
|
||||
listener.apply(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of clock event.
|
||||
*/
|
||||
export enum ClockEvent {
|
||||
Play,
|
||||
Tick,
|
||||
Pause,
|
||||
Stop,
|
||||
RateChange,
|
||||
}
|
||||
|
||||
/**
|
||||
* The clock interface. A clock is used by a renderer as a source of {@link libjass.renderers.ClockEvent}s.
|
||||
*/
|
||||
export interface Clock {
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
currentTime: number;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
enabled: boolean;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
paused: boolean;
|
||||
|
||||
/**
|
||||
* Gets the rate of the clock - how fast the clock ticks compared to real time.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
rate: number;
|
||||
|
||||
/**
|
||||
* Enable the clock.
|
||||
*
|
||||
* @return {boolean} True if the clock is now enabled, false if it was already enabled.
|
||||
*/
|
||||
enable(): boolean;
|
||||
|
||||
/**
|
||||
* Disable the clock.
|
||||
*
|
||||
* @return {boolean} True if the clock is now disabled, false if it was already disabled.
|
||||
*/
|
||||
disable(): boolean;
|
||||
|
||||
/**
|
||||
* Toggle the clock.
|
||||
*/
|
||||
toggle(): void;
|
||||
|
||||
/**
|
||||
* Enable or disable the clock.
|
||||
*
|
||||
* @param {boolean} enabled If true, the clock is enabled, otherwise it's disabled.
|
||||
* @return {boolean} True if the clock is now in the given state, false if it was already in that state.
|
||||
*/
|
||||
setEnabled(enabled: boolean): boolean;
|
||||
|
||||
// EventSource members
|
||||
|
||||
/**
|
||||
* @param {number} type
|
||||
* @param {!Function} listener
|
||||
*/
|
||||
addEventListener(type: ClockEvent, listener: Function): void;
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { mixin } from "../../utility/mixin";
|
||||
import { Map } from "../../utility/map";
|
||||
|
||||
import { Clock, ClockEvent, EventSource } from "./base";
|
||||
|
||||
/**
|
||||
* An implementation of {@link libjass.renderers.Clock} that allows user script to manually trigger {@link libjass.renderers.ClockEvent}s.
|
||||
*/
|
||||
export class ManualClock implements Clock, EventSource<ClockEvent> {
|
||||
private _currentTime: number = -1;
|
||||
private _rate: number = 1;
|
||||
|
||||
private _enabled: boolean = true;
|
||||
private _paused: boolean = true;
|
||||
|
||||
/**
|
||||
* Trigger a {@link libjass.renderers.ClockEvent.Play}
|
||||
*/
|
||||
play(): void {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._paused = false;
|
||||
|
||||
this._dispatchEvent(ClockEvent.Play, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a {@link libjass.renderers.ClockEvent.Tick} with the given time.
|
||||
*
|
||||
* @param {number} currentTime
|
||||
*/
|
||||
tick(currentTime: number): void {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._currentTime === currentTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.play();
|
||||
|
||||
this._currentTime = currentTime;
|
||||
this._dispatchEvent(ClockEvent.Tick, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to the given time. Unlike {@link libjass.renderers.ManualClock.tick} this is used to represent a discontinuous jump, such as the user seeking
|
||||
* via the video element's position bar.
|
||||
*
|
||||
* @param {number} time
|
||||
*/
|
||||
seek(time: number): void {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pause();
|
||||
|
||||
if (this._currentTime === time) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop();
|
||||
|
||||
this.tick(time);
|
||||
|
||||
this.pause();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a {@link libjass.renderers.ClockEvent.Pause}
|
||||
*/
|
||||
pause(): void {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._paused = true;
|
||||
|
||||
this._dispatchEvent(ClockEvent.Pause, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a {@link libjass.renderers.ClockEvent.Stop}
|
||||
*/
|
||||
stop(): void {
|
||||
this._dispatchEvent(ClockEvent.Stop, []);
|
||||
}
|
||||
|
||||
// Clock members
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get currentTime(): number {
|
||||
return this._currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get enabled(): boolean {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get paused(): boolean {
|
||||
return this._paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rate of the clock - how fast the clock ticks compared to real time.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get rate(): number {
|
||||
return this._rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the rate of the clock - how fast the clock ticks compared to real time.
|
||||
*
|
||||
* @param {number} rate The new rate of the clock.
|
||||
*/
|
||||
setRate(rate: number): void {
|
||||
if (this._rate === rate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._rate = rate;
|
||||
|
||||
this._dispatchEvent(ClockEvent.RateChange, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the clock.
|
||||
*
|
||||
* @return {boolean} True if the clock is now enabled, false if it was already enabled.
|
||||
*/
|
||||
enable(): boolean {
|
||||
if (this._enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._enabled = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the clock.
|
||||
*
|
||||
* @return {boolean} True if the clock is now disabled, false if it was already disabled.
|
||||
*/
|
||||
disable(): boolean {
|
||||
if (!this._enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.pause();
|
||||
|
||||
this.stop();
|
||||
|
||||
this._enabled = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the clock.
|
||||
*/
|
||||
toggle(): void {
|
||||
if (this._enabled) {
|
||||
this.disable();
|
||||
}
|
||||
else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the clock.
|
||||
*
|
||||
* @param {boolean} enabled If true, the clock is enabled, otherwise it's disabled.
|
||||
* @return {boolean} True if the clock is now in the given state, false if it was already in that state.
|
||||
*/
|
||||
setEnabled(enabled: boolean): boolean {
|
||||
if (enabled) {
|
||||
return this.enable();
|
||||
}
|
||||
else {
|
||||
return this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
// EventSource members
|
||||
|
||||
/**
|
||||
* @type {!Map.<T, !Array.<Function>>}
|
||||
*/
|
||||
_eventListeners: Map<ClockEvent, Function[]> = new Map<ClockEvent, Function[]>();
|
||||
|
||||
/**
|
||||
* @type {function(number, !Function)}
|
||||
*/
|
||||
addEventListener: (type: ClockEvent, listener: Function) => void;
|
||||
|
||||
/**
|
||||
* @type {function(number, Array.<*>)}
|
||||
*/
|
||||
_dispatchEvent: (type: ClockEvent, args: Object[]) => void;
|
||||
}
|
||||
mixin(ManualClock, [EventSource]);
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AutoClock } from "./auto";
|
||||
import { Clock, ClockEvent } from "./base";
|
||||
|
||||
/**
|
||||
* An implementation of libjass.renderers.Clock that generates {@link libjass.renderers.ClockEvent}s according to the state of a <video> element.
|
||||
*
|
||||
* @param {!HTMLVideoElement} video
|
||||
*/
|
||||
export class VideoClock implements Clock {
|
||||
private _autoClock: AutoClock;
|
||||
|
||||
constructor(video: HTMLVideoElement) {
|
||||
this._autoClock = new AutoClock(() => video.currentTime, 100);
|
||||
video.addEventListener("playing", () => this._autoClock.play(), false);
|
||||
video.addEventListener("pause", () => this._autoClock.pause(), false);
|
||||
video.addEventListener("seeking", () => this._autoClock.seeking(), false);
|
||||
video.addEventListener("ratechange", () => this._autoClock.setRate(video.playbackRate), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get currentTime(): number {
|
||||
return this._autoClock.currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get enabled(): boolean {
|
||||
return this._autoClock.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get paused(): boolean {
|
||||
return this._autoClock.paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the rate of the clock - how fast the clock ticks compared to real time.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get rate(): number {
|
||||
return this._autoClock.rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the clock.
|
||||
*
|
||||
* @return {boolean} True if the clock is now enabled, false if it was already enabled.
|
||||
*/
|
||||
enable(): boolean {
|
||||
return this._autoClock.enable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the clock.
|
||||
*
|
||||
* @return {boolean} True if the clock is now disabled, false if it was already disabled.
|
||||
*/
|
||||
disable(): boolean {
|
||||
return this._autoClock.disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the clock.
|
||||
*/
|
||||
toggle(): void {
|
||||
if (this._autoClock.enabled) {
|
||||
this.disable();
|
||||
}
|
||||
else {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the clock.
|
||||
*
|
||||
* @param {boolean} enabled If true, the clock is enabled, otherwise it's disabled.
|
||||
* @return {boolean} True if the clock is now in the given state, false if it was already in that state.
|
||||
*/
|
||||
setEnabled(enabled: boolean): boolean {
|
||||
if (enabled) {
|
||||
return this.enable();
|
||||
}
|
||||
else {
|
||||
return this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} type
|
||||
* @param {!Function} listener
|
||||
*/
|
||||
addEventListener(type: ClockEvent, listener: Function): void {
|
||||
this._autoClock.addEventListener(type, listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { VideoClock } from "./clocks/video";
|
||||
import { RendererSettings } from "./settings";
|
||||
import { WebRenderer } from "./web/renderer";
|
||||
|
||||
import { ASS } from "../types/ass";
|
||||
|
||||
/**
|
||||
* A default renderer implementation.
|
||||
*
|
||||
* @param {!HTMLVideoElement} video
|
||||
* @param {!libjass.ASS} ass
|
||||
* @param {libjass.renderers.RendererSettings} settings
|
||||
*/
|
||||
export class DefaultRenderer extends WebRenderer {
|
||||
constructor(private _video: HTMLVideoElement, ass: ASS, settings?: RendererSettings) {
|
||||
super(ass, new VideoClock(_video), document.createElement("div"), settings);
|
||||
|
||||
this._video.parentElement.replaceChild(this.libjassSubsWrapper, this._video);
|
||||
this.libjassSubsWrapper.insertBefore(this._video, this.libjassSubsWrapper.firstElementChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the subtitles to the dimensions of the video element.
|
||||
*
|
||||
* This method accounts for letterboxing if the video element's size is not the same ratio as the video resolution.
|
||||
*/
|
||||
resize(): void {
|
||||
// Handle letterboxing around the video. If the width or height are greater than the video can be, then consider that dead space.
|
||||
|
||||
const videoWidth = this._video.videoWidth;
|
||||
const videoHeight = this._video.videoHeight;
|
||||
const videoOffsetWidth = this._video.offsetWidth;
|
||||
const videoOffsetHeight = this._video.offsetHeight;
|
||||
|
||||
const ratio = Math.min(videoOffsetWidth / videoWidth, videoOffsetHeight / videoHeight);
|
||||
const subsWrapperWidth = videoWidth * ratio;
|
||||
const subsWrapperHeight = videoHeight * ratio;
|
||||
const subsWrapperLeft = (videoOffsetWidth - subsWrapperWidth) / 2;
|
||||
const subsWrapperTop = (videoOffsetHeight - subsWrapperHeight) / 2;
|
||||
|
||||
super.resize(subsWrapperWidth, subsWrapperHeight, subsWrapperLeft, subsWrapperTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
resizeVideo(): void {
|
||||
console.warn("`DefaultRenderer.resizeVideo(width, height)` has been deprecated. Use `DefaultRenderer.resize()` instead.");
|
||||
this.resize();
|
||||
}
|
||||
|
||||
protected _ready(): void {
|
||||
this.resize();
|
||||
|
||||
super._ready();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { Clock, ClockEvent, EventSource } from "./clocks/base";
|
||||
export { AutoClock } from "./clocks/auto";
|
||||
export { ManualClock } from "./clocks/manual";
|
||||
export { VideoClock } from "./clocks/video";
|
||||
|
||||
export { DefaultRenderer } from "./default";
|
||||
export { NullRenderer } from "./null";
|
||||
export { WebRenderer } from "./web/renderer";
|
||||
export { RendererSettings } from "./settings";
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Clock, ClockEvent } from "./clocks/base";
|
||||
|
||||
import { RendererSettings } from "./settings";
|
||||
|
||||
import { debugMode, verboseMode } from "../settings";
|
||||
|
||||
import { ASS } from "../types/ass";
|
||||
import { Dialogue } from "../types/dialogue";
|
||||
|
||||
/**
|
||||
* A renderer implementation that doesn't output anything.
|
||||
*
|
||||
* @param {!libjass.ASS} ass
|
||||
* @param {!libjass.renderers.Clock} clock
|
||||
* @param {libjass.renderers.RendererSettings} settings
|
||||
*/
|
||||
export class NullRenderer {
|
||||
private static _lastRendererId = -1;
|
||||
|
||||
private _id: number;
|
||||
|
||||
private _settings: RendererSettings;
|
||||
|
||||
constructor(private _ass: ASS, private _clock: Clock, settings?: RendererSettings) {
|
||||
this._id = ++NullRenderer._lastRendererId;
|
||||
|
||||
this._settings = RendererSettings.from(settings);
|
||||
|
||||
this._clock.addEventListener(ClockEvent.Play, () => this._onClockPlay());
|
||||
this._clock.addEventListener(ClockEvent.Tick, () => this._onClockTick());
|
||||
this._clock.addEventListener(ClockEvent.Pause, () => this._onClockPause());
|
||||
this._clock.addEventListener(ClockEvent.Stop, () => this._onClockStop());
|
||||
this._clock.addEventListener(ClockEvent.RateChange, () => this._onClockRateChange());
|
||||
}
|
||||
|
||||
/**
|
||||
* The unique ID of this renderer. Auto-generated.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get id(): number {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {!libjass.ASS}
|
||||
*/
|
||||
get ass(): ASS {
|
||||
return this._ass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {!libjass.renderers.Clock}
|
||||
*/
|
||||
get clock(): Clock {
|
||||
return this._clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {!libjass.renderers.RendererSettings}
|
||||
*/
|
||||
get settings(): RendererSettings {
|
||||
return this._settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-render a dialogue. This is a no-op for this type.
|
||||
*
|
||||
* @param {!libjass.Dialogue} dialogue
|
||||
*/
|
||||
preRender(dialogue: Dialogue): void { }
|
||||
|
||||
/**
|
||||
* Draw a dialogue. This is a no-op for this type.
|
||||
*
|
||||
* @param {!libjass.Dialogue} dialogue
|
||||
*/
|
||||
draw(dialogue: Dialogue): void { }
|
||||
|
||||
/**
|
||||
* Enable the renderer.
|
||||
*
|
||||
* @return {boolean} True if the renderer is now enabled, false if it was already enabled.
|
||||
*/
|
||||
enable(): boolean {
|
||||
return this._clock.enable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the renderer.
|
||||
*
|
||||
* @return {boolean} True if the renderer is now disabled, false if it was already disabled.
|
||||
*/
|
||||
disable(): boolean {
|
||||
return this._clock.disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the renderer.
|
||||
*/
|
||||
toggle(): void {
|
||||
this._clock.toggle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable the renderer.
|
||||
*
|
||||
* @param {boolean} enabled If true, the renderer is enabled, otherwise it's disabled.
|
||||
* @return {boolean} True if the renderer is now in the given state, false if it was already in that state.
|
||||
*/
|
||||
setEnabled(enabled: boolean): boolean {
|
||||
return this._clock.setEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get enabled(): boolean {
|
||||
return this._clock.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when the clock is enabled, or starts playing, or is resumed from pause.
|
||||
*/
|
||||
protected _onClockPlay(): void {
|
||||
if (verboseMode) {
|
||||
console.log("NullRenderer._onClockPlay");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when the clock's current time changed. This might be a result of either regular playback or seeking.
|
||||
*/
|
||||
protected _onClockTick(): void {
|
||||
const currentTime = this._clock.currentTime;
|
||||
|
||||
if (verboseMode) {
|
||||
console.log(`NullRenderer._onClockTick: currentTime = ${ currentTime }`);
|
||||
}
|
||||
|
||||
for (const dialogue of this._ass.dialogues) {
|
||||
try {
|
||||
if (dialogue.end > currentTime) {
|
||||
if (dialogue.start <= currentTime) {
|
||||
// This dialogue is visible right now. Draw it.
|
||||
this.draw(dialogue);
|
||||
}
|
||||
else if (dialogue.start <= (currentTime + this._settings.preRenderTime)) {
|
||||
// This dialogue will be visible soon. Pre-render it.
|
||||
this.preRender(dialogue);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
if (debugMode) {
|
||||
console.error(`Rendering dialogue ${ dialogue.id } failed.`, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when the clock is paused.
|
||||
*/
|
||||
protected _onClockPause(): void {
|
||||
if (verboseMode) {
|
||||
console.log("NullRenderer._onClockPause");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when the clock is disabled.
|
||||
*/
|
||||
protected _onClockStop(): void {
|
||||
if (verboseMode) {
|
||||
console.log("NullRenderer._onClockStop");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs when the clock changes its rate.
|
||||
*/
|
||||
protected _onClockRateChange(): void {
|
||||
if (verboseMode) {
|
||||
console.log("NullRenderer._onClockRateChange");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
interface Array<T> {
|
||||
/**
|
||||
* Returns the elements of an array that meet the condition specified in a callback function.
|
||||
* @param callbackfn A function that accepts up to three arguments. The filter method calls the callbackfn function one time for each element in the array.
|
||||
* @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
|
||||
*/
|
||||
filter<S extends T>(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { debugMode } from "../settings";
|
||||
import { Map } from "../utility/map";
|
||||
|
||||
/**
|
||||
* Settings for the renderer.
|
||||
*/
|
||||
export class RendererSettings {
|
||||
/**
|
||||
* A map of font name to one or more URLs of that font. If provided, the fonts in this map are pre-loaded by the WebRenderer when it's created.
|
||||
*
|
||||
* The key of each entry of the map is the font name used in the ASS script. There are three choices for the value:
|
||||
*
|
||||
* - A single string that you would use for the src attribute of a @font-face rule. Eg: `'url("/fonts.foo.ttf"), url("/fonts/foo-fallback.ttf"), local("Arial.ttf")'`
|
||||
*
|
||||
* - An array of the individual sources that you would use for the src attribute of a @font-face rule. Eg: `['url("/fonts.foo.ttf")', 'url("/fonts/foo-fallback.ttf")', 'local("Arial")']`
|
||||
*
|
||||
* - An array of URLs. Eg: `["/fonts.foo.ttf", "/fonts/foo-fallback.ttf"]`
|
||||
*
|
||||
* Only the first and second forms allow you to use local fonts. The third form only allows you to use remote fonts.
|
||||
*
|
||||
* If you have a <style> or <link> element on the page containing @font-face rules, you can use the {@link libjass.renderers.RendererSettings.makeFontMapFromStyleElement}
|
||||
* convenience method to create a font map.
|
||||
*
|
||||
* Defaults to null.
|
||||
*
|
||||
* @type {Map.<string, (string|!Array.<string>)>}
|
||||
*/
|
||||
fontMap: Map<string, string | string[]> | null;
|
||||
|
||||
/**
|
||||
* Subtitles will be pre-rendered for this amount of time (seconds).
|
||||
*
|
||||
* Defaults to 5.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
preRenderTime: number;
|
||||
|
||||
/**
|
||||
* Subtitle outlines will be rendered in full detail. When false, the value of blur is used to draw less outlines for better performance and (hopefully) similar output.
|
||||
*
|
||||
* Defaults to false.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
preciseOutlines: boolean;
|
||||
|
||||
/**
|
||||
* Outlines and blur are implemented using SVG filters by default. When false, they will be rendered using alternative means.
|
||||
*
|
||||
* IE 11 and below do not support SVG filters on HTML elements so this should be set to false there. See http://caniuse.com/svg-html for details.
|
||||
*
|
||||
* Defaults to true.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
enableSvg: boolean;
|
||||
|
||||
/**
|
||||
* Comma-separated list of fonts to be used when font specified in ASS Styles not loaded.
|
||||
*
|
||||
* The value should be a valid CSS font-family property (i.e. comma-separated and individual names in quotes if necessary). Use empty string to disable fallback.
|
||||
*
|
||||
* Defaults to 'Arial, Helvetica, sans-serif, "Segoe UI Symbol"'.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
fallbackFonts: string;
|
||||
|
||||
/**
|
||||
* If true, attached TTF fonts in the ASS script will be used. The font is loaded as a data: URI. Requires ES6 typed arrays (ArrayBuffer, DataView, Uint8Array, etc).
|
||||
*
|
||||
* The font is naively parsed to extract the strings that will be used as the font family. Do not use this option with untrusted fonts or scripts.
|
||||
*
|
||||
* Defaults to false.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
useAttachedFonts: boolean;
|
||||
|
||||
/**
|
||||
* A convenience method to create a font map from a <style> or <link> element that contains @font-face rules. There should be one @font-face rule for each font name, mapping to a font file URL.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* @font-face {
|
||||
* font-family: "Helvetica";
|
||||
* src: url("/fonts/helvetica.ttf"), local("Arial");
|
||||
* }
|
||||
*
|
||||
* More complicated @font-face syntax like format() or multi-line src are not supported.
|
||||
*
|
||||
* @param {!LinkStyle} linkStyle
|
||||
* @return {!Map.<string, string>}
|
||||
*/
|
||||
static makeFontMapFromStyleElement(linkStyle: LinkStyle): Map<string, string> {
|
||||
const fontMap = new Map<string, string>();
|
||||
|
||||
const styleSheet = linkStyle.sheet as CSSStyleSheet;
|
||||
for (let i = 0; i < styleSheet.cssRules.length; i++) {
|
||||
const rule = styleSheet.cssRules[i];
|
||||
|
||||
if (isFontFaceRule(rule)) {
|
||||
const name = rule.style.getPropertyValue("font-family").match(/^["']?(.*?)["']?$/)![1];
|
||||
|
||||
let src = rule.style.getPropertyValue("src");
|
||||
if (!src) {
|
||||
src = rule.cssText.split("\n")
|
||||
.map(line => line.match(/src:\s*([^;]+?)\s*;/))
|
||||
.filter((matches): matches is RegExpMatchArray => matches !== null)
|
||||
.map(matches => matches[1])[0];
|
||||
}
|
||||
|
||||
fontMap.set(name, src);
|
||||
}
|
||||
}
|
||||
|
||||
return fontMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an arbitrary object into a {@link libjass.renderers.RendererSettings} object.
|
||||
*
|
||||
* @param {*} object
|
||||
* @return {!libjass.renderers.RendererSettings}
|
||||
*/
|
||||
static from(object?: any): RendererSettings {
|
||||
if (object === undefined || object === null) {
|
||||
object = {};
|
||||
}
|
||||
|
||||
const {
|
||||
fontMap = null,
|
||||
preRenderTime = 5,
|
||||
preciseOutlines = false,
|
||||
enableSvg = testSupportsSvg(),
|
||||
fallbackFonts = 'Arial, Helvetica, sans-serif, "Segoe UI Symbol"',
|
||||
useAttachedFonts = false,
|
||||
} = object as RendererSettings;
|
||||
|
||||
const result = new RendererSettings();
|
||||
result.fontMap = fontMap;
|
||||
result.preRenderTime = preRenderTime;
|
||||
result.preciseOutlines = preciseOutlines;
|
||||
result.enableSvg = enableSvg;
|
||||
result.fallbackFonts = fallbackFonts;
|
||||
result.useAttachedFonts = useAttachedFonts;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!CSSRule} rule
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isFontFaceRule(rule: CSSRule): rule is CSSFontFaceRule {
|
||||
return rule.type === CSSRule.FONT_FACE_RULE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this environment may support SVG filter effects. May return false positives.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
function testSupportsSvg(): boolean {
|
||||
if (debugMode) {
|
||||
console.log("Testing whether SVG filter effects are supported.");
|
||||
}
|
||||
|
||||
if (typeof document === "undefined") {
|
||||
if (debugMode) {
|
||||
console.log("This doesn't look like a browser. Assuming it doesn't support SVG filter effects.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const morphologyFilter = document.createElementNS("http://www.w3.org/2000/svg", "feMorphology");
|
||||
|
||||
// https://connect.microsoft.com/IE/feedback/details/2375800
|
||||
try {
|
||||
morphologyFilter.radiusX.baseVal = 1;
|
||||
}
|
||||
catch (ex) {
|
||||
if (debugMode) {
|
||||
if (ex instanceof DOMException) {
|
||||
const domException = ex as DOMException;
|
||||
if (domException.code === DOMException.NO_MODIFICATION_ALLOWED_ERR) {
|
||||
console.log("Setting SVGFEMorphologyElement.radiusX.baseVal threw NoModificationAllowedError. This browser doesn't support SVG DOM correctly.");
|
||||
}
|
||||
else {
|
||||
console.log(`Setting SVGFEMorphologyElement.radiusX.baseVal threw unexpected DOMException code ${ domException.code }. This browser doesn't support SVG DOM correctly.`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log(`Setting SVGFEMorphologyElement.radiusX.baseVal threw unexpected exception ${ ex }. This browser doesn't support SVG SVG DOM correctly.`);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://connect.microsoft.com/IE/feedback/details/2375757
|
||||
morphologyFilter.setAttribute("radius", "1");
|
||||
if (morphologyFilter.cloneNode().getAttribute("radius") !== "1") {
|
||||
if (debugMode) {
|
||||
console.log("SVGFEMorphologyElement's radius attribute was corrupted when cloned. This browser doesn't support SVG DOM correctly.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (debugMode) {
|
||||
console.log("This browser may support SVG filter effects.");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Keyframe } from "./keyframe";
|
||||
|
||||
import { NullRenderer } from "../null";
|
||||
|
||||
import { Map } from "../../utility/map";
|
||||
|
||||
/**
|
||||
* This class represents a collection of animations. Each animation contains one or more keyframes.
|
||||
* The collection can then be converted to a CSS3 representation.
|
||||
*
|
||||
* @param {!libjass.renderers.NullRenderer} renderer The renderer that this collection is associated with
|
||||
* @param {!HTMLStyleElement} style A <style> element to insert the animation rules into
|
||||
*/
|
||||
export class AnimationCollection {
|
||||
private static _nextId: number = 0;
|
||||
|
||||
private _id: string;
|
||||
private _rate: number;
|
||||
|
||||
private _animationStyle: string = "";
|
||||
private _animationDelays: Map<string, number> = new Map<string, number>();
|
||||
private _numAnimations: number = 0;
|
||||
|
||||
constructor(renderer: NullRenderer, private _style: HTMLStyleElement) {
|
||||
this._id = `${ renderer.id }-${ AnimationCollection._nextId++ }`;
|
||||
this._rate = renderer.clock.rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* This string should be set as the "animation" CSS property of the target element.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
get animationStyle(): string {
|
||||
return this._animationStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* This array should be used to set the "animation-delay" CSS property of the target element.
|
||||
*
|
||||
* @type {!Array.<number>}
|
||||
*/
|
||||
get animationDelays(): Map<string, number> {
|
||||
return this._animationDelays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an animation to this collection. The given keyframes together make one animation.
|
||||
*
|
||||
* @param {string} timingFunction One of the acceptable values for the "animation-timing-function" CSS property
|
||||
* @param {!Array.<!libjass.renderers.Keyframe>} keyframes
|
||||
*/
|
||||
add(timingFunction: string, keyframes: Keyframe[]): void {
|
||||
let start: number | null = null;
|
||||
let end: number | null = null;
|
||||
|
||||
for (const keyframe of keyframes) {
|
||||
if (start === null) {
|
||||
start = keyframe.time;
|
||||
}
|
||||
|
||||
end = keyframe.time;
|
||||
}
|
||||
|
||||
if (start === null || end === null) {
|
||||
throw new Error("Atleast one keyframe must be provided.");
|
||||
}
|
||||
|
||||
let ruleCssText = "";
|
||||
|
||||
for (const keyframe of keyframes) {
|
||||
ruleCssText +=
|
||||
` ${ (100 * ((end - start === 0) ? 1 : ((keyframe.time - start) / (end - start)))).toFixed(3) }% {
|
||||
`;
|
||||
|
||||
keyframe.properties.forEach((value, name) => {
|
||||
ruleCssText +=
|
||||
` ${ name }: ${ value };
|
||||
`;
|
||||
});
|
||||
|
||||
ruleCssText +=
|
||||
` }
|
||||
`;
|
||||
}
|
||||
|
||||
const animationName = `animation-${ this._id }-${ this._numAnimations++ }`;
|
||||
|
||||
this._style.appendChild(document.createTextNode(
|
||||
`@-webkit-keyframes ${ animationName } {
|
||||
${ ruleCssText }
|
||||
}`));
|
||||
|
||||
this._style.appendChild(document.createTextNode(
|
||||
`@keyframes ${ animationName } {
|
||||
${ ruleCssText }
|
||||
}`));
|
||||
|
||||
if (this._animationStyle !== "") {
|
||||
this._animationStyle += ",";
|
||||
}
|
||||
|
||||
this._animationStyle += `${ animationName } ${ ((end - start) / this._rate).toFixed(3) }s ${ timingFunction }`;
|
||||
this._animationDelays.set(animationName, start);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as parts from "../../parts";
|
||||
|
||||
/**
|
||||
* This class represents an ASS drawing - a set of drawing instructions between {\p} tags.
|
||||
*
|
||||
* @param {number} outputScaleX
|
||||
* @param {number} outputScaleY
|
||||
*/
|
||||
export class DrawingStyles {
|
||||
private _scale: number = 1;
|
||||
private _baselineOffset: number = 0;
|
||||
|
||||
constructor(private _outputScaleX: number, private _outputScaleY: number) { }
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
set scale(value: number) {
|
||||
this._scale = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
set baselineOffset(value: number) {
|
||||
this._baselineOffset = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this drawing to an <svg> element.
|
||||
*
|
||||
* @param {!libjass.parts.DrawingInstructions} drawingInstructions
|
||||
* @param {!libjass.parts.Color} fillColor
|
||||
* @return {!SVGSVGElement}
|
||||
*/
|
||||
toSVG(drawingInstructions: parts.DrawingInstructions, fillColor: parts.Color): SVGSVGElement {
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("version", "1.1");
|
||||
|
||||
if (drawingInstructions.instructions.length === 0) {
|
||||
return svg;
|
||||
}
|
||||
|
||||
const scaleFactor = Math.pow(2, this._scale - 1);
|
||||
const scaleX = this._outputScaleX / scaleFactor;
|
||||
const scaleY = this._outputScaleY / scaleFactor;
|
||||
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
let pathString = "";
|
||||
|
||||
let bboxMinX = Infinity;
|
||||
let bboxMaxX = -Infinity;
|
||||
let bboxMinY = Infinity;
|
||||
let bboxMaxY = -Infinity;
|
||||
|
||||
for (const instruction of drawingInstructions.instructions) {
|
||||
if (instruction instanceof parts.drawing.MoveInstruction) {
|
||||
pathString += ` M ${ instruction.x.toFixed(3) } ${ (instruction.y + this._baselineOffset).toFixed(3) }`;
|
||||
|
||||
bboxMinX = Math.min(bboxMinX, instruction.x);
|
||||
bboxMaxX = Math.max(bboxMaxX, instruction.x);
|
||||
bboxMinY = Math.min(bboxMinY, instruction.y + this._baselineOffset);
|
||||
bboxMaxY = Math.max(bboxMaxY, instruction.y + this._baselineOffset);
|
||||
}
|
||||
else if (instruction instanceof parts.drawing.LineInstruction) {
|
||||
pathString += ` L ${ instruction.x.toFixed(3) } ${ (instruction.y + this._baselineOffset).toFixed(3) }`;
|
||||
|
||||
bboxMinX = Math.min(bboxMinX, instruction.x);
|
||||
bboxMaxX = Math.max(bboxMaxX, instruction.x);
|
||||
bboxMinY = Math.min(bboxMinY, instruction.y + this._baselineOffset);
|
||||
bboxMaxY = Math.max(bboxMaxY, instruction.y + this._baselineOffset);
|
||||
}
|
||||
else if (instruction instanceof parts.drawing.CubicBezierCurveInstruction) {
|
||||
pathString += ` C ${ instruction.x1.toFixed(3) } ${ (instruction.y1 + this._baselineOffset).toFixed(3) } ${ instruction.x2.toFixed(3) } ${ (instruction.y2 + this._baselineOffset).toFixed(3) } ${ instruction.x3.toFixed(3) } ${ (instruction.y3 + this._baselineOffset).toFixed(3) }`;
|
||||
|
||||
bboxMinX = Math.min(bboxMinX, instruction.x1, instruction.x2, instruction.x3);
|
||||
bboxMaxX = Math.max(bboxMaxX, instruction.x1, instruction.x2, instruction.x3);
|
||||
bboxMinY = Math.min(bboxMinY, instruction.y1 + this._baselineOffset, instruction.y2 + this._baselineOffset, instruction.y3 + this._baselineOffset);
|
||||
bboxMaxY = Math.max(bboxMaxY, instruction.y1 + this._baselineOffset, instruction.y2 + this._baselineOffset, instruction.y3 + this._baselineOffset);
|
||||
}
|
||||
}
|
||||
|
||||
bboxMinX *= scaleX;
|
||||
bboxMaxX *= scaleX;
|
||||
bboxMinY *= scaleY;
|
||||
bboxMaxY *= scaleY;
|
||||
|
||||
const bboxWidth = bboxMaxX - bboxMinX;
|
||||
const bboxHeight = bboxMaxY - bboxMinY;
|
||||
|
||||
svg.width.baseVal.valueAsString = `${ bboxWidth.toFixed(3) }px`;
|
||||
svg.height.baseVal.valueAsString = `${ bboxHeight.toFixed(3) }px`;
|
||||
|
||||
// svg.viewBox.baseVal is null in atleast FF. See https://bugzilla.mozilla.org/show_bug.cgi?id=888307 which justifies it with SVG 1.2 spec.
|
||||
svg.setAttribute("viewBox", `${ bboxMinX } ${ bboxMinY } ${ bboxWidth } ${ bboxHeight }`);
|
||||
|
||||
svg.style.position = "relative";
|
||||
svg.style.left = `${ bboxMinX.toFixed(3) }px`;
|
||||
svg.style.top = `${ bboxMinY.toFixed(3) }px`;
|
||||
|
||||
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
svg.appendChild(g);
|
||||
g.setAttribute("transform", `scale(${ scaleX.toFixed(3) } ${ scaleY.toFixed(3) })`);
|
||||
|
||||
g.appendChild(path);
|
||||
path.setAttribute("d", pathString);
|
||||
path.setAttribute("fill", fillColor.toString());
|
||||
|
||||
return svg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Map } from "../../utility/map";
|
||||
import { Promise } from "../../utility/promise";
|
||||
|
||||
/**
|
||||
* @param {string} fontFamily
|
||||
* @param {number} fontSize
|
||||
* @param {string} fallbackFonts
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
*/
|
||||
function prepareFontSizeElement(fontFamily: string, fontSize: number, fallbackFonts: string, fontSizeElement: HTMLDivElement): void {
|
||||
let fonts = `"${ fontFamily }"`;
|
||||
if (fallbackFonts !== "") {
|
||||
fonts += `, ${ fallbackFonts }`;
|
||||
}
|
||||
|
||||
fontSizeElement.style.fontFamily = fonts;
|
||||
fontSizeElement.style.fontSize = `${ fontSize }px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fontFamily
|
||||
* @param {number} fontSize
|
||||
* @param {string} fallbackFonts
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
* @return {!Promise.<number>}
|
||||
*/
|
||||
function lineHeightForFontSize(fontFamily: string, fontSize: number, fallbackFonts: string, fontSizeElement: HTMLDivElement): Promise<number> {
|
||||
prepareFontSizeElement(fontFamily, fontSize, fallbackFonts, fontSizeElement);
|
||||
|
||||
return new Promise(resolve => setTimeout(() => resolve(fontSizeElement.offsetHeight), 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fontFamily
|
||||
* @param {number} fontSize
|
||||
* @param {string} fallbackFonts
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
* @return {number}
|
||||
*/
|
||||
function lineHeightForFontSizeSync(fontFamily: string, fontSize: number, fallbackFonts: string, fontSizeElement: HTMLDivElement): number {
|
||||
prepareFontSizeElement(fontFamily, fontSize, fallbackFonts, fontSizeElement);
|
||||
|
||||
return fontSizeElement.offsetHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} lowerLineHeight
|
||||
* @param {number} upperLineHeight
|
||||
* @return {[number, number]}
|
||||
*/
|
||||
function fontMetricsFromLineHeights(lowerLineHeight: number, upperLineHeight: number): [number, number] {
|
||||
return [lowerLineHeight, (360 - 180) / (upperLineHeight - lowerLineHeight)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates font metrics for the given font family.
|
||||
*
|
||||
* @param {string} fontFamily
|
||||
* @param {string} fallbackFonts
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
* @return {!Promise.<number>}
|
||||
*/
|
||||
export function calculateFontMetrics(fontFamily: string, fallbackFonts: string, fontSizeElement: HTMLDivElement): Promise<[number, number]> {
|
||||
return lineHeightForFontSize(fontFamily, 180, fallbackFonts, fontSizeElement).then(lowerLineHeight =>
|
||||
lineHeightForFontSize(fontFamily, 360, fallbackFonts, fontSizeElement).then(upperLineHeight =>
|
||||
fontMetricsFromLineHeights(lowerLineHeight, upperLineHeight)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} lineHeight
|
||||
* @param {number} lowerLineHeight
|
||||
* @param {number} factor
|
||||
* @return {number}
|
||||
*/
|
||||
function fontSizeFromMetrics(lineHeight: number, lowerLineHeight: number, factor: number): number {
|
||||
return 180 + (lineHeight - lowerLineHeight) * factor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses linear interpolation to calculate the CSS font size that would give the specified line height for the specified font family.
|
||||
*
|
||||
* WARNING: If fontMetricsCache doesn't already contain a cached value for this font family, and it is not a font already installed on the user's device, then this function
|
||||
* may return wrong values. To avoid this, make sure to preload the font using the {@link libjass.renderers.RendererSettings.fontMap} property when constructing the renderer.
|
||||
*
|
||||
* @param {string} fontFamily
|
||||
* @param {number} lineHeight
|
||||
* @param {string} fallbackFonts
|
||||
* @param {!HTMLDivElement} fontSizeElement
|
||||
* @param {!Map.<string, [number, number]>} fontMetricsCache
|
||||
* @return {number}
|
||||
*/
|
||||
export function fontSizeForLineHeight(fontFamily: string, lineHeight: number, fallbackFonts: string, fontSizeElement: HTMLDivElement, fontMetricsCache: Map<string, [number, number]>): number {
|
||||
let existingMetrics = fontMetricsCache.get(fontFamily);
|
||||
if (existingMetrics === undefined) {
|
||||
const lowerLineHeight = lineHeightForFontSizeSync(fontFamily, 180, fallbackFonts, fontSizeElement);
|
||||
const upperLineHeight = lineHeightForFontSizeSync(fontFamily, 360, fallbackFonts, fontSizeElement);
|
||||
fontMetricsCache.set(fontFamily, existingMetrics = fontMetricsFromLineHeights(lowerLineHeight, upperLineHeight));
|
||||
}
|
||||
|
||||
const [lowerLineHeight, factor] = existingMetrics;
|
||||
return fontSizeFromMetrics(lineHeight, lowerLineHeight, factor);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Map } from "../../utility/map";
|
||||
|
||||
/**
|
||||
* This class represents a single keyframe. It has a list of CSS properties (names and values) associated with a point in time. Multiple keyframes make up an animation.
|
||||
*
|
||||
* @param {number} time
|
||||
* @param {!Map.<string, string>} properties
|
||||
*/
|
||||
export class Keyframe {
|
||||
constructor(private _time: number, private _properties: Map<string, string>) { }
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get time(): number {
|
||||
return this._time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {!Map.<string, string>}
|
||||
*/
|
||||
get properties(): Map<string, string> {
|
||||
return this._properties;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
interface Node {
|
||||
cloneNode(deep?: boolean): this;
|
||||
}
|
||||
|
||||
interface SVGFEComponentTransferElement {
|
||||
appendChild(newChild: SVGFEFuncAElement): SVGFEFuncAElement;
|
||||
appendChild(newChild: SVGFEFuncBElement): SVGFEFuncBElement;
|
||||
appendChild(newChild: SVGFEFuncGElement): SVGFEFuncGElement;
|
||||
appendChild(newChild: SVGFEFuncRElement): SVGFEFuncRElement;
|
||||
}
|
||||
|
||||
interface SVGFEMergeElement {
|
||||
appendChild(newChild: SVGFEMergeNodeElement): SVGFEMergeNodeElement;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Map } from "./utility/map";
|
||||
|
||||
const classes = new Map<number, Function & { fromJSON?: (obj: any) => any }>();
|
||||
|
||||
/**
|
||||
* Registers a class as a serializable type.
|
||||
*
|
||||
* @param {function(new:*)} clazz
|
||||
*/
|
||||
export function registerClass(clazz: Function & { fromJSON?: (obj: any) => any }): void {
|
||||
clazz.prototype._classTag = classes.size;
|
||||
classes.set(clazz.prototype._classTag, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the given object.
|
||||
*
|
||||
* @param {*} obj
|
||||
* @return {string}
|
||||
*/
|
||||
export function serialize(obj: any): string {
|
||||
return JSON.stringify(obj, (/* ujs:unreferenced */ key: string, value: any) => {
|
||||
if (value && (value._classTag !== undefined) && !Object.prototype.hasOwnProperty.call(value, "_classTag")) {
|
||||
// Copy the _classTag from this object's prototype to itself, so that it will be serialized.
|
||||
value._classTag = value._classTag;
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @return {*}
|
||||
*/
|
||||
export function deserialize(str: string): any {
|
||||
return JSON.parse(str, (/* ujs:unreferenced */ key: string, value: any) => {
|
||||
if (value && (value._classTag !== undefined)) {
|
||||
const clazz = classes.get(value._classTag);
|
||||
if (clazz === undefined) {
|
||||
throw new Error(`Unknown class of tag ${ value._classTag } cannot be deserialized.`);
|
||||
}
|
||||
|
||||
if (typeof clazz.fromJSON === "function") {
|
||||
value = clazz.fromJSON(value);
|
||||
}
|
||||
else {
|
||||
const hydratedValue = Object.create(clazz.prototype);
|
||||
for (const key of Object.keys(value)) {
|
||||
hydratedValue[key] = value[key];
|
||||
}
|
||||
value = hydratedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Debug mode. When true, libjass logs some debug messages.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
export let debugMode: boolean = false;
|
||||
|
||||
/**
|
||||
* Verbose debug mode. When true, libjass logs some more debug messages. This setting is independent of {@link libjass.debugMode}
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
export let verboseMode: boolean = false;
|
||||
|
||||
/**
|
||||
* Sets the debug mode.
|
||||
*
|
||||
* @param {boolean} value
|
||||
*/
|
||||
export function setDebugMode(value: boolean): void {
|
||||
debugMode = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the verbose debug mode.
|
||||
*
|
||||
* @param {boolean} value
|
||||
*/
|
||||
export function setVerboseMode(value: boolean): void {
|
||||
verboseMode = value;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["es5", "dom"],
|
||||
|
||||
"experimentalDecorators": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": false,
|
||||
"strictNullChecks": true,
|
||||
|
||||
"target": "es5",
|
||||
"module": "amd",
|
||||
"moduleResolution": "node",
|
||||
"outFile": "../lib/libjass.js",
|
||||
"noImplicitUseStrict": true,
|
||||
"sourceMap": true,
|
||||
"inlineSources": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Attachment } from "./attachment";
|
||||
import { Dialogue } from "./dialogue";
|
||||
import { Style } from "./style";
|
||||
import { ScriptProperties } from "./script-properties";
|
||||
|
||||
import { Format } from "./misc";
|
||||
|
||||
import { debugMode, verboseMode } from "../settings";
|
||||
|
||||
import * as parser from "../parser";
|
||||
import { parseLineIntoTypedTemplate } from "../parser/misc";
|
||||
import { ReadableStream, TextDecoderConstructor } from "../parser/streams";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
import { Promise } from "../utility/promise";
|
||||
|
||||
import { registerClass as serializable } from "../serialization";
|
||||
|
||||
declare const global: {
|
||||
fetch?(url: string): Promise<{ body: ReadableStream; ok?: boolean; status?: number; }>;
|
||||
ReadableStream?: Function & { prototype: ReadableStream; };
|
||||
TextDecoder?: TextDecoderConstructor;
|
||||
};
|
||||
|
||||
/**
|
||||
* This class represents an ASS script. It contains the {@link libjass.ScriptProperties}, an array of {@link libjass.Style}s, and an array of {@link libjass.Dialogue}s.
|
||||
*/
|
||||
@serializable
|
||||
export class ASS {
|
||||
private _properties: ScriptProperties = new ScriptProperties();
|
||||
private _styles: Map<string, Style> = new Map<string, Style>();
|
||||
private _dialogues: Dialogue[] = [];
|
||||
private _attachments: Attachment[] = [];
|
||||
|
||||
private _stylesFormatSpecifier: string[] | null = null;
|
||||
private _dialoguesFormatSpecifier: string[] | null = null;
|
||||
|
||||
/**
|
||||
* The properties of this script.
|
||||
*
|
||||
* @type {!libjass.ScriptProperties}
|
||||
*/
|
||||
get properties(): ScriptProperties {
|
||||
return this._properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* The styles in this script.
|
||||
*
|
||||
* @type {!Map.<string, !libjass.Style>}
|
||||
*/
|
||||
get styles(): Map<string, Style> {
|
||||
return this._styles;
|
||||
}
|
||||
|
||||
/**
|
||||
* The dialogues in this script.
|
||||
*
|
||||
* @type {!Array.<!libjass.Dialogue>}
|
||||
*/
|
||||
get dialogues(): Dialogue[] {
|
||||
return this._dialogues;
|
||||
}
|
||||
|
||||
/**
|
||||
* The attachments of this script.
|
||||
*
|
||||
* @type {!Array.<!libjass.Attachment>}
|
||||
*/
|
||||
get attachments(): Attachment[] {
|
||||
return this._attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* The format specifier for the styles section.
|
||||
*
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
get stylesFormatSpecifier(): string[] | null {
|
||||
return this._stylesFormatSpecifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* The format specifier for the styles section.
|
||||
*
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
get dialoguesFormatSpecifier(): string[] | null {
|
||||
return this._dialoguesFormatSpecifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* The format specifier for the events section.
|
||||
*
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
set stylesFormatSpecifier(value: string[] | null) {
|
||||
this._stylesFormatSpecifier = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The format specifier for the events section.
|
||||
*
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
set dialoguesFormatSpecifier(value: string[] | null) {
|
||||
this._dialoguesFormatSpecifier = value;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
// Deprecated constructor argument
|
||||
if (arguments.length === 1) {
|
||||
throw new Error("Constructor `new ASS(rawASS)` has been deprecated. Use `ASS.fromString(rawASS)` instead.");
|
||||
}
|
||||
|
||||
this._styles.set("Default", new Style(new Map([["Name", "Default"]])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a style to this ASS script.
|
||||
*
|
||||
* @param {string} line The line from the script that contains the new style.
|
||||
*/
|
||||
addStyle(line: string): void {
|
||||
if (this._stylesFormatSpecifier === null) {
|
||||
throw new Error("stylesFormatSpecifier is not set.");
|
||||
}
|
||||
|
||||
const styleLine = parseLineIntoTypedTemplate(line, this._stylesFormatSpecifier);
|
||||
if (styleLine === null || styleLine.type !== "Style") {
|
||||
return;
|
||||
}
|
||||
|
||||
const styleTemplate = styleLine.template;
|
||||
|
||||
if (verboseMode) {
|
||||
let repr = "";
|
||||
styleTemplate.forEach((value, key) => repr += `${ key } = ${ value }, `);
|
||||
console.log(`Read style: ${ repr }`);
|
||||
}
|
||||
|
||||
// Create the dialogue and add it to the dialogues array
|
||||
const style = new Style(styleTemplate);
|
||||
this._styles.set(style.name, style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an event to this ASS script.
|
||||
*
|
||||
* @param {string} line The line from the script that contains the new event.
|
||||
*/
|
||||
addEvent(line: string): void {
|
||||
if (this._dialoguesFormatSpecifier === null) {
|
||||
throw new Error("dialoguesFormatSpecifier is not set.");
|
||||
}
|
||||
|
||||
const dialogueLine = parseLineIntoTypedTemplate(line, this._dialoguesFormatSpecifier);
|
||||
if (dialogueLine === null || dialogueLine.type !== "Dialogue") {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogueTemplate = dialogueLine.template;
|
||||
|
||||
if (verboseMode) {
|
||||
let repr = "";
|
||||
dialogueTemplate.forEach((value, key) => repr += `${ key } = ${ value }, `);
|
||||
console.log(`Read dialogue: ${ repr }`);
|
||||
}
|
||||
|
||||
// Create the dialogue and add it to the dialogues array
|
||||
this.dialogues.push(new Dialogue(dialogueTemplate, this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an attachment to this ASS script.
|
||||
*
|
||||
* @param {!libjass.Attachment} attachment
|
||||
*/
|
||||
addAttachment(attachment: Attachment): void {
|
||||
this._attachments.push(attachment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom JSON serialization for ASS objects.
|
||||
*
|
||||
* @return {!*}
|
||||
*/
|
||||
toJSON(): any {
|
||||
const result = Object.create(null);
|
||||
|
||||
result._properties = this._properties;
|
||||
|
||||
result._styles = Object.create(null);
|
||||
this._styles.forEach((style, name) => result._styles[name] = style);
|
||||
|
||||
result._dialogues = this._dialogues;
|
||||
result._attachments = this._attachments;
|
||||
result._stylesFormatSpecifier = this._stylesFormatSpecifier;
|
||||
result._dialoguesFormatSpecifier = this._dialoguesFormatSpecifier;
|
||||
|
||||
result._classTag = (ASS.prototype as any)._classTag;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ASS object from the raw text of an ASS script.
|
||||
*
|
||||
* @param {string} raw The raw text of the script.
|
||||
* @param {(number|string)=0} type The type of the script. One of the {@link libjass.Format} constants, or one of the strings "ass" and "srt".
|
||||
* @return {!Promise.<!libjass.ASS>}
|
||||
*/
|
||||
static fromString(raw: string, type: Format | "ass" | "srt" = Format.ASS): Promise<ASS> {
|
||||
return ASS.fromStream(new parser.StringStream(raw), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ASS object from the given {@link libjass.parser.Stream}.
|
||||
*
|
||||
* @param {!libjass.parser.Stream} stream The stream to parse the script from
|
||||
* @param {(number|string)=0} type The type of the script. One of the {@link libjass.Format} constants, or one of the strings "ass" and "srt".
|
||||
* @return {!Promise.<!libjass.ASS>} A promise that will be resolved with the ASS object when it has been fully parsed
|
||||
*/
|
||||
static fromStream(stream: parser.Stream, type: Format | "ass" | "srt" = Format.ASS): Promise<ASS> {
|
||||
switch (type) {
|
||||
case Format.ASS:
|
||||
case "ass":
|
||||
return new parser.StreamParser(stream).ass;
|
||||
case Format.SRT:
|
||||
case "srt":
|
||||
return new parser.SrtStreamParser(stream).ass;
|
||||
default:
|
||||
throw new Error(`Invalid value of type: ${ type }`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ASS object from the given URL.
|
||||
*
|
||||
* @param {string} url The URL of the script.
|
||||
* @param {(number|string)=0} type The type of the script. One of the {@link libjass.Format} constants, or one of the strings "ass" and "srt".
|
||||
* @return {!Promise.<!libjass.ASS>} A promise that will be resolved with the ASS object when it has been fully parsed
|
||||
*/
|
||||
static fromUrl(url: string, type: Format | "ass" | "srt" = Format.ASS): Promise<ASS> {
|
||||
let fetchPromise: Promise<ASS>;
|
||||
|
||||
if (
|
||||
typeof global.fetch === "function" &&
|
||||
typeof global.ReadableStream === "function" && typeof global.ReadableStream.prototype.getReader === "function" &&
|
||||
typeof global.TextDecoder === "function"
|
||||
) {
|
||||
fetchPromise = global.fetch(url).then(response => {
|
||||
if (response.ok === false || (response.ok === undefined && (response.status < 200 || response.status > 299))) {
|
||||
throw new Error(`HTTP request for ${ url } failed with status code ${ response.status }`);
|
||||
}
|
||||
|
||||
return ASS.fromReadableStream(response.body, "utf-8", type);
|
||||
});
|
||||
}
|
||||
else {
|
||||
fetchPromise = Promise.reject<ASS>(new Error("Not supported."));
|
||||
}
|
||||
|
||||
return fetchPromise.catch(reason => {
|
||||
if (debugMode) {
|
||||
console.log("fetch() failed, falling back to XHR: %o", reason);
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
const result = ASS.fromStream(new parser.XhrStream(xhr), type);
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ASS object from the given ReadableStream.
|
||||
*
|
||||
* @param {!ReadableStream} stream
|
||||
* @param {string="utf-8"} encoding
|
||||
* @param {(number|string)=0} type The type of the script. One of the {@link libjass.Format} constants, or one of the strings "ass" and "srt".
|
||||
* @return {!Promise.<!libjass.ASS>} A promise that will be resolved with the ASS object when it has been fully parsed
|
||||
*/
|
||||
static fromReadableStream(stream: ReadableStream, encoding: string = "utf-8", type: Format | "ass" | "srt" = Format.ASS): Promise<ASS> {
|
||||
return ASS.fromStream(new parser.BrowserReadableStream(stream, encoding), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom deserialization for ASS objects.
|
||||
*
|
||||
* @param {!*} obj
|
||||
* @return {!libjass.ASS}
|
||||
*/
|
||||
static fromJSON(obj: any): ASS {
|
||||
const result: ASS = Object.create(ASS.prototype);
|
||||
|
||||
result._properties = obj._properties;
|
||||
|
||||
result._styles = new Map<string, Style>();
|
||||
for (const name of Object.keys(obj._styles)) {
|
||||
const style = obj._styles[name];
|
||||
result._styles.set(name, style);
|
||||
}
|
||||
|
||||
result._dialogues = obj._dialogues;
|
||||
result._attachments = obj._attachments;
|
||||
result._stylesFormatSpecifier = obj._stylesFormatSpecifier;
|
||||
result._dialoguesFormatSpecifier = obj._dialoguesFormatSpecifier;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { registerClass as serializable } from "../serialization";
|
||||
|
||||
/**
|
||||
* The type of an attachment.
|
||||
*/
|
||||
export enum AttachmentType {
|
||||
Font,
|
||||
Graphic,
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents an attachment in a {@link libjass.ASS} script.
|
||||
*
|
||||
* @param {string} filename The filename of this attachment.
|
||||
* @param {number} type The type of this attachment.
|
||||
*/
|
||||
@serializable
|
||||
export class Attachment {
|
||||
private _contents: string = "";
|
||||
|
||||
constructor(private _filename: string, private _type: AttachmentType) { }
|
||||
|
||||
/**
|
||||
* The filename of this attachment.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get filename(): string {
|
||||
return this._filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of this attachment.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get type(): AttachmentType {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
/**
|
||||
* The contents of this attachment in base64 encoding.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get contents(): string {
|
||||
return this._contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* The contents of this attachment in base64 encoding.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
set contents(value: string) {
|
||||
this._contents = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ASS } from "./ass";
|
||||
import { Style } from "./style";
|
||||
|
||||
import { valueOrDefault } from "./misc";
|
||||
|
||||
import { parse } from "../parser/parse";
|
||||
|
||||
import * as parts from "../parts";
|
||||
|
||||
import { registerClass as serializable } from "../serialization";
|
||||
|
||||
import { debugMode } from "../settings";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
|
||||
/**
|
||||
* This class represents a dialogue in a {@link libjass.ASS} script.
|
||||
*
|
||||
* @param {!Map.<string, string>} template The template object that contains the dialogue's properties. It is a map of the string values read from the ASS file.
|
||||
* @param {string} template["Style"] The name of the default style of this dialogue
|
||||
* @param {string} template["Start"] The start time
|
||||
* @param {string} template["End"] The end time
|
||||
* @param {string} template["Layer"] The layer number
|
||||
* @param {string} template["Text"] The text of this dialogue
|
||||
* @param {!libjass.ASS} ass The ASS object to which this dialogue belongs
|
||||
*/
|
||||
@serializable
|
||||
export class Dialogue {
|
||||
private static _lastDialogueId = -1;
|
||||
|
||||
private _id: number;
|
||||
|
||||
private _style: Style;
|
||||
|
||||
private _start: number;
|
||||
private _end: number;
|
||||
|
||||
private _layer: number;
|
||||
private _alignment: number;
|
||||
|
||||
private _rawPartsString: string;
|
||||
private _parts: parts.Part[] | null = null;
|
||||
|
||||
private _containsTransformTag: boolean = false;
|
||||
|
||||
constructor(template: Map<string, string>, ass: ASS) {
|
||||
{
|
||||
const normalizedTemplate = new Map<string, string>();
|
||||
template.forEach((value, key) => {
|
||||
normalizedTemplate.set(key.toLowerCase(), value);
|
||||
});
|
||||
template = normalizedTemplate;
|
||||
}
|
||||
|
||||
this._id = ++Dialogue._lastDialogueId;
|
||||
|
||||
let styleName = template.get("style");
|
||||
if (typeof styleName === "string") {
|
||||
styleName = styleName.replace(/^\*+/, "");
|
||||
if (styleName.match(/^Default$/i) !== null) {
|
||||
styleName = "Default";
|
||||
}
|
||||
}
|
||||
|
||||
let style = (styleName !== undefined) ? ass.styles.get(styleName) : undefined;
|
||||
if (style === undefined) {
|
||||
if (debugMode) {
|
||||
console.warn(`Unrecognized style ${ styleName }. Falling back to "Default"`);
|
||||
}
|
||||
|
||||
style = ass.styles.get("Default");
|
||||
if (style === undefined) {
|
||||
throw new Error(`Unrecognized style ${ styleName }. Could not fall back to "Default" style since it doesn't exist.`);
|
||||
}
|
||||
}
|
||||
this._style = style;
|
||||
|
||||
const start = template.get("start");
|
||||
if (typeof start !== "string") {
|
||||
throw new Error(`Dialogue start time ${ start } is not a string.`);
|
||||
}
|
||||
this._start = toTime(start);
|
||||
|
||||
const end = template.get("end");
|
||||
if (typeof end !== "string") {
|
||||
throw new Error(`Dialogue end time ${ end } is not a string.`);
|
||||
}
|
||||
this._end = toTime(end);
|
||||
|
||||
this._layer = Math.max(valueOrDefault(template, "layer", parseInt, value => !isNaN(value), "0"), 0);
|
||||
|
||||
const text = template.get("text");
|
||||
if (typeof text !== "string") {
|
||||
throw new Error(`Dialogue text ${ text } is not a string.`);
|
||||
}
|
||||
this._rawPartsString = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unique ID of this dialogue. Auto-generated.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get id(): number {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The start time of this dialogue.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get start(): number {
|
||||
return this._start;
|
||||
}
|
||||
|
||||
/**
|
||||
* The end time of this dialogue.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get end(): number {
|
||||
return this._end;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default style of this dialogue.
|
||||
*
|
||||
* @type {!libjass.Style}
|
||||
*/
|
||||
get style(): Style {
|
||||
return this._style;
|
||||
}
|
||||
|
||||
/**
|
||||
* The alignment number of this dialogue.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get alignment(): number {
|
||||
if (this._parts === null) {
|
||||
this._parsePartsString();
|
||||
}
|
||||
|
||||
return this._alignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* The layer number of this dialogue.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get layer(): number {
|
||||
return this._layer;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link libjass.parts} of this dialogue.
|
||||
*
|
||||
* @type {!Array.<!libjass.parts.Part>}
|
||||
*/
|
||||
get parts(): parts.Part[] {
|
||||
if (this._parts === null) {
|
||||
this._parsePartsString();
|
||||
}
|
||||
|
||||
return this._parts!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience getter for whether this dialogue contains a {\t} tag.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
get containsTransformTag(): boolean {
|
||||
if (this._parts === null) {
|
||||
this._parsePartsString();
|
||||
}
|
||||
|
||||
return this._containsTransformTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string} A simple representation of this dialogue's properties and parts.
|
||||
*/
|
||||
toString(): string {
|
||||
return `#${ this._id } [${ this._start.toFixed(3) }-${ this._end.toFixed(3) }] ${ (this._parts !== null) ? this._parts.join(", ") : this._rawPartsString }`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses this dialogue's parts from the raw parts string.
|
||||
*/
|
||||
private _parsePartsString(): void {
|
||||
this._parts = parse(this._rawPartsString, "dialogueParts") as parts.Part[];
|
||||
|
||||
this._alignment = this._style.alignment;
|
||||
|
||||
this._parts.forEach((part, index) => {
|
||||
if (part instanceof parts.Alignment) {
|
||||
this._alignment = part.value;
|
||||
}
|
||||
else if (part instanceof parts.Move) {
|
||||
if (part.t1 === null || part.t2 === null) {
|
||||
this._parts![index] =
|
||||
new parts.Move(
|
||||
part.x1, part.y1, part.x2, part.y2,
|
||||
0, this._end - this._start
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (part instanceof parts.Transform) {
|
||||
if (part.start === null || part.end === null || part.accel === null) {
|
||||
this._parts![index] =
|
||||
new parts.Transform(
|
||||
(part.start === null) ? 0 : part.start,
|
||||
(part.end === null) ? (this._end - this._start) : part.end,
|
||||
(part.accel === null) ? 1 : part.accel,
|
||||
part.tags
|
||||
);
|
||||
}
|
||||
|
||||
this._containsTransformTag = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (debugMode) {
|
||||
const possiblyIncorrectParses = this._parts.filter(part => part instanceof parts.Comment && part.value.indexOf("\\") !== -1);
|
||||
if (possiblyIncorrectParses.length > 0) {
|
||||
console.warn(
|
||||
`Possible incorrect parse:
|
||||
${ this._rawPartsString }
|
||||
was parsed as
|
||||
${ this.toString() }
|
||||
The possibly incorrect parses are:
|
||||
${ possiblyIncorrectParses.join("\n") }`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this string into the number of seconds it represents. This string must be in the form of hh:mm:ss.MMM
|
||||
*
|
||||
* @param {string} str
|
||||
* @return {number}
|
||||
*/
|
||||
function toTime(str: string): number {
|
||||
return str.split(":").reduce<number>((previousValue, currentValue) => previousValue * 60 + parseFloat(currentValue), 0);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
|
||||
/**
|
||||
* The format of the string passed to {@link libjass.ASS.fromString}
|
||||
*/
|
||||
export enum Format {
|
||||
ASS,
|
||||
SRT,
|
||||
}
|
||||
|
||||
/**
|
||||
* The wrapping style defined in the {@link libjass.ScriptProperties}
|
||||
*/
|
||||
export enum WrappingStyle {
|
||||
SmartWrappingWithWiderTopLine = 0,
|
||||
SmartWrappingWithWiderBottomLine = 3,
|
||||
EndOfLineWrapping = 1,
|
||||
NoLineWrapping = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* The border style defined in the {@link libjass.Style} properties.
|
||||
*/
|
||||
export enum BorderStyle {
|
||||
Outline = 1,
|
||||
OpaqueBox = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* A property.
|
||||
*/
|
||||
export interface Property {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A template object with a particular type.
|
||||
*/
|
||||
export interface TypedTemplate {
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* @type {!Map.<string, string>}
|
||||
*/
|
||||
template: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Map.<string, string>} template
|
||||
* @param {string} key
|
||||
* @param {function(string):T} converter
|
||||
* @param {?function(T):boolean} validator
|
||||
* @param {T} defaultValue
|
||||
* @return {T}
|
||||
*/
|
||||
export function valueOrDefault<T>(template: Map<string, string>, key: string, converter: (str: string) => T, validator: ((value: T) => boolean) | null, defaultValue: string): T {
|
||||
const value = template.get(key);
|
||||
if (value === undefined) {
|
||||
return converter(defaultValue);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = converter(value);
|
||||
|
||||
if (validator !== null && !validator(result)) {
|
||||
throw new Error("Validation failed.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (ex) {
|
||||
throw new Error(`Property ${ key } has invalid value ${ value } - ${ ex.stack }`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { registerClass as serializable } from "../serialization";
|
||||
|
||||
import { WrappingStyle } from "./misc";
|
||||
|
||||
/**
|
||||
* This class represents the properties of a {@link libjass.ASS} script.
|
||||
*/
|
||||
@serializable
|
||||
export class ScriptProperties {
|
||||
private _resolutionX: number;
|
||||
private _resolutionY: number;
|
||||
private _wrappingStyle: WrappingStyle;
|
||||
private _scaleBorderAndShadow: boolean;
|
||||
|
||||
/**
|
||||
* The horizontal script resolution.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get resolutionX(): number {
|
||||
return this._resolutionX;
|
||||
}
|
||||
|
||||
/**
|
||||
* The horizontal script resolution.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
set resolutionX(value: number) {
|
||||
this._resolutionX = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The vertical script resolution.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get resolutionY(): number {
|
||||
return this._resolutionY;
|
||||
}
|
||||
|
||||
/**
|
||||
* The vertical script resolution.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
set resolutionY(value: number) {
|
||||
this._resolutionY = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The wrap style. One of the {@link libjass.WrappingStyle} constants.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get wrappingStyle(): WrappingStyle {
|
||||
return this._wrappingStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* The wrap style. One of the {@link libjass.WrappingStyle} constants.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
set wrappingStyle(value: WrappingStyle) {
|
||||
this._wrappingStyle = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to scale outline widths and shadow depths from script resolution to video resolution or not. If true, widths and depths are scaled.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
get scaleBorderAndShadow(): boolean {
|
||||
return this._scaleBorderAndShadow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to scale outline widths and shadow depths from script resolution to video resolution or not. If true, widths and depths are scaled.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
set scaleBorderAndShadow(value: boolean) {
|
||||
this._scaleBorderAndShadow = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { valueOrDefault, BorderStyle } from "./misc";
|
||||
|
||||
import { parse } from "../parser/parse";
|
||||
|
||||
import { Color } from "../parts";
|
||||
|
||||
import { registerClass as serializable } from "../serialization";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
|
||||
/**
|
||||
* This class represents a single global style declaration in a {@link libjass.ASS} script. The styles can be obtained via the {@link libjass.ASS.styles} property.
|
||||
*
|
||||
* @param {!Map.<string, string>} template The template object that contains the style's properties. It is a map of the string values read from the ASS file.
|
||||
* @param {string} template["Name"] The name of the style
|
||||
* @param {string} template["Italic"] -1 if the style is italicized
|
||||
* @param {string} template["Bold"] -1 if the style is bold
|
||||
* @param {string} template["Underline"] -1 if the style is underlined
|
||||
* @param {string} template["StrikeOut"] -1 if the style is struck-through
|
||||
* @param {string} template["Fontname"] The name of the font
|
||||
* @param {string} template["Fontsize"] The size of the font
|
||||
* @param {string} template["ScaleX"] The horizontal scaling of the font
|
||||
* @param {string} template["ScaleY"] The vertical scaling of the font
|
||||
* @param {string} template["Spacing"] The letter spacing of the font
|
||||
* @param {string} template["PrimaryColour"] The primary color
|
||||
* @param {string} template["OutlineColour"] The outline color
|
||||
* @param {string} template["BackColour"] The shadow color
|
||||
* @param {string} template["Outline"] The outline thickness
|
||||
* @param {string} template["Shadow"] The shadow depth
|
||||
* @param {string} template["Alignment"] The alignment number
|
||||
* @param {string} template["MarginL"] The left margin
|
||||
* @param {string} template["MarginR"] The right margin
|
||||
* @param {string} template["MarginV"] The vertical margin
|
||||
*/
|
||||
@serializable
|
||||
export class Style {
|
||||
private _name: string;
|
||||
|
||||
private _italic: boolean;
|
||||
private _bold: boolean;
|
||||
private _underline: boolean;
|
||||
private _strikeThrough: boolean;
|
||||
|
||||
private _fontName: string;
|
||||
private _fontSize: number;
|
||||
|
||||
private _fontScaleX: number;
|
||||
private _fontScaleY: number;
|
||||
|
||||
private _letterSpacing: number;
|
||||
|
||||
private _rotationZ: number;
|
||||
|
||||
private _primaryColor: Color;
|
||||
private _secondaryColor: Color;
|
||||
private _outlineColor: Color;
|
||||
private _shadowColor: Color;
|
||||
|
||||
private _outlineThickness: number;
|
||||
private _borderStyle: BorderStyle;
|
||||
|
||||
private _shadowDepth: number;
|
||||
|
||||
private _alignment: number;
|
||||
|
||||
private _marginLeft: number;
|
||||
private _marginRight: number;
|
||||
private _marginVertical: number;
|
||||
|
||||
constructor(template: Map<string, string>) {
|
||||
{
|
||||
const normalizedTemplate = new Map<string, string>();
|
||||
template.forEach((value, key) => {
|
||||
normalizedTemplate.set(key.toLowerCase(), value);
|
||||
});
|
||||
template = normalizedTemplate;
|
||||
}
|
||||
|
||||
const name = template.get("name");
|
||||
if (typeof name !== "string") {
|
||||
throw new Error(`Style name ${ name } is not a string.`);
|
||||
}
|
||||
this._name = name.replace(/^\*+/, "");
|
||||
|
||||
this._italic = !!valueOrDefault(template, "italic", parseFloat, value => !isNaN(value), "0");
|
||||
this._bold = !!valueOrDefault(template, "bold", parseFloat, value => !isNaN(value), "0");
|
||||
this._underline = !!valueOrDefault(template, "underline", parseFloat, value => !isNaN(value), "0");
|
||||
this._strikeThrough = !!valueOrDefault(template, "strikeout", parseFloat, value => !isNaN(value), "0");
|
||||
|
||||
this._fontName = valueOrDefault(template, "fontname", str => str, value => value.constructor === String, "sans-serif");
|
||||
this._fontSize = valueOrDefault(template, "fontsize", parseFloat, value => !isNaN(value), "18");
|
||||
|
||||
this._fontScaleX = valueOrDefault(template, "scalex", parseFloat, value => value >= 0, "100") / 100;
|
||||
this._fontScaleY = valueOrDefault(template, "scaley", parseFloat, value => value >= 0, "100") / 100;
|
||||
|
||||
this._letterSpacing = valueOrDefault(template, "spacing", parseFloat, value => value >= 0, "0");
|
||||
|
||||
this._rotationZ = valueOrDefault(template, "angle", parseFloat, value => !isNaN(value), "0");
|
||||
|
||||
this._primaryColor = valueOrDefault(template, "primarycolour", str => parse(str, "colorWithAlpha") as Color, null, "&H00FFFFFF");
|
||||
this._secondaryColor = valueOrDefault(template, "secondarycolour", str => parse(str, "colorWithAlpha") as Color, null, "&H00FFFF00");
|
||||
this._outlineColor = valueOrDefault(template, "outlinecolour", str => parse(str, "colorWithAlpha") as Color, null, "&H00000000");
|
||||
this._shadowColor = valueOrDefault(template, "backcolour", str => parse(str, "colorWithAlpha") as Color, null, "&H80000000");
|
||||
|
||||
this._outlineThickness = valueOrDefault(template, "outline", parseFloat, value => value >= 0, "2");
|
||||
this._borderStyle = valueOrDefault(template, "borderstyle", parseInt, value => (BorderStyle as any)[(BorderStyle as any)[value]] === value, "1");
|
||||
|
||||
this._shadowDepth = valueOrDefault(template, "shadow", parseFloat, value => value >= 0, "3");
|
||||
|
||||
this._alignment = valueOrDefault(template, "alignment", parseInt, value => value >= 1 && value <= 9, "2");
|
||||
|
||||
this._marginLeft = valueOrDefault(template, "marginl", parseFloat, value => !isNaN(value), "20");
|
||||
this._marginRight = valueOrDefault(template, "marginr", parseFloat, value => !isNaN(value), "20");
|
||||
this._marginVertical = valueOrDefault(template, "marginv", parseFloat, value => !isNaN(value), "20");
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of this style.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
get name(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this style is italicized or not.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
get italic(): boolean {
|
||||
return this._italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this style is bold or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
get bold(): boolean {
|
||||
return this._bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this style is underlined or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
get underline(): boolean {
|
||||
return this._underline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this style is struck-through or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
get strikeThrough(): boolean {
|
||||
return this._strikeThrough;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of this style's font.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
get fontName(): string {
|
||||
return this._fontName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of this style's font.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get fontSize(): number {
|
||||
return this._fontSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* The horizontal scaling of this style's font.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get fontScaleX(): number {
|
||||
return this._fontScaleX;
|
||||
}
|
||||
|
||||
/**
|
||||
* The vertical scaling of this style's font.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get fontScaleY(): number {
|
||||
return this._fontScaleY;
|
||||
}
|
||||
|
||||
/**
|
||||
* The letter spacing scaling of this style's font.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get letterSpacing(): number {
|
||||
return this._letterSpacing;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default Z-rotation of this style.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get rotationZ(): number {
|
||||
return this._rotationZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* The color of this style's font.
|
||||
*
|
||||
* @type {!libjass.parts.Color}
|
||||
*/
|
||||
get primaryColor(): Color {
|
||||
return this._primaryColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The alternate color of this style's font, used in karaoke.
|
||||
*
|
||||
* @type {!libjass.parts.Color}
|
||||
*/
|
||||
get secondaryColor(): Color {
|
||||
return this._secondaryColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The color of this style's outline.
|
||||
*
|
||||
* @type {!libjass.parts.Color}
|
||||
*/
|
||||
get outlineColor(): Color {
|
||||
return this._outlineColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The color of this style's shadow.
|
||||
*
|
||||
* @type {!libjass.parts.Color}
|
||||
*/
|
||||
get shadowColor(): Color {
|
||||
return this._shadowColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The thickness of this style's outline.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get outlineThickness(): number {
|
||||
return this._outlineThickness;
|
||||
}
|
||||
|
||||
/**
|
||||
* The border style of this style.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get borderStyle(): BorderStyle {
|
||||
return this._borderStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* The depth of this style's shadow.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get shadowDepth(): number {
|
||||
return this._shadowDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* The alignment of dialogues of this style.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get alignment(): number {
|
||||
return this._alignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* The left margin of dialogues of this style.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get marginLeft(): number {
|
||||
return this._marginLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* The right margin of dialogues of this style.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get marginRight(): number {
|
||||
return this._marginRight;
|
||||
}
|
||||
|
||||
/**
|
||||
* The vertical margin of dialogues of this style.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get marginVertical(): number {
|
||||
return this._marginVertical;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface Map<K, V> {
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {?V}
|
||||
*/
|
||||
get(key: K): V | undefined;
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {boolean}
|
||||
*/
|
||||
has(key: K): boolean;
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @param {V} value
|
||||
* @return {libjass.Map.<K, V>} This map
|
||||
*/
|
||||
set(key: K, value: V): this;
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {boolean} true if the key was present before being deleted, false otherwise
|
||||
*/
|
||||
delete(key: K): boolean;
|
||||
|
||||
/**
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* @param {function(V, K, libjass.Map.<K, V>)} callbackfn A function that is called with each key and value in the map.
|
||||
* @param {*} thisArg
|
||||
*/
|
||||
forEach(callbackfn: (value: V, index: K, map: this) => void, thisArg?: any): void;
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map implementation for browsers that don't support it. Only supports keys which are of Number or String type, or which have a property called "id".
|
||||
*
|
||||
* Keys and values are stored as properties of an object, with property names derived from the key type.
|
||||
*
|
||||
* @param {!Array.<!Array.<*>>=} iterable Only an array of elements (where each element is a 2-tuple of key and value) is supported.
|
||||
*/
|
||||
class SimpleMap<K, V> implements Map<K, V> {
|
||||
private _keys: { [key: string]: K };
|
||||
private _values: { [key: string]: V };
|
||||
private _size: number;
|
||||
|
||||
constructor(iterable?: [K, V][]) {
|
||||
this.clear();
|
||||
|
||||
if (iterable === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(iterable)) {
|
||||
throw new Error("Non-array iterables are not supported by the SimpleMap constructor.");
|
||||
}
|
||||
|
||||
for (const element of iterable) {
|
||||
this.set(element[0], element[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {?V}
|
||||
*/
|
||||
get(key: K): V | undefined {
|
||||
const property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._values[property];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {boolean}
|
||||
*/
|
||||
has(key: K): boolean {
|
||||
const property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return property in this._keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @param {V} value
|
||||
* @return {libjass.Map.<K, V>} This map
|
||||
*/
|
||||
set(key: K, value: V): this {
|
||||
const property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
throw new Error("This Map implementation only supports Number and String keys, or keys with an id property.");
|
||||
}
|
||||
|
||||
if (!(property in this._keys)) {
|
||||
this._size++;
|
||||
}
|
||||
|
||||
this._keys[property] = key;
|
||||
this._values[property] = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @return {boolean} true if the key was present before being deleted, false otherwise
|
||||
*/
|
||||
delete(key: K): boolean {
|
||||
const property = this._keyToProperty(key);
|
||||
|
||||
if (property === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = property in this._keys;
|
||||
|
||||
if (result) {
|
||||
delete this._keys[property];
|
||||
delete this._values[property];
|
||||
this._size--;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
clear(): void {
|
||||
this._keys = Object.create(null);
|
||||
this._values = Object.create(null);
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(V, K, libjass.Map.<K, V>)} callbackfn A function that is called with each key and value in the map.
|
||||
* @param {*} thisArg
|
||||
*/
|
||||
forEach(callbackfn: (value: V, index: K, map: this) => void, thisArg?: any): void {
|
||||
for (const property of Object.keys(this._keys)) {
|
||||
callbackfn.call(thisArg, this._values[property], this._keys[property], this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given key into a property name for the internal map.
|
||||
*
|
||||
* @param {K} key
|
||||
* @return {?string}
|
||||
*/
|
||||
private _keyToProperty(key: K): string | null {
|
||||
if (typeof key === "number") {
|
||||
return `#${ key }`;
|
||||
}
|
||||
|
||||
if (typeof key === "string") {
|
||||
return `'${ key }`;
|
||||
}
|
||||
|
||||
if ((key as any).id !== undefined) {
|
||||
return `!${ (key as any).id }`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to the global implementation of Map if the environment has one, else set to {@link ./utility/map.SimpleMap}
|
||||
*
|
||||
* Can be set to a value using {@link libjass.configure}
|
||||
*
|
||||
* Set it to null to force {@link ./utility/map.SimpleMap} to be used even if a global Map is present.
|
||||
*
|
||||
* @type {function(new:Map, !Array.<!Array.<*>>=)}
|
||||
*/
|
||||
export var Map: {
|
||||
new <K, V>(iterable?: [K, V][]): Map<K, V>;
|
||||
prototype: Map<any, any>;
|
||||
} = global.Map || SimpleMap;
|
||||
|
||||
if (typeof Map.prototype.forEach !== "function" || (() => {
|
||||
try {
|
||||
return new Map([[1, "foo"], [2, "bar"]]).size !== 2;
|
||||
}
|
||||
catch (ex) {
|
||||
return true;
|
||||
}
|
||||
})()) {
|
||||
Map = SimpleMap;
|
||||
}
|
||||
|
||||
declare var global: {
|
||||
Map?: typeof Map;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the Map implementation used by libjass to the provided one. If null, {@link ./utility/map.SimpleMap} is used.
|
||||
*
|
||||
* @param {?function(new:Map, !Array.<!Array.<*>>=)} value
|
||||
*/
|
||||
export function setImplementation(value: typeof Map | null): void {
|
||||
if (value !== null) {
|
||||
Map = value;
|
||||
}
|
||||
else {
|
||||
Map = SimpleMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds properties of the given mixins' prototypes to the given class's prototype.
|
||||
*
|
||||
* @param {!*} clazz
|
||||
* @param {!Array.<*>} mixins
|
||||
*/
|
||||
export function mixin(clazz: any, mixins: any[]): void {
|
||||
for (const mixin of mixins) {
|
||||
for (const name of Object.getOwnPropertyNames(mixin.prototype)) {
|
||||
clazz.prototype[name] = mixin.prototype[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface Thenable<T> {
|
||||
/** @type {function(this:!Thenable.<T>, function(T|!Thenable.<T>), function(*))} */
|
||||
then: ThenableThen<T>;
|
||||
}
|
||||
|
||||
export interface ThenableThen<T> {
|
||||
/** @type {function(this:!Thenable.<T>, function(T|!Thenable.<T>), function(*))} */
|
||||
(this: Thenable<T>, resolve: ((resolution: T | Thenable<T>) => void) | undefined, reject: ((reason: any) => void) | undefined): void;
|
||||
}
|
||||
|
||||
export interface Promise<T> extends Thenable<T> {
|
||||
/**
|
||||
* @param {function(T):!Thenable.<U>} onFulfilled
|
||||
* @param {?function(*):(U|!Thenable.<U>)} onRejected
|
||||
* @return {!Promise.<U>}
|
||||
*/
|
||||
then<U>(onFulfilled: (value: T) => Thenable<U> | undefined, onRejected?: (reason: any) => U | Thenable<U>): Promise<U>;
|
||||
|
||||
/**
|
||||
* @param {function(T):U} onFulfilled
|
||||
* @param {?function(*):(U|!Thenable.<U>)} onRejected
|
||||
* @return {!Promise.<U>}
|
||||
*/
|
||||
then<U>(onFulfilled: (value: T) => U | undefined, onRejected?: (reason: any) => U | Thenable<U>): Promise<U>;
|
||||
|
||||
/**
|
||||
* @param {function(*):(T|!Thenable.<T>)} onRejected
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
catch(onRejected: (reason: any) => T | Thenable<T>): Promise<T>;
|
||||
}
|
||||
|
||||
// Based on https://github.com/petkaantonov/bluebird/blob/1b1467b95442c12378d0ea280ede61d640ab5510/src/schedule.js
|
||||
const enqueueJob: (callback: () => void) => void = (function () {
|
||||
const MutationObserver = global.MutationObserver || global.WebkitMutationObserver;
|
||||
if (global.process !== undefined && typeof global.process.nextTick === "function") {
|
||||
const process = global.process;
|
||||
return (callback: () => void) => {
|
||||
process.nextTick(callback);
|
||||
};
|
||||
}
|
||||
else if (MutationObserver !== undefined) {
|
||||
const pending: (() => void)[] = [];
|
||||
let currentlyPending = false;
|
||||
|
||||
const div = document.createElement("div");
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
const processing = pending.splice(0, pending.length);
|
||||
|
||||
for (const callback of processing) {
|
||||
callback();
|
||||
}
|
||||
|
||||
currentlyPending = false;
|
||||
|
||||
if (pending.length > 0) {
|
||||
div.classList.toggle("foo");
|
||||
currentlyPending = true;
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(div, { attributes: true });
|
||||
|
||||
return (callback: () => void) => {
|
||||
pending.push(callback);
|
||||
|
||||
if (!currentlyPending) {
|
||||
div.classList.toggle("foo");
|
||||
currentlyPending = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
return (callback: () => void) => setTimeout(callback, 0);
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Promise implementation for browsers that don't support it.
|
||||
*
|
||||
* @param {function(function(T|!Thenable.<T>), function(*))} executor
|
||||
*/
|
||||
class SimplePromise<T> {
|
||||
private _state: SimplePromiseState = SimplePromiseState.PENDING;
|
||||
|
||||
private _fulfillReactions: FulfilledPromiseReaction<T, any>[] = [];
|
||||
private _rejectReactions: RejectedPromiseReaction<any>[] = [];
|
||||
|
||||
private _fulfilledValue: T | null = null;
|
||||
private _rejectedReason: any = null;
|
||||
|
||||
constructor(executor: (resolve: (resolution: T | Thenable<T>) => void, reject: (reason: any) => void) => void) {
|
||||
if (typeof executor !== "function") {
|
||||
throw new TypeError(`typeof executor !== "function"`);
|
||||
}
|
||||
|
||||
const { resolve, reject } = this._createResolvingFunctions();
|
||||
try {
|
||||
executor(resolve, reject);
|
||||
}
|
||||
catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?function(T):(U|!Thenable.<U>)} onFulfilled
|
||||
* @param {?function(*):(U|!Thenable.<U>)} onRejected
|
||||
* @return {!Promise.<U>}
|
||||
*/
|
||||
then<U>(onFulfilled: ((value: T) => U | Thenable<U>) | undefined, onRejected?: (reason: any) => U | Thenable<U>): Promise<U> {
|
||||
const resultCapability = new DeferredPromise<U>();
|
||||
|
||||
if (typeof onFulfilled !== "function") {
|
||||
onFulfilled = (value: T) => value as any as U;
|
||||
}
|
||||
|
||||
if (typeof onRejected !== "function") {
|
||||
onRejected = (reason: any): U => { throw reason; };
|
||||
}
|
||||
|
||||
const fulfillReaction: FulfilledPromiseReaction<T, U> = {
|
||||
capabilities: resultCapability,
|
||||
handler: onFulfilled,
|
||||
};
|
||||
|
||||
const rejectReaction: RejectedPromiseReaction<U> = {
|
||||
capabilities: resultCapability,
|
||||
handler: onRejected,
|
||||
};
|
||||
|
||||
switch (this._state) {
|
||||
case SimplePromiseState.PENDING:
|
||||
this._fulfillReactions.push(fulfillReaction);
|
||||
this._rejectReactions.push(rejectReaction);
|
||||
break;
|
||||
|
||||
case SimplePromiseState.FULFILLED:
|
||||
this._enqueueFulfilledReactionJob(fulfillReaction, this._fulfilledValue!);
|
||||
break;
|
||||
|
||||
case SimplePromiseState.REJECTED:
|
||||
this._enqueueRejectedReactionJob(rejectReaction, this._rejectedReason);
|
||||
break;
|
||||
}
|
||||
|
||||
return resultCapability.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(*):(T|!Thenable.<T>)} onRejected
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
catch(onRejected: (reason: any) => T | Thenable<T>): Promise<T> {
|
||||
return this.then(undefined, onRejected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T|!Thenable.<T>} value
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
static resolve<T>(value: T | Thenable<T>): Promise<T> {
|
||||
if (value instanceof SimplePromise) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return new Promise<T>(resolve => resolve(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} reason
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
static reject<T>(reason: any): Promise<T> {
|
||||
return new Promise<T>((/* ujs:unreferenced */ resolve, reject) => reject(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array.<T|!Thenable.<T>>} values
|
||||
* @return {!Promise.<!Array.<T>>}
|
||||
*/
|
||||
static all<T>(values: (T | Thenable<T>)[]): Promise<T[]> {
|
||||
return new Promise<T[]>((resolve, reject) => {
|
||||
const result: T[] = [];
|
||||
|
||||
let numUnresolved = values.length;
|
||||
if (numUnresolved === 0) {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
values.forEach((value, index) => Promise.resolve(value).then(value => {
|
||||
result[index] = value;
|
||||
numUnresolved--;
|
||||
|
||||
if (numUnresolved === 0) {
|
||||
resolve(result);
|
||||
}
|
||||
}, reject));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array.<T|!Thenable.<T>>} values
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
static race<T>(values: (T | Thenable<T>)[]): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
for (const value of values) {
|
||||
Promise.resolve(value).then(resolve, reject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {{ resolve(T|!Thenable.<T>), reject(*) }}
|
||||
*/
|
||||
private _createResolvingFunctions(): { resolve(resolution: T | Thenable<T>): void; reject(reason: any): void; } {
|
||||
let alreadyResolved = false;
|
||||
|
||||
const resolve = (resolution: T | Thenable<T>): void => {
|
||||
if (alreadyResolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
alreadyResolved = true;
|
||||
|
||||
if (resolution === this) {
|
||||
this._reject(new TypeError(`resolution === this`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolution === null || (typeof resolution !== "object" && typeof resolution !== "function")) {
|
||||
this._fulfill(resolution as T);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var then = (resolution as Thenable<T>).then;
|
||||
}
|
||||
catch (ex) {
|
||||
this._reject(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof then !== "function") {
|
||||
this._fulfill(resolution as T);
|
||||
return;
|
||||
}
|
||||
|
||||
enqueueJob(() => this._resolveWithThenable(resolution as Thenable<T>, then));
|
||||
};
|
||||
|
||||
const reject = (reason: any): void => {
|
||||
if (alreadyResolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
alreadyResolved = true;
|
||||
|
||||
this._reject(reason);
|
||||
};
|
||||
|
||||
return { resolve, reject };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Thenable.<T>} thenable
|
||||
* @param {{function(this:!Thenable.<T>, function(T|!Thenable.<T>), function(*))}} then
|
||||
*/
|
||||
private _resolveWithThenable(thenable: Thenable<T>, then: ThenableThen<T>): void {
|
||||
const { resolve, reject } = this._createResolvingFunctions();
|
||||
|
||||
try {
|
||||
then.call(thenable, resolve, reject);
|
||||
}
|
||||
catch (ex) {
|
||||
reject(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
*/
|
||||
private _fulfill(value: T): void {
|
||||
const reactions = this._fulfillReactions;
|
||||
|
||||
this._fulfilledValue = value;
|
||||
this._fulfillReactions = [];
|
||||
this._rejectReactions = [];
|
||||
this._state = SimplePromiseState.FULFILLED;
|
||||
|
||||
for (const reaction of reactions) {
|
||||
this._enqueueFulfilledReactionJob(reaction, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} reason
|
||||
*/
|
||||
private _reject(reason: any): void {
|
||||
const reactions = this._rejectReactions;
|
||||
|
||||
this._rejectedReason = reason;
|
||||
this._fulfillReactions = [];
|
||||
this._rejectReactions = [];
|
||||
this._state = SimplePromiseState.REJECTED;
|
||||
|
||||
for (const reaction of reactions) {
|
||||
this._enqueueRejectedReactionJob(reaction, reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!FulfilledPromiseReaction.<T, *>} reaction
|
||||
* @param {T} value
|
||||
*/
|
||||
private _enqueueFulfilledReactionJob(reaction: FulfilledPromiseReaction<T, any>, value: T): void {
|
||||
enqueueJob(() => {
|
||||
const { capabilities: { resolve, reject }, handler } = reaction;
|
||||
|
||||
let handlerResult: any | Thenable<any>;
|
||||
|
||||
try {
|
||||
handlerResult = handler(value);
|
||||
}
|
||||
catch (ex) {
|
||||
reject(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(handlerResult);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!RejectedPromiseReaction.<*>} reaction
|
||||
* @param {*} reason
|
||||
*/
|
||||
private _enqueueRejectedReactionJob(reaction: RejectedPromiseReaction<any>, reason: any): void {
|
||||
enqueueJob(() => {
|
||||
const { capabilities: { resolve, reject }, handler } = reaction;
|
||||
|
||||
let handlerResult: any | Thenable<any>;
|
||||
|
||||
try {
|
||||
handlerResult = handler(reason);
|
||||
}
|
||||
catch (ex) {
|
||||
reject(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(handlerResult);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to the global implementation of Promise if the environment has one, else set to {@link ./utility/promise.SimplePromise}
|
||||
*
|
||||
* Can be set to a value using {@link libjass.configure}
|
||||
*
|
||||
* Set it to null to force {@link ./utility/promise.SimplePromise} to be used even if a global Promise is present.
|
||||
*
|
||||
* @type {function(new:Promise)}
|
||||
*/
|
||||
export var Promise: {
|
||||
new <T>(init: (resolve: (value: T | Thenable<T>) => void, reject: (reason: any) => void) => void): Promise<T>;
|
||||
prototype: Promise<any>;
|
||||
resolve<T>(value: T | Thenable<T>): Promise<T>;
|
||||
reject<T>(reason: any): Promise<T>;
|
||||
all<T>(values: (T | Thenable<T>)[]): Promise<T[]>;
|
||||
race<T>(values: (T | Thenable<T>)[]): Promise<T>;
|
||||
} = global.Promise || SimplePromise;
|
||||
|
||||
declare var global: {
|
||||
Promise?: typeof Promise;
|
||||
MutationObserver?: typeof MutationObserver;
|
||||
WebkitMutationObserver?: typeof MutationObserver;
|
||||
process?: {
|
||||
nextTick(callback: () => void): void;
|
||||
}
|
||||
};
|
||||
|
||||
interface FulfilledPromiseReaction<T, U> {
|
||||
/** @type {!libjass.DeferredPromise.<U>} */
|
||||
capabilities: DeferredPromise<U>;
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
* @return {U|!Thenable.<U>}
|
||||
*/
|
||||
handler(value: T): U | Thenable<U>;
|
||||
}
|
||||
|
||||
interface RejectedPromiseReaction<U> {
|
||||
/** @type {!libjass.DeferredPromise.<U>} */
|
||||
capabilities: DeferredPromise<U>;
|
||||
|
||||
/**
|
||||
* @param {*} reason
|
||||
* @return {U|!Thenable.<U>}
|
||||
*/
|
||||
handler(reason: any): U | Thenable<U>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the {@link ./utility/promise.SimplePromise}
|
||||
*/
|
||||
enum SimplePromiseState {
|
||||
PENDING = 0,
|
||||
FULFILLED = 1,
|
||||
REJECTED = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Promise implementation used by libjass to the provided one. If null, {@link ./utility/promise.SimplePromise} is used.
|
||||
*
|
||||
* @param {?function(new:Promise)} value
|
||||
*/
|
||||
export function setImplementation(value: typeof Promise | null): void {
|
||||
if (value !== null) {
|
||||
Promise = value;
|
||||
}
|
||||
else {
|
||||
Promise = SimplePromise;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A deferred promise.
|
||||
*/
|
||||
export class DeferredPromise<T> {
|
||||
private _promise: Promise<T>;
|
||||
|
||||
/**
|
||||
* @type {function(T|!Thenable.<T>)}
|
||||
*/
|
||||
resolve: (value: T | Thenable<T>) => void;
|
||||
|
||||
/**
|
||||
* @type {function(*)} reason
|
||||
*/
|
||||
reject: (reason: any) => void;
|
||||
|
||||
constructor() {
|
||||
this._promise = new Promise<T>((resolve, reject) => {
|
||||
Object.defineProperties(this, {
|
||||
resolve: { value: resolve, enumerable: true },
|
||||
reject: { value: reject, enumerable: true },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {!Promise.<T>}
|
||||
*/
|
||||
get promise(): Promise<T> {
|
||||
return this._promise;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the first (in iteration order) promise that fulfills, and rejects if all the promises reject.
|
||||
*
|
||||
* @param {!Array.<!Promise.<T>>} promises
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
export function first<T>(promises: Promise<T>[]): Promise<T> {
|
||||
return first_rec(promises, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!Array.<!Promise.<T>>} promises
|
||||
* @param {!Array.<*>} previousRejections
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
function first_rec<T>(promises: Promise<T>[], previousRejections: any[]): Promise<T> {
|
||||
if (promises.length === 0) {
|
||||
return Promise.reject(previousRejections);
|
||||
}
|
||||
|
||||
const [head, ...tail] = promises;
|
||||
return head.catch(reason => first_rec(tail, previousRejections.concat(reason)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to the first (in time order) promise that fulfills, and rejects if all the promises reject.
|
||||
*
|
||||
* @param {!Array.<!Promise.<T>>} promises
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
export function any<T>(promises: Promise<T>[]): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) =>
|
||||
Promise.all<any>(promises.map(promise => promise.then(resolve, reason => reason))).then(reject));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that runs the given callback when the promise has resolved regardless of whether it fulfilled or rejected.
|
||||
*
|
||||
* @param {!Promise.<T>} promise
|
||||
* @param {function()} body
|
||||
* @return {!Promise.<T>}
|
||||
*/
|
||||
export function lastly<T>(promise: Promise<T>, body: () => void): Promise<T> {
|
||||
return promise.then<any>(value => {
|
||||
body();
|
||||
return value;
|
||||
}, reason => {
|
||||
body();
|
||||
throw reason;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface Set<T> {
|
||||
/**
|
||||
* @param {T} value
|
||||
* @return {libjass.Set.<T>} This set
|
||||
*/
|
||||
add(value: T): this;
|
||||
|
||||
/**
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
* @return {boolean}
|
||||
*/
|
||||
has(value: T): boolean;
|
||||
|
||||
/**
|
||||
* @param {function(T, T, libjass.Set.<T>)} callbackfn A function that is called with each value in the set.
|
||||
* @param {*} thisArg
|
||||
*/
|
||||
forEach(callbackfn: (value: T, index: T, set: this) => void, thisArg?: any): void;
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set implementation for browsers that don't support it. Only supports Number and String elements.
|
||||
*
|
||||
* Elements are stored as properties of an object, with names derived from their type.
|
||||
*
|
||||
* @param {!Array.<T>=} iterable Only an array of values is supported.
|
||||
*/
|
||||
class SimpleSet<T> implements Set<T> {
|
||||
private _elements: { [key: string]: T };
|
||||
private _size: number;
|
||||
|
||||
constructor(iterable?: T[]) {
|
||||
this.clear();
|
||||
|
||||
if (iterable === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(iterable)) {
|
||||
throw new Error("Non-array iterables are not supported by the SimpleSet constructor.");
|
||||
}
|
||||
|
||||
for (const value of iterable) {
|
||||
this.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
* @return {libjass.Set.<T>} This set
|
||||
*/
|
||||
add(value: T): this {
|
||||
const property = this._toProperty(value);
|
||||
|
||||
if (property === null) {
|
||||
throw new Error("This Set implementation only supports Number and String values.");
|
||||
}
|
||||
|
||||
if (!(property in this._elements)) {
|
||||
this._size++;
|
||||
}
|
||||
|
||||
this._elements[property] = value;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
clear(): void {
|
||||
this._elements = Object.create(null);
|
||||
this._size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {T} value
|
||||
* @return {boolean}
|
||||
*/
|
||||
has(value: T): boolean {
|
||||
const property = this._toProperty(value);
|
||||
|
||||
if (property === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return property in this._elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {function(T, T, libjass.Set.<T>)} callbackfn A function that is called with each value in the set.
|
||||
* @param {*} thisArg
|
||||
*/
|
||||
forEach(callbackfn: (value: T, index: T, set: this) => void, thisArg?: any): void {
|
||||
for (const property of Object.keys(this._elements)) {
|
||||
const element = this._elements[property];
|
||||
callbackfn.call(thisArg, element, element, this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {number}
|
||||
*/
|
||||
get size(): number {
|
||||
return this._size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given value into a property name for the internal map.
|
||||
*
|
||||
* @param {T} value
|
||||
* @return {?string}
|
||||
*/
|
||||
private _toProperty(value: T): string | null {
|
||||
if (typeof value === "number") {
|
||||
return `#${ value }`;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return `'${ value }`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to the global implementation of Set if the environment has one, else set to {@link ./utility/set.SimpleSet}
|
||||
*
|
||||
* Can be set to a value using {@link libjass.configure}
|
||||
*
|
||||
* Set it to null to force {@link ./utility/set.SimpleSet} to be used even if a global Set is present.
|
||||
*
|
||||
* @type {function(new:Set, !Array.<T>=)}
|
||||
*/
|
||||
export var Set: {
|
||||
new <T>(iterable?: T[]): Set<T>;
|
||||
prototype: Set<any>;
|
||||
} = global.Set || SimpleSet;
|
||||
|
||||
if (typeof Set.prototype.forEach !== "function" || (() => {
|
||||
try {
|
||||
return new Set([1, 2]).size !== 2;
|
||||
}
|
||||
catch (ex) {
|
||||
return true;
|
||||
}
|
||||
})()) {
|
||||
Set = SimpleSet;
|
||||
}
|
||||
|
||||
declare var global: {
|
||||
Set?: typeof Set;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the Set implementation used by libjass to the provided one. If null, {@link ./utility/set.SimpleSet} is used.
|
||||
*
|
||||
* @param {?function(new:Set, !Array.<T>=)} value
|
||||
*/
|
||||
export function setImplementation(value: typeof Set | null): void {
|
||||
if (value !== null) {
|
||||
Set = value;
|
||||
}
|
||||
else {
|
||||
Set = SimpleSet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { serialize, deserialize } from "../serialization";
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
|
||||
import { Promise, DeferredPromise } from "../utility/promise";
|
||||
|
||||
import { WorkerCommands } from "./commands";
|
||||
import { getWorkerCommandHandler, registerWorkerCommand } from "./misc";
|
||||
|
||||
/**
|
||||
* Represents a communication channel between the host and the web worker. An instance of this class is created by calling {@link libjass.webworker.createWorker}
|
||||
*/
|
||||
export interface WorkerChannel {
|
||||
/**
|
||||
* Sends a request to the other side to execute the given command with the given parameters.
|
||||
*
|
||||
* @param {number} command
|
||||
* @param {*} parameters
|
||||
* @return {!Promise.<*>} A promise that will get resolved when the other side computes the result
|
||||
*/
|
||||
request(command: WorkerCommands, parameters: any): Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The signature of a handler registered to handle a particular command in {@link libjass.webworker.WorkerCommands}
|
||||
*/
|
||||
export interface WorkerCommandHandler {
|
||||
(parameters: any): Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface implemented by a communication channel to the other side.
|
||||
*/
|
||||
export interface WorkerCommunication {
|
||||
/**
|
||||
* @param {"message"} type
|
||||
* @param {function(!MessageEvent): *} listener
|
||||
* @param {?boolean} useCapture
|
||||
*/
|
||||
addEventListener(type: "message", listener: (ev: MessageEvent) => any, useCapture?: boolean): void;
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {!EventListener} listener
|
||||
* @param {?boolean} useCapture
|
||||
*/
|
||||
addEventListener(type: string, listener: EventListener, useCapture?: boolean): void;
|
||||
|
||||
/**
|
||||
* @param {*} message
|
||||
*/
|
||||
postMessage(message: any): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface implemented by a request sent to the other side of the communication channel.
|
||||
*/
|
||||
interface WorkerRequestMessage {
|
||||
/**
|
||||
* An internal identifier for this request. Used to connect responses to their corresponding requests.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
requestId: number;
|
||||
|
||||
/**
|
||||
* The command type of this request.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
command: WorkerCommands;
|
||||
|
||||
/**
|
||||
* Any parameters serialized with this request.
|
||||
*
|
||||
* @type {*}
|
||||
*/
|
||||
parameters: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface implemented by a response received from the other side of the communication channel.
|
||||
*/
|
||||
interface WorkerResponseMessage {
|
||||
/**
|
||||
* An internal identifier for this response. Used to connect responses to their corresponding requests.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
requestId: number;
|
||||
|
||||
/**
|
||||
* Set if the computation of this response resulted in an error.
|
||||
*
|
||||
* @type {*}
|
||||
*/
|
||||
error: any;
|
||||
|
||||
/**
|
||||
* The result of computing this response.
|
||||
*
|
||||
* @type {*}
|
||||
*/
|
||||
result: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation of libjass.webworker.WorkerChannel
|
||||
*
|
||||
* @param {!*} comm The object used to talk to the other side of the channel. When created by the main thread, this is the Worker object.
|
||||
* When created by the web worker, this is its global object.
|
||||
*/
|
||||
export class WorkerChannelImpl implements WorkerChannel {
|
||||
private static _lastRequestId: number = -1;
|
||||
|
||||
private _pendingRequests = new Map<number, DeferredPromise<any>>();
|
||||
|
||||
constructor(private _comm: WorkerCommunication) {
|
||||
this._comm.addEventListener("message", ev => this._onMessage(ev.data as string), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} command
|
||||
* @param {*} parameters
|
||||
* @return {!Promise.<*>}
|
||||
*/
|
||||
request(command: WorkerCommands, parameters: any): Promise<any> {
|
||||
const deferred = new DeferredPromise<any>();
|
||||
const requestId = ++WorkerChannelImpl._lastRequestId;
|
||||
this._pendingRequests.set(requestId, deferred);
|
||||
|
||||
const requestMessage: WorkerRequestMessage = { requestId, command, parameters };
|
||||
this._comm.postMessage(serialize(requestMessage));
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} requestId
|
||||
*/
|
||||
cancelRequest(requestId: number): void {
|
||||
const deferred = this._pendingRequests.get(requestId);
|
||||
if (deferred === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pendingRequests.delete(requestId);
|
||||
deferred.reject(new Error("Cancelled."));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!WorkerResponseMessage} message
|
||||
*/
|
||||
private _respond(message: WorkerResponseMessage): void {
|
||||
let { requestId, error, result } = message;
|
||||
if (error instanceof Error) {
|
||||
error = { message: error.message, stack: error.stack };
|
||||
}
|
||||
this._comm.postMessage(serialize({ command: WorkerCommands.Response, requestId, error, result }));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} rawMessage
|
||||
*/
|
||||
private _onMessage(rawMessage: string): void {
|
||||
const message = deserialize(rawMessage) as { command: WorkerCommands };
|
||||
|
||||
if (message.command === WorkerCommands.Response) {
|
||||
const responseMessage = message as any as WorkerResponseMessage;
|
||||
|
||||
const deferred = this._pendingRequests.get(responseMessage.requestId);
|
||||
if (deferred !== undefined) {
|
||||
this._pendingRequests.delete(responseMessage.requestId);
|
||||
if (responseMessage.error === null) {
|
||||
deferred.resolve(responseMessage.result);
|
||||
}
|
||||
else {
|
||||
deferred.reject(responseMessage.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const requestMessage = message as WorkerRequestMessage;
|
||||
const requestId = requestMessage.requestId;
|
||||
|
||||
const commandCallback = getWorkerCommandHandler(requestMessage.command);
|
||||
if (commandCallback === undefined) {
|
||||
this._respond({ requestId, error: new Error(`No handler registered for command ${ requestMessage.command }`), result: null });
|
||||
return;
|
||||
}
|
||||
|
||||
commandCallback(requestMessage.parameters).then<WorkerResponseMessage>(
|
||||
result => ({ requestId, error: null, result }),
|
||||
error => ({ requestId, error, result: null })
|
||||
).then(responseMessage => this._respond(responseMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerWorkerCommand(WorkerCommands.Ping, parameters => Promise.resolve(null));
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The commands that can be sent to or from a web worker.
|
||||
*/
|
||||
export enum WorkerCommands {
|
||||
Response = 0,
|
||||
Parse = 1,
|
||||
Ping = 2,
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { WorkerChannel, WorkerChannelImpl } from "./channel";
|
||||
export { WorkerChannel } from "./channel";
|
||||
|
||||
export { WorkerCommands } from "./commands";
|
||||
|
||||
/**
|
||||
* Indicates whether web workers are supposed in this environment or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
export const supported = typeof Worker !== "undefined";
|
||||
|
||||
const _scriptNode = (typeof document !== "undefined" && document.currentScript !== undefined) ? (document.currentScript as HTMLScriptElement) : null;
|
||||
|
||||
/**
|
||||
* Create a new web worker and returns a {@link libjass.webworker.WorkerChannel} to it.
|
||||
*
|
||||
* @param {string=} scriptPath The path to libjass.js to be loaded in the web worker. If the browser supports document.currentScript, the parameter is optional and, if not provided,
|
||||
* the path will be determined from the src attribute of the <script> element that contains the currently running copy of libjass.js
|
||||
* @return {!libjass.webworker.WorkerChannel} A communication channel to the new web worker.
|
||||
*/
|
||||
export function createWorker(scriptPath?: string): WorkerChannel {
|
||||
if (scriptPath === undefined) {
|
||||
if (_scriptNode === null) {
|
||||
throw new Error("Could not auto-detect path of libjass.js, and explicit path was not passed in.");
|
||||
}
|
||||
|
||||
scriptPath = _scriptNode.src;
|
||||
}
|
||||
|
||||
return new WorkerChannelImpl(new Worker(scriptPath));
|
||||
}
|
||||
|
||||
declare const global: any;
|
||||
|
||||
if (typeof WorkerGlobalScope !== "undefined" && global instanceof WorkerGlobalScope) {
|
||||
// This is a web worker. Set up a channel to talk back to the main thread.
|
||||
new WorkerChannelImpl(global);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Map } from "../utility/map";
|
||||
|
||||
import { WorkerCommands } from "./commands";
|
||||
import { WorkerCommandHandler } from "./channel";
|
||||
|
||||
const workerCommands = new Map<WorkerCommands, WorkerCommandHandler>();
|
||||
|
||||
/**
|
||||
* Registers a handler for the given worker command.
|
||||
*
|
||||
* @param {number} command The command that this handler will handle. One of the {@link libjass.webworker.WorkerCommands} constants.
|
||||
* @param {function(*, function(*, *))} handler The handler. A function of the form (parameters: *, response: function(error: *, result: *): void): void
|
||||
*/
|
||||
export function registerWorkerCommand(command: WorkerCommands, handler: WorkerCommandHandler): void {
|
||||
workerCommands.set(command, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the handler for the given worker command.
|
||||
*
|
||||
* @param {number} command
|
||||
* @return {?function(*, function(*, *))}
|
||||
*/
|
||||
export function getWorkerCommandHandler(command: WorkerCommands): WorkerCommandHandler | undefined {
|
||||
return workerCommands.get(command);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
interface WorkerGlobalScope {
|
||||
/**
|
||||
* @param {*} message
|
||||
*/
|
||||
postMessage(message: any): void;
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {function(*)} listener
|
||||
* @param {boolean} useCapture
|
||||
*/
|
||||
addEventListener(type: string, listener: (message: any) => void, useCapture: boolean): void;
|
||||
}
|
||||
declare var WorkerGlobalScope: {
|
||||
prototype: WorkerGlobalScope;
|
||||
new (): WorkerGlobalScope;
|
||||
};
|
||||
@@ -1,383 +0,0 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
///<reference path="libjass.ts" />
|
||||
|
||||
module libjass {
|
||||
export module tags {
|
||||
/**
|
||||
* Represents a CSS color with red, green, blue and alpha components.
|
||||
*
|
||||
* Instances of this class are immutable.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
export class Color {
|
||||
constructor(private _red: number, private _green: number, private _blue: number, private _alpha: number = 1) { }
|
||||
|
||||
get red(): number {
|
||||
return this._red;
|
||||
}
|
||||
|
||||
get green(): number {
|
||||
return this._green;
|
||||
}
|
||||
|
||||
get blue(): number {
|
||||
return this._blue;
|
||||
}
|
||||
|
||||
get alpha(): number {
|
||||
return this._alpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value The new alpha. If null, the existing alpha is used.
|
||||
* @return {Color} Returns a new Color instance with the same color but the provided alpha.
|
||||
*/
|
||||
withAlpha(value: number): Color {
|
||||
return new Color(this._red, this._green, this._blue, (value !== null) ? value : this._alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string} The CSS representation "rgba(...)" of this color.
|
||||
*/
|
||||
toString(): string {
|
||||
return "rgba(" + this._red + ", " + this._green + ", " + this._blue + ", " + this._alpha + ")";
|
||||
}
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export class TagBase implements Tag {
|
||||
constructor(private _name: string, ... private _propertyNames: string[]) { }
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
toString(): string {
|
||||
return (
|
||||
this._name + " { " +
|
||||
this._propertyNames.map(name => name + ": " + this[name]).join(", ") +
|
||||
((this._propertyNames.length > 0) ? " " : "") +
|
||||
"}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Comment extends TagBase {
|
||||
constructor(private _value: string) {
|
||||
super("Comment", "value");
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class HardSpace extends TagBase {
|
||||
constructor() {
|
||||
super("HardSpace");
|
||||
}
|
||||
}
|
||||
|
||||
export class NewLine extends TagBase {
|
||||
constructor() {
|
||||
super("NewLine");
|
||||
}
|
||||
}
|
||||
|
||||
export class Text extends TagBase {
|
||||
constructor(private _value: string) {
|
||||
super("Text", "value");
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class Italic extends TagBase {
|
||||
constructor(private _value: boolean) {
|
||||
super("Italic", "value");
|
||||
}
|
||||
|
||||
get value(): boolean {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class Bold extends TagBase {
|
||||
constructor(private _value: Object) {
|
||||
super("Bold", "value");
|
||||
}
|
||||
|
||||
get value(): Object {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class Underline extends TagBase {
|
||||
constructor(private _value: boolean) {
|
||||
super("Underline", "value");
|
||||
}
|
||||
|
||||
get value(): boolean {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class StrikeThrough extends TagBase {
|
||||
constructor(private _value: boolean) {
|
||||
super("StrikeThrough", "value");
|
||||
}
|
||||
|
||||
get value(): boolean {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class Border extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("Border", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class BorderX extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("BorderX", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class BorderY extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("BorderY", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class Blur extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("Blur", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class FontName extends TagBase {
|
||||
constructor(private _value: string) {
|
||||
super("FontName", "value");
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class FontSize extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("FontSize", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class FontScaleX extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("FontScaleX", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class FontScaleY extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("FontScaleX", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class LetterSpacing extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("LetterSpacing", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class RotateX extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("RotateX", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class RotateY extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("RotateY", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class RotateZ extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("RotateZ", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class SkewX extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("SkewX", "value");
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class SkewY extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("SkewY", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class PrimaryColor extends TagBase {
|
||||
constructor(private _value: Color) {
|
||||
super("PrimaryColor", "value");
|
||||
}
|
||||
|
||||
get value(): Color {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class OutlineColor extends TagBase {
|
||||
constructor(private _value: Color) {
|
||||
super("OutlineColor", "value");
|
||||
}
|
||||
|
||||
get value(): Color {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class Alpha extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("Alpha", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class PrimaryAlpha extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("PrimaryAlpha", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
export class OutlineAlpha extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("OutlineAlpha", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class Alignment extends TagBase {
|
||||
constructor(private _value: number) {
|
||||
super("Alignment", "value");
|
||||
}
|
||||
|
||||
get value(): number {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class Reset extends TagBase {
|
||||
constructor(private _value: string) {
|
||||
super("Reset", "value");
|
||||
}
|
||||
|
||||
get value(): string {
|
||||
return this._value;
|
||||
}
|
||||
}
|
||||
|
||||
export class Pos extends TagBase {
|
||||
constructor(private _x: number, private _y: number) {
|
||||
super("Pos", "x", "y");
|
||||
}
|
||||
|
||||
get x(): number {
|
||||
return this._x;
|
||||
}
|
||||
|
||||
get y(): number {
|
||||
return this._y;
|
||||
}
|
||||
}
|
||||
|
||||
export class Fade extends TagBase {
|
||||
constructor(private _start: number, private _end: number) {
|
||||
super("Fade", "start", "end");
|
||||
}
|
||||
|
||||
get start(): number {
|
||||
return this._start;
|
||||
}
|
||||
|
||||
get end(): number {
|
||||
return this._end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
define(["intern!tdd", "intern/chai!assert", "intern/dojo/node!fs", "intern/dojo/node!path", "intern/dojo/node!sax"], function (tdd, assert, fs, path, sax) {
|
||||
tdd.suite("Documentation tests", function () {
|
||||
var ids = [];
|
||||
var hrefs = [];
|
||||
|
||||
tdd.before(function () {
|
||||
var parser = sax.parser(true);
|
||||
|
||||
parser.onopentag = function (node) {
|
||||
if (node.name === "a" && node.attributes.href[0] === "#") {
|
||||
hrefs.push(node.attributes.href.substr(1));
|
||||
}
|
||||
|
||||
if (node.attributes.id !== undefined) {
|
||||
ids.push(node.attributes.id);
|
||||
}
|
||||
};
|
||||
|
||||
parser.onerror = function (error) { throw error; };
|
||||
|
||||
parser.write(fs.readFileSync(require.toUrl("../libjass-gh-pages/api.xhtml"), { encoding: "utf8" })).close();
|
||||
});
|
||||
|
||||
tdd.test("api.xhtml", function () {
|
||||
var brokenLinks = hrefs.filter(function (href) {
|
||||
return ids.indexOf(href) === -1;
|
||||
});
|
||||
|
||||
assert.equal(brokenLinks.length, 0, "Broken link(s): " + brokenLinks.map(function (href) { return '"' + href + '"'; }).join(", "));
|
||||
});
|
||||
|
||||
tdd.test("README.md", function () {
|
||||
var regex = /\[[^\]]+\]\(http:\/\/arnavion\.github\.io\/libjass\/api\.xhtml#([^)]+)\)/g;
|
||||
var brokenLinks = [];
|
||||
|
||||
var readme = fs.readFileSync(require.toUrl("./README.md"), { encoding: "utf8" });
|
||||
|
||||
var match;
|
||||
while ((match = regex.exec(readme)) !== null) {
|
||||
if (ids.indexOf(match[1]) === -1) {
|
||||
brokenLinks.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(brokenLinks.length, 0, "Broken link(s): " + brokenLinks.map(function (href) { return '"' + href + '"'; }).join(", "));
|
||||
});
|
||||
|
||||
tdd.test("{@link} tags in the source", function () {
|
||||
var regex = /\{@link ([^}]+)\}/g;
|
||||
var brokenLinks = [];
|
||||
|
||||
var libjassJs = fs.readFileSync(require.toUrl("./lib/libjass.js"), { encoding: "utf8" });
|
||||
|
||||
var match;
|
||||
while ((match = regex.exec(libjassJs)) !== null) {
|
||||
if (ids.indexOf(match[1]) === -1) {
|
||||
brokenLinks.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(brokenLinks.length, 0, "Broken link(s): " + brokenLinks.map(function (href) { return '"' + href + '"'; }).join(", "));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
[Script Info]
|
||||
Title:
|
||||
ScriptType: v4.00+
|
||||
WrapStyle: 0
|
||||
PlayResX: 1280
|
||||
PlayResY: 720
|
||||
Scroll Position: 0
|
||||
Active Line: 0
|
||||
Video Zoom Percent: 1
|
||||
ScaledBorderAndShadow: yes
|
||||
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,Arial,500,&H7F0000FF,&H00FFFFFF,&H7F000000,&H7F000000,0,0,0,0,100,100,0,0,1,10,0,2,75,75,75,1
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 0,0:00:00.00,0:10:00.00,Default,,0,0,0,,X
|
||||
@@ -18,16 +18,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
var libjass = require("../libjass.js");
|
||||
var parserTest = require("./parser-test.js");
|
||||
|
||||
suite("Miscellaneous", function () {
|
||||
parserTest("herkz", "{\\pos(311,4)\\blur0.8\\fs40\\bord0\\c&H3F171F&\\t(3820,3820,\\blur6}Chi{\\c&H422CB1&}tose {\\c&H3F171F&}Furu", "dialogueParts", [
|
||||
new libjass.tags.Comment("\\pos(311,4)\\blur0.8\\fs40\\bord0\\c&H3F171F&\\t(3820,3820,\\blur6"),
|
||||
new libjass.tags.Text("Chi"),
|
||||
new libjass.tags.PrimaryColor(new libjass.tags.Color(177, 44, 66, 1)),
|
||||
new libjass.tags.Text("tose "),
|
||||
new libjass.tags.PrimaryColor(new libjass.tags.Color(31, 23, 63, 1)),
|
||||
new libjass.tags.Text("Furu")
|
||||
]);
|
||||
define(["intern!tdd", "require", "tests/support/test-page"], function (tdd, require, TestPage) {
|
||||
tdd.suite("alpha", function () {
|
||||
tdd.test("Basic", function () {
|
||||
var testPage = new TestPage(this.remote, require.toUrl("tests/support/browser-test-page.html"), "/tests/functional/alpha/alpha.ass", 1280, 720, "rgb(47, 163, 254)");
|
||||
return testPage
|
||||
.prepare()
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(1, require.toUrl("./alpha-1.png")); })
|
||||
.then(function (testPage) { return testPage.done(); });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
define(["intern!tdd", "intern/chai!assert", "require", "intern/dojo/node!leadfoot/helpers/pollUntil"], function (tdd, assert, require, pollUntil) {
|
||||
tdd.suite("Auto clock", function () {
|
||||
tdd.test("Operations", function () {
|
||||
this.remote.session.setExecuteAsyncTimeout(10000);
|
||||
|
||||
return this.remote
|
||||
.get(require.toUrl("tests/support/browser-test-page.html"))
|
||||
.then(pollUntil('return (document.readyState === "complete") ? true : null;', 100))
|
||||
.execute(function () {
|
||||
window.driverStartTime = 0;
|
||||
window.driverStartTimeAt = new Date();
|
||||
window.driver = function () {
|
||||
return (new Date() - driverStartTimeAt) / 1000 + driverStartTime;
|
||||
};
|
||||
|
||||
window.clock = new libjass.renderers.AutoClock(driver);
|
||||
|
||||
window.events = [];
|
||||
|
||||
clock.addEventListener(libjass.renderers.ClockEvent.Play, function () {
|
||||
events.push("play");
|
||||
});
|
||||
|
||||
clock.addEventListener(libjass.renderers.ClockEvent.Tick, function () {
|
||||
events.push("tick");
|
||||
});
|
||||
|
||||
clock.addEventListener(libjass.renderers.ClockEvent.Pause, function () {
|
||||
events.push("pause");
|
||||
});
|
||||
|
||||
clock.addEventListener(libjass.renderers.ClockEvent.Stop, function () {
|
||||
events.push("stop");
|
||||
});
|
||||
|
||||
return { enabled: clock.enabled, paused: clock.paused, rate: clock.rate, events: events.slice() };
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, true);
|
||||
assert.strictEqual(clock.rate, 1);
|
||||
assert.deepEqual(clock.events, []);
|
||||
})
|
||||
.executeAsync(function (callback) {
|
||||
clock.play();
|
||||
|
||||
setTimeout(function () {
|
||||
callback({ enabled: clock.enabled, paused: clock.paused, events: events.slice() });
|
||||
}, 1000);
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, false);
|
||||
|
||||
assert(clock.events.length >= 2);
|
||||
assert.strictEqual(clock.events[0], "play");
|
||||
for (var i = 1; i < clock.events.length; i++) {
|
||||
assert.strictEqual(clock.events[i], "tick");
|
||||
}
|
||||
})
|
||||
.execute(function () {
|
||||
events = [];
|
||||
|
||||
clock.disable();
|
||||
|
||||
return { enabled: clock.enabled, paused: clock.paused, events: events.slice() };
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, false);
|
||||
assert.strictEqual(clock.paused, true);
|
||||
assert.deepEqual(clock.events, ["pause", "stop"]);
|
||||
})
|
||||
.execute(function () {
|
||||
events = [];
|
||||
|
||||
clock.enable();
|
||||
|
||||
return { enabled: clock.enabled, paused: clock.paused, events: events.slice() };
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, true);
|
||||
assert.deepEqual(clock.events, []);
|
||||
})
|
||||
.executeAsync(function (callback) {
|
||||
setTimeout(function () {
|
||||
callback({ enabled: clock.enabled, paused: clock.paused, events: events.slice() });
|
||||
}, 1000);
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, false);
|
||||
|
||||
assert(clock.events.length >= 2);
|
||||
assert.strictEqual(clock.events[0], "play");
|
||||
for (var i = 1; i < clock.events.length; i++) {
|
||||
assert.strictEqual(clock.events[i], "tick");
|
||||
}
|
||||
})
|
||||
.execute(function () {
|
||||
events = [];
|
||||
|
||||
driverStartTime = 30000;
|
||||
clock.seeking();
|
||||
|
||||
var result = { enabled: clock.enabled, paused: clock.paused, currentTime: clock.currentTime, events: events.slice() };
|
||||
events = [];
|
||||
return result;
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, true);
|
||||
assert(clock.currentTime >= 30);
|
||||
assert.deepEqual(clock.events, ["pause", "stop", "play", "tick", "pause"]);
|
||||
})
|
||||
.executeAsync(function (callback) {
|
||||
setTimeout(function () {
|
||||
callback({ enabled: clock.enabled, paused: clock.paused, currentTime: clock.currentTime, events: events.slice() });
|
||||
}, 1000);
|
||||
})
|
||||
.then(function (clock) {
|
||||
assert.strictEqual(clock.enabled, true);
|
||||
assert.strictEqual(clock.paused, false);
|
||||
assert(clock.currentTime >= 31);
|
||||
|
||||
assert(clock.events.length >= 2);
|
||||
assert.strictEqual(clock.events[0], "play");
|
||||
for (var i = 1; i < clock.events.length; i++) {
|
||||
assert.strictEqual(clock.events[i], "tick");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,32 @@
|
||||
[Script Info]
|
||||
Title:
|
||||
ScriptType: v4.00+
|
||||
WrapStyle: 0
|
||||
PlayResX: 1280
|
||||
PlayResY: 720
|
||||
Scroll Position: 0
|
||||
Active Line: 0
|
||||
Video Zoom Percent: 1
|
||||
ScaledBorderAndShadow: yes
|
||||
|
||||
[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default,Arial,90,&H00FFFFFF,&H00000000,&H00000000,&H96000000,0,0,0,0,100,100,0,0,1,0,0,2,75,75,75,1
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 1,0:00:00.00,0:00:01.00,Default,,0,0,0,,{\an1\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:00.00,0:00:01.00,Default,,0,0,0,,{\an2\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:00.00,0:00:01.00,Default,,0,0,0,,{\an3\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:00.00,0:00:01.00,Default,,0,0,0,,{\an7\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:00.00,0:00:01.00,Default,,0,0,0,,{\an8\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:00.00,0:00:01.00,Default,,0,0,0,,{\an9\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:01.00,0:00:02.00,Default,,0,0,0,,{\an1\pos(640,360)\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:02.00,0:00:03.00,Default,,0,0,0,,{\an2\pos(640,360)\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:03.00,0:00:04.00,Default,,0,0,0,,{\an3\pos(640,360)\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:04.00,0:00:05.00,Default,,0,0,0,,{\an4\pos(640,360)\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:05.00,0:00:06.00,Default,,0,0,0,,{\an5\pos(640,360)\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:06.00,0:00:07.00,Default,,0,0,0,,{\an6\pos(640,360)\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:07.00,0:00:08.00,Default,,0,0,0,,{\an7\pos(640,360)\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:08.00,0:00:09.00,Default,,0,0,0,,{\an8\pos(640,360)\frz45}MM\NMM
|
||||
Dialogue: 1,0:00:09.00,0:00:10.00,Default,,0,0,0,,{\an9\pos(640,360)\frz45}MM\NMM
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* libjass
|
||||
*
|
||||
* https://github.com/Arnavion/libjass
|
||||
*
|
||||
* Copyright 2013 Arnav Singh
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
define(["intern!tdd", "require", "tests/support/test-page"], function (tdd, require, TestPage) {
|
||||
tdd.suite("fr", function () {
|
||||
tdd.test("frz", function () {
|
||||
var testPage = new TestPage(this.remote, require.toUrl("tests/support/browser-test-page.html"), "/tests/functional/fr/frz.ass", 1280, 720);
|
||||
return testPage
|
||||
.prepare()
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(0.5, require.toUrl("./frz-01.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(1.5, require.toUrl("./frz-02.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(2.5, require.toUrl("./frz-03.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(3.5, require.toUrl("./frz-04.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(4.5, require.toUrl("./frz-05.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(5.5, require.toUrl("./frz-06.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(6.5, require.toUrl("./frz-07.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(7.5, require.toUrl("./frz-08.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(8.5, require.toUrl("./frz-09.png")); })
|
||||
.then(function (testPage) { return testPage.seekAndCompareScreenshot(9.5, require.toUrl("./frz-10.png")); })
|
||||
.then(function (testPage) { return testPage.done(); });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 19 KiB |