From f67fe00aefd33d764d79b1944411a464876c8415 Mon Sep 17 00:00:00 2001 From: FelisCatus Date: Sat, 20 Sep 2014 23:49:04 +0800 Subject: [PATCH] Initial commit. --- .gitignore | 2 + .tern-project | 9 + AUTHORS | 12 + COPYING | 674 ++ omega-build/Gruntfile.coffee | 22 + omega-build/package.json | 13 + omega-i18n/en/messages.json | 551 ++ omega-i18n/zh_CN/messages.json | 551 ++ omega-pac/.gitignore | 2 + omega-pac/Gruntfile.coffee | 1 + omega-pac/grunt/aliases.coffee | 6 + omega-pac/grunt/browserify.coffee | 28 + omega-pac/grunt/coffeelint.coffee | 20 + omega-pac/grunt/mochaTest.coffee | 6 + omega-pac/grunt/watch.coffee | 10 + omega-pac/index.coffee | 9 + omega-pac/package.json | 27 + omega-pac/src/conditions.coffee | 440 ++ omega-pac/src/pac_generator.coffee | 141 + omega-pac/src/profiles.coffee | 389 + omega-pac/src/rule_list.coffee | 91 + omega-pac/src/shexp_utils.coffee | 51 + omega-pac/src/utils.coffee | 34 + omega-pac/test/conditions.coffee | 183 + omega-pac/test/pac_generator.coffee | 56 + omega-pac/test/profiles.coffee | 199 + omega-pac/test/rule_list.coffee | 236 + omega-pac/test/shexp_utils.coffee | 15 + omega-pac/uglifyjs-shim.js | 2 + omega-pac/uglifyjs.js | 6723 +++++++++++++++++ omega-target-chromium-extension/.gitignore | 4 + .../Gruntfile.coffee | 1 + .../background.coffee | 196 + .../grunt/aliases.coffee | 8 + .../grunt/browserify.coffee | 28 + .../grunt/coffee.coffee | 10 + .../grunt/coffeelint.coffee | 20 + .../grunt/copy.coffee | 28 + .../grunt/mochaTest.coffee | 6 + .../grunt/watch.coffee | 25 + omega-target-chromium-extension/index.coffee | 8 + .../omega_target_shim.js | 1 + .../omega_target_web.coffee | 105 + .../omega_target_web_basics.coffee | 11 + .../overlay/background.html | 17 + .../overlay/js/background_preload.js | 2 + .../overlay/manifest.json | 35 + omega-target-chromium-extension/package.json | 31 + .../src/chrome_api.coffee | 19 + .../src/options.coffee | 217 + .../src/parse_external_profile.coffee | 110 + .../src/storage.coffee | 63 + .../src/tabs.coffee | 60 + .../src/upgrade.coffee | 146 + omega-target/.gitignore | 2 + omega-target/Gruntfile.coffee | 1 + omega-target/grunt/aliases.coffee | 6 + omega-target/grunt/browserify.coffee | 28 + omega-target/grunt/coffeelint.coffee | 20 + omega-target/grunt/mochaTest.coffee | 6 + omega-target/grunt/watch.coffee | 10 + omega-target/index.coffee | 9 + omega-target/omega_pac_shim.js | 1 + omega-target/package.json | 30 + omega-target/src/browser_storage.coffee | 50 + omega-target/src/default_options.coffee | 43 + omega-target/src/log.coffee | 61 + omega-target/src/options.coffee | 619 ++ omega-target/src/storage.coffee | 59 + omega-target/src/utils.coffee | 1 + omega-target/test/conditions.coffee | 193 + omega-target/test/pac_generator.coffee | 56 + omega-target/test/profiles.coffee | 198 + omega-target/test/rule_list.coffee | 211 + omega-target/test/shexp_utils.coffee | 15 + omega-web/.gitignore | 2 + omega-web/Gruntfile.coffee | 1 + omega-web/bower.json | 107 + omega-web/grunt/aliases.coffee | 12 + omega-web/grunt/autoprefixer.coffee | 8 + omega-web/grunt/bower.coffee | 5 + omega-web/grunt/coffee.coffee | 10 + omega-web/grunt/coffeelint.coffee | 20 + omega-web/grunt/copy.coffee | 14 + omega-web/grunt/jade.coffee | 18 + omega-web/grunt/less.coffee | 7 + omega-web/grunt/mochaTest.coffee | 6 + omega-web/grunt/ngAnnotate.coffee | 6 + omega-web/grunt/watch.coffee | 35 + omega-web/img/icons/omega-128.png | Bin 0 -> 2818 bytes omega-web/img/icons/omega-16.png | Bin 0 -> 444 bytes omega-web/img/icons/omega-32.png | Bin 0 -> 833 bytes omega-web/img/icons/omega-48.png | Bin 0 -> 1262 bytes omega-web/img/icons/omega-64.png | Bin 0 -> 1616 bytes omega-web/img/icons/omega.svg | 14 + omega-web/img/icons/omega_svg.js | 37 + omega-web/lib/jquery-ui-1.10.4.custom.min.js | 6 + omega-web/package.json | 25 + omega-web/src/coffee/log_error.coffee | 8 + omega-web/src/coffee/omega_decoration.coffee | 10 + omega-web/src/coffee/options.coffee | 43 + omega-web/src/coffee/popup.coffee | 109 + omega-web/src/coffee/popup_basics.coffee | 18 + omega-web/src/less/options.less | 288 + omega-web/src/less/popup.less | 130 + omega-web/src/omega/app.coffee | 37 + .../omega/controllers/fixed_profile.coffee | 58 + omega-web/src/omega/controllers/io.coffee | 41 + omega-web/src/omega/controllers/master.coffee | 222 + .../src/omega/controllers/profile.coffee | 77 + .../src/omega/controllers/quick_switch.coffee | 15 + .../controllers/rule_list_profile.coffee | 2 + .../omega/controllers/switch_profile.coffee | 57 + omega-web/src/omega/directives.coffee | 32 + omega-web/src/omega/filters.coffee | 19 + omega-web/src/options.jade | 68 + .../src/partials/apply_options_confirm.jade | 11 + .../src/partials/cannot_delete_profile.jade | 17 + omega-web/src/partials/delete_profile.jade | 15 + omega-web/src/partials/general.jade | 15 + omega-web/src/partials/input_group_clear.jade | 7 + omega-web/src/partials/io.jade | 31 + omega-web/src/partials/new_profile.jade | 49 + omega-web/src/partials/profile.jade | 20 + omega-web/src/partials/profile_fixed.jade | 43 + omega-web/src/partials/profile_pac.jade | 12 + omega-web/src/partials/profile_rule_list.jade | 30 + omega-web/src/partials/profile_switch.jade | 52 + .../src/partials/profile_unsupported.jade | 3 + omega-web/src/partials/rename_profile.jade | 16 + .../src/partials/rule_remove_confirm.jade | 19 + .../src/partials/rule_reset_confirm.jade | 15 + omega-web/src/partials/ui.jade | 42 + omega-web/src/popup.jade | 133 + 134 files changed, 15370 insertions(+) create mode 100644 .gitignore create mode 100644 .tern-project create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 omega-build/Gruntfile.coffee create mode 100644 omega-build/package.json create mode 100644 omega-i18n/en/messages.json create mode 100644 omega-i18n/zh_CN/messages.json create mode 100644 omega-pac/.gitignore create mode 100644 omega-pac/Gruntfile.coffee create mode 100644 omega-pac/grunt/aliases.coffee create mode 100644 omega-pac/grunt/browserify.coffee create mode 100644 omega-pac/grunt/coffeelint.coffee create mode 100644 omega-pac/grunt/mochaTest.coffee create mode 100644 omega-pac/grunt/watch.coffee create mode 100644 omega-pac/index.coffee create mode 100644 omega-pac/package.json create mode 100644 omega-pac/src/conditions.coffee create mode 100644 omega-pac/src/pac_generator.coffee create mode 100644 omega-pac/src/profiles.coffee create mode 100644 omega-pac/src/rule_list.coffee create mode 100644 omega-pac/src/shexp_utils.coffee create mode 100644 omega-pac/src/utils.coffee create mode 100644 omega-pac/test/conditions.coffee create mode 100644 omega-pac/test/pac_generator.coffee create mode 100644 omega-pac/test/profiles.coffee create mode 100644 omega-pac/test/rule_list.coffee create mode 100644 omega-pac/test/shexp_utils.coffee create mode 100644 omega-pac/uglifyjs-shim.js create mode 100644 omega-pac/uglifyjs.js create mode 100644 omega-target-chromium-extension/.gitignore create mode 100644 omega-target-chromium-extension/Gruntfile.coffee create mode 100644 omega-target-chromium-extension/background.coffee create mode 100644 omega-target-chromium-extension/grunt/aliases.coffee create mode 100644 omega-target-chromium-extension/grunt/browserify.coffee create mode 100644 omega-target-chromium-extension/grunt/coffee.coffee create mode 100644 omega-target-chromium-extension/grunt/coffeelint.coffee create mode 100644 omega-target-chromium-extension/grunt/copy.coffee create mode 100644 omega-target-chromium-extension/grunt/mochaTest.coffee create mode 100644 omega-target-chromium-extension/grunt/watch.coffee create mode 100644 omega-target-chromium-extension/index.coffee create mode 100644 omega-target-chromium-extension/omega_target_shim.js create mode 100644 omega-target-chromium-extension/omega_target_web.coffee create mode 100644 omega-target-chromium-extension/omega_target_web_basics.coffee create mode 100644 omega-target-chromium-extension/overlay/background.html create mode 100644 omega-target-chromium-extension/overlay/js/background_preload.js create mode 100644 omega-target-chromium-extension/overlay/manifest.json create mode 100644 omega-target-chromium-extension/package.json create mode 100644 omega-target-chromium-extension/src/chrome_api.coffee create mode 100644 omega-target-chromium-extension/src/options.coffee create mode 100644 omega-target-chromium-extension/src/parse_external_profile.coffee create mode 100644 omega-target-chromium-extension/src/storage.coffee create mode 100644 omega-target-chromium-extension/src/tabs.coffee create mode 100644 omega-target-chromium-extension/src/upgrade.coffee create mode 100644 omega-target/.gitignore create mode 100644 omega-target/Gruntfile.coffee create mode 100644 omega-target/grunt/aliases.coffee create mode 100644 omega-target/grunt/browserify.coffee create mode 100644 omega-target/grunt/coffeelint.coffee create mode 100644 omega-target/grunt/mochaTest.coffee create mode 100644 omega-target/grunt/watch.coffee create mode 100644 omega-target/index.coffee create mode 100644 omega-target/omega_pac_shim.js create mode 100644 omega-target/package.json create mode 100644 omega-target/src/browser_storage.coffee create mode 100644 omega-target/src/default_options.coffee create mode 100644 omega-target/src/log.coffee create mode 100644 omega-target/src/options.coffee create mode 100644 omega-target/src/storage.coffee create mode 100644 omega-target/src/utils.coffee create mode 100644 omega-target/test/conditions.coffee create mode 100644 omega-target/test/pac_generator.coffee create mode 100644 omega-target/test/profiles.coffee create mode 100644 omega-target/test/rule_list.coffee create mode 100644 omega-target/test/shexp_utils.coffee create mode 100644 omega-web/.gitignore create mode 100644 omega-web/Gruntfile.coffee create mode 100644 omega-web/bower.json create mode 100644 omega-web/grunt/aliases.coffee create mode 100644 omega-web/grunt/autoprefixer.coffee create mode 100644 omega-web/grunt/bower.coffee create mode 100644 omega-web/grunt/coffee.coffee create mode 100644 omega-web/grunt/coffeelint.coffee create mode 100644 omega-web/grunt/copy.coffee create mode 100644 omega-web/grunt/jade.coffee create mode 100644 omega-web/grunt/less.coffee create mode 100644 omega-web/grunt/mochaTest.coffee create mode 100644 omega-web/grunt/ngAnnotate.coffee create mode 100644 omega-web/grunt/watch.coffee create mode 100644 omega-web/img/icons/omega-128.png create mode 100644 omega-web/img/icons/omega-16.png create mode 100644 omega-web/img/icons/omega-32.png create mode 100644 omega-web/img/icons/omega-48.png create mode 100644 omega-web/img/icons/omega-64.png create mode 100644 omega-web/img/icons/omega.svg create mode 100644 omega-web/img/icons/omega_svg.js create mode 100644 omega-web/lib/jquery-ui-1.10.4.custom.min.js create mode 100644 omega-web/package.json create mode 100644 omega-web/src/coffee/log_error.coffee create mode 100644 omega-web/src/coffee/omega_decoration.coffee create mode 100644 omega-web/src/coffee/options.coffee create mode 100644 omega-web/src/coffee/popup.coffee create mode 100644 omega-web/src/coffee/popup_basics.coffee create mode 100644 omega-web/src/less/options.less create mode 100644 omega-web/src/less/popup.less create mode 100644 omega-web/src/omega/app.coffee create mode 100644 omega-web/src/omega/controllers/fixed_profile.coffee create mode 100644 omega-web/src/omega/controllers/io.coffee create mode 100644 omega-web/src/omega/controllers/master.coffee create mode 100644 omega-web/src/omega/controllers/profile.coffee create mode 100644 omega-web/src/omega/controllers/quick_switch.coffee create mode 100644 omega-web/src/omega/controllers/rule_list_profile.coffee create mode 100644 omega-web/src/omega/controllers/switch_profile.coffee create mode 100644 omega-web/src/omega/directives.coffee create mode 100644 omega-web/src/omega/filters.coffee create mode 100644 omega-web/src/options.jade create mode 100644 omega-web/src/partials/apply_options_confirm.jade create mode 100644 omega-web/src/partials/cannot_delete_profile.jade create mode 100644 omega-web/src/partials/delete_profile.jade create mode 100644 omega-web/src/partials/general.jade create mode 100644 omega-web/src/partials/input_group_clear.jade create mode 100644 omega-web/src/partials/io.jade create mode 100644 omega-web/src/partials/new_profile.jade create mode 100644 omega-web/src/partials/profile.jade create mode 100644 omega-web/src/partials/profile_fixed.jade create mode 100644 omega-web/src/partials/profile_pac.jade create mode 100644 omega-web/src/partials/profile_rule_list.jade create mode 100644 omega-web/src/partials/profile_switch.jade create mode 100644 omega-web/src/partials/profile_unsupported.jade create mode 100644 omega-web/src/partials/rename_profile.jade create mode 100644 omega-web/src/partials/rule_remove_confirm.jade create mode 100644 omega-web/src/partials/rule_reset_confirm.jade create mode 100644 omega-web/src/partials/ui.jade create mode 100644 omega-web/src/popup.jade diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a088b6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +bower_components diff --git a/.tern-project b/.tern-project new file mode 100644 index 0000000..23c97ec --- /dev/null +++ b/.tern-project @@ -0,0 +1,9 @@ +{ + "libs": [ + "chai" + ], + "plugins": { + "node": {}, + "coffee": {} + } +} diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..0b74694 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,12 @@ +# SwitchyOmega authors: + +FelisCatus + +# SwitchyOmega includes or links to (unchanged): +# * jQuery UI (custom build) +# => omega-web/lib/jquery-ui-*.js +# => Copyright 2014 jQuery Foundation and other contributors; Licensed MIT +# * Many npm packages and bower packages +# => **/node_modules, **/bower_components +# => Please refer to their project homepages or npm package pages for the +# copyright and license information of each package. diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/omega-build/Gruntfile.coffee b/omega-build/Gruntfile.coffee new file mode 100644 index 0000000..67c105e --- /dev/null +++ b/omega-build/Gruntfile.coffee @@ -0,0 +1,22 @@ +module.exports = (grunt) -> + submodules = ['omega-pac', 'omega-target', 'omega-web', 'omega-target-*'] + hubConfig = + all: + options: + concurrent: Infinity + src: "../*/Gruntfile.*" + for module in submodules + hubConfig[module] = + src: "../#{module}/Gruntfile.*" + + hubAll = (task) -> "hub:#{module}:#{task}" for module in submodules + + grunt.initConfig { + hub: hubConfig + } + + grunt.loadNpmTasks 'grunt-hub' + + grunt.registerTask 'default', hubAll('default') + grunt.registerTask 'test', hubAll('test') + grunt.registerTask 'watch', ['hub:all:watch'] diff --git a/omega-build/package.json b/omega-build/package.json new file mode 100644 index 0000000..6070ab4 --- /dev/null +++ b/omega-build/package.json @@ -0,0 +1,13 @@ +{ + "name": "omega-build", + "version": "0.0.1", + "private": true, + "devDependencies": { + "grunt": "~0.4.1", + "grunt-hub": "^0.7.0" + }, + "scripts": { + "deps": "npm install && (cd ../omega-pac && npm install); (cd ../omega-target && npm install); (cd ../omega-web && npm install && bower install); (cd ../omega-target-chromium-extension/ && npm install);", + "dev": "(cd ../omega-pac && npm run dev); (cd ../omega-target && npm run dev); (cd ../omega-target-chromium-extension/ && npm run dev);" + } +} diff --git a/omega-i18n/en/messages.json b/omega-i18n/en/messages.json new file mode 100644 index 0000000..535b704 --- /dev/null +++ b/omega-i18n/en/messages.json @@ -0,0 +1,551 @@ +{ + "appNameShort": { + "message": "SwitchyOmega", + "description": "A short name of the application." + }, + + "manifest_app_name": { + "message": "Proxy SwitchyOmega", + "description": "Displayed as the name of the extension." + }, + "manifest_app_description": { + "message": "Manage and switch between multiple proxies quickly & easily.", + "description": "Displayed as a longer description of the extension." + }, + "manifest_icon_default_title": { + "message": "Loading...", + "description": "Displayed when the background page is loading." + }, + + "profile_direct" : { + "message": "[Direct]" + }, + "profile_system" : { + "message": "[System Proxy]" + }, + + "condition_hostWildcard" : { + "message": "Host wildcard" + }, + "condition_hostRegex" : { + "message": "Host regex" + }, + "condition_hostLevels" : { + "message": "Host levels" + }, + "condition_urlWildcard" : { + "message": "URL wildcard" + }, + "condition_urlRegex" : { + "message": "URL regex" + }, + "condition_keyword" : { + "message": "Keyword" + }, + "condition_always" : { + "message": "Always" + }, + "condition_always_details" : { + "message": "(Always matches)" + }, + "condition_never" : { + "message": "Never" + }, + "condition_never_details" : { + "message": "(Never matches)" + }, + + "rulelistFormat_Switchy": { + "message": "Switchy" + }, + "rulelistFormat_AutoProxy": { + "message": "AutoProxy" + }, + + "dialog_close": { + "message": "Close" + }, + "dialog_save": { + "message": "Save changes" + }, + "dialog_ok": { + "message": "OK" + }, + "dialog_cancel": { + "message": "Cancel" + }, + + "inputClear_clear": { + "message": "Clear" + }, + "inputClear_restore": { + "message": "Restore" + }, + + "options_title": { + "message": "SwitchyOmega Options" + }, + "options_navHeader_setting": { + "message": "Settings" + }, + "options_navHeader_profiles": { + "message": "Profiles" + }, + "options_navHeader_actions" : { + "message": "Actions" + }, + "options_tab_ui": { + "message": "Interface" + }, + "options_tab_general": { + "message": "General" + }, + "options_tab_importExport": { + "message": "Import/Export" + }, + "options_newProfile": { + "message": "New profile..." + }, + "options_apply": { + "message": "Apply changes" + }, + "options_discard": { + "message": "Discard changes" + }, + "options_reset": { + "message": "Reset options" + }, + "options_group_miscOptions": { + "message": "Misc Options" + }, + "options_confirmDeletion": { + "message": "Confirm on condition deletion." + }, + "options_refreshOnProfileChange": { + "message": "Refresh current tab on profile change." + }, + "options_group_switchOptions": { + "message": "Switch Options" + }, + "options_startupProfile": { + "message": "Startup Profile" + }, + "options_startupProfile_none": { + "message": "(Current profile)" + }, + "options_quickSwitch": { + "message": "Quick Switch" + }, + "options_cycledProfiles": { + "message": "Cycled Profiles" + }, + "options_cycledProfilesHelp": { + "message": "When you click on the icon, the following profiles will be applied in their order." + }, + "options_cycledProfilesTooFew": { + "message": "You need to select at least 2 profiles to enable this function! You can drag them from the box below." + }, + "options_notCycledProfiles": { + "message": "Not Cycled Profiles" + }, + "options_group_proxyChanges": { + "message": "Proxy Changes" + }, + "options_revertProxyChanges": { + "message": "Revert proxy changes done by other apps." + }, + "options_downloadOptions": { + "message": "Download Options" + }, + "options_downloadOptionsHelp": { + "message": "Configure the update frequency of online rule lists and PAC scripts." + }, + "options_downloadInterval": { + "message": "Download Interval" + }, + "options_downloadInterval_15": { + "message": "15 Minutes" + }, + "options_downloadInterval_60": { + "message": "1 Hour" + }, + "options_downloadInterval_180": { + "message": "3 Hours" + }, + "options_downloadInterval_360": { + "message": "6 Hours" + }, + "options_downloadInterval_720": { + "message": "12 Hours" + }, + "options_downloadInterval_1440": { + "message": "Every day" + }, + "options_downloadInterval_never": { + "message": "Never" + }, + "options_group_importExportProfile": { + "message": "Profile" + }, + "options_exportPacFile": { + "message": "Export as PAC File" + }, + "options_exportPacFileHelp": { + "message": "Export the current profile as a PAC file, so you can use it in other browsers." + }, + "options_group_importExportSettings": { + "message": "Settings" + }, + "options_makeBackup": { + "message": "Make backup" + }, + "options_makeBackupHelp": { + "message": "Make a full backup of your options (including profiles and all other options)." + }, + "options_restoreLocal": { + "message": "Restore from file" + }, + "options_restoreLocalHelp": { + "message": "Restore your SwitchyOmega options from a local file." + }, + "options_restoreOnline": { + "message": "Restore from online" + }, + "options_restoreOnlinePlaceholder": { + "message": "Options file URL (e.g. 'http://example.com/switchy.bak')" + }, + "options_restoreOnlineSubmit": { + "message": "Restore" + }, + "options_profileTabPrefix": { + "message": "Profile :: " + }, + "options_renameProfile": { + "message": "Rename" + }, + "options_deleteProfile": { + "message": "Delete" + }, + "options_profileExportPac": { + "message": "Export PAC" + }, + "options_profileUnsupported": { + "message": "Unsupported profile type $TYPE$!", + "placeholders": { + "type": { + "content": "$1", + "example": "BogusProfile" + } + } + }, + "options_profileUnsupportedHelp": { + "message": "The options could be broken, or from a newer version of this program." + }, + "options_group_proxyServers": { + "message": "Proxy servers" + }, + "options_proxy_scheme": { + "message": "Scheme" + }, + "options_proxy_protocol": { + "message": "Protocol" + }, + "options_proxy_server": { + "message": "Server" + }, + "options_proxy_port": { + "message": "Port" + }, + "options_scheme_default": { + "message": "(default)" + }, + "options_protocol_direct": { + "message": "DIRECT" + }, + "options_protocol_useDefault": { + "message": "(use default)" + }, + "options_proxy_single": { + "message": "Use the proxy above for all protocols." + }, + "options_proxy_expand": { + "message": "Show Advanced" + }, + "options_group_bypassList": { + "message": "Bypass List" + }, + "options_bypassListHelp": { + "message": "Servers for which you do not want to use any proxy: (One server on each line.)" + }, + "options_bypassListHelpLinkText": { + "message": "(Wildcards and more available...)" + }, + "options_group_pacUrl": { + "message": "PAC URL" + }, + "options_pacUrlHelp": { + "message": "The PAC script will be updated from this URL. If it is left blank, the following scripts will be used directly instead." + }, + "options_group_pacScript": { + "message": "PAC Script" + }, + "options_group_ruleListConfig": { + "message": "Rule List Config" + }, + "options_ruleListFormat": { + "message": "Rule List Format" + }, + "options_group_ruleListResult": { + "message": "Rule list result profiles" + }, + "options_ruleListMatchProfile": { + "message": "Match profile" + }, + "options_ruleListDefaultProfile": { + "message": "Default profile" + }, + "options_group_ruleListUrl": { + "message": "Rule List URL" + }, + "options_ruleListUrlHelp": { + "message": "The rule list will be updated from this URL. If it is left blank, the following text will be parsed instead." + }, + "options_group_ruleListText": { + "message": "Rule List Text" + }, + "options_group_switchRules": { + "message": "Switch rules" + }, + "options_sort": { + "message": "Sort" + }, + "options_conditionType": { + "message": "Condition Type" + }, + "options_conditionDetails": { + "message": "Condition Details" + }, + "options_resultProfile": { + "message": "Profile" + }, + "options_conditionActions": { + "message": "Actions" + }, + "options_addCondition": { + "message": "Add condition" + }, + "options_switchDefaultProfile": { + "message": "Default" + }, + "options_hostLevelsBetween": { + "message": "\u2264 host levels \u2264" + }, + "options_modalHeader_applyOptions": { + "message": "Apply Options" + }, + "options_optionsNotSaved": { + "message": "Your modifications to the options have not been saved and will be lost if you proceed!" + }, + "options_applyOptionsRequired": { + "message": "Your changes to the options must be applied before you proceed." + }, + "options_applyOptionsConfirm": { + "message": "Do you want to save and apply the options?" + }, + "options_modalHeader_renameProfile": { + "message": "Rename Profile" + }, + "options_renameProfileName": { + "message": "New profile name" + }, + "options_profileNameConflict": { + "message": "A profile with this name already exists." + }, + "options_modalHeader_deleteProfile": { + "message": "Delete Profile" + }, + "options_deleteProfileConfirm": { + "message": "Do you really want to delete the following profile?" + }, + "options_modalHeader_cannotDeleteProfile": { + "message": "Unable to Delete Profile" + }, + "options_profileReferredBy": { + "message": "This profile cannot be deleted because it is referred by the following profiles:" + }, + "options_modifyReferringProfiles": { + "message": "You must modify these profiles and make them stop referring to this profile before you can delete it." + }, + "options_profileNameEmpty": { + "message": "The name of the profile must not be empty." + }, + "popup_title": { + "message": "SwitchyOmega Popup", + "description": "The page title of the popup. Normally you won't see it." + }, + "options_modalHeader_deleteRule": { + "message": "Delete Rule" + }, + "options_deleteRuleConfirm": { + "message": "Do you really want to delete the following rule?" + }, + "options_deleteRule": { + "message": "Delete" + }, + "options_modalHeader_resetRules" : { + "message": "Reset rules" + }, + "options_resetRulesConfirm" : { + "message": "Are you sure to set the result profile of ALL rules to the following profile?" + }, + "options_resetRules" : { + "message": "Reset rules" + }, + "options_resetRules_help" : { + "message": "Set profile for all rules" + }, + "options_modalHeader_newProfile" : { + "message": "New Profile" + }, + "options_newProfileName": { + "message": "Profile name" + }, + "options_profileType" : { + "message": "Please select the type of the profile:" + }, + "options_profileTypeFixedProfile" : { + "message": "Fixed Profile" + }, + "options_profileDescFixedProfile" : { + "message": "Tunneling traffic through proxy servers." + }, + "options_profileTypePacProfile" : { + "message": "PAC Profile" + }, + "options_profileDescPacProfile" : { + "message": "Choosing proxies using an online/local PAC script." + }, + "options_profileTypeSwitchProfile" : { + "message": "Switch Profile" + }, + "options_profileDescSwitchProfile" : { + "message": "Applying different profiles automatically on various conditions such as domains or patterns. (Replaces AutoSwitch mode.)" + }, + "options_profileTypeRuleListProfile" : { + "message": "Rulelist Profile" + }, + "options_profileDescRuleListProfile" : { + "message": "Reusing an online collection of conditions published by others." + }, + "options_createProfile" : { + "message": "Create" + }, + "options_modalHeader_resetOptions": { + "message": "Reset Options" + }, + "options_resetOptionsConfirm": { + "message": "Do you really want to reset the options? All profiles and settings will be LOST!" + }, + "options_formInvalid": { + "message": "Please correct the errors in this page." + }, + "options_resetSuccess": { + "message": "Options reset." + }, + "options_saveSuccess": { + "message": "Options saved." + }, + "options_importSuccess": { + "message": "Options imported." + }, + "options_importFormatError": { + "message": "Invalid backup file!" + }, + "options_importDownloadError": { + "message": "Error downloading backup file!" + }, + "options_profileDownloadSuccess": { + "message": "Successfully updated profile." + }, + "options_profileDownloadError": { + "message": "Error downloading profile data!" + }, + "options_downloadProfileNow": { + "message": "Download Profile Now" + }, + "popup_externalProfile": { + "message": "(External Profile)" + }, + "popup_externalProfileName": { + "message": "profile name" + }, + "popup_proxyNotControllable_app": { + "message": "The proxy settings are controlled by other app(s) or extension(s). Please disable or uninstall the apps or extensions in conflict." + }, + "popup_proxyNotControllable_policy": { + "message": "The proxy settings are overruled by policies. Please contact your administrator." + }, + "popup_proxyNotControllable_unknown": { + "message": "The proxy settings cannot be controlled. Please check your system and browser settings." + }, + "popup_proxyNotControllableDetails": { + "message": "You cannot switch profiles with SwitchyOmega unless you fix the problem above." + }, + "popup_addConditionTo": { + "message": "Add condition to" + }, + "popup_addCondition": { + "message": "Add condition" + }, + "popup_showOptions": { + "message": "Options" + }, + "popup_reportIssues": { + "message": "Report issues" + }, + "popup_errorLog": { + "message": "Error log" + }, + "browserAction_titleNormal": { + "message": "SwitchyOmega:: $PROFILE$", + "placeholders": { + "profile": { + "content": "$1", + "example": "direct" + } + } + }, + "browserAction_titleWithResult": { + "message": "SwitchyOmega:: $PROFILE$\n$DETAILS$", + "placeholders": { + "profile": { + "content": "$1", + "example": "autoswitch" + }, + "result": { + "content": "$2", + "example": "direct" + }, + "details": { + "content": "$3", + "example": "DIRECT" + } + } + }, + "browserAction_titleNewerOptions": { + "message": "ERROR: A newer version of SwitchOmega is required to load the stored options." + }, + "browserAction_titleOptionError": { + "message": "ERROR: The stored options are corrupted. Click here to RESET OPTIONS." + }, + "browserAction_titleDownloadFail": { + "message": "Warning: Failed to download PAC scripts and/or rule lists." + }, + "browserAction_titleExternalProxy": { + "message": "Note: The proxy settings are currently controlled by other app(s)." + }, + "browserAction_tempRulePrefix": { + "message": "(TEMP) ", + "description": "The prefix to indicate a temp rule on browserAction title. Should be very short." + } +} diff --git a/omega-i18n/zh_CN/messages.json b/omega-i18n/zh_CN/messages.json new file mode 100644 index 0000000..c73d726 --- /dev/null +++ b/omega-i18n/zh_CN/messages.json @@ -0,0 +1,551 @@ +{ + "appNameShort": { + "message": "SwitchyOmega", + "description": "应用的简短名称" + }, + + "manifest_app_name": { + "message": "Proxy SwitchyOmega", + "description": "用作应用的标题" + }, + "manifest_app_description": { + "message": "轻松快捷地管理和切换多个代理设置。", + "description": "作为应用的描述性说明而显示。" + }, + "manifest_icon_default_title": { + "message": "正在加载……", + "description": "应用后台页面正在加载时显示。" + }, + + "profile_direct" : { + "message": "[直接连接]" + }, + "profile_system" : { + "message": "[系统代理]" + }, + + "condition_hostWildcard" : { + "message": "主机通配符" + }, + "condition_hostRegex" : { + "message": "主机正则表达式" + }, + "condition_hostLevels" : { + "message": "主机层数" + }, + "condition_urlWildcard" : { + "message": "网址通配符" + }, + "condition_urlRegex" : { + "message": "网址正则表达式" + }, + "condition_keyword" : { + "message": "关键字" + }, + "condition_always" : { + "message": "总是" + }, + "condition_always_details" : { + "message": "(匹配所有请求)" + }, + "condition_never" : { + "message": "从不" + }, + "condition_never_details" : { + "message": "(不匹配任何请求)" + }, + + "rulelistFormat_Switchy": { + "message": "Switchy" + }, + "rulelistFormat_AutoProxy": { + "message": "AutoProxy" + }, + + "dialog_close": { + "message": "关闭" + }, + "dialog_save": { + "message": "保存更改" + }, + "dialog_ok": { + "message": "确定" + }, + "dialog_cancel": { + "message": "取消" + }, + + "inputClear_clear": { + "message": "清空" + }, + "inputClear_restore": { + "message": "还原" + }, + + "options_title": { + "message": "SwitchyOmega 选项" + }, + "options_navHeader_setting": { + "message": "设定" + }, + "options_navHeader_profiles": { + "message": "情景模式" + }, + "options_navHeader_actions" : { + "message": "操作" + }, + "options_tab_ui": { + "message": "界面" + }, + "options_tab_general": { + "message": "通用" + }, + "options_tab_importExport": { + "message": "导入/导出" + }, + "options_newProfile": { + "message": "新建情景模式..." + }, + "options_apply": { + "message": "应用选项" + }, + "options_discard": { + "message": "撤销更改" + }, + "options_reset": { + "message": "重置选项" + }, + "options_group_miscOptions": { + "message": "其他设置" + }, + "options_confirmDeletion": { + "message": "删除切换条件时需要确认" + }, + "options_refreshOnProfileChange": { + "message": "当更改情景模式时刷新当前标签" + }, + "options_group_switchOptions": { + "message": "切换选项" + }, + "options_startupProfile": { + "message": "初始情景模式" + }, + "options_startupProfile_none": { + "message": "(当前情景模式)" + }, + "options_quickSwitch": { + "message": "快速切换" + }, + "options_cycledProfiles": { + "message": "循环切换以下情景模式:" + }, + "options_cycledProfilesHelp": { + "message": "点击图标时,依次循环切换到以下情景模式。" + }, + "options_cycledProfilesTooFew": { + "message": "必须至少选择2个情景模式才能进行切换。请从下方框中拖动情景模式到此框。" + }, + "options_notCycledProfiles": { + "message": "不循环切换的情景模式 (拖动到上面的框中启用切换)" + }, + "options_group_proxyChanges": { + "message": "代理设置变化" + }, + "options_revertProxyChanges": { + "message": "撤消其他扩展对代理的更改。" + }, + "options_downloadOptions": { + "message": "下载选项" + }, + "options_downloadOptionsHelp": { + "message": "设置规则列表和PAC脚本的更新间隔。" + }, + "options_downloadInterval": { + "message": "更新间隔" + }, + "options_downloadInterval_15": { + "message": "15分钟" + }, + "options_downloadInterval_60": { + "message": "1小时" + }, + "options_downloadInterval_180": { + "message": "3小时" + }, + "options_downloadInterval_360": { + "message": "6小时" + }, + "options_downloadInterval_720": { + "message": "12小时" + }, + "options_downloadInterval_1440": { + "message": "每天一次" + }, + "options_downloadInterval_never": { + "message": "从不更新" + }, + "options_group_importExportProfile": { + "message": "情景模式" + }, + "options_exportPacFile": { + "message": "导出PAC文件" + }, + "options_exportPacFileHelp": { + "message": "导出PAC(代理自动设置)文件,以便在其它浏览器使用。" + }, + "options_group_importExportSettings": { + "message": "选项" + }, + "options_makeBackup": { + "message": "生成备份文件" + }, + "options_makeBackupHelp": { + "message": "导出一份包括情景模式和其他所有选项的备份文件。" + }, + "options_restoreLocal": { + "message": "从备份文件恢复" + }, + "options_restoreLocalHelp": { + "message": "导入本地的备份文件以恢复所有选项。" + }, + "options_restoreOnline": { + "message": "在线恢复" + }, + "options_restoreOnlinePlaceholder": { + "message": "备份文件地址 (如:http://example.com/switchy.bak)" + }, + "options_restoreOnlineSubmit": { + "message": "恢复" + }, + "options_profileTabPrefix": { + "message": "情景模式: " + }, + "options_renameProfile": { + "message": "更改名称" + }, + "options_deleteProfile": { + "message": "删除" + }, + "options_profileExportPac": { + "message": "导出PAC" + }, + "options_profileUnsupported": { + "message": "不支持的情景模式类型: $TYPE$!", + "placeholders": { + "type": { + "content": "$1", + "example": "BogusProfile" + } + } + }, + "options_profileUnsupportedHelp": { + "message": "选项文件已经损坏,或者当前版本过低无法处理选项。" + }, + "options_group_proxyServers": { + "message": "代理服务器" + }, + "options_proxy_scheme": { + "message": "网址协议" + }, + "options_proxy_protocol": { + "message": "代理协议" + }, + "options_proxy_server": { + "message": "代理服务器" + }, + "options_proxy_port": { + "message": "代理端口" + }, + "options_scheme_default": { + "message": "(默认)" + }, + "options_protocol_direct": { + "message": "直接连接" + }, + "options_protocol_useDefault": { + "message": "(同默认)" + }, + "options_proxy_single": { + "message": "对于所有代理使用相同服务器。" + }, + "options_proxy_expand": { + "message": "显示高级设置" + }, + "options_group_bypassList": { + "message": "不代理的地址列表" + }, + "options_bypassListHelp": { + "message": "不经过代理连接的主机列表: (每行一个主机)" + }, + "options_bypassListHelpLinkText": { + "message": "(可使用通配符等匹配规则...)" + }, + "options_group_pacUrl": { + "message": "PAC 网址" + }, + "options_pacUrlHelp": { + "message": "应用将从此网址下载PAC脚本。如果网址留空,则直接使用下方的脚本内容。" + }, + "options_group_pacScript": { + "message": "PAC 脚本" + }, + "options_group_ruleListConfig": { + "message": "规则列表设置" + }, + "options_ruleListFormat": { + "message": "规则列表格式" + }, + "options_group_ruleListResult": { + "message": "规则列表结果情景模式" + }, + "options_ruleListMatchProfile": { + "message": "匹配则使用情景模式" + }, + "options_ruleListDefaultProfile": { + "message": "不匹配则使用情景模式" + }, + "options_group_ruleListUrl": { + "message": "规则列表网址" + }, + "options_ruleListUrlHelp": { + "message": "应用将从此网址下载规则列表。如果网址留空,则以下文本会被直接处理后作为规则列表使用。" + }, + "options_group_ruleListText": { + "message": "规则列表正文" + }, + "options_group_switchRules": { + "message": "切换规则" + }, + "options_sort": { + "message": "排序" + }, + "options_conditionType": { + "message": "条件类型" + }, + "options_conditionDetails": { + "message": "条件设置" + }, + "options_resultProfile": { + "message": "情景模式" + }, + "options_conditionActions": { + "message": "操作" + }, + "options_addCondition": { + "message": "添加条件" + }, + "options_switchDefaultProfile": { + "message": "默认情景模式" + }, + "options_hostLevelsBetween": { + "message": "\u2264 主机层数 \u2264" + }, + "options_modalHeader_applyOptions": { + "message": "应用选项" + }, + "options_optionsNotSaved": { + "message": "当前设置还未保存。如果您继续此操作,则刚才的所有修改都会丢失!" + }, + "options_applyOptionsRequired": { + "message": "必须保存当前选项才能继续操作。" + }, + "options_applyOptionsConfirm": { + "message": "是否保存并应用现在的选项?" + }, + "options_modalHeader_renameProfile": { + "message": "重命名" + }, + "options_renameProfileName": { + "message": "新的名称" + }, + "options_profileNameConflict": { + "message": "已经存在相同名称的情景模式。" + }, + "options_modalHeader_deleteProfile": { + "message": "删除情景模式" + }, + "options_deleteProfileConfirm": { + "message": "真的要删除这个情景模式吗?" + }, + "options_modalHeader_cannotDeleteProfile": { + "message": "情景模式无法删除" + }, + "options_profileReferredBy": { + "message": "这个情景模式仍然被以下情景模式使用,所以无法删除。" + }, + "options_modifyReferringProfiles": { + "message": "修改以上所有情景模式并移除对此情景模式的引用后,方可删除此情景模式。" + }, + "options_profileNameEmpty": { + "message": "情景模式名称不能为空。" + }, + "popup_title": { + "message": "SwitchyOmega 弹出菜单", + "description": "弹出菜单的标题。正常情况下不可见。" + }, + "options_modalHeader_deleteRule": { + "message": "删除规则" + }, + "options_deleteRuleConfirm": { + "message": "真的要删除这个规则吗?" + }, + "options_deleteRule": { + "message": "删除" + }, + "options_modalHeader_resetRules" : { + "message": "重置全部规则" + }, + "options_resetRulesConfirm" : { + "message": "真的要设置所有规则对应的情景模式为以下情景模式吗?" + }, + "options_resetRules" : { + "message": "重置规则" + }, + "options_resetRules_help" : { + "message": "批量设置所有规则的情景模式" + }, + "options_modalHeader_newProfile" : { + "message": "新建情景模式" + }, + "options_newProfileName": { + "message": "情景模式名称" + }, + "options_profileType" : { + "message": "请选择情景模式的类型:" + }, + "options_profileTypeFixedProfile" : { + "message": "代理服务器" + }, + "options_profileDescFixedProfile" : { + "message": "经过代理服务器访问网站。" + }, + "options_profileTypePacProfile" : { + "message": "PAC情景模式" + }, + "options_profileDescPacProfile" : { + "message": "根据在线或本地的PAC脚本选择代理。" + }, + "options_profileTypeSwitchProfile" : { + "message": "自动切换模式" + }, + "options_profileDescSwitchProfile" : { + "message": "根据多种条件,如域名或网址等自动选择情景模式。" + }, + "options_profileTypeRuleListProfile" : { + "message": "规则列表" + }, + "options_profileDescRuleListProfile" : { + "message": "使用他人发布的在线规则列表来切换情景模式。" + }, + "options_createProfile" : { + "message": "创建" + }, + "options_modalHeader_resetOptions": { + "message": "重置选项" + }, + "options_resetOptionsConfirm": { + "message": "真的确定要重置选项吗?如果继续,现有的所有情景模式和选项将会丢失!" + }, + "options_formInvalid": { + "message": "请更正这个页面中的错误。" + }, + "options_resetSuccess": { + "message": "选项已经重置。" + }, + "options_saveSuccess": { + "message": "保存选项成功!" + }, + "options_importSuccess": { + "message": "导入选项成功。" + }, + "options_importFormatError": { + "message": "备份文件格式错误!" + }, + "options_importDownloadError": { + "message": "下载备份文件时出错!" + }, + "options_profileDownloadSuccess": { + "message": "情景模式已经更新成功。" + }, + "options_profileDownloadError": { + "message": "下载情景模式数据时出错。" + }, + "options_downloadProfileNow": { + "message": "立即更新情景模式" + }, + "popup_externalProfile": { + "message": "(外部情景模式)" + }, + "popup_externalProfileName": { + "message": "保存名称" + }, + "popup_proxyNotControllable_app": { + "message": "其他应用正在控制代理设置。请禁用或者卸载发生冲突的应用。" + }, + "popup_proxyNotControllable_policy": { + "message": "代理设置被本地策略强制指定,无法修改。请联系系统管理员。" + }, + "popup_proxyNotControllable_unknown": { + "message": "无法设置代理设置。请检查系统和浏览器设置。" + }, + "popup_proxyNotControllableDetails": { + "message": "如果不解决以上问题,则无法使用SwitchyOmega切换代理。" + }, + "popup_addConditionTo": { + "message": "添加条件到情景模式" + }, + "popup_addCondition": { + "message": "添加条件" + }, + "popup_showOptions": { + "message": "选项" + }, + "popup_reportIssues": { + "message": "反馈问题" + }, + "popup_errorLog": { + "message": "错误日志" + }, + "browserAction_titleNormal": { + "message": "SwitchyOmega:: $PROFILE$", + "placeholders": { + "profile": { + "content": "$1", + "example": "direct" + } + } + }, + "browserAction_titleWithResult": { + "message": "SwitchyOmega:: $PROFILE$\n$DETAILS$", + "placeholders": { + "profile": { + "content": "$1", + "example": "autoswitch" + }, + "result": { + "content": "$2", + "example": "direct" + }, + "details": { + "content": "$3", + "example": "DIRECT" + } + } + }, + "browserAction_titleNewerOptions": { + "message": "错误:需要新版本的SwitchyOmega才能加载当前选项。" + }, + "browserAction_titleOptionError": { + "message": "错误:选项文件已经损坏,点击此处重置选项。" + }, + "browserAction_titleDownloadFail": { + "message": "警告:更新PAC文件或规则列表失败。" + }, + "browserAction_titleExternalProxy": { + "message": "注意:其他应用正在控制当前代理设置。" + }, + "browserAction_tempRulePrefix": { + "message": "(临时) ", + "description": "在图标悬停提示上显示临时规则的前缀。文字应该非常短。" + } +} diff --git a/omega-pac/.gitignore b/omega-pac/.gitignore new file mode 100644 index 0000000..af45663 --- /dev/null +++ b/omega-pac/.gitignore @@ -0,0 +1,2 @@ +/index.js +/omega_pac.min.js diff --git a/omega-pac/Gruntfile.coffee b/omega-pac/Gruntfile.coffee new file mode 100644 index 0000000..522b59e --- /dev/null +++ b/omega-pac/Gruntfile.coffee @@ -0,0 +1 @@ +module.exports = require('load-grunt-config') diff --git a/omega-pac/grunt/aliases.coffee b/omega-pac/grunt/aliases.coffee new file mode 100644 index 0000000..650fde5 --- /dev/null +++ b/omega-pac/grunt/aliases.coffee @@ -0,0 +1,6 @@ +module.exports = + default: [ + 'coffeelint' + 'browserify' + ] + test: ['mochaTest'] diff --git a/omega-pac/grunt/browserify.coffee b/omega-pac/grunt/browserify.coffee new file mode 100644 index 0000000..ce730a9 --- /dev/null +++ b/omega-pac/grunt/browserify.coffee @@ -0,0 +1,28 @@ +module.exports = + index: + files: + 'index.js': 'index.coffee' + options: + transform: ['coffeeify'] + exclude: ['uglify-js', 'ipv6'] + browserifyOptions: + extensions: '.coffee' + builtins: [] + standalone: 'index.coffee' + debug: true + browser: + files: + 'omega_pac.min.js': './index.coffee' + options: + alias: [ + './index.coffee:OmegaPac' + ] + transform: ['coffeeify'] + plugin: + if process.env.BUILD == 'release' + [['minifyify', {map: false}]] + else + [] + browserifyOptions: + extensions: '.coffee' + standalone: 'OmegaPac' diff --git a/omega-pac/grunt/coffeelint.coffee b/omega-pac/grunt/coffeelint.coffee new file mode 100644 index 0000000..518657b --- /dev/null +++ b/omega-pac/grunt/coffeelint.coffee @@ -0,0 +1,20 @@ +module.exports = + options: + arrow_spacing: level: 'error' + colon_assignment_spacing: + level: 'error' + spacing: + left: 0 + right: 1 + line_endings: level: 'error' + missing_fat_arrows: level: 'warn' + newlines_after_classes: level: 'error' + no_empty_functions: level: 'error' + no_empty_param_list: level: 'error' + no_interpolation_in_single_quotes: level: 'error' + no_stand_alone_at: level: 'error' + space_operators: level: 'error' + + gruntfile: ['Gruntfile.coffee'] + tasks: ['grunt/**/*.coffee'] + src: ['src/**/*.coffee', 'test/**/*.coffee'] diff --git a/omega-pac/grunt/mochaTest.coffee b/omega-pac/grunt/mochaTest.coffee new file mode 100644 index 0000000..0bb9539 --- /dev/null +++ b/omega-pac/grunt/mochaTest.coffee @@ -0,0 +1,6 @@ +module.exports = + test: + options: + reporter: 'spec' + require: 'coffee-script/register' + src: ['test/**/*.coffee'] diff --git a/omega-pac/grunt/watch.coffee b/omega-pac/grunt/watch.coffee new file mode 100644 index 0000000..fdc8573 --- /dev/null +++ b/omega-pac/grunt/watch.coffee @@ -0,0 +1,10 @@ +module.exports = + grunt: + options: + reload: true + files: + 'grunt/*' + tasks: ['coffeelint:tasks', 'default'] + src: + files: ['src/**/*.coffee', 'test/**/*.coffee'] + tasks: ['default'] diff --git a/omega-pac/index.coffee b/omega-pac/index.coffee new file mode 100644 index 0000000..6f58934 --- /dev/null +++ b/omega-pac/index.coffee @@ -0,0 +1,9 @@ +module.exports = + Conditions: require('./src/conditions') + PacGenerator: require('./src/pac_generator') + Profiles: require('./src/profiles') + Rulelist: require('./src/rule_list') + ShexpUtils: require('./src/shexp_utils') + +for name, value of require('./src/utils.coffee') + module.exports[name] = value diff --git a/omega-pac/package.json b/omega-pac/package.json new file mode 100644 index 0000000..9a4bab2 --- /dev/null +++ b/omega-pac/package.json @@ -0,0 +1,27 @@ +{ + "name": "omega-pac", + "version": "0.0.1", + "private": true, + "main": "./index.js", + "devDependencies": { + "chai": "~1.9.1", + "coffee-script": "^1.7.1", + "coffeeify": "^0.7.0", + "grunt": "^0.4.5", + "grunt-browserify": "^3.0.0", + "grunt-coffeelint": "^0.0.13", + "grunt-contrib-coffee": "^0.11.1", + "grunt-contrib-watch": "^0.6.1", + "grunt-mocha-test": "~0.11.0", + "load-grunt-config": "^0.13.1", + "minifyify": "^4.1.1" + }, + "dependencies": { + "ipv6": "^3.1.1", + "uglify-js": "^2.4.15" + }, + "browser": { + "uglify-js": "./uglifyjs-shim.js", + "uglify-js-real": "./uglifyjs.js" + } +} diff --git a/omega-pac/src/conditions.coffee b/omega-pac/src/conditions.coffee new file mode 100644 index 0000000..27c0c46 --- /dev/null +++ b/omega-pac/src/conditions.coffee @@ -0,0 +1,440 @@ +U2 = require 'uglify-js' +IP = require 'ipv6' +Url = require 'url' +{shExp2RegExp, escapeSlash} = require './shexp_utils' +{AttachedCache} = require './utils' + +module.exports = exports = + requestFromUrl: (url) -> + if typeof url == 'string' + url = Url.parse url + req = + url: Url.format(url) + host: url.hostname + scheme: url.protocol.replace(':', '') + tag: (condition) -> exports._condCache.tag(condition) + analyze: (condition) -> exports._condCache.get condition, -> { + analyzed: exports._handler(condition.conditionType).analyze.call( + exports, condition) + } + match: (condition, request) -> + cache = exports.analyze(condition) + exports._handler(condition.conditionType).match.call(exports, condition, + request, cache) + compile: (condition) -> + cache = exports.analyze(condition) + return cache.compiled if cache.compiled + handler = exports._handler(condition.conditionType) + cache.compiled = handler.compile.call(exports, condition, cache) + + comment: (comment, node) -> + return unless comment + node.start ?= {} + # This hack is needed to allow dumping comments in repeated print call. + Object.defineProperty node.start, '_comments_dumped', + get: -> false + set: -> false + node.start.comments_before ?= [] + node.start.comments_before.push {type: 'comment2', value: comment} + node + + regTest: (expr, regexp) -> + if typeof regexp == 'string' + # Escape (unescaped) forward slash for use in regex literals. + regexp = new RegExp escapeSlash regexp + if typeof expr == 'string' + expr = new U2.AST_SymbolRef name: expr + new U2.AST_Call + args: [expr] + expression: new U2.AST_Dot( + property: 'test' + expression: new U2.AST_RegExp value: regexp + ) + isInt: (num) -> + (typeof num == 'number' and !isNaN(num) and + parseFloat(num) == parseInt(num, 10)) + between: (val, min, max, comment) -> + if min == max + if typeof min == 'number' + min = new U2.AST_Number value: min + return exports.comment comment, new U2.AST_Binary( + left: val + operator: '===' + right: new U2.AST_Number value: min + ) + if exports.isInt(min) and exports.isInt(max) and max - min < 32 + comment ||= "#{min} <= value && value <= #{max}" + tmpl = "0123456789abcdefghijklmnopqrstuvwxyz" + str = + if max < tmpl.length + tmpl.substr(min, max - min + 1) + else + tmpl.substr(0, max - min + 1) + pos = if min == 0 then val else + new U2.AST_Binary( + left: val + operator: '-' + right: new U2.AST_Number value: min + ) + return exports.comment comment, new U2.AST_Binary( + left: new U2.AST_Call( + expression: new U2.AST_Dot( + expression: new U2.AST_String value: str + property: 'charCodeAt' + ) + args: [pos] + ) + operator: '>' + right: new U2.AST_Number value: 0 + ) + if typeof min == 'number' + min = new U2.AST_Number value: min + if typeof max == 'number' + max = new U2.AST_Number value: max + exports.comment comment, new U2.AST_Call( + args: [val, min, max] + expression: new U2.AST_Function ( + argnames: [ + new U2.AST_SymbolFunarg name: 'value' + new U2.AST_SymbolFunarg name: 'min' + new U2.AST_SymbolFunarg name: 'max' + ] + body: [ + new U2.AST_Return value: new U2.AST_Binary( + left: new U2.AST_Binary( + left: new U2.AST_SymbolRef name: 'min' + operator: '<=' + right: new U2.AST_SymbolRef name: 'value' + ) + operator: '&&' + right: new U2.AST_Binary( + left: new U2.AST_SymbolRef name: 'value' + operator: '<=' + right: new U2.AST_SymbolRef name: 'max' + ) + ) + ] + ) + ) + + parseIp: (ip) -> + if ip.charCodeAt(0) == '['.charCodeAt(0) + ip = ip.substr 1, ip.length - 2 + addr = new IP.v4.Address(ip) + if not addr.isValid() + addr = new IP.v6.Address(ip) + if not addr.isValid() + return null + return addr + normalizeIp: (addr) -> + return (addr.correctForm ? addr.canonicalForm).call(addr) + + localHosts: ["127.0.0.1", "[::1]", "localhost"] + + _condCache: new AttachedCache (condition) -> + condition.conditionType + '$' + + exports._handler(condition.conditionType).tag.apply(exports, arguments) + + _setProp: (obj, prop, value) -> + if not Object::hasOwnProperty.call obj, prop + Object.defineProperty obj, prop, writable: true + obj[prop] = value + + _handler: (conditionType) -> + if typeof conditionType != 'string' + conditionType = conditionType.conditionType + handler = exports._conditionTypes[conditionType] + + if not handler? + throw new Error "Unknown condition type: #{conditionType}" + return handler + + _conditionTypes: + # These functions are .call()-ed with `this` set to module.exports. + # coffeelint: disable=missing_fat_arrows + 'TrueCondition': + tag: (condition) -> '' + analyze: (condition) -> null + match: -> true + compile: (condition) -> new U2.AST_True + 'FalseCondition': + tag: (condition) -> '' + analyze: (condition) -> null + match: -> false + compile: (condition) -> new U2.AST_False + 'UrlRegexCondition': + tag: (condition) -> condition.pattern + analyze: (condition) -> new RegExp escapeSlash condition.pattern + match: (condition, request, cache) -> + return cache.analyzed.test(request.url) + compile: (condition, cache) -> + @regTest 'url', cache.analyzed + + 'UrlWildcardCondition': + tag: (condition) -> condition.pattern + analyze: (condition) -> + parts = for pattern in condition.pattern.split('|') when pattern + shExp2RegExp pattern, trimAsterisk: true + new RegExp parts.join('|') + match: (condition, request, cache) -> + return cache.analyzed.test(request.url) + compile: (condition, cache) -> + @regTest 'url', cache.analyzed + + 'HostRegexCondition': + tag: (condition) -> condition.pattern + analyze: (condition) -> new RegExp escapeSlash condition.pattern + match: (condition, request, cache) -> + return cache.analyzed.test(request.host) + compile: (condition, cache) -> + @regTest 'host', cache.analyzed + + 'HostWildcardCondition': + tag: (condition) -> condition.pattern + analyze: (condition) -> + parts = for pattern in condition.pattern.split('|') when pattern + # Get the magical regex of this pattern. See + # https://github.com/FelisCatus/SwitchyOmega/wiki/Host-wildcard-condition + # for the magic. + if pattern.charCodeAt(0) == '.'.charCodeAt(0) + pattern = '*' + pattern + + if pattern.indexOf('**.') == 0 + shExp2RegExp pattern.substring(1), trimAsterisk: true + else if pattern.indexOf('*.') == 0 + shExp2RegExp(pattern.substring(2), trimAsterisk: true) + .replace(/./, '(?:^|\\.)') + else + shExp2RegExp pattern, trimAsterisk: true + new RegExp parts.join('|') + match: (condition, request, cache) -> + return cache.analyzed.test(request.host) + compile: (condition, cache) -> + @regTest 'host', cache.analyzed + + 'BypassCondition': + tag: (condition) -> condition.pattern + analyze: (condition) -> + # See https://developer.chrome.com/extensions/proxy#bypass_list + cache = + host: null + ip: null + scheme: null + url: null + server = condition.pattern + if server == '' + cache.host = server + return cache + parts = server.split '://' + if parts.length > 1 + cache.scheme = parts[0] + server = parts[1] + + parts = server.split '/' + if parts.length > 1 + cache.ip = + conditionType: 'IpCondition' + ip: parts[0] + prefixLength: parseInt parts[1] + else + if server.charCodeAt(server.length - 1) != ']'.charCodeAt(0) + pos = server.lastIndexOf(':') + if pos >= 0 + matchPort = server.substring(pos + 1) + server = server.substring(0, pos) + serverIp = @parseIp server + serverRegex = null + if serverIp? + if serverIp.regularExpressionString? + # TODO(felis): IPv6 regex is not fully supported by the ipv6 + # module. Even simple addresses like ::1 will fail. Shall we + # implement that instead? + regexStr = serverIp.regularExpressionString() + regexStr = regexStr.substring 2, regexStr.length - 2 + serverRegex = '\\[' + regexStr + '\\]' + else + server = @normalizeIp serverIp + else if server.charCodeAt(0) == '.'.charCodeAt(0) + server = '*' + server + if matchPort + if not serverRegex? + serverRegex = shExp2RegExp(server) + serverRegex = serverRegex.substring(1, serverRegex.length - 1) + scheme = cache.scheme ? '[^:]+' + cache.url = new RegExp('^' + scheme + ':\\/\\/' + serverRegex + + ':' + matchPort + '\\/') + else if server != '*' + if serverRegex + serverRegex = '^' + serverRegex + '$' + else + serverRegex = shExp2RegExp server, trimAsterisk: true + cache.host = new RegExp serverRegex + return cache + match: (condition, request, cache) -> + cache = cache.analyzed + return false if cache.scheme? and cache.scheme != request.scheme + return false if cache.ip? and @match cache.ip, request + if cache.host? + if cache.host == '' + return request.host in @localHosts + else + return false if not cache.host.test(request.host) + return false if cache.url? and !cache.url.test(request.url) + return true + compile: (condition, cache) -> + cache = cache.analyzed + if cache.url? + return @regTest 'url', cache.url + conditions = [] + if cache.host == '' + hostEquals = (host) -> new U2.AST_Binary( + left: new U2.AST_SymbolRef name: 'host' + operator: '===' + right: new U2.AST_String value: host + ) + return new U2.AST_Binary( + left: new U2.AST_Binary( + left: hostEquals '[::1]' + operator: '||' + right: hostEquals 'localhost' + ) + operator: '||' + right: hostEquals '127.0.0.1' + ) + if cache.scheme? + conditions.push new U2.AST_Binary( + left: new U2.AST_SymbolRef name: 'scheme' + operator: '===' + right: new U2.AST_String value: cache.scheme + ) + if cache.host? + conditions.push @regTest 'host', cache.host + else if cache.ip? + conditions.push @compile cache.ip + switch conditions.length + when 0 then new U2.AST_True + when 1 then conditions[0] + when 2 then new U2.AST_Binary( + left: conditions[0] + operator: '&&' + right: conditions[1] + ) + 'KeywordCondition': + tag: (condition) -> condition.pattern + analyze: (condition) -> null + match: (condition, request) -> + request.scheme == 'http' and request.url.indexOf(condition.pattern) >= 0 + compile: (condition) -> + new U2.AST_Binary( + left: new U2.AST_Binary( + left: new U2.AST_SymbolRef name: 'scheme' + operator: '===' + right: new U2.AST_String value: 'http' + ) + operator: '&&' + right: new U2.AST_Binary( + left: new U2.AST_Call( + expression: new U2.AST_Dot( + expression: new U2.AST_SymbolRef name: 'url' + property: 'indexOf' + ) + args: [new U2.AST_String value: condition.pattern] + ) + operator: '>=' + right: new U2.AST_Number value: 0 + ) + ) + + 'IpCondition': + tag: (condition) -> condition.ip + '/' + condition.prefixLength + analyze: (condition) -> + cache = + addr: null + normalized: null + ip = condition.ip + if ip.charCodeAt(0) == '['.charCodeAt(0) + ip = ip.substr 1, ip.length - 2 + addr = ip + '/' + condition.prefixLength + cache.addr = @parseIp addr + if not cache.addr? + throw new Error "Invalid IP address #{addr}" + cache.normalized = @normalizeIp cache.addr + cache.mask = @normalizeIp cache.addr.startAddress() + cache + match: (condition, request, cache) -> + addr = @parseIp addr + return false if not addr? + cache = cache.analyzed + return false if addr.v4 != cache.addr.v4 + return addr.isInSubnet cache.addr + compile: (condition, cache) -> + cache = cache.analyzed + new U2.AST_Call( + expression: new U2.AST_SymbolRef name: 'isInNet' + args: [ + new U2.AST_SymbolRef name: 'host' + new U2.AST_String value: cache.normalized + new U2.AST_String value: cache.mask + ] + ) + 'HostLevelsCondition': + tag: (condition) -> condition.minValue + '~' + condition.maxValue + analyze: (condition) -> '.'.charCodeAt 0 + match: (condition, request, cache) -> + dotCharCode = cache.analyzed + dotCount = 0 + for i in [0...request.host.length] + if request.host.charCodeAt(i) == dotCharCode + dotCount++ + return false if dotCount > condition.maxValue + return dotCount >= condition.minValue + compile: (condition) -> + val = new U2.AST_Dot( + property: 'length' + expression: new U2.AST_Call( + args: [new U2.AST_String value: '.'] + expression: new U2.AST_Dot( + expression: new U2.AST_SymbolRef name: 'host' + property: 'split' + ) + ) + ) + @between(val, condition.minValue + 1, condition.maxValue + 1, + "#{condition.minValue} <= hostLevels <= #{condition.maxValue}") + 'WeekdayCondition': + tag: (condition) -> condition.startDay + '~' + condition.endDay + analyze: (condition) -> null + match: (condition, request) -> + day = new Date().getDay() + return condition.startDay <= day and day <= condition.endDay + compile: (condition) -> + val = new U2.AST_Call( + args: [] + expression: new U2.AST_Dot( + property: 'getDay' + expression: new U2.AST_New( + args: [] + expression: new U2.AST_SymbolRef name: 'Date' + ) + ) + ) + @between val, condition.startDay, condition.endDay + 'TimeCondition': + tag: (condition) -> condition.startHour + '~' + condition.endHour + analyze: (condition) -> null + match: (condition, request) -> + hour = new Date().getHours() + return condition.startHour <= hour and hour <= condition.endHour + compile: (condition) -> + val = new U2.AST_Call( + args: [] + expression: new U2.AST_Dot( + property: 'getHours' + expression: new U2.AST_New( + args: [] + expression: new U2.AST_SymbolRef name: 'Date' + ) + ) + ) + @between val, condition.startHour, condition.endHour + # coffeelint: enable=missing_fat_arrows diff --git a/omega-pac/src/pac_generator.coffee b/omega-pac/src/pac_generator.coffee new file mode 100644 index 0000000..fa3a6ef --- /dev/null +++ b/omega-pac/src/pac_generator.coffee @@ -0,0 +1,141 @@ +U2 = require 'uglify-js' +Profiles = require './profiles' + +# PacGenerator is used like a singleton class instance. +# coffeelint: disable=missing_fat_arrows +module.exports = + ascii: (str) -> + throw new Error "WTF" + str.replace /[\u0080-\uffff]/g, (char) -> + hex = char.charCodeAt(0).toString(16) + result = '\\u' + result += '0' for _ in [hex.length..4] + return result + + compress: (ast) -> + ast.figure_out_scope() + compressor = U2.Compressor(warnings: false, keep_fargs: true, + if_return: false) + compressed_ast = ast.transform(compressor) + compressed_ast.figure_out_scope() + compressed_ast.compute_char_frequency() + compressed_ast.mangle_names() + compressed_ast + + script: (options, profile) -> + if typeof profile == 'string' + profile = Profiles.byName(profile, options) + refs = Profiles.allReferenceSet(profile, options) + profiles = new U2.AST_Object properties: + for key, name of refs when key != '+direct' + new U2.AST_ObjectKeyVal( + key: key + value: Profiles.compile(Profiles.byName(name, options) ? profile), + ) + + factory = new U2.AST_Function( + argnames: [ + new U2.AST_SymbolFunarg name: 'init' + new U2.AST_SymbolFunarg name: 'profiles' + ] + body: [new U2.AST_Return value: new U2.AST_Function( + argnames: [ + new U2.AST_SymbolFunarg name: 'url' + new U2.AST_SymbolFunarg name: 'host' + ] + body: [ + new U2.AST_Directive value: 'use strict' + new U2.AST_Var definitions: [ + new U2.AST_VarDef name: new U2.AST_SymbolVar(name: 'result'), value: + new U2.AST_SymbolRef name: 'init' + new U2.AST_VarDef name: new U2.AST_SymbolVar(name: 'scheme'), value: + new U2.AST_Call( + expression: new U2.AST_Dot( + expression: new U2.AST_SymbolRef name: 'url' + property: 'substr' + ) + args: [ + new U2.AST_Number value: 0 + new U2.AST_Call( + expression: new U2.AST_Dot( + expression: new U2.AST_SymbolRef name: 'url' + property: 'indexOf' + ) + args: [new U2.AST_String value: ':'] + ) + ] + ) + ] + new U2.AST_Do( + body: new U2.AST_BlockStatement body: [ + new U2.AST_SimpleStatement body: new U2.AST_Assign( + left: new U2.AST_SymbolRef name: 'result' + operator: '=' + right: new U2.AST_Sub( + expression: new U2.AST_SymbolRef name: 'profiles' + property: new U2.AST_SymbolRef name: 'result' + ) + ) + new U2.AST_If( + condition: new U2.AST_Binary( + left: new U2.AST_UnaryPrefix( + operator: 'typeof' + expression: new U2.AST_SymbolRef name: 'result' + ) + operator: '===' + right: new U2.AST_String value: 'function' + ) + body: new U2.AST_SimpleStatement body: new U2.AST_Assign( + left: new U2.AST_SymbolRef name: 'result' + operator: '=' + right: new U2.AST_Call( + expression: new U2.AST_SymbolRef name: 'result' + args: [ + new U2.AST_SymbolRef name: 'url' + new U2.AST_SymbolRef name: 'host' + new U2.AST_SymbolRef name: 'scheme' + ] + ) + ) + ) + ] + condition: new U2.AST_Binary( + left: new U2.AST_Binary( + left: new U2.AST_UnaryPrefix( + operator: 'typeof' + expression: new U2.AST_SymbolRef name: 'result' + ) + operator: '!==' + right: new U2.AST_String value: 'string' + ) + operator: '||' + right: new U2.AST_Binary( + left: new U2.AST_Call( + expression: new U2.AST_Dot( + expression: new U2.AST_SymbolRef name: 'result' + property: 'charCodeAt' + ) + args: [new U2.AST_Number(value: 0)] + ) + operator: '===' + right: new U2.AST_Number value: '+'.charCodeAt(0) + ) + ) + ) + new U2.AST_Return value: new U2.AST_SymbolRef name: 'result' + ] + )] + ) + new U2.AST_Toplevel body: [new U2.AST_Var definitions: [ + new U2.AST_VarDef( + name: new U2.AST_SymbolVar name: 'FindProxyForURL' + value: new U2.AST_Call( + expression: factory + args: [ + Profiles.profileResult profile.name + profiles + ] + ) + ) + ]] + # coffeelint: enable=missing_fat_arrows diff --git a/omega-pac/src/profiles.coffee b/omega-pac/src/profiles.coffee new file mode 100644 index 0000000..756ca67 --- /dev/null +++ b/omega-pac/src/profiles.coffee @@ -0,0 +1,389 @@ +U2 = require 'uglify-js' +ShexpUtils = require './shexp_utils' +Conditions = require './conditions' +RuleList = require './rule_list' +{AttachedCache, Revision} = require './utils' + +# coffeelint: disable=camel_case_classes +class AST_Raw extends U2.AST_SymbolRef + # coffeelint: enable=camel_case_classes + constructor: (raw) -> + U2.AST_SymbolRef.call(this, name: raw) + @aborts = -> false + +module.exports = exports = + builtinProfiles: + '+direct': + name: 'direct' + profileType: 'DirectProfile' + color: '#aaaaaa' + builtin: true + '+system': + name: 'system' + profileType: 'SystemProfile' + color: '#aaaaaa' + builtin: true + + schemes: [ + {scheme: 'http', prop: 'proxyForHttp'} + {scheme: 'https', prop: 'proxyForHttps'} + {scheme: 'ftp', prop: 'proxyForFtp'} + {scheme: '', prop: 'fallbackProxy'} + ] + + pacProtocols: { + 'http': 'PROXY' + 'https': 'HTTPS' + 'socks4': 'SOCKS' + 'socks5': 'SOCKS5' + } + + formatByType: { + 'SwitchyRuleListProfile': 'Switchy' + 'AutoProxyRuleListProfile': 'AutoProxy' + } + + ruleListFormats: [ + 'Switchy' + 'AutoProxy' + ] + + parseHostPort: (str, scheme) -> + sep = str.lastIndexOf(':') + port = parseInt(str.substr(sep + 1)) || 80 + host = str.substr(0, sep) + return { + scheme: scheme + host: host + port: port + } + + pacResult: (proxy) -> + if proxy + "#{exports.pacProtocols[proxy.scheme]} #{proxy.host}:#{proxy.port}" + else + 'DIRECT' + + nameAsKey: (profileName) -> + if typeof profileName != 'string' + profileName = profileName.name + '+' + profileName + byName: (profileName, options) -> + if typeof profileName == 'string' + key = exports.nameAsKey(profileName) + profileName = exports.builtinProfiles[key] ? options[key] + profileName + byKey: (key, options) -> + if typeof key == 'string' + key = exports.builtinProfiles[key] ? options[key] + key + + each: (options, callback) -> + charCodePlus = '+'.charCodeAt(0) + for key, profile of options when key.charCodeAt(0) == charCodePlus + callback(key, profile) + for key, profile of exports.builtinProfiles + if key.charCodeAt(0) == charCodePlus + callback(key, profile) + + profileResult: (profileName) -> + key = exports.nameAsKey(profileName) + if key == '+direct' + key = exports.pacResult() + new U2.AST_String value: key + + isIncludable: (profile) -> !!exports._handler(profile).includable + isInclusive: (profile) -> !!exports._handler(profile).inclusive + + updateUrl: (profile) -> exports._handler(profile).updateUrl?(profile) + update: (profile, data) -> exports._handler(profile).update(profile, data) + + tag: (profile) -> exports._profileCache.tag(profile) + create: (profile, opt_profileType) -> + if typeof profile == 'string' + profile = + name: profile + profileType: opt_profileType + else if opt_profileType + profile.profileType = opt_profileType + create = exports._handler(profile).create + return profile unless create + create.call(exports, profile) + profile + updateRevision: (profile, revision) -> + revision ?= Revision.fromTime() + profile.revision = revision + replaceRef: (profile, fromName, toName) -> + return false if not exports.isInclusive(profile) + handler = exports._handler(profile) + handler.replaceRef.call(exports, profile, fromName, toName) + analyze: (profile) -> + cache = exports._profileCache.get profile, {} + if not Object::hasOwnProperty.call(cache, 'analyzed') + analyze = exports._handler(profile).analyze + result = analyze?.call(exports, profile) + cache.analyzed = result + return cache + directReferenceSet: (profile) -> + return {} if not exports.isInclusive(profile) + cache = exports._profileCache.get profile, {} + return cache.directReferenceSet if cache.directReferenceSet + handler = exports._handler(profile) + cache.directReferenceSet = handler.directReferenceSet.call(exports, profile) + allReferenceSet: (profile, options, opt_out) -> + profile = exports.byName(profile, options) + result = opt_out ? {} + result[exports.nameAsKey(profile.name)] = profile.name + for key, name of exports.directReferenceSet(profile) + exports.allReferenceSet(name, options, result) + result + referencedBySet: (profile, options, opt_out) -> + profileKey = exports.nameAsKey(profile) + result = opt_out ? {} + exports.each options, (key, prof) -> + if exports.directReferenceSet(prof)[profileKey] + result[key] = prof.name + exports.referencedBySet(prof, options, result) + result + validResultProfilesFor: (profile, options) -> + profile = exports.byName(profile, options) + return [] if not exports.isInclusive(profile) + profileKey = exports.nameAsKey(profile) + ref = exports.referencedBySet(profile, options) + ref[profileKey] = profileKey + result = [] + exports.each options, (key, prof) -> + if not ref[key] and exports.isIncludable(prof) + result.push(prof) + result + match: (profile, request, opt_profileType) -> + opt_profileType ?= profile.profileType + cache = exports.analyze(profile) + match = exports._handler(opt_profileType).match + match?.call(exports, profile, request, cache) + compile: (profile, opt_profileType) -> + opt_profileType ?= profile.profileType + cache = exports.analyze(profile) + return cache.compiled if cache.compiled + handler = exports._handler(opt_profileType) + cache.compiled = handler.compile.call(exports, profile, cache) + + _profileCache: new AttachedCache (profile) -> profile.revision + + _handler: (profileType) -> + if typeof profileType != 'string' + profileType = profileType.profileType + + handler = profileType + while typeof handler == 'string' + handler = exports._profileTypes[handler] + if not handler? + throw new Error "Unknown profile type: #{profileType}" + return handler + + _profileTypes: + # These functions are .call()-ed with `this` set to module.exports. + # coffeelint: disable=missing_fat_arrows + 'SystemProfile': + compile: (profile) -> + throw new Error "SystemProfile cannot be used in PAC scripts" + 'DirectProfile': + includable: true + compile: (profile) -> + return new U2.AST_String(value: @pacResult()) + 'FixedProfile': + includable: true + create: (profile) -> + profile.bypassList ?= [{ + conditionType: 'BypassCondition' + pattern: '' + }] + match: (profile, request) -> + if profile.bypassList + for cond in profile.bypassList + if Conditions.match(cond, request) + return [@pacResult(), cond] + for s in @schemes when s.scheme == request.scheme and profile[s.prop] + return [@pacResult(profile[s.prop]), s.scheme] + return [@pacResult(profile.fallbackProxy), ''] + compile: (profile) -> + if ((not profile.bypassList or not profile.fallbackProxy) and + not profile.proxyForHttp and not profile.proxyForHttps and + not profile.proxyForFtp) + return new U2.AST_String value: + @pacResult profile.fallbackProxy + body = [ + new U2.AST_Directive value: 'use strict' + ] + if profile.bypassList + conditions = null + for cond in profile.bypassList + condition = Conditions.compile cond + if conditions? + conditions = new U2.AST_Binary( + left: conditions + operator: '||' + right: condition + ) + else + conditions = condition + body.push new U2.AST_If( + condition: conditions + body: new U2.AST_Return value: new U2.AST_String value: @pacResult() + ) + if (not profile.proxyForHttp and not profile.proxyForHttps and + not profile.proxyForFtp) + body.push new U2.AST_Return value: + new U2.AST_String value: @pacResult profile.fallbackProxy + else + body.push new U2.AST_Switch( + expression: new U2.AST_SymbolRef name: 'scheme' + body: for s in @schemes when not s.scheme or profile[s.prop] + ret = [new U2.AST_Return value: + new U2.AST_String value: @pacResult profile[s.prop] + ] + if s.scheme + new U2.AST_Case( + expression: new U2.AST_String value: s.scheme + body: ret + ) + else + new U2.AST_Default body: ret + ) + new U2.AST_Function( + argnames: [ + new U2.AST_SymbolFunarg name: 'url' + new U2.AST_SymbolFunarg name: 'host' + new U2.AST_SymbolFunarg name: 'scheme' + ] + body: body + ) + 'PacProfile': + includable: true + create: (profile) -> + profile.pacScript ?= ''' + function FindProxyForURL(url, host) { + return "DIRECT"; + } + ''' + compile: (profile) -> + new U2.AST_Call args: [new U2.AST_This], expression: + new U2.AST_Dot property: 'call', expression: new U2.AST_Function( + argnames: [] + body: [ + # TODO(catus): Remove the hack needed to insert raw code. + new AST_Raw ';\n' + profile.pacScript + ';' + new U2.AST_Return value: + new U2.AST_SymbolRef name: 'FindProxyForURL' + ] + ) + updateUrl: (profile) -> profile.pacUrl + update: (profile, data) -> + profile.pacScript = data + 'AutoDetectProfile': 'PacProfile' + 'SwitchProfile': + includable: true + inclusive: true + create: (profile) -> + profile.defaultProfileName ?= 'direct' + profile.rules ?= [] + directReferenceSet: (profile) -> + refs = {} + refs[exports.nameAsKey(profile.defaultProfileName)] = + profile.defaultProfileName + for rule in profile.rules + refs[exports.nameAsKey(rule.profileName)] = rule.profileName + refs + analyze: (profile) -> profile.rules + replaceRef: (profile, fromName, toName) -> + changed = false + if profile.defaultProfileName == fromName + profile.defaultProfileName = toName + changed = true + for rule in profile.rules + if rule.profileName == fromName + rule.profileName = toName + changed = true + return changed + match: (profile, request, cache) -> + for rule in cache.analyzed + if Conditions.match(rule.condition, request) + return rule + return [exports.nameAsKey(profile.defaultProfileName), null] + compile: (profile, cache) -> + body = [ + new U2.AST_Directive value: 'use strict' + ] + rules = cache.analyzed + for rule in rules + body.push new U2.AST_If + condition: Conditions.compile rule.condition + body: new U2.AST_Return value: + @profileResult(rule.profileName) + body.push new U2.AST_Return value: + @profileResult profile.defaultProfileName + new U2.AST_Function( + argnames: [ + new U2.AST_SymbolFunarg name: 'url' + new U2.AST_SymbolFunarg name: 'host' + new U2.AST_SymbolFunarg name: 'scheme' + ] + body: body + ) + 'RuleListProfile': + includable: true + inclusive: true + create: (profile) -> + profile.profileType ?= 'RuleListProfile' + profile.format ?= exports.formatByType[profile.profileType] ? 'Switchy' + profile.defaultProfileName ?= 'direct' + profile.matchProfileName ?= 'direct' + profile.ruleList ?= '' + directReferenceSet: (profile) -> + refs = {} + for name in [profile.matchProfileName, profile.defaultProfileName] + refs[exports.nameAsKey(name)] = name + refs + replaceRef: (profile, fromName, toName) -> + changed = false + if profile.defaultProfileName == fromName + profile.defaultProfileName = toName + changed = true + if profile.matchProfileName == fromName + profile.matchProfileName = toName + changed = true + return changed + analyze: (profile) -> + format = profile.format ? exports.formatByType[profile.profileType] + formatHandler = RuleList[format] + if not formatHandler + throw new Error "Unsupported rule list format #{format}!" + ruleList = profile.ruleList + if formatHandler.preprocess? + ruleList = formatHandler.preprocess(ruleList) + return formatHandler.parse(ruleList, profile.matchProfileName, + profile.defaultProfileName) + match: (profile, request) -> + result = exports.match(profile, request, 'SwitchProfile') + compile: (profile) -> + exports.compile(profile, 'SwitchProfile') + updateUrl: (profile) -> profile.sourceUrl + update: (profile, data) -> + original = profile.format ? exports.formatByType[profile.profileType] + profile.profileType = 'RuleListProfile' + format = original + if RuleList[format].detect?(data) == false + # Wrong data for the current format. + format = null + for own formatName of RuleList + result = RuleList[formatName].detect?(data) + if result == true or (result != false and not format?) + profile.format = format = formatName + format ?= original + formatHandler = RuleList[format] + if formatHandler.preprocess? + data = formatHandler.preprocess(data) + profile.ruleList = data + 'SwitchyRuleListProfile': 'RuleListProfile' + 'AutoProxyRuleListProfile': 'RuleListProfile' + # coffeelint: enable=missing_fat_arrows diff --git a/omega-pac/src/rule_list.coffee b/omega-pac/src/rule_list.coffee new file mode 100644 index 0000000..083a538 --- /dev/null +++ b/omega-pac/src/rule_list.coffee @@ -0,0 +1,91 @@ +Buffer = require('buffer').Buffer + +strStartsWith = (str, prefix) -> + str.substr(0, prefix.length) == prefix + +module.exports = exports = + 'AutoProxy': + magicPrefix: 'W0F1dG9Qcm94' # Detect base-64 encoded "[AutoProxy". + detect: (text) -> + if strStartsWith(text, exports['AutoProxy'].magicPrefix) + return true + else if strStartsWith(text, '[AutoProxy') + return true + return + preprocess: (text) -> + text = text.trim() + if strStartsWith(text, exports['AutoProxy'].magicPrefix) + text = new Buffer(text, 'base64').toString('utf8') + return text + parse: (text, matchProfileName, defaultProfileName) -> + text = text.trim() + normal_rules = [] + exclusive_rules = [] + for line in text.split(/\n|\r/) + line = line.trim() + continue if line.length == 0 || line[0] == '!' || line[0] == '[' + source = line + profile = matchProfileName + list = normal_rules + if line[0] == '@' and line[1] == '@' + profile = defaultProfileName + list = exclusive_rules + line = line.substring(2) + cond = + if line[0] == '/' + conditionType: 'UrlRegexCondition' + pattern: line.substring(1, line.length - 1) + else if line[0] == '|' + if line[1] == '|' + conditionType: 'HostWildcardCondition' + pattern: "*." + line.substring(2) + else + conditionType: 'UrlWildcardCondition' + pattern: line.substring(1) + "*" + else if line.indexOf('*') < 0 + conditionType: 'KeywordCondition' + pattern: line + else + conditionType: 'UrlWildcardCondition' + pattern: 'http://*' + line + '*' + list.push({condition: cond, profileName: profile, source: source}) + # Exclusive rules have higher priority, so they come first. + return exclusive_rules.concat normal_rules + 'Switchy': + parse: (text, matchProfileName, defaultProfileName) -> + text = text.trim() + normal_rules = [] + exclusive_rules = [] + begin = false + for line in text.split(/\n|\r/) + line = line.trim() + continue if line.length == 0 || line[0] == ';' + if not begin + if line == '#BEGIN' + begin = true + continue + if line == '#END' + break + if line[0] == '[' and line[line.length - 1] == ']' + section = line.substring(1, line.length - 1) + continue + source = line + profile = matchProfileName + list = normal_rules + if line[0] == '!' + profile = defaultProfileName + list = exclusive_rules + line = line.substring(1) + cond = switch section + when 'Wildcard' + conditionType: 'UrlWildcardCondition' + pattern: line + when 'RegExp' + conditionType: 'UrlRegexCondition' + pattern: line + else + null + if cond? + list.push({condition: cond, profileName: profile, source: source}) + # Exclusive rules have higher priority, so they come first. + return exclusive_rules.concat normal_rules diff --git a/omega-pac/src/shexp_utils.coffee b/omega-pac/src/shexp_utils.coffee new file mode 100644 index 0000000..05931c7 --- /dev/null +++ b/omega-pac/src/shexp_utils.coffee @@ -0,0 +1,51 @@ +module.exports = exports = + regExpMetaChars: do -> + chars = '''[\^$.|?*+(){}/''' + set = {} + for i in [0...chars.length] + set[chars.charCodeAt(i)] = true + set + escapeSlash: (pattern) -> + charCodeSlash = 47 # / + charCodeBackSlash = 92 # \ + escaped = false + start = 0 + result = '' + for i in [0...pattern.length] + code = pattern.charCodeAt(i) + if code == charCodeSlash and not escaped + result += pattern.substring start, i + result += '\\' + start = i + escaped = (code == charCodeBackSlash and not escaped) + result += pattern.substr start + shExp2RegExp: (pattern, options) -> + trimAsterisk = options?.trimAsterisk || false + start = 0 + end = pattern.length + charCodeAsterisk = 42 # '*' + charCodeQuestion = 63 # '?' + if trimAsterisk + while start < end && pattern.charCodeAt(start) == charCodeAsterisk + start++ + while start < end && pattern.charCodeAt(end - 1) == charCodeAsterisk + end-- + if end - start == 1 && pattern.charCodeAt(start) == charCodeAsterisk + return '' + regex = '' + if start == 0 + regex += '^' + for i in [start...end] + code = pattern.charCodeAt(i) + switch code + when charCodeAsterisk then regex += '.*' + when charCodeQuestion then regex += '.' + else + if exports.regExpMetaChars[code] >= 0 + regex += '\\' + regex += pattern[i] + + if end == pattern.length + regex += '$' + + return regex diff --git a/omega-pac/src/utils.coffee b/omega-pac/src/utils.coffee new file mode 100644 index 0000000..d315d11 --- /dev/null +++ b/omega-pac/src/utils.coffee @@ -0,0 +1,34 @@ +Revision = + fromTime: (time) -> + time = if time then new Date(time) else new Date() + return time.getTime().toString(16) + compare: (a, b) -> + return 1 if a.length > b.length + return -1 if a.length < b.length + return 1 if a > b + return -1 if a < b + return 0 + +exports.Revision = Revision + +class AttachedCache + constructor: (opt_prop, @tag) -> + @prop = opt_prop + if typeof @tag == 'undefined' + @tag = opt_prop + @prop = '_cache' + get: (obj, otherwise) -> + tag = @tag(obj) + cache = @_getCache(obj) + if cache? and cache.tag == tag + return cache.value + value = if typeof otherwise == 'function' then otherwise() else otherwise + @_setCache(obj, {tag: tag, value: value}) + return value + _getCache: (obj) -> obj[@prop] + _setCache: (obj, value) -> + if not Object::hasOwnProperty.call obj, @prop + Object.defineProperty obj, @prop, writable: true + obj[@prop] = value + +exports.AttachedCache = AttachedCache diff --git a/omega-pac/test/conditions.coffee b/omega-pac/test/conditions.coffee new file mode 100644 index 0000000..c18d128 --- /dev/null +++ b/omega-pac/test/conditions.coffee @@ -0,0 +1,183 @@ +chai = require 'chai' +should = chai.should() + +describe 'Conditions', -> + Conditions = require '../src/conditions' + U2 = require 'uglify-js' + testCond = (condition, request, should_match) -> + o_request = request + should_match = !!should_match + if typeof request == 'string' + request = Conditions.requestFromUrl(request) + + matchResult = Conditions.match(condition, request) + condExpr = Conditions.compile(condition, request) + testFunc = new U2.AST_Function( + argnames: [ + new U2.AST_SymbolFunarg name: 'url' + new U2.AST_SymbolFunarg name: 'host' + new U2.AST_SymbolFunarg name: 'scheme' + ] + body: [ + new U2.AST_Return value: condExpr + ] + ) + testFunc = eval '(' + testFunc.print_to_string() + ')' + compileResult = testFunc(request.url, request.host, request.scheme) + + friendlyError = (compiled) -> + # Try to give friendly assert messages instead of something like + # "expect true to be false". + printCond = JSON.stringify(condition) + printCompiled = if compiled then 'COMPILED ' else '' + printMatch = if should_match then 'to match' else 'not to match' + msg = ("expect #{printCompiled}condition #{printCond} " + + "#{printMatch} request #{o_request}") + chai.assert(false, msg) + + if matchResult != should_match + friendlyError() + + if compileResult != should_match + friendlyError('compiled') + + return matchResult + + describe 'TrueCondition', -> + it 'should always return true', -> + testCond({conditionType: 'TrueCondition'}, {}, 'match') + describe 'FalseCondition', -> + it 'should always return false', -> + testCond({conditionType: 'FalseCondition'}, {}, not 'match') + describe 'UrlRegexCondition', -> + cond = + conditionType: 'UrlRegexCondition' + pattern: 'example\\.com' + it 'should match requests based on regex pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should not match requests not matching the pattern', -> + testCond(cond, 'http://www.example.net/', not 'match') + it 'should support regex meta chars', -> + con = + conditionType: 'UrlRegexCondition' + pattern: 'exam.*\\.com' + testCond(cond, 'http://www.example.com/', 'match') + describe 'UrlWildcardCondition', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '*example.com*' + it 'should match requests based on wildcard pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should not match requests not matching the pattern', -> + testCond(cond, 'http://www.example.net/', not 'match') + it 'should support wildcard question marks', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '*exam???.com*' + testCond(cond, 'http://www.example.com/', 'match') + it 'should not support regex meta chars', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '.*example.com.*' + testCond(cond, 'http://example.com/', not 'match') + it 'should support multiple patterns in one condition', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '*.example.com/*|*.example.net/*' + testCond(cond, 'http://a.example.com/abc', 'match') + testCond(cond, 'http://b.example.net/def', 'match') + testCond(cond, 'http://c.example.org/ghi', not 'match') + describe 'HostRegexCondition', -> + cond = + conditionType: 'HostRegexCondition' + pattern: '.*\\.example\\.com' + it 'should match requests based on regex pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should not match requests not matching the pattern', -> + testCond(cond, 'http://example.com/', not 'match') + it 'should not match URL parts other than the host', -> + testCond(cond, 'http://example.net/www.example.com') + .should.be.false + + describe 'HostWildcardCondition', -> + cond = + conditionType: 'HostWildcardCondition' + pattern: '*.example.com' + it 'should match requests based on wildcard pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should also match hostname without the optional level', -> + # https://github.com/FelisCatus/SwitchyOmega/wiki/Host-wildcard-condition + testCond(cond, 'http://example.com/', 'match') + it 'should allow override of the magical behavior', -> + con = + conditionType: 'HostWildcardCondition' + pattern: '**.example.com' + testCond(con, 'http://www.example.com/', 'match') + testCond(con, 'http://example.com/', not 'match') + it 'should not match URL parts other than the host', -> + testCond(cond, 'http://example.net/www.example.com') + .should.be.false + it 'should support multiple patterns in one condition', -> + cond = + conditionType: 'HostWildcardCondition' + pattern: '*.example.com|*.example.net' + testCond(cond, 'http://a.example.com/abc', 'match') + testCond(cond, 'http://example.net/def', 'match') + testCond(cond, 'http://c.example.org/ghi', not 'match') + + describe 'BypassCondition', -> + # See https://developer.chrome.com/extensions/proxy#bypass_list + it 'should correctly support patterns containing hosts', -> + cond = + conditionType: 'BypassCondition' + pattern: '.example.com' + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://example.com/', not 'match') + cond.pattern = '*.example.com' + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://example.com/', not 'match') + cond.pattern = 'example.com' + testCond(cond, 'http://example.com/', 'match') + testCond(cond, 'http://www.example.com/', not 'match') + cond.pattern = '*example.com' + testCond(cond, 'http://example.com/', 'match') + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://anotherexample.com/', 'match') + it 'should match the scheme specified in the pattern', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://example.com' + testCond(cond, 'http://example.com/', 'match') + testCond(cond, 'https://example.com/', not 'match') + it 'should match the port specified in the pattern', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://example.com:8080' + testCond(cond, 'http://example.com:8080/', 'match') + testCond(cond, 'http://example.com:888/', not 'match') + it 'should correctly support patterns using IPv4 literals', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://127.0.0.1:8080' + testCond(cond, 'http://127.0.0.1:8080/', 'match') + testCond(cond, 'http://127.0.0.2:8080/', not 'match') + # TODO(felis): Not yet supported. See the code for BypassCondition. + it.skip 'should correctly support IPv6 canonicalization', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://[0:0::1]:8080' + Conditions.analyze(cond) + cond._analyzed().url.should.equal '999' + testCond(cond, 'http://[::1]:8080/', 'match') + testCond(cond, 'http://[1::1]:8080/', not 'match') + + describe 'KeywordCondition', -> + cond = + conditionType: 'KeywordCondition' + pattern: 'example.com' + it 'should match requests based on substring', -> + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://www.example.net/', not 'match') + it 'should not match HTTPS requests', -> + testCond(cond, 'https://example.com/', not 'match') + testCond(cond, 'https://example.net/', not 'match') diff --git a/omega-pac/test/pac_generator.coffee b/omega-pac/test/pac_generator.coffee new file mode 100644 index 0000000..cd84f47 --- /dev/null +++ b/omega-pac/test/pac_generator.coffee @@ -0,0 +1,56 @@ +chai = require 'chai' +should = chai.should() + +describe 'PacGenerator', -> + PacGenerator = require '../src/pac_generator.coffee' + + options = + '+auto': + name: 'auto' + profileType: 'SwitchProfile' + revision: 'test' + defaultProfileName: 'direct' + rules: [ + {profileName: 'proxy', condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://(www|www2)\\.example\\.com/' + } + {profileName: 'direct', condition: + conditionType: 'HostLevelsCondition' + minValue: 3 + maxValue: 8 + } + { + profileName: 'proxy' + condition: {conditionType: 'KeywordCondition', pattern: 'keyword'} + } + {profileName: 'proxy', condition: + conditionType: 'UrlWildcardCondition' + pattern: 'https://ssl.example.com/*' + } + ] + '+proxy': + name: 'proxy' + profileType: 'FixedProfile' + revision: 'test' + fallbackProxy: {scheme: 'http', host: '127.0.0.1', port: 8888} + bypassList: [ + {conditionType: 'BypassCondition', pattern: '127.0.0.1:8080'} + {conditionType: 'BypassCondition', pattern: '127.0.0.1'} + {conditionType: 'BypassCondition', pattern: ''} + ] + + it 'should generate pac scripts from options', -> + ast = PacGenerator.script(options, 'auto') + pac = ast.print_to_string(beautify: true, comments: true) + pac.should.not.be.empty + func = eval("(function () { #{pac}\n return FindProxyForURL; })()") + result = func('http://www.example.com/', 'www.example.com') + result.should.equal('PROXY 127.0.0.1:8888') + it 'should be able to compress pac scripts', -> + ast = PacGenerator.script(options, 'auto') + pac = PacGenerator.compress(ast).print_to_string() + pac.should.not.be.empty + func = eval("(function () { #{pac}\n return FindProxyForURL; })()") + result = func('http://www.example.com/', 'www.example.com') + result.should.equal('PROXY 127.0.0.1:8888') diff --git a/omega-pac/test/profiles.coffee b/omega-pac/test/profiles.coffee new file mode 100644 index 0000000..953034a --- /dev/null +++ b/omega-pac/test/profiles.coffee @@ -0,0 +1,199 @@ +chai = require 'chai' +should = chai.should() + +describe 'Profiles', -> + Profiles = require '../src/profiles' + Conditions = require '../src/conditions' + U2 = require 'uglify-js' + ruleListResult = (profileName, source) -> + profileName: profileName + source: source + testProfile = (profile, request, expected, expectedCompiled) -> + o_request = request + if typeof request == 'string' + request = Conditions.requestFromUrl(request) + expectedCompiled ?= expected[0] ? Profiles.nameAsKey(expected.profileName) + + compiled = Profiles.compile(profile) + compileResult = eval '(' + compiled.print_to_string() + ')' + if typeof compileResult == 'function' + compileResult = compileResult(request.url, request.host, request.scheme) + + if expected? + matchResult = Profiles.match(profile, request) + try + if expected.source? + chai.assert.equal(matchResult.profileName, expected.profileName) + chai.assert.equal(matchResult.source, expected.source) + else + chai.assert.deepEqual(matchResult, expected) + catch + printResult = JSON.stringify(matchResult) + msg = ("expect profile to return #{JSON.stringify(expected)} " + + "instead of #{printResult} for request #{o_request}") + chai.assert(false, msg) + + if compileResult != expectedCompiled + msg = ("expect COMPILED profile to return #{expectedCompiled} " + + "instead of #{compileResult} for request #{o_request}") + chai.assert(false, msg) + + return expected + + describe '#pacResult', -> + it 'should return DIRECT for no proxy', -> + Profiles.pacResult().should.equal("DIRECT") + it 'should return a valid PAC result for a proxy', -> + proxy = {scheme: "http", host: "127.0.0.1", port: 8888} + Profiles.pacResult(proxy).should.equal("PROXY 127.0.0.1:8888") + describe '#byName', -> + it 'should get profiles from builtin profiles', -> + profile = Profiles.byName('direct') + profile.should.be.an('object') + profile.profileType.should.equal('DirectProfile') + it 'should get profiles from given options', -> + profile = {} + profile = Profiles.byName('profile', {"+profile": profile}) + profile.should.equal(profile) + describe 'SystemProfile', -> + it 'should be builtin with the name "system"', -> + profile = Profiles.byName('system') + profile.should.be.an('object') + profile.profileType.should.equal('SystemProfile') + it 'should not match request to profiles', -> + profile = Profiles.byName('system') + should.not.exist Profiles.match(profile, {}) + it 'should throw when trying to compile', -> + profile = Profiles.byName('system') + should.throw(-> Profiles.compile(profile)) + describe 'DirectProfile', -> + it 'should be builtin with the name "direct"', -> + profile = Profiles.byName('direct') + profile.should.be.an('object') + profile.profileType.should.equal('DirectProfile') + it 'should return "DIRECT" when compiled', -> + profile = Profiles.byName('direct') + testProfile(profile, {}, null, 'DIRECT') + describe 'FixedProfile', -> + profile = + profileType: 'FixedProfile' + bypassList: [{ + conditionType: 'BypassCondition' + pattern: '' + }] + proxyForHttps: + scheme: 'http' + host: '127.0.0.1' + port: 1234 + fallbackProxy: + scheme: 'socks5' + host: '127.0.0.1' + port: 1234 + it 'should use protocol-specific proxies if suitable', -> + testProfile(profile, 'https://www.example.com/', + ['PROXY 127.0.0.1:1234', 'https']) + it 'should use fallback proxies for other protocols', -> + testProfile(profile, 'ftp://www.example.com/', + ['SOCKS5 127.0.0.1:1234', '']) + it 'should not use any proxy for requests matching the bypassList', -> + testProfile profile, 'ftp://localhost/', ['DIRECT', profile.bypassList[0]] + describe 'PacProfile', -> + profile = Profiles.create('test', 'PacProfile') + profile.pacScript = ''' + function FindProxyForURL(url, host) { + return "PROXY " + host + ":8080"; + } + ''' + it 'should return the result of the pac script', -> + testProfile(profile, 'ftp://www.example.com:9999/abc', null, + 'PROXY www.example.com:8080') + describe 'SwitchProfile', -> + profile = Profiles.create('test', 'SwitchProfile') + profile.rules = [ + { + condition: + conditionType: 'HostWildcardCondition' + pattern: 'company.abc.example.com' + profileName: 'company' + }, + { + condition: + conditionType: 'HostWildcardCondition' + pattern: '*.example.com' + profileName: 'example' + }, + { + condition: + conditionType: 'HostWildcardCondition' + pattern: '*.abc.example.com' + profileName: 'abc' + } + ] + profile.defaultProfileName = 'default' + it 'should match requests based on rules', -> + testProfile(profile, 'http://company.abc.example.com:998/abc', + profile.rules[0]) + it 'should respect the order of rules', -> + testProfile(profile, 'http://abc.example.com:9999/abc', + profile.rules[1]) + testProfile(profile, 'http://www.example.com:9999/abc', + profile.rules[1]) + it 'should return defaultProfileName when no rules match', -> + testProfile(profile, 'http://www.example.org:9999/abc', + ['+default', null]) + it 'should calulate directly referenced profiles correctly', -> + set = Profiles.directReferenceSet(profile) + set.should.eql( + '+company': 'company' + '+example': 'example' + '+abc': 'abc' + '+default': 'default' + ) + it 'should clear the reference cache on profile revision change', -> + profile.revision = 'a' + set = Profiles.directReferenceSet(profile) + # Remove 'default' from references. + profile.defaultProfileName = 'abc' + profile.revision = 'b' + newSet = Profiles.directReferenceSet(profile) + newSet.should.eql( + '+company': 'company' + '+example': 'example' + '+abc': 'abc' + ) + describe 'RulelistProfile', -> + profile = Profiles.create('test', 'AutoProxyRuleListProfile') + profile.defaultProfileName = 'default' + profile.matchProfileName = 'example' + profile.ruleList = 'example.com' + profile.revision = 'a' + it 'should calulate directly referenced profiles correctly', -> + set = Profiles.directReferenceSet(profile) + set.should.eql( + '+example': 'example' + '+default': 'default' + ) + it 'should match requests based on the rule list', -> + testProfile(profile, 'http://localhost/example.com', + ruleListResult('example', 'example.com')) + testProfile(profile, 'http://localhost/example.org', ['+default', null]) + it 'should update rule list on update', -> + Profiles.update(profile, 'example.org') + profile.revision = 'b' + testProfile(profile, 'http://localhost/example.com', ['+default', null]) + testProfile(profile, 'http://localhost/example.org', + ruleListResult('example', 'example.org')) + it 'should switch to AutoProxy format on update if detected', -> + profile = Profiles.create('test2', 'RuleListProfile') + profile.format = 'Switchy' + profile.defaultProfileName = 'default' + profile.matchProfileName = 'example' + + profile.format.should.equal 'Switchy' + Profiles.update(profile, '[AutoProxy]\nexample.org') + profile.format.should.equal 'AutoProxy' + + testProfile(profile, 'http://localhost/example.com', + ['+default', null]) + testProfile(profile, 'http://localhost/example.org', + ruleListResult('example', 'example.org')) diff --git a/omega-pac/test/rule_list.coffee b/omega-pac/test/rule_list.coffee new file mode 100644 index 0000000..2b483da --- /dev/null +++ b/omega-pac/test/rule_list.coffee @@ -0,0 +1,236 @@ +chai = require 'chai' +should = chai.should() + +describe 'RuleList', -> + RuleList = require '../src/rule_list' + describe 'AutoProxy', -> + parse = RuleList['AutoProxy'].parse + it 'should parse keyword conditions', -> + line = 'example.com' + result = parse(line, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + source: line + profileName: 'match' + condition: + conditionType: 'KeywordCondition' + pattern: 'example.com' + ) + it 'should parse keyword conditions with asterisks', -> + line = 'example*.com' + result = parse(line, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + source: line + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'http://*example*.com*' + ) + it 'should parse host conditions', -> + line = '||example.com' + result = parse(line, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + source: line + profileName: 'match' + condition: + conditionType: 'HostWildcardCondition' + pattern: '*.example.com' + ) + it 'should parse "starts-with" conditions', -> + line = '|https://ssl.example.com' + result = parse(line, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + source: line + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'https://ssl.example.com*' + ) + it 'should parse "starts-with" conditions for the HTTP scheme', -> + line = '|http://example.com' + result = parse(line, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + source: line + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'http://example.com*' + ) + it 'should parse url regex conditions', -> + line = '/^https?:\\/\\/[^\\/]+example\.com/' + result = parse(line, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + source: line + profileName: 'match' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^https?:\\/\\/[^\\/]+example\.com' + ) + it 'should ignore comment lines', -> + result = parse('!example.com', 'match', 'notmatch') + result.should.have.length(0) + it 'should parse multiple lines', -> + result = parse 'example.com\n!comment\n||example.com', 'match', 'notmatch' + result.should.have.length(2) + result[0].should.eql( + source: 'example.com' + profileName: 'match' + condition: + conditionType: 'KeywordCondition' + pattern: 'example.com' + ) + result[1].should.eql( + source: '||example.com' + profileName: 'match' + condition: + conditionType: 'HostWildcardCondition' + pattern: '*.example.com' + ) + it 'should put exclusive rules first', -> + result = parse 'example.com\n@@||example.com', 'match', 'notmatch' + result.should.have.length(2) + result[0].should.eql( + source: '@@||example.com' + profileName: 'notmatch' + condition: + conditionType: 'HostWildcardCondition' + pattern: '*.example.com' + ) + result[1].should.eql( + source: 'example.com' + profileName: 'match' + condition: + conditionType: 'KeywordCondition' + pattern: 'example.com' + ) + + describe 'Switchy', -> + parse = RuleList['Switchy'].parse + compose = (sections) -> + list = '#BEGIN\r\n\r\n' + for sec, rules of sections + list += "[#{sec}]\r\n" + for rule in rules + list += rule + list += '\r\n' + list += '\r\n\r\n#END\r\n' + it 'should parse empty rule lists', -> + list = compose {} + result = parse(list, 'match', 'notmatch') + result.should.have.length(0) + it 'should ignore stuff before #BEGIN or after #END.', -> + list = compose {} + list += '[RegExp]\r\ntest\r\n' + list = '[Wildcard]\r\ntest\r\n' + list + result = parse(list, 'match', 'notmatch') + result.should.have.length(0) + it 'should parse wildcard rules', -> + list = compose 'Wildcard': [ + '*://example.com/*' + ] + result = parse(list, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + source: '*://example.com/*' + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: '*://example.com/*' + ) + it 'should parse RegExp rules', -> + list = compose 'RegExp': [ + '^http://www\.example\.com/.*' + ] + result = parse(list, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + source: '^http://www\.example\.com/.*' + profileName: 'match' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://www\.example\.com/.*' + ) + it 'should parse exclusive rules', -> + list = compose 'RegExp': [ + '!^http://www\.example\.com/.*' + ] + result = parse(list, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + source: '!^http://www\.example\.com/.*' + profileName: 'notmatch' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://www\.example\.com/.*' + ) + it 'should parse multiple rules in multiple sections', -> + list = compose { + 'Wildcard': [ + 'http://www.example.com/*' + 'http://example.com/*' + ] + 'RegExp': [ + '^http://www\.example\.com/.*' + '^http://example\.com/.*' + ] + } + result = parse(list, 'match', 'notmatch') + result.should.have.length(4) + result[0].should.eql( + source: 'http://www.example.com/*' + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'http://www.example.com/*' + ) + result[1].should.eql( + source: 'http://example.com/*' + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'http://example.com/*' + ) + result[2].should.eql( + source: '^http://www\.example\.com/.*' + profileName: 'match' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://www\.example\.com/.*' + ) + result[3].should.eql( + source: '^http://example\.com/.*' + profileName: 'match' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://example\.com/.*' + ) + it 'should put exclusive rules first', -> + list = compose { + 'Wildcard': [ + 'http://www\.example\.com/*' + ] + 'RegExp': [ + '!^http://www\.example\.com/.*' + ] + } + result = parse(list, 'match', 'notmatch') + result.should.have.length(2) + result[0].should.eql( + source: '!^http://www\.example\.com/.*' + profileName: 'notmatch' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://www.example\.com/.*' + ) + result[1].should.eql( + source: 'http://www\.example\.com/*' + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'http://www.example.com/*' + ) diff --git a/omega-pac/test/shexp_utils.coffee b/omega-pac/test/shexp_utils.coffee new file mode 100644 index 0000000..56452cc --- /dev/null +++ b/omega-pac/test/shexp_utils.coffee @@ -0,0 +1,15 @@ +chai = require 'chai' +should = chai.should() + +describe 'ShexpUtils', -> + ShexpUtils = require '../src/shexp_utils' + describe '#escapeSlash', -> + it 'should escape all forward slashes', -> + regex = ShexpUtils.escapeSlash '/test/' + regex.should.equal '\\/test\\/' + it 'should not escape slashes that are already escaped', -> + regex = ShexpUtils.escapeSlash '\\/test\\/' + regex.should.equal '\\/test\\/' + it 'should know the difference between escaped and unescaped slashes', -> + regex = ShexpUtils.escapeSlash '\\\\/\\/test\\/' + regex.should.equal '\\\\\\/\\/test\\/' diff --git a/omega-pac/uglifyjs-shim.js b/omega-pac/uglifyjs-shim.js new file mode 100644 index 0000000..70f40d4 --- /dev/null +++ b/omega-pac/uglifyjs-shim.js @@ -0,0 +1,2 @@ +require('uglify-js-real'); +module.exports = UglifyJS; diff --git a/omega-pac/uglifyjs.js b/omega-pac/uglifyjs.js new file mode 100644 index 0000000..d0e268d --- /dev/null +++ b/omega-pac/uglifyjs.js @@ -0,0 +1,6723 @@ +(function(exports, global) { + global["UglifyJS"] = exports; + "use strict"; + function array_to_hash(a) { + var ret = Object.create(null); + for (var i = 0; i < a.length; ++i) ret[a[i]] = true; + return ret; + } + function slice(a, start) { + return Array.prototype.slice.call(a, start || 0); + } + function characters(str) { + return str.split(""); + } + function member(name, array) { + for (var i = array.length; --i >= 0; ) if (array[i] == name) return true; + return false; + } + function find_if(func, array) { + for (var i = 0, n = array.length; i < n; ++i) { + if (func(array[i])) return array[i]; + } + } + function repeat_string(str, i) { + if (i <= 0) return ""; + if (i == 1) return str; + var d = repeat_string(str, i >> 1); + d += d; + if (i & 1) d += str; + return d; + } + function DefaultsError(msg, defs) { + Error.call(this, msg); + this.msg = msg; + this.defs = defs; + } + DefaultsError.prototype = Object.create(Error.prototype); + DefaultsError.prototype.constructor = DefaultsError; + DefaultsError.croak = function(msg, defs) { + throw new DefaultsError(msg, defs); + }; + function defaults(args, defs, croak) { + if (args === true) args = {}; + var ret = args || {}; + if (croak) for (var i in ret) if (ret.hasOwnProperty(i) && !defs.hasOwnProperty(i)) DefaultsError.croak("`" + i + "` is not a supported option", defs); + for (var i in defs) if (defs.hasOwnProperty(i)) { + ret[i] = args && args.hasOwnProperty(i) ? args[i] : defs[i]; + } + return ret; + } + function merge(obj, ext) { + for (var i in ext) if (ext.hasOwnProperty(i)) { + obj[i] = ext[i]; + } + return obj; + } + function noop() {} + var MAP = function() { + function MAP(a, f, backwards) { + var ret = [], top = [], i; + function doit() { + var val = f(a[i], i); + var is_last = val instanceof Last; + if (is_last) val = val.v; + if (val instanceof AtTop) { + val = val.v; + if (val instanceof Splice) { + top.push.apply(top, backwards ? val.v.slice().reverse() : val.v); + } else { + top.push(val); + } + } else if (val !== skip) { + if (val instanceof Splice) { + ret.push.apply(ret, backwards ? val.v.slice().reverse() : val.v); + } else { + ret.push(val); + } + } + return is_last; + } + if (a instanceof Array) { + if (backwards) { + for (i = a.length; --i >= 0; ) if (doit()) break; + ret.reverse(); + top.reverse(); + } else { + for (i = 0; i < a.length; ++i) if (doit()) break; + } + } else { + for (i in a) if (a.hasOwnProperty(i)) if (doit()) break; + } + return top.concat(ret); + } + MAP.at_top = function(val) { + return new AtTop(val); + }; + MAP.splice = function(val) { + return new Splice(val); + }; + MAP.last = function(val) { + return new Last(val); + }; + var skip = MAP.skip = {}; + function AtTop(val) { + this.v = val; + } + function Splice(val) { + this.v = val; + } + function Last(val) { + this.v = val; + } + return MAP; + }(); + function push_uniq(array, el) { + if (array.indexOf(el) < 0) array.push(el); + } + function string_template(text, props) { + return text.replace(/\{(.+?)\}/g, function(str, p) { + return props[p]; + }); + } + function remove(array, el) { + for (var i = array.length; --i >= 0; ) { + if (array[i] === el) array.splice(i, 1); + } + } + function mergeSort(array, cmp) { + if (array.length < 2) return array.slice(); + function merge(a, b) { + var r = [], ai = 0, bi = 0, i = 0; + while (ai < a.length && bi < b.length) { + cmp(a[ai], b[bi]) <= 0 ? r[i++] = a[ai++] : r[i++] = b[bi++]; + } + if (ai < a.length) r.push.apply(r, a.slice(ai)); + if (bi < b.length) r.push.apply(r, b.slice(bi)); + return r; + } + function _ms(a) { + if (a.length <= 1) return a; + var m = Math.floor(a.length / 2), left = a.slice(0, m), right = a.slice(m); + left = _ms(left); + right = _ms(right); + return merge(left, right); + } + return _ms(array); + } + function set_difference(a, b) { + return a.filter(function(el) { + return b.indexOf(el) < 0; + }); + } + function set_intersection(a, b) { + return a.filter(function(el) { + return b.indexOf(el) >= 0; + }); + } + function makePredicate(words) { + if (!(words instanceof Array)) words = words.split(" "); + if (typeof UglifyJS_NoUnsafeEval !== "undefined") { + return function(str) { + return words.indexOf(str) >= 0; + }; + } + var f = "", cats = []; + out: for (var i = 0; i < words.length; ++i) { + for (var j = 0; j < cats.length; ++j) if (cats[j][0].length == words[i].length) { + cats[j].push(words[i]); + continue out; + } + cats.push([ words[i] ]); + } + function compareTo(arr) { + if (arr.length == 1) return f += "return str === " + JSON.stringify(arr[0]) + ";"; + f += "switch(str){"; + for (var i = 0; i < arr.length; ++i) f += "case " + JSON.stringify(arr[i]) + ":"; + f += "return true}return false;"; + } + if (cats.length > 3) { + cats.sort(function(a, b) { + return b.length - a.length; + }); + f += "switch(str.length){"; + for (var i = 0; i < cats.length; ++i) { + var cat = cats[i]; + f += "case " + cat[0].length + ":"; + compareTo(cat); + } + f += "}"; + } else { + compareTo(words); + } + return new Function("str", f); + } + function all(array, predicate) { + for (var i = array.length; --i >= 0; ) if (!predicate(array[i])) return false; + return true; + } + function Dictionary() { + this._values = Object.create(null); + this._size = 0; + } + Dictionary.prototype = { + set: function(key, val) { + if (!this.has(key)) ++this._size; + this._values["$" + key] = val; + return this; + }, + add: function(key, val) { + if (this.has(key)) { + this.get(key).push(val); + } else { + this.set(key, [ val ]); + } + return this; + }, + get: function(key) { + return this._values["$" + key]; + }, + del: function(key) { + if (this.has(key)) { + --this._size; + delete this._values["$" + key]; + } + return this; + }, + has: function(key) { + return "$" + key in this._values; + }, + each: function(f) { + for (var i in this._values) f(this._values[i], i.substr(1)); + }, + size: function() { + return this._size; + }, + map: function(f) { + var ret = []; + for (var i in this._values) ret.push(f(this._values[i], i.substr(1))); + return ret; + } + }; + "use strict"; + function DEFNODE(type, props, methods, base) { + if (arguments.length < 4) base = AST_Node; + if (!props) props = []; else props = props.split(/\s+/); + var self_props = props; + if (base && base.PROPS) props = props.concat(base.PROPS); + var proto = base && new base(); + var ctor; + if (typeof UglifyJS_NoUnsafeEval === "undefined") { + var code = "return function AST_" + type + "(props){ if (props) { "; + for (var i = props.length; --i >= 0; ) { + code += "this." + props[i] + " = props." + props[i] + ";"; + } + if (proto && proto.initialize || methods && methods.initialize) code += "this.initialize();"; + code += "}}"; + ctor = new Function(code)(); + } else { + ctor = function(values) { + if (values) { + for (var i = props.length; --i >= 0; ) this[props[i]] = values[props[i]]; + if (proto && proto.initialize || methods && methods.initialize) this.initialize(); + } + }; + } + if (proto) { + ctor.prototype = proto; + ctor.BASE = base; + } + if (base) base.SUBCLASSES.push(ctor); + ctor.prototype.CTOR = ctor; + ctor.PROPS = props || null; + ctor.SELF_PROPS = self_props; + ctor.SUBCLASSES = []; + if (type) { + ctor.prototype.TYPE = ctor.TYPE = type; + } + if (methods) for (i in methods) if (methods.hasOwnProperty(i)) { + if (/^\$/.test(i)) { + ctor[i.substr(1)] = methods[i]; + } else { + ctor.prototype[i] = methods[i]; + } + } + ctor.DEFMETHOD = function(name, method) { + this.prototype[name] = method; + }; + return ctor; + } + var AST_Token = DEFNODE("Token", "type value line col pos endpos nlb comments_before file", {}, null); + var AST_Node = DEFNODE("Node", "start end", { + clone: function() { + return new this.CTOR(this); + }, + $documentation: "Base class of all AST nodes", + $propdoc: { + start: "[AST_Token] The first token of this node", + end: "[AST_Token] The last token of this node" + }, + _walk: function(visitor) { + return visitor._visit(this); + }, + walk: function(visitor) { + return this._walk(visitor); + } + }, null); + AST_Node.warn_function = null; + AST_Node.warn = function(txt, props) { + if (AST_Node.warn_function) AST_Node.warn_function(string_template(txt, props)); + }; + var AST_Statement = DEFNODE("Statement", null, { + $documentation: "Base class of all statements" + }); + var AST_Debugger = DEFNODE("Debugger", null, { + $documentation: "Represents a debugger statement" + }, AST_Statement); + var AST_Directive = DEFNODE("Directive", "value scope", { + $documentation: 'Represents a directive, like "use strict";', + $propdoc: { + value: "[string] The value of this directive as a plain string (it's not an AST_String!)", + scope: "[AST_Scope/S] The scope that this directive affects" + } + }, AST_Statement); + var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", { + $documentation: "A statement consisting of an expression, i.e. a = 1 + 2", + $propdoc: { + body: "[AST_Node] an expression node (should not be instanceof AST_Statement)" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.body._walk(visitor); + }); + } + }, AST_Statement); + function walk_body(node, visitor) { + if (node.body instanceof AST_Statement) { + node.body._walk(visitor); + } else node.body.forEach(function(stat) { + stat._walk(visitor); + }); + } + var AST_Block = DEFNODE("Block", "body", { + $documentation: "A body of statements (usually bracketed)", + $propdoc: { + body: "[AST_Statement*] an array of statements" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + walk_body(this, visitor); + }); + } + }, AST_Statement); + var AST_BlockStatement = DEFNODE("BlockStatement", null, { + $documentation: "A block statement" + }, AST_Block); + var AST_EmptyStatement = DEFNODE("EmptyStatement", null, { + $documentation: "The empty statement (empty block or simply a semicolon)", + _walk: function(visitor) { + return visitor._visit(this); + } + }, AST_Statement); + var AST_StatementWithBody = DEFNODE("StatementWithBody", "body", { + $documentation: "Base class for all statements that contain one nested body: `For`, `ForIn`, `Do`, `While`, `With`", + $propdoc: { + body: "[AST_Statement] the body; this should always be present, even if it's an AST_EmptyStatement" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.body._walk(visitor); + }); + } + }, AST_Statement); + var AST_LabeledStatement = DEFNODE("LabeledStatement", "label", { + $documentation: "Statement with a label", + $propdoc: { + label: "[AST_Label] a label definition" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.label._walk(visitor); + this.body._walk(visitor); + }); + } + }, AST_StatementWithBody); + var AST_IterationStatement = DEFNODE("IterationStatement", null, { + $documentation: "Internal class. All loops inherit from it." + }, AST_StatementWithBody); + var AST_DWLoop = DEFNODE("DWLoop", "condition", { + $documentation: "Base class for do/while statements", + $propdoc: { + condition: "[AST_Node] the loop condition. Should not be instanceof AST_Statement" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.condition._walk(visitor); + this.body._walk(visitor); + }); + } + }, AST_IterationStatement); + var AST_Do = DEFNODE("Do", null, { + $documentation: "A `do` statement" + }, AST_DWLoop); + var AST_While = DEFNODE("While", null, { + $documentation: "A `while` statement" + }, AST_DWLoop); + var AST_For = DEFNODE("For", "init condition step", { + $documentation: "A `for` statement", + $propdoc: { + init: "[AST_Node?] the `for` initialization code, or null if empty", + condition: "[AST_Node?] the `for` termination clause, or null if empty", + step: "[AST_Node?] the `for` update clause, or null if empty" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + if (this.init) this.init._walk(visitor); + if (this.condition) this.condition._walk(visitor); + if (this.step) this.step._walk(visitor); + this.body._walk(visitor); + }); + } + }, AST_IterationStatement); + var AST_ForIn = DEFNODE("ForIn", "init name object", { + $documentation: "A `for ... in` statement", + $propdoc: { + init: "[AST_Node] the `for/in` initialization code", + name: "[AST_SymbolRef?] the loop variable, only if `init` is AST_Var", + object: "[AST_Node] the object that we're looping through" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.init._walk(visitor); + this.object._walk(visitor); + this.body._walk(visitor); + }); + } + }, AST_IterationStatement); + var AST_With = DEFNODE("With", "expression", { + $documentation: "A `with` statement", + $propdoc: { + expression: "[AST_Node] the `with` expression" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.expression._walk(visitor); + this.body._walk(visitor); + }); + } + }, AST_StatementWithBody); + var AST_Scope = DEFNODE("Scope", "directives variables functions uses_with uses_eval parent_scope enclosed cname", { + $documentation: "Base class for all statements introducing a lexical scope", + $propdoc: { + directives: "[string*/S] an array of directives declared in this scope", + variables: "[Object/S] a map of name -> SymbolDef for all variables/functions defined in this scope", + functions: "[Object/S] like `variables`, but only lists function declarations", + uses_with: "[boolean/S] tells whether this scope uses the `with` statement", + uses_eval: "[boolean/S] tells whether this scope contains a direct call to the global `eval`", + parent_scope: "[AST_Scope?/S] link to the parent scope", + enclosed: "[SymbolDef*/S] a list of all symbol definitions that are accessed from this scope or any subscopes", + cname: "[integer/S] current index for mangling variables (used internally by the mangler)" + } + }, AST_Block); + var AST_Toplevel = DEFNODE("Toplevel", "globals", { + $documentation: "The toplevel scope", + $propdoc: { + globals: "[Object/S] a map of name -> SymbolDef for all undeclared names" + }, + wrap_enclose: function(arg_parameter_pairs) { + var self = this; + var args = []; + var parameters = []; + arg_parameter_pairs.forEach(function(pair) { + var splitAt = pair.lastIndexOf(":"); + args.push(pair.substr(0, splitAt)); + parameters.push(pair.substr(splitAt + 1)); + }); + var wrapped_tl = "(function(" + parameters.join(",") + "){ '$ORIG'; })(" + args.join(",") + ")"; + wrapped_tl = parse(wrapped_tl); + wrapped_tl = wrapped_tl.transform(new TreeTransformer(function before(node) { + if (node instanceof AST_Directive && node.value == "$ORIG") { + return MAP.splice(self.body); + } + })); + return wrapped_tl; + }, + wrap_commonjs: function(name, export_all) { + var self = this; + var to_export = []; + if (export_all) { + self.figure_out_scope(); + self.walk(new TreeWalker(function(node) { + if (node instanceof AST_SymbolDeclaration && node.definition().global) { + if (!find_if(function(n) { + return n.name == node.name; + }, to_export)) to_export.push(node); + } + })); + } + var wrapped_tl = "(function(exports, global){ global['" + name + "'] = exports; '$ORIG'; '$EXPORTS'; }({}, (function(){return this}())))"; + wrapped_tl = parse(wrapped_tl); + wrapped_tl = wrapped_tl.transform(new TreeTransformer(function before(node) { + if (node instanceof AST_SimpleStatement) { + node = node.body; + if (node instanceof AST_String) switch (node.getValue()) { + case "$ORIG": + return MAP.splice(self.body); + + case "$EXPORTS": + var body = []; + to_export.forEach(function(sym) { + body.push(new AST_SimpleStatement({ + body: new AST_Assign({ + left: new AST_Sub({ + expression: new AST_SymbolRef({ + name: "exports" + }), + property: new AST_String({ + value: sym.name + }) + }), + operator: "=", + right: new AST_SymbolRef(sym) + }) + })); + }); + return MAP.splice(body); + } + } + })); + return wrapped_tl; + } + }, AST_Scope); + var AST_Lambda = DEFNODE("Lambda", "name argnames uses_arguments", { + $documentation: "Base class for functions", + $propdoc: { + name: "[AST_SymbolDeclaration?] the name of this function", + argnames: "[AST_SymbolFunarg*] array of function arguments", + uses_arguments: "[boolean/S] tells whether this function accesses the arguments array" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + if (this.name) this.name._walk(visitor); + this.argnames.forEach(function(arg) { + arg._walk(visitor); + }); + walk_body(this, visitor); + }); + } + }, AST_Scope); + var AST_Accessor = DEFNODE("Accessor", null, { + $documentation: "A setter/getter function. The `name` property is always null." + }, AST_Lambda); + var AST_Function = DEFNODE("Function", null, { + $documentation: "A function expression" + }, AST_Lambda); + var AST_Defun = DEFNODE("Defun", null, { + $documentation: "A function definition" + }, AST_Lambda); + var AST_Jump = DEFNODE("Jump", null, { + $documentation: "Base class for “jumps” (for now that's `return`, `throw`, `break` and `continue`)" + }, AST_Statement); + var AST_Exit = DEFNODE("Exit", "value", { + $documentation: "Base class for “exits” (`return` and `throw`)", + $propdoc: { + value: "[AST_Node?] the value returned or thrown by this statement; could be null for AST_Return" + }, + _walk: function(visitor) { + return visitor._visit(this, this.value && function() { + this.value._walk(visitor); + }); + } + }, AST_Jump); + var AST_Return = DEFNODE("Return", null, { + $documentation: "A `return` statement" + }, AST_Exit); + var AST_Throw = DEFNODE("Throw", null, { + $documentation: "A `throw` statement" + }, AST_Exit); + var AST_LoopControl = DEFNODE("LoopControl", "label", { + $documentation: "Base class for loop control statements (`break` and `continue`)", + $propdoc: { + label: "[AST_LabelRef?] the label, or null if none" + }, + _walk: function(visitor) { + return visitor._visit(this, this.label && function() { + this.label._walk(visitor); + }); + } + }, AST_Jump); + var AST_Break = DEFNODE("Break", null, { + $documentation: "A `break` statement" + }, AST_LoopControl); + var AST_Continue = DEFNODE("Continue", null, { + $documentation: "A `continue` statement" + }, AST_LoopControl); + var AST_If = DEFNODE("If", "condition alternative", { + $documentation: "A `if` statement", + $propdoc: { + condition: "[AST_Node] the `if` condition", + alternative: "[AST_Statement?] the `else` part, or null if not present" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.condition._walk(visitor); + this.body._walk(visitor); + if (this.alternative) this.alternative._walk(visitor); + }); + } + }, AST_StatementWithBody); + var AST_Switch = DEFNODE("Switch", "expression", { + $documentation: "A `switch` statement", + $propdoc: { + expression: "[AST_Node] the `switch` “discriminant”" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.expression._walk(visitor); + walk_body(this, visitor); + }); + } + }, AST_Block); + var AST_SwitchBranch = DEFNODE("SwitchBranch", null, { + $documentation: "Base class for `switch` branches" + }, AST_Block); + var AST_Default = DEFNODE("Default", null, { + $documentation: "A `default` switch branch" + }, AST_SwitchBranch); + var AST_Case = DEFNODE("Case", "expression", { + $documentation: "A `case` switch branch", + $propdoc: { + expression: "[AST_Node] the `case` expression" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.expression._walk(visitor); + walk_body(this, visitor); + }); + } + }, AST_SwitchBranch); + var AST_Try = DEFNODE("Try", "bcatch bfinally", { + $documentation: "A `try` statement", + $propdoc: { + bcatch: "[AST_Catch?] the catch block, or null if not present", + bfinally: "[AST_Finally?] the finally block, or null if not present" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + walk_body(this, visitor); + if (this.bcatch) this.bcatch._walk(visitor); + if (this.bfinally) this.bfinally._walk(visitor); + }); + } + }, AST_Block); + var AST_Catch = DEFNODE("Catch", "argname", { + $documentation: "A `catch` node; only makes sense as part of a `try` statement", + $propdoc: { + argname: "[AST_SymbolCatch] symbol for the exception" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.argname._walk(visitor); + walk_body(this, visitor); + }); + } + }, AST_Block); + var AST_Finally = DEFNODE("Finally", null, { + $documentation: "A `finally` node; only makes sense as part of a `try` statement" + }, AST_Block); + var AST_Definitions = DEFNODE("Definitions", "definitions", { + $documentation: "Base class for `var` or `const` nodes (variable declarations/initializations)", + $propdoc: { + definitions: "[AST_VarDef*] array of variable definitions" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.definitions.forEach(function(def) { + def._walk(visitor); + }); + }); + } + }, AST_Statement); + var AST_Var = DEFNODE("Var", null, { + $documentation: "A `var` statement" + }, AST_Definitions); + var AST_Const = DEFNODE("Const", null, { + $documentation: "A `const` statement" + }, AST_Definitions); + var AST_VarDef = DEFNODE("VarDef", "name value", { + $documentation: "A variable declaration; only appears in a AST_Definitions node", + $propdoc: { + name: "[AST_SymbolVar|AST_SymbolConst] name of the variable", + value: "[AST_Node?] initializer, or null of there's no initializer" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.name._walk(visitor); + if (this.value) this.value._walk(visitor); + }); + } + }); + var AST_Call = DEFNODE("Call", "expression args", { + $documentation: "A function call expression", + $propdoc: { + expression: "[AST_Node] expression to invoke as function", + args: "[AST_Node*] array of arguments" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.expression._walk(visitor); + this.args.forEach(function(arg) { + arg._walk(visitor); + }); + }); + } + }); + var AST_New = DEFNODE("New", null, { + $documentation: "An object instantiation. Derives from a function call since it has exactly the same properties" + }, AST_Call); + var AST_Seq = DEFNODE("Seq", "car cdr", { + $documentation: "A sequence expression (two comma-separated expressions)", + $propdoc: { + car: "[AST_Node] first element in sequence", + cdr: "[AST_Node] second element in sequence" + }, + $cons: function(x, y) { + var seq = new AST_Seq(x); + seq.car = x; + seq.cdr = y; + return seq; + }, + $from_array: function(array) { + if (array.length == 0) return null; + if (array.length == 1) return array[0].clone(); + var list = null; + for (var i = array.length; --i >= 0; ) { + list = AST_Seq.cons(array[i], list); + } + var p = list; + while (p) { + if (p.cdr && !p.cdr.cdr) { + p.cdr = p.cdr.car; + break; + } + p = p.cdr; + } + return list; + }, + to_array: function() { + var p = this, a = []; + while (p) { + a.push(p.car); + if (p.cdr && !(p.cdr instanceof AST_Seq)) { + a.push(p.cdr); + break; + } + p = p.cdr; + } + return a; + }, + add: function(node) { + var p = this; + while (p) { + if (!(p.cdr instanceof AST_Seq)) { + var cell = AST_Seq.cons(p.cdr, node); + return p.cdr = cell; + } + p = p.cdr; + } + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.car._walk(visitor); + if (this.cdr) this.cdr._walk(visitor); + }); + } + }); + var AST_PropAccess = DEFNODE("PropAccess", "expression property", { + $documentation: 'Base class for property access expressions, i.e. `a.foo` or `a["foo"]`', + $propdoc: { + expression: "[AST_Node] the “container” expression", + property: "[AST_Node|string] the property to access. For AST_Dot this is always a plain string, while for AST_Sub it's an arbitrary AST_Node" + } + }); + var AST_Dot = DEFNODE("Dot", null, { + $documentation: "A dotted property access expression", + _walk: function(visitor) { + return visitor._visit(this, function() { + this.expression._walk(visitor); + }); + } + }, AST_PropAccess); + var AST_Sub = DEFNODE("Sub", null, { + $documentation: 'Index-style property access, i.e. `a["foo"]`', + _walk: function(visitor) { + return visitor._visit(this, function() { + this.expression._walk(visitor); + this.property._walk(visitor); + }); + } + }, AST_PropAccess); + var AST_Unary = DEFNODE("Unary", "operator expression", { + $documentation: "Base class for unary expressions", + $propdoc: { + operator: "[string] the operator", + expression: "[AST_Node] expression that this unary operator applies to" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.expression._walk(visitor); + }); + } + }); + var AST_UnaryPrefix = DEFNODE("UnaryPrefix", null, { + $documentation: "Unary prefix expression, i.e. `typeof i` or `++i`" + }, AST_Unary); + var AST_UnaryPostfix = DEFNODE("UnaryPostfix", null, { + $documentation: "Unary postfix expression, i.e. `i++`" + }, AST_Unary); + var AST_Binary = DEFNODE("Binary", "left operator right", { + $documentation: "Binary expression, i.e. `a + b`", + $propdoc: { + left: "[AST_Node] left-hand side expression", + operator: "[string] the operator", + right: "[AST_Node] right-hand side expression" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.left._walk(visitor); + this.right._walk(visitor); + }); + } + }); + var AST_Conditional = DEFNODE("Conditional", "condition consequent alternative", { + $documentation: "Conditional expression using the ternary operator, i.e. `a ? b : c`", + $propdoc: { + condition: "[AST_Node]", + consequent: "[AST_Node]", + alternative: "[AST_Node]" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.condition._walk(visitor); + this.consequent._walk(visitor); + this.alternative._walk(visitor); + }); + } + }); + var AST_Assign = DEFNODE("Assign", null, { + $documentation: "An assignment expression — `a = b + 5`" + }, AST_Binary); + var AST_Array = DEFNODE("Array", "elements", { + $documentation: "An array literal", + $propdoc: { + elements: "[AST_Node*] array of elements" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.elements.forEach(function(el) { + el._walk(visitor); + }); + }); + } + }); + var AST_Object = DEFNODE("Object", "properties", { + $documentation: "An object literal", + $propdoc: { + properties: "[AST_ObjectProperty*] array of properties" + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.properties.forEach(function(prop) { + prop._walk(visitor); + }); + }); + } + }); + var AST_ObjectProperty = DEFNODE("ObjectProperty", "key value", { + $documentation: "Base class for literal object properties", + $propdoc: { + key: "[string] the property name converted to a string for ObjectKeyVal. For setters and getters this is an arbitrary AST_Node.", + value: "[AST_Node] property value. For setters and getters this is an AST_Function." + }, + _walk: function(visitor) { + return visitor._visit(this, function() { + this.value._walk(visitor); + }); + } + }); + var AST_ObjectKeyVal = DEFNODE("ObjectKeyVal", null, { + $documentation: "A key: value object property" + }, AST_ObjectProperty); + var AST_ObjectSetter = DEFNODE("ObjectSetter", null, { + $documentation: "An object setter property" + }, AST_ObjectProperty); + var AST_ObjectGetter = DEFNODE("ObjectGetter", null, { + $documentation: "An object getter property" + }, AST_ObjectProperty); + var AST_Symbol = DEFNODE("Symbol", "scope name thedef", { + $propdoc: { + name: "[string] name of this symbol", + scope: "[AST_Scope/S] the current scope (not necessarily the definition scope)", + thedef: "[SymbolDef/S] the definition of this symbol" + }, + $documentation: "Base class for all symbols" + }); + var AST_SymbolAccessor = DEFNODE("SymbolAccessor", null, { + $documentation: "The name of a property accessor (setter/getter function)" + }, AST_Symbol); + var AST_SymbolDeclaration = DEFNODE("SymbolDeclaration", "init", { + $documentation: "A declaration symbol (symbol in var/const, function name or argument, symbol in catch)", + $propdoc: { + init: "[AST_Node*/S] array of initializers for this declaration." + } + }, AST_Symbol); + var AST_SymbolVar = DEFNODE("SymbolVar", null, { + $documentation: "Symbol defining a variable" + }, AST_SymbolDeclaration); + var AST_SymbolConst = DEFNODE("SymbolConst", null, { + $documentation: "A constant declaration" + }, AST_SymbolDeclaration); + var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, { + $documentation: "Symbol naming a function argument" + }, AST_SymbolVar); + var AST_SymbolDefun = DEFNODE("SymbolDefun", null, { + $documentation: "Symbol defining a function" + }, AST_SymbolDeclaration); + var AST_SymbolLambda = DEFNODE("SymbolLambda", null, { + $documentation: "Symbol naming a function expression" + }, AST_SymbolDeclaration); + var AST_SymbolCatch = DEFNODE("SymbolCatch", null, { + $documentation: "Symbol naming the exception in catch" + }, AST_SymbolDeclaration); + var AST_Label = DEFNODE("Label", "references", { + $documentation: "Symbol naming a label (declaration)", + $propdoc: { + references: "[AST_LoopControl*] a list of nodes referring to this label" + }, + initialize: function() { + this.references = []; + this.thedef = this; + } + }, AST_Symbol); + var AST_SymbolRef = DEFNODE("SymbolRef", null, { + $documentation: "Reference to some symbol (not definition/declaration)" + }, AST_Symbol); + var AST_LabelRef = DEFNODE("LabelRef", null, { + $documentation: "Reference to a label symbol" + }, AST_Symbol); + var AST_This = DEFNODE("This", null, { + $documentation: "The `this` symbol" + }, AST_Symbol); + var AST_Constant = DEFNODE("Constant", null, { + $documentation: "Base class for all constants", + getValue: function() { + return this.value; + } + }); + var AST_String = DEFNODE("String", "value", { + $documentation: "A string literal", + $propdoc: { + value: "[string] the contents of this string" + } + }, AST_Constant); + var AST_Number = DEFNODE("Number", "value", { + $documentation: "A number literal", + $propdoc: { + value: "[number] the numeric value" + } + }, AST_Constant); + var AST_RegExp = DEFNODE("RegExp", "value", { + $documentation: "A regexp literal", + $propdoc: { + value: "[RegExp] the actual regexp" + } + }, AST_Constant); + var AST_Atom = DEFNODE("Atom", null, { + $documentation: "Base class for atoms" + }, AST_Constant); + var AST_Null = DEFNODE("Null", null, { + $documentation: "The `null` atom", + value: null + }, AST_Atom); + var AST_NaN = DEFNODE("NaN", null, { + $documentation: "The impossible value", + value: 0 / 0 + }, AST_Atom); + var AST_Undefined = DEFNODE("Undefined", null, { + $documentation: "The `undefined` value", + value: function() {}() + }, AST_Atom); + var AST_Hole = DEFNODE("Hole", null, { + $documentation: "A hole in an array", + value: function() {}() + }, AST_Atom); + var AST_Infinity = DEFNODE("Infinity", null, { + $documentation: "The `Infinity` value", + value: 1 / 0 + }, AST_Atom); + var AST_Boolean = DEFNODE("Boolean", null, { + $documentation: "Base class for booleans" + }, AST_Atom); + var AST_False = DEFNODE("False", null, { + $documentation: "The `false` atom", + value: false + }, AST_Boolean); + var AST_True = DEFNODE("True", null, { + $documentation: "The `true` atom", + value: true + }, AST_Boolean); + function TreeWalker(callback) { + this.visit = callback; + this.stack = []; + } + TreeWalker.prototype = { + _visit: function(node, descend) { + this.stack.push(node); + var ret = this.visit(node, descend ? function() { + descend.call(node); + } : noop); + if (!ret && descend) { + descend.call(node); + } + this.stack.pop(); + return ret; + }, + parent: function(n) { + return this.stack[this.stack.length - 2 - (n || 0)]; + }, + push: function(node) { + this.stack.push(node); + }, + pop: function() { + return this.stack.pop(); + }, + self: function() { + return this.stack[this.stack.length - 1]; + }, + find_parent: function(type) { + var stack = this.stack; + for (var i = stack.length; --i >= 0; ) { + var x = stack[i]; + if (x instanceof type) return x; + } + }, + has_directive: function(type) { + return this.find_parent(AST_Scope).has_directive(type); + }, + in_boolean_context: function() { + var stack = this.stack; + var i = stack.length, self = stack[--i]; + while (i > 0) { + var p = stack[--i]; + if (p instanceof AST_If && p.condition === self || p instanceof AST_Conditional && p.condition === self || p instanceof AST_DWLoop && p.condition === self || p instanceof AST_For && p.condition === self || p instanceof AST_UnaryPrefix && p.operator == "!" && p.expression === self) { + return true; + } + if (!(p instanceof AST_Binary && (p.operator == "&&" || p.operator == "||"))) return false; + self = p; + } + }, + loopcontrol_target: function(label) { + var stack = this.stack; + if (label) for (var i = stack.length; --i >= 0; ) { + var x = stack[i]; + if (x instanceof AST_LabeledStatement && x.label.name == label.name) { + return x.body; + } + } else for (var i = stack.length; --i >= 0; ) { + var x = stack[i]; + if (x instanceof AST_Switch || x instanceof AST_IterationStatement) return x; + } + } + }; + "use strict"; + var KEYWORDS = "break case catch const continue debugger default delete do else finally for function if in instanceof new return switch throw try typeof var void while with"; + var KEYWORDS_ATOM = "false null true"; + var RESERVED_WORDS = "abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized this throws transient volatile yield" + " " + KEYWORDS_ATOM + " " + KEYWORDS; + var KEYWORDS_BEFORE_EXPRESSION = "return new delete throw else case"; + KEYWORDS = makePredicate(KEYWORDS); + RESERVED_WORDS = makePredicate(RESERVED_WORDS); + KEYWORDS_BEFORE_EXPRESSION = makePredicate(KEYWORDS_BEFORE_EXPRESSION); + KEYWORDS_ATOM = makePredicate(KEYWORDS_ATOM); + var OPERATOR_CHARS = makePredicate(characters("+-*&%=<>!?|~^")); + var RE_HEX_NUMBER = /^0x[0-9a-f]+$/i; + var RE_OCT_NUMBER = /^0[0-7]+$/; + var RE_DEC_NUMBER = /^\d*\.?\d*(?:e[+-]?\d*(?:\d\.?|\.?\d)\d*)?$/i; + var OPERATORS = makePredicate([ "in", "instanceof", "typeof", "new", "void", "delete", "++", "--", "+", "-", "!", "~", "&", "|", "^", "*", "/", "%", ">>", "<<", ">>>", "<", ">", "<=", ">=", "==", "===", "!=", "!==", "?", "=", "+=", "-=", "/=", "*=", "%=", ">>=", "<<=", ">>>=", "|=", "^=", "&=", "&&", "||" ]); + var WHITESPACE_CHARS = makePredicate(characters("  \n\r \f ​᠎              ")); + var PUNC_BEFORE_EXPRESSION = makePredicate(characters("[{(,.;:")); + var PUNC_CHARS = makePredicate(characters("[]{}(),;:")); + var REGEXP_MODIFIERS = makePredicate(characters("gmsiy")); + var UNICODE = { + letter: new RegExp("[\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0523\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0621-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971\\u0972\\u097B-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D28\\u0D2A-\\u0D39\\u0D3D\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC\\u0EDD\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8B\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10D0-\\u10FA\\u10FC\\u1100-\\u1159\\u115F-\\u11A2\\u11A8-\\u11F9\\u1200-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u1676\\u1681-\\u169A\\u16A0-\\u16EA\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19A9\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u2094\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2C6F\\u2C71-\\u2C7D\\u2C80-\\u2CE4\\u2D00-\\u2D25\\u2D30-\\u2D65\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31B7\\u31F0-\\u31FF\\u3400\\u4DB5\\u4E00\\u9FC3\\uA000-\\uA48C\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA65F\\uA662-\\uA66E\\uA67F-\\uA697\\uA717-\\uA71F\\uA722-\\uA788\\uA78B\\uA78C\\uA7FB-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA90A-\\uA925\\uA930-\\uA946\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAC00\\uD7A3\\uF900-\\uFA2D\\uFA30-\\uFA6A\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC]"), + non_spacing_mark: new RegExp("[\\u0300-\\u036F\\u0483-\\u0487\\u0591-\\u05BD\\u05BF\\u05C1\\u05C2\\u05C4\\u05C5\\u05C7\\u0610-\\u061A\\u064B-\\u065E\\u0670\\u06D6-\\u06DC\\u06DF-\\u06E4\\u06E7\\u06E8\\u06EA-\\u06ED\\u0711\\u0730-\\u074A\\u07A6-\\u07B0\\u07EB-\\u07F3\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u082D\\u0900-\\u0902\\u093C\\u0941-\\u0948\\u094D\\u0951-\\u0955\\u0962\\u0963\\u0981\\u09BC\\u09C1-\\u09C4\\u09CD\\u09E2\\u09E3\\u0A01\\u0A02\\u0A3C\\u0A41\\u0A42\\u0A47\\u0A48\\u0A4B-\\u0A4D\\u0A51\\u0A70\\u0A71\\u0A75\\u0A81\\u0A82\\u0ABC\\u0AC1-\\u0AC5\\u0AC7\\u0AC8\\u0ACD\\u0AE2\\u0AE3\\u0B01\\u0B3C\\u0B3F\\u0B41-\\u0B44\\u0B4D\\u0B56\\u0B62\\u0B63\\u0B82\\u0BC0\\u0BCD\\u0C3E-\\u0C40\\u0C46-\\u0C48\\u0C4A-\\u0C4D\\u0C55\\u0C56\\u0C62\\u0C63\\u0CBC\\u0CBF\\u0CC6\\u0CCC\\u0CCD\\u0CE2\\u0CE3\\u0D41-\\u0D44\\u0D4D\\u0D62\\u0D63\\u0DCA\\u0DD2-\\u0DD4\\u0DD6\\u0E31\\u0E34-\\u0E3A\\u0E47-\\u0E4E\\u0EB1\\u0EB4-\\u0EB9\\u0EBB\\u0EBC\\u0EC8-\\u0ECD\\u0F18\\u0F19\\u0F35\\u0F37\\u0F39\\u0F71-\\u0F7E\\u0F80-\\u0F84\\u0F86\\u0F87\\u0F90-\\u0F97\\u0F99-\\u0FBC\\u0FC6\\u102D-\\u1030\\u1032-\\u1037\\u1039\\u103A\\u103D\\u103E\\u1058\\u1059\\u105E-\\u1060\\u1071-\\u1074\\u1082\\u1085\\u1086\\u108D\\u109D\\u135F\\u1712-\\u1714\\u1732-\\u1734\\u1752\\u1753\\u1772\\u1773\\u17B7-\\u17BD\\u17C6\\u17C9-\\u17D3\\u17DD\\u180B-\\u180D\\u18A9\\u1920-\\u1922\\u1927\\u1928\\u1932\\u1939-\\u193B\\u1A17\\u1A18\\u1A56\\u1A58-\\u1A5E\\u1A60\\u1A62\\u1A65-\\u1A6C\\u1A73-\\u1A7C\\u1A7F\\u1B00-\\u1B03\\u1B34\\u1B36-\\u1B3A\\u1B3C\\u1B42\\u1B6B-\\u1B73\\u1B80\\u1B81\\u1BA2-\\u1BA5\\u1BA8\\u1BA9\\u1C2C-\\u1C33\\u1C36\\u1C37\\u1CD0-\\u1CD2\\u1CD4-\\u1CE0\\u1CE2-\\u1CE8\\u1CED\\u1DC0-\\u1DE6\\u1DFD-\\u1DFF\\u20D0-\\u20DC\\u20E1\\u20E5-\\u20F0\\u2CEF-\\u2CF1\\u2DE0-\\u2DFF\\u302A-\\u302F\\u3099\\u309A\\uA66F\\uA67C\\uA67D\\uA6F0\\uA6F1\\uA802\\uA806\\uA80B\\uA825\\uA826\\uA8C4\\uA8E0-\\uA8F1\\uA926-\\uA92D\\uA947-\\uA951\\uA980-\\uA982\\uA9B3\\uA9B6-\\uA9B9\\uA9BC\\uAA29-\\uAA2E\\uAA31\\uAA32\\uAA35\\uAA36\\uAA43\\uAA4C\\uAAB0\\uAAB2-\\uAAB4\\uAAB7\\uAAB8\\uAABE\\uAABF\\uAAC1\\uABE5\\uABE8\\uABED\\uFB1E\\uFE00-\\uFE0F\\uFE20-\\uFE26]"), + space_combining_mark: new RegExp("[\\u0903\\u093E-\\u0940\\u0949-\\u094C\\u094E\\u0982\\u0983\\u09BE-\\u09C0\\u09C7\\u09C8\\u09CB\\u09CC\\u09D7\\u0A03\\u0A3E-\\u0A40\\u0A83\\u0ABE-\\u0AC0\\u0AC9\\u0ACB\\u0ACC\\u0B02\\u0B03\\u0B3E\\u0B40\\u0B47\\u0B48\\u0B4B\\u0B4C\\u0B57\\u0BBE\\u0BBF\\u0BC1\\u0BC2\\u0BC6-\\u0BC8\\u0BCA-\\u0BCC\\u0BD7\\u0C01-\\u0C03\\u0C41-\\u0C44\\u0C82\\u0C83\\u0CBE\\u0CC0-\\u0CC4\\u0CC7\\u0CC8\\u0CCA\\u0CCB\\u0CD5\\u0CD6\\u0D02\\u0D03\\u0D3E-\\u0D40\\u0D46-\\u0D48\\u0D4A-\\u0D4C\\u0D57\\u0D82\\u0D83\\u0DCF-\\u0DD1\\u0DD8-\\u0DDF\\u0DF2\\u0DF3\\u0F3E\\u0F3F\\u0F7F\\u102B\\u102C\\u1031\\u1038\\u103B\\u103C\\u1056\\u1057\\u1062-\\u1064\\u1067-\\u106D\\u1083\\u1084\\u1087-\\u108C\\u108F\\u109A-\\u109C\\u17B6\\u17BE-\\u17C5\\u17C7\\u17C8\\u1923-\\u1926\\u1929-\\u192B\\u1930\\u1931\\u1933-\\u1938\\u19B0-\\u19C0\\u19C8\\u19C9\\u1A19-\\u1A1B\\u1A55\\u1A57\\u1A61\\u1A63\\u1A64\\u1A6D-\\u1A72\\u1B04\\u1B35\\u1B3B\\u1B3D-\\u1B41\\u1B43\\u1B44\\u1B82\\u1BA1\\u1BA6\\u1BA7\\u1BAA\\u1C24-\\u1C2B\\u1C34\\u1C35\\u1CE1\\u1CF2\\uA823\\uA824\\uA827\\uA880\\uA881\\uA8B4-\\uA8C3\\uA952\\uA953\\uA983\\uA9B4\\uA9B5\\uA9BA\\uA9BB\\uA9BD-\\uA9C0\\uAA2F\\uAA30\\uAA33\\uAA34\\uAA4D\\uAA7B\\uABE3\\uABE4\\uABE6\\uABE7\\uABE9\\uABEA\\uABEC]"), + connector_punctuation: new RegExp("[\\u005F\\u203F\\u2040\\u2054\\uFE33\\uFE34\\uFE4D-\\uFE4F\\uFF3F]") + }; + function is_letter(code) { + return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 170 && UNICODE.letter.test(String.fromCharCode(code)); + } + function is_digit(code) { + return code >= 48 && code <= 57; + } + function is_alphanumeric_char(code) { + return is_digit(code) || is_letter(code); + } + function is_unicode_combining_mark(ch) { + return UNICODE.non_spacing_mark.test(ch) || UNICODE.space_combining_mark.test(ch); + } + function is_unicode_connector_punctuation(ch) { + return UNICODE.connector_punctuation.test(ch); + } + function is_identifier(name) { + return !RESERVED_WORDS(name) && /^[a-z_$][a-z0-9_$]*$/i.test(name); + } + function is_identifier_start(code) { + return code == 36 || code == 95 || is_letter(code); + } + function is_identifier_char(ch) { + var code = ch.charCodeAt(0); + return is_identifier_start(code) || is_digit(code) || code == 8204 || code == 8205 || is_unicode_combining_mark(ch) || is_unicode_connector_punctuation(ch); + } + function is_identifier_string(str) { + return /^[a-z_$][a-z0-9_$]*$/i.test(str); + } + function parse_js_number(num) { + if (RE_HEX_NUMBER.test(num)) { + return parseInt(num.substr(2), 16); + } else if (RE_OCT_NUMBER.test(num)) { + return parseInt(num.substr(1), 8); + } else if (RE_DEC_NUMBER.test(num)) { + return parseFloat(num); + } + } + function JS_Parse_Error(message, line, col, pos) { + this.message = message; + this.line = line; + this.col = col; + this.pos = pos; + this.stack = new Error().stack; + } + JS_Parse_Error.prototype.toString = function() { + return this.message + " (line: " + this.line + ", col: " + this.col + ", pos: " + this.pos + ")" + "\n\n" + this.stack; + }; + function js_error(message, filename, line, col, pos) { + throw new JS_Parse_Error(message, line, col, pos); + } + function is_token(token, type, val) { + return token.type == type && (val == null || token.value == val); + } + var EX_EOF = {}; + function tokenizer($TEXT, filename, html5_comments) { + var S = { + text: $TEXT.replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/\uFEFF/g, ""), + filename: filename, + pos: 0, + tokpos: 0, + line: 1, + tokline: 0, + col: 0, + tokcol: 0, + newline_before: false, + regex_allowed: false, + comments_before: [] + }; + function peek() { + return S.text.charAt(S.pos); + } + function next(signal_eof, in_string) { + var ch = S.text.charAt(S.pos++); + if (signal_eof && !ch) throw EX_EOF; + if (ch == "\n") { + S.newline_before = S.newline_before || !in_string; + ++S.line; + S.col = 0; + } else { + ++S.col; + } + return ch; + } + function forward(i) { + while (i-- > 0) next(); + } + function looking_at(str) { + return S.text.substr(S.pos, str.length) == str; + } + function find(what, signal_eof) { + var pos = S.text.indexOf(what, S.pos); + if (signal_eof && pos == -1) throw EX_EOF; + return pos; + } + function start_token() { + S.tokline = S.line; + S.tokcol = S.col; + S.tokpos = S.pos; + } + var prev_was_dot = false; + function token(type, value, is_comment) { + S.regex_allowed = type == "operator" && !UNARY_POSTFIX(value) || type == "keyword" && KEYWORDS_BEFORE_EXPRESSION(value) || type == "punc" && PUNC_BEFORE_EXPRESSION(value); + prev_was_dot = type == "punc" && value == "."; + var ret = { + type: type, + value: value, + line: S.tokline, + col: S.tokcol, + pos: S.tokpos, + endpos: S.pos, + nlb: S.newline_before, + file: filename + }; + if (!is_comment) { + ret.comments_before = S.comments_before; + S.comments_before = []; + for (var i = 0, len = ret.comments_before.length; i < len; i++) { + ret.nlb = ret.nlb || ret.comments_before[i].nlb; + } + } + S.newline_before = false; + return new AST_Token(ret); + } + function skip_whitespace() { + while (WHITESPACE_CHARS(peek())) next(); + } + function read_while(pred) { + var ret = "", ch, i = 0; + while ((ch = peek()) && pred(ch, i++)) ret += next(); + return ret; + } + function parse_error(err) { + js_error(err, filename, S.tokline, S.tokcol, S.tokpos); + } + function read_num(prefix) { + var has_e = false, after_e = false, has_x = false, has_dot = prefix == "."; + var num = read_while(function(ch, i) { + var code = ch.charCodeAt(0); + switch (code) { + case 120: + case 88: + return has_x ? false : has_x = true; + + case 101: + case 69: + return has_x ? true : has_e ? false : has_e = after_e = true; + + case 45: + return after_e || i == 0 && !prefix; + + case 43: + return after_e; + + case after_e = false, 46: + return !has_dot && !has_x && !has_e ? has_dot = true : false; + } + return is_alphanumeric_char(code); + }); + if (prefix) num = prefix + num; + var valid = parse_js_number(num); + if (!isNaN(valid)) { + return token("num", valid); + } else { + parse_error("Invalid syntax: " + num); + } + } + function read_escaped_char(in_string) { + var ch = next(true, in_string); + switch (ch.charCodeAt(0)) { + case 110: + return "\n"; + + case 114: + return "\r"; + + case 116: + return " "; + + case 98: + return "\b"; + + case 118: + return " "; + + case 102: + return "\f"; + + case 48: + return "\x00"; + + case 120: + return String.fromCharCode(hex_bytes(2)); + + case 117: + return String.fromCharCode(hex_bytes(4)); + + case 10: + return ""; + + default: + return ch; + } + } + function hex_bytes(n) { + var num = 0; + for (;n > 0; --n) { + var digit = parseInt(next(true), 16); + if (isNaN(digit)) parse_error("Invalid hex-character pattern in string"); + num = num << 4 | digit; + } + return num; + } + var read_string = with_eof_error("Unterminated string constant", function() { + var quote = next(), ret = ""; + for (;;) { + var ch = next(true); + if (ch == "\\") { + var octal_len = 0, first = null; + ch = read_while(function(ch) { + if (ch >= "0" && ch <= "7") { + if (!first) { + first = ch; + return ++octal_len; + } else if (first <= "3" && octal_len <= 2) return ++octal_len; else if (first >= "4" && octal_len <= 1) return ++octal_len; + } + return false; + }); + if (octal_len > 0) ch = String.fromCharCode(parseInt(ch, 8)); else ch = read_escaped_char(true); + } else if (ch == quote) break; + ret += ch; + } + return token("string", ret); + }); + function skip_line_comment(type) { + var regex_allowed = S.regex_allowed; + var i = find("\n"), ret; + if (i == -1) { + ret = S.text.substr(S.pos); + S.pos = S.text.length; + } else { + ret = S.text.substring(S.pos, i); + S.pos = i; + } + S.comments_before.push(token(type, ret, true)); + S.regex_allowed = regex_allowed; + return next_token(); + } + var skip_multiline_comment = with_eof_error("Unterminated multiline comment", function() { + var regex_allowed = S.regex_allowed; + var i = find("*/", true); + var text = S.text.substring(S.pos, i); + var a = text.split("\n"), n = a.length; + S.pos = i + 2; + S.line += n - 1; + if (n > 1) S.col = a[n - 1].length; else S.col += a[n - 1].length; + S.col += 2; + var nlb = S.newline_before = S.newline_before || text.indexOf("\n") >= 0; + S.comments_before.push(token("comment2", text, true)); + S.regex_allowed = regex_allowed; + S.newline_before = nlb; + return next_token(); + }); + function read_name() { + var backslash = false, name = "", ch, escaped = false, hex; + while ((ch = peek()) != null) { + if (!backslash) { + if (ch == "\\") escaped = backslash = true, next(); else if (is_identifier_char(ch)) name += next(); else break; + } else { + if (ch != "u") parse_error("Expecting UnicodeEscapeSequence -- uXXXX"); + ch = read_escaped_char(); + if (!is_identifier_char(ch)) parse_error("Unicode char: " + ch.charCodeAt(0) + " is not valid in identifier"); + name += ch; + backslash = false; + } + } + if (KEYWORDS(name) && escaped) { + hex = name.charCodeAt(0).toString(16).toUpperCase(); + name = "\\u" + "0000".substr(hex.length) + hex + name.slice(1); + } + return name; + } + var read_regexp = with_eof_error("Unterminated regular expression", function(regexp) { + var prev_backslash = false, ch, in_class = false; + while (ch = next(true)) if (prev_backslash) { + regexp += "\\" + ch; + prev_backslash = false; + } else if (ch == "[") { + in_class = true; + regexp += ch; + } else if (ch == "]" && in_class) { + in_class = false; + regexp += ch; + } else if (ch == "/" && !in_class) { + break; + } else if (ch == "\\") { + prev_backslash = true; + } else { + regexp += ch; + } + var mods = read_name(); + return token("regexp", new RegExp(regexp, mods)); + }); + function read_operator(prefix) { + function grow(op) { + if (!peek()) return op; + var bigger = op + peek(); + if (OPERATORS(bigger)) { + next(); + return grow(bigger); + } else { + return op; + } + } + return token("operator", grow(prefix || next())); + } + function handle_slash() { + next(); + switch (peek()) { + case "/": + next(); + return skip_line_comment("comment1"); + + case "*": + next(); + return skip_multiline_comment(); + } + return S.regex_allowed ? read_regexp("") : read_operator("/"); + } + function handle_dot() { + next(); + return is_digit(peek().charCodeAt(0)) ? read_num(".") : token("punc", "."); + } + function read_word() { + var word = read_name(); + if (prev_was_dot) return token("name", word); + return KEYWORDS_ATOM(word) ? token("atom", word) : !KEYWORDS(word) ? token("name", word) : OPERATORS(word) ? token("operator", word) : token("keyword", word); + } + function with_eof_error(eof_error, cont) { + return function(x) { + try { + return cont(x); + } catch (ex) { + if (ex === EX_EOF) parse_error(eof_error); else throw ex; + } + }; + } + function next_token(force_regexp) { + if (force_regexp != null) return read_regexp(force_regexp); + skip_whitespace(); + start_token(); + if (html5_comments) { + if (looking_at("") && S.newline_before) { + forward(3); + return skip_line_comment("comment4"); + } + } + var ch = peek(); + if (!ch) return token("eof"); + var code = ch.charCodeAt(0); + switch (code) { + case 34: + case 39: + return read_string(); + + case 46: + return handle_dot(); + + case 47: + return handle_slash(); + } + if (is_digit(code)) return read_num(); + if (PUNC_CHARS(ch)) return token("punc", next()); + if (OPERATOR_CHARS(ch)) return read_operator(); + if (code == 92 || is_identifier_start(code)) return read_word(); + parse_error("Unexpected character '" + ch + "'"); + } + next_token.context = function(nc) { + if (nc) S = nc; + return S; + }; + return next_token; + } + var UNARY_PREFIX = makePredicate([ "typeof", "void", "delete", "--", "++", "!", "~", "-", "+" ]); + var UNARY_POSTFIX = makePredicate([ "--", "++" ]); + var ASSIGNMENT = makePredicate([ "=", "+=", "-=", "/=", "*=", "%=", ">>=", "<<=", ">>>=", "|=", "^=", "&=" ]); + var PRECEDENCE = function(a, ret) { + for (var i = 0; i < a.length; ++i) { + var b = a[i]; + for (var j = 0; j < b.length; ++j) { + ret[b[j]] = i + 1; + } + } + return ret; + }([ [ "||" ], [ "&&" ], [ "|" ], [ "^" ], [ "&" ], [ "==", "===", "!=", "!==" ], [ "<", ">", "<=", ">=", "in", "instanceof" ], [ ">>", "<<", ">>>" ], [ "+", "-" ], [ "*", "/", "%" ] ], {}); + var STATEMENTS_WITH_LABELS = array_to_hash([ "for", "do", "while", "switch" ]); + var ATOMIC_START_TOKEN = array_to_hash([ "atom", "num", "string", "regexp", "name" ]); + function parse($TEXT, options) { + options = defaults(options, { + strict: false, + filename: null, + toplevel: null, + expression: false, + html5_comments: true + }); + var S = { + input: typeof $TEXT == "string" ? tokenizer($TEXT, options.filename, options.html5_comments) : $TEXT, + token: null, + prev: null, + peeked: null, + in_function: 0, + in_directives: true, + in_loop: 0, + labels: [] + }; + S.token = next(); + function is(type, value) { + return is_token(S.token, type, value); + } + function peek() { + return S.peeked || (S.peeked = S.input()); + } + function next() { + S.prev = S.token; + if (S.peeked) { + S.token = S.peeked; + S.peeked = null; + } else { + S.token = S.input(); + } + S.in_directives = S.in_directives && (S.token.type == "string" || is("punc", ";")); + return S.token; + } + function prev() { + return S.prev; + } + function croak(msg, line, col, pos) { + var ctx = S.input.context(); + js_error(msg, ctx.filename, line != null ? line : ctx.tokline, col != null ? col : ctx.tokcol, pos != null ? pos : ctx.tokpos); + } + function token_error(token, msg) { + croak(msg, token.line, token.col); + } + function unexpected(token) { + if (token == null) token = S.token; + token_error(token, "Unexpected token: " + token.type + " (" + token.value + ")"); + } + function expect_token(type, val) { + if (is(type, val)) { + return next(); + } + token_error(S.token, "Unexpected token " + S.token.type + " «" + S.token.value + "»" + ", expected " + type + " «" + val + "»"); + } + function expect(punc) { + return expect_token("punc", punc); + } + function can_insert_semicolon() { + return !options.strict && (S.token.nlb || is("eof") || is("punc", "}")); + } + function semicolon() { + if (is("punc", ";")) next(); else if (!can_insert_semicolon()) unexpected(); + } + function parenthesised() { + expect("("); + var exp = expression(true); + expect(")"); + return exp; + } + function embed_tokens(parser) { + return function() { + var start = S.token; + var expr = parser(); + var end = prev(); + expr.start = start; + expr.end = end; + return expr; + }; + } + function handle_regexp() { + if (is("operator", "/") || is("operator", "/=")) { + S.peeked = null; + S.token = S.input(S.token.value.substr(1)); + } + } + var statement = embed_tokens(function() { + var tmp; + handle_regexp(); + switch (S.token.type) { + case "string": + var dir = S.in_directives, stat = simple_statement(); + if (dir && stat.body instanceof AST_String && !is("punc", ",")) return new AST_Directive({ + value: stat.body.value + }); + return stat; + + case "num": + case "regexp": + case "operator": + case "atom": + return simple_statement(); + + case "name": + return is_token(peek(), "punc", ":") ? labeled_statement() : simple_statement(); + + case "punc": + switch (S.token.value) { + case "{": + return new AST_BlockStatement({ + start: S.token, + body: block_(), + end: prev() + }); + + case "[": + case "(": + return simple_statement(); + + case ";": + next(); + return new AST_EmptyStatement(); + + default: + unexpected(); + } + + case "keyword": + switch (tmp = S.token.value, next(), tmp) { + case "break": + return break_cont(AST_Break); + + case "continue": + return break_cont(AST_Continue); + + case "debugger": + semicolon(); + return new AST_Debugger(); + + case "do": + return new AST_Do({ + body: in_loop(statement), + condition: (expect_token("keyword", "while"), tmp = parenthesised(), semicolon(), + tmp) + }); + + case "while": + return new AST_While({ + condition: parenthesised(), + body: in_loop(statement) + }); + + case "for": + return for_(); + + case "function": + return function_(AST_Defun); + + case "if": + return if_(); + + case "return": + if (S.in_function == 0) croak("'return' outside of function"); + return new AST_Return({ + value: is("punc", ";") ? (next(), null) : can_insert_semicolon() ? null : (tmp = expression(true), + semicolon(), tmp) + }); + + case "switch": + return new AST_Switch({ + expression: parenthesised(), + body: in_loop(switch_body_) + }); + + case "throw": + if (S.token.nlb) croak("Illegal newline after 'throw'"); + return new AST_Throw({ + value: (tmp = expression(true), semicolon(), tmp) + }); + + case "try": + return try_(); + + case "var": + return tmp = var_(), semicolon(), tmp; + + case "const": + return tmp = const_(), semicolon(), tmp; + + case "with": + return new AST_With({ + expression: parenthesised(), + body: statement() + }); + + default: + unexpected(); + } + } + }); + function labeled_statement() { + var label = as_symbol(AST_Label); + if (find_if(function(l) { + return l.name == label.name; + }, S.labels)) { + croak("Label " + label.name + " defined twice"); + } + expect(":"); + S.labels.push(label); + var stat = statement(); + S.labels.pop(); + if (!(stat instanceof AST_IterationStatement)) { + label.references.forEach(function(ref) { + if (ref instanceof AST_Continue) { + ref = ref.label.start; + croak("Continue label `" + label.name + "` refers to non-IterationStatement.", ref.line, ref.col, ref.pos); + } + }); + } + return new AST_LabeledStatement({ + body: stat, + label: label + }); + } + function simple_statement(tmp) { + return new AST_SimpleStatement({ + body: (tmp = expression(true), semicolon(), tmp) + }); + } + function break_cont(type) { + var label = null, ldef; + if (!can_insert_semicolon()) { + label = as_symbol(AST_LabelRef, true); + } + if (label != null) { + ldef = find_if(function(l) { + return l.name == label.name; + }, S.labels); + if (!ldef) croak("Undefined label " + label.name); + label.thedef = ldef; + } else if (S.in_loop == 0) croak(type.TYPE + " not inside a loop or switch"); + semicolon(); + var stat = new type({ + label: label + }); + if (ldef) ldef.references.push(stat); + return stat; + } + function for_() { + expect("("); + var init = null; + if (!is("punc", ";")) { + init = is("keyword", "var") ? (next(), var_(true)) : expression(true, true); + if (is("operator", "in")) { + if (init instanceof AST_Var && init.definitions.length > 1) croak("Only one variable declaration allowed in for..in loop"); + next(); + return for_in(init); + } + } + return regular_for(init); + } + function regular_for(init) { + expect(";"); + var test = is("punc", ";") ? null : expression(true); + expect(";"); + var step = is("punc", ")") ? null : expression(true); + expect(")"); + return new AST_For({ + init: init, + condition: test, + step: step, + body: in_loop(statement) + }); + } + function for_in(init) { + var lhs = init instanceof AST_Var ? init.definitions[0].name : null; + var obj = expression(true); + expect(")"); + return new AST_ForIn({ + init: init, + name: lhs, + object: obj, + body: in_loop(statement) + }); + } + var function_ = function(ctor) { + var in_statement = ctor === AST_Defun; + var name = is("name") ? as_symbol(in_statement ? AST_SymbolDefun : AST_SymbolLambda) : null; + if (in_statement && !name) unexpected(); + expect("("); + return new ctor({ + name: name, + argnames: function(first, a) { + while (!is("punc", ")")) { + if (first) first = false; else expect(","); + a.push(as_symbol(AST_SymbolFunarg)); + } + next(); + return a; + }(true, []), + body: function(loop, labels) { + ++S.in_function; + S.in_directives = true; + S.in_loop = 0; + S.labels = []; + var a = block_(); + --S.in_function; + S.in_loop = loop; + S.labels = labels; + return a; + }(S.in_loop, S.labels) + }); + }; + function if_() { + var cond = parenthesised(), body = statement(), belse = null; + if (is("keyword", "else")) { + next(); + belse = statement(); + } + return new AST_If({ + condition: cond, + body: body, + alternative: belse + }); + } + function block_() { + expect("{"); + var a = []; + while (!is("punc", "}")) { + if (is("eof")) unexpected(); + a.push(statement()); + } + next(); + return a; + } + function switch_body_() { + expect("{"); + var a = [], cur = null, branch = null, tmp; + while (!is("punc", "}")) { + if (is("eof")) unexpected(); + if (is("keyword", "case")) { + if (branch) branch.end = prev(); + cur = []; + branch = new AST_Case({ + start: (tmp = S.token, next(), tmp), + expression: expression(true), + body: cur + }); + a.push(branch); + expect(":"); + } else if (is("keyword", "default")) { + if (branch) branch.end = prev(); + cur = []; + branch = new AST_Default({ + start: (tmp = S.token, next(), expect(":"), tmp), + body: cur + }); + a.push(branch); + } else { + if (!cur) unexpected(); + cur.push(statement()); + } + } + if (branch) branch.end = prev(); + next(); + return a; + } + function try_() { + var body = block_(), bcatch = null, bfinally = null; + if (is("keyword", "catch")) { + var start = S.token; + next(); + expect("("); + var name = as_symbol(AST_SymbolCatch); + expect(")"); + bcatch = new AST_Catch({ + start: start, + argname: name, + body: block_(), + end: prev() + }); + } + if (is("keyword", "finally")) { + var start = S.token; + next(); + bfinally = new AST_Finally({ + start: start, + body: block_(), + end: prev() + }); + } + if (!bcatch && !bfinally) croak("Missing catch/finally blocks"); + return new AST_Try({ + body: body, + bcatch: bcatch, + bfinally: bfinally + }); + } + function vardefs(no_in, in_const) { + var a = []; + for (;;) { + a.push(new AST_VarDef({ + start: S.token, + name: as_symbol(in_const ? AST_SymbolConst : AST_SymbolVar), + value: is("operator", "=") ? (next(), expression(false, no_in)) : null, + end: prev() + })); + if (!is("punc", ",")) break; + next(); + } + return a; + } + var var_ = function(no_in) { + return new AST_Var({ + start: prev(), + definitions: vardefs(no_in, false), + end: prev() + }); + }; + var const_ = function() { + return new AST_Const({ + start: prev(), + definitions: vardefs(false, true), + end: prev() + }); + }; + var new_ = function() { + var start = S.token; + expect_token("operator", "new"); + var newexp = expr_atom(false), args; + if (is("punc", "(")) { + next(); + args = expr_list(")"); + } else { + args = []; + } + return subscripts(new AST_New({ + start: start, + expression: newexp, + args: args, + end: prev() + }), true); + }; + function as_atom_node() { + var tok = S.token, ret; + switch (tok.type) { + case "name": + case "keyword": + ret = _make_symbol(AST_SymbolRef); + break; + + case "num": + ret = new AST_Number({ + start: tok, + end: tok, + value: tok.value + }); + break; + + case "string": + ret = new AST_String({ + start: tok, + end: tok, + value: tok.value + }); + break; + + case "regexp": + ret = new AST_RegExp({ + start: tok, + end: tok, + value: tok.value + }); + break; + + case "atom": + switch (tok.value) { + case "false": + ret = new AST_False({ + start: tok, + end: tok + }); + break; + + case "true": + ret = new AST_True({ + start: tok, + end: tok + }); + break; + + case "null": + ret = new AST_Null({ + start: tok, + end: tok + }); + break; + } + break; + } + next(); + return ret; + } + var expr_atom = function(allow_calls) { + if (is("operator", "new")) { + return new_(); + } + var start = S.token; + if (is("punc")) { + switch (start.value) { + case "(": + next(); + var ex = expression(true); + ex.start = start; + ex.end = S.token; + expect(")"); + return subscripts(ex, allow_calls); + + case "[": + return subscripts(array_(), allow_calls); + + case "{": + return subscripts(object_(), allow_calls); + } + unexpected(); + } + if (is("keyword", "function")) { + next(); + var func = function_(AST_Function); + func.start = start; + func.end = prev(); + return subscripts(func, allow_calls); + } + if (ATOMIC_START_TOKEN[S.token.type]) { + return subscripts(as_atom_node(), allow_calls); + } + unexpected(); + }; + function expr_list(closing, allow_trailing_comma, allow_empty) { + var first = true, a = []; + while (!is("punc", closing)) { + if (first) first = false; else expect(","); + if (allow_trailing_comma && is("punc", closing)) break; + if (is("punc", ",") && allow_empty) { + a.push(new AST_Hole({ + start: S.token, + end: S.token + })); + } else { + a.push(expression(false)); + } + } + next(); + return a; + } + var array_ = embed_tokens(function() { + expect("["); + return new AST_Array({ + elements: expr_list("]", !options.strict, true) + }); + }); + var object_ = embed_tokens(function() { + expect("{"); + var first = true, a = []; + while (!is("punc", "}")) { + if (first) first = false; else expect(","); + if (!options.strict && is("punc", "}")) break; + var start = S.token; + var type = start.type; + var name = as_property_name(); + if (type == "name" && !is("punc", ":")) { + if (name == "get") { + a.push(new AST_ObjectGetter({ + start: start, + key: as_atom_node(), + value: function_(AST_Accessor), + end: prev() + })); + continue; + } + if (name == "set") { + a.push(new AST_ObjectSetter({ + start: start, + key: as_atom_node(), + value: function_(AST_Accessor), + end: prev() + })); + continue; + } + } + expect(":"); + a.push(new AST_ObjectKeyVal({ + start: start, + key: name, + value: expression(false), + end: prev() + })); + } + next(); + return new AST_Object({ + properties: a + }); + }); + function as_property_name() { + var tmp = S.token; + next(); + switch (tmp.type) { + case "num": + case "string": + case "name": + case "operator": + case "keyword": + case "atom": + return tmp.value; + + default: + unexpected(); + } + } + function as_name() { + var tmp = S.token; + next(); + switch (tmp.type) { + case "name": + case "operator": + case "keyword": + case "atom": + return tmp.value; + + default: + unexpected(); + } + } + function _make_symbol(type) { + var name = S.token.value; + return new (name == "this" ? AST_This : type)({ + name: String(name), + start: S.token, + end: S.token + }); + } + function as_symbol(type, noerror) { + if (!is("name")) { + if (!noerror) croak("Name expected"); + return null; + } + var sym = _make_symbol(type); + next(); + return sym; + } + var subscripts = function(expr, allow_calls) { + var start = expr.start; + if (is("punc", ".")) { + next(); + return subscripts(new AST_Dot({ + start: start, + expression: expr, + property: as_name(), + end: prev() + }), allow_calls); + } + if (is("punc", "[")) { + next(); + var prop = expression(true); + expect("]"); + return subscripts(new AST_Sub({ + start: start, + expression: expr, + property: prop, + end: prev() + }), allow_calls); + } + if (allow_calls && is("punc", "(")) { + next(); + return subscripts(new AST_Call({ + start: start, + expression: expr, + args: expr_list(")"), + end: prev() + }), true); + } + return expr; + }; + var maybe_unary = function(allow_calls) { + var start = S.token; + if (is("operator") && UNARY_PREFIX(start.value)) { + next(); + handle_regexp(); + var ex = make_unary(AST_UnaryPrefix, start.value, maybe_unary(allow_calls)); + ex.start = start; + ex.end = prev(); + return ex; + } + var val = expr_atom(allow_calls); + while (is("operator") && UNARY_POSTFIX(S.token.value) && !S.token.nlb) { + val = make_unary(AST_UnaryPostfix, S.token.value, val); + val.start = start; + val.end = S.token; + next(); + } + return val; + }; + function make_unary(ctor, op, expr) { + if ((op == "++" || op == "--") && !is_assignable(expr)) croak("Invalid use of " + op + " operator"); + return new ctor({ + operator: op, + expression: expr + }); + } + var expr_op = function(left, min_prec, no_in) { + var op = is("operator") ? S.token.value : null; + if (op == "in" && no_in) op = null; + var prec = op != null ? PRECEDENCE[op] : null; + if (prec != null && prec > min_prec) { + next(); + var right = expr_op(maybe_unary(true), prec, no_in); + return expr_op(new AST_Binary({ + start: left.start, + left: left, + operator: op, + right: right, + end: right.end + }), min_prec, no_in); + } + return left; + }; + function expr_ops(no_in) { + return expr_op(maybe_unary(true), 0, no_in); + } + var maybe_conditional = function(no_in) { + var start = S.token; + var expr = expr_ops(no_in); + if (is("operator", "?")) { + next(); + var yes = expression(false); + expect(":"); + return new AST_Conditional({ + start: start, + condition: expr, + consequent: yes, + alternative: expression(false, no_in), + end: prev() + }); + } + return expr; + }; + function is_assignable(expr) { + if (!options.strict) return true; + if (expr instanceof AST_This) return false; + return expr instanceof AST_PropAccess || expr instanceof AST_Symbol; + } + var maybe_assign = function(no_in) { + var start = S.token; + var left = maybe_conditional(no_in), val = S.token.value; + if (is("operator") && ASSIGNMENT(val)) { + if (is_assignable(left)) { + next(); + return new AST_Assign({ + start: start, + left: left, + operator: val, + right: maybe_assign(no_in), + end: prev() + }); + } + croak("Invalid assignment"); + } + return left; + }; + var expression = function(commas, no_in) { + var start = S.token; + var expr = maybe_assign(no_in); + if (commas && is("punc", ",")) { + next(); + return new AST_Seq({ + start: start, + car: expr, + cdr: expression(true, no_in), + end: peek() + }); + } + return expr; + }; + function in_loop(cont) { + ++S.in_loop; + var ret = cont(); + --S.in_loop; + return ret; + } + if (options.expression) { + return expression(true); + } + return function() { + var start = S.token; + var body = []; + while (!is("eof")) body.push(statement()); + var end = prev(); + var toplevel = options.toplevel; + if (toplevel) { + toplevel.body = toplevel.body.concat(body); + toplevel.end = end; + } else { + toplevel = new AST_Toplevel({ + start: start, + body: body, + end: end + }); + } + return toplevel; + }(); + } + "use strict"; + function TreeTransformer(before, after) { + TreeWalker.call(this); + this.before = before; + this.after = after; + } + TreeTransformer.prototype = new TreeWalker(); + (function(undefined) { + function _(node, descend) { + node.DEFMETHOD("transform", function(tw, in_list) { + var x, y; + tw.push(this); + if (tw.before) x = tw.before(this, descend, in_list); + if (x === undefined) { + if (!tw.after) { + x = this; + descend(x, tw); + } else { + tw.stack[tw.stack.length - 1] = x = this.clone(); + descend(x, tw); + y = tw.after(x, in_list); + if (y !== undefined) x = y; + } + } + tw.pop(); + return x; + }); + } + function do_list(list, tw) { + return MAP(list, function(node) { + return node.transform(tw, true); + }); + } + _(AST_Node, noop); + _(AST_LabeledStatement, function(self, tw) { + self.label = self.label.transform(tw); + self.body = self.body.transform(tw); + }); + _(AST_SimpleStatement, function(self, tw) { + self.body = self.body.transform(tw); + }); + _(AST_Block, function(self, tw) { + self.body = do_list(self.body, tw); + }); + _(AST_DWLoop, function(self, tw) { + self.condition = self.condition.transform(tw); + self.body = self.body.transform(tw); + }); + _(AST_For, function(self, tw) { + if (self.init) self.init = self.init.transform(tw); + if (self.condition) self.condition = self.condition.transform(tw); + if (self.step) self.step = self.step.transform(tw); + self.body = self.body.transform(tw); + }); + _(AST_ForIn, function(self, tw) { + self.init = self.init.transform(tw); + self.object = self.object.transform(tw); + self.body = self.body.transform(tw); + }); + _(AST_With, function(self, tw) { + self.expression = self.expression.transform(tw); + self.body = self.body.transform(tw); + }); + _(AST_Exit, function(self, tw) { + if (self.value) self.value = self.value.transform(tw); + }); + _(AST_LoopControl, function(self, tw) { + if (self.label) self.label = self.label.transform(tw); + }); + _(AST_If, function(self, tw) { + self.condition = self.condition.transform(tw); + self.body = self.body.transform(tw); + if (self.alternative) self.alternative = self.alternative.transform(tw); + }); + _(AST_Switch, function(self, tw) { + self.expression = self.expression.transform(tw); + self.body = do_list(self.body, tw); + }); + _(AST_Case, function(self, tw) { + self.expression = self.expression.transform(tw); + self.body = do_list(self.body, tw); + }); + _(AST_Try, function(self, tw) { + self.body = do_list(self.body, tw); + if (self.bcatch) self.bcatch = self.bcatch.transform(tw); + if (self.bfinally) self.bfinally = self.bfinally.transform(tw); + }); + _(AST_Catch, function(self, tw) { + self.argname = self.argname.transform(tw); + self.body = do_list(self.body, tw); + }); + _(AST_Definitions, function(self, tw) { + self.definitions = do_list(self.definitions, tw); + }); + _(AST_VarDef, function(self, tw) { + self.name = self.name.transform(tw); + if (self.value) self.value = self.value.transform(tw); + }); + _(AST_Lambda, function(self, tw) { + if (self.name) self.name = self.name.transform(tw); + self.argnames = do_list(self.argnames, tw); + self.body = do_list(self.body, tw); + }); + _(AST_Call, function(self, tw) { + self.expression = self.expression.transform(tw); + self.args = do_list(self.args, tw); + }); + _(AST_Seq, function(self, tw) { + self.car = self.car.transform(tw); + self.cdr = self.cdr.transform(tw); + }); + _(AST_Dot, function(self, tw) { + self.expression = self.expression.transform(tw); + }); + _(AST_Sub, function(self, tw) { + self.expression = self.expression.transform(tw); + self.property = self.property.transform(tw); + }); + _(AST_Unary, function(self, tw) { + self.expression = self.expression.transform(tw); + }); + _(AST_Binary, function(self, tw) { + self.left = self.left.transform(tw); + self.right = self.right.transform(tw); + }); + _(AST_Conditional, function(self, tw) { + self.condition = self.condition.transform(tw); + self.consequent = self.consequent.transform(tw); + self.alternative = self.alternative.transform(tw); + }); + _(AST_Array, function(self, tw) { + self.elements = do_list(self.elements, tw); + }); + _(AST_Object, function(self, tw) { + self.properties = do_list(self.properties, tw); + }); + _(AST_ObjectProperty, function(self, tw) { + self.value = self.value.transform(tw); + }); + })(); + "use strict"; + function SymbolDef(scope, index, orig) { + this.name = orig.name; + this.orig = [ orig ]; + this.scope = scope; + this.references = []; + this.global = false; + this.mangled_name = null; + this.undeclared = false; + this.constant = false; + this.index = index; + } + SymbolDef.prototype = { + unmangleable: function(options) { + return this.global && !(options && options.toplevel) || this.undeclared || !(options && options.eval) && (this.scope.uses_eval || this.scope.uses_with); + }, + mangle: function(options) { + if (!this.mangled_name && !this.unmangleable(options)) { + var s = this.scope; + if (!options.screw_ie8 && this.orig[0] instanceof AST_SymbolLambda) s = s.parent_scope; + this.mangled_name = s.next_mangled(options, this); + } + } + }; + AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) { + options = defaults(options, { + screw_ie8: false + }); + var self = this; + var scope = self.parent_scope = null; + var defun = null; + var nesting = 0; + var tw = new TreeWalker(function(node, descend) { + if (options.screw_ie8 && node instanceof AST_Catch) { + var save_scope = scope; + scope = new AST_Scope(node); + scope.init_scope_vars(nesting); + scope.parent_scope = save_scope; + descend(); + scope = save_scope; + return true; + } + if (node instanceof AST_Scope) { + node.init_scope_vars(nesting); + var save_scope = node.parent_scope = scope; + var save_defun = defun; + defun = scope = node; + ++nesting; + descend(); + --nesting; + scope = save_scope; + defun = save_defun; + return true; + } + if (node instanceof AST_Directive) { + node.scope = scope; + push_uniq(scope.directives, node.value); + return true; + } + if (node instanceof AST_With) { + for (var s = scope; s; s = s.parent_scope) s.uses_with = true; + return; + } + if (node instanceof AST_Symbol) { + node.scope = scope; + } + if (node instanceof AST_SymbolLambda) { + defun.def_function(node); + } else if (node instanceof AST_SymbolDefun) { + (node.scope = defun.parent_scope).def_function(node); + } else if (node instanceof AST_SymbolVar || node instanceof AST_SymbolConst) { + var def = defun.def_variable(node); + def.constant = node instanceof AST_SymbolConst; + def.init = tw.parent().value; + } else if (node instanceof AST_SymbolCatch) { + (options.screw_ie8 ? scope : defun).def_variable(node); + } + }); + self.walk(tw); + var func = null; + var globals = self.globals = new Dictionary(); + var tw = new TreeWalker(function(node, descend) { + if (node instanceof AST_Lambda) { + var prev_func = func; + func = node; + descend(); + func = prev_func; + return true; + } + if (node instanceof AST_SymbolRef) { + var name = node.name; + var sym = node.scope.find_variable(name); + if (!sym) { + var g; + if (globals.has(name)) { + g = globals.get(name); + } else { + g = new SymbolDef(self, globals.size(), node); + g.undeclared = true; + g.global = true; + globals.set(name, g); + } + node.thedef = g; + if (name == "eval" && tw.parent() instanceof AST_Call) { + for (var s = node.scope; s && !s.uses_eval; s = s.parent_scope) s.uses_eval = true; + } + if (func && name == "arguments") { + func.uses_arguments = true; + } + } else { + node.thedef = sym; + } + node.reference(); + return true; + } + }); + self.walk(tw); + }); + AST_Scope.DEFMETHOD("init_scope_vars", function(nesting) { + this.directives = []; + this.variables = new Dictionary(); + this.functions = new Dictionary(); + this.uses_with = false; + this.uses_eval = false; + this.parent_scope = null; + this.enclosed = []; + this.cname = -1; + this.nesting = nesting; + }); + AST_Scope.DEFMETHOD("strict", function() { + return this.has_directive("use strict"); + }); + AST_Lambda.DEFMETHOD("init_scope_vars", function() { + AST_Scope.prototype.init_scope_vars.apply(this, arguments); + this.uses_arguments = false; + }); + AST_SymbolRef.DEFMETHOD("reference", function() { + var def = this.definition(); + def.references.push(this); + var s = this.scope; + while (s) { + push_uniq(s.enclosed, def); + if (s === def.scope) break; + s = s.parent_scope; + } + this.frame = this.scope.nesting - def.scope.nesting; + }); + AST_Scope.DEFMETHOD("find_variable", function(name) { + if (name instanceof AST_Symbol) name = name.name; + return this.variables.get(name) || this.parent_scope && this.parent_scope.find_variable(name); + }); + AST_Scope.DEFMETHOD("has_directive", function(value) { + return this.parent_scope && this.parent_scope.has_directive(value) || (this.directives.indexOf(value) >= 0 ? this : null); + }); + AST_Scope.DEFMETHOD("def_function", function(symbol) { + this.functions.set(symbol.name, this.def_variable(symbol)); + }); + AST_Scope.DEFMETHOD("def_variable", function(symbol) { + var def; + if (!this.variables.has(symbol.name)) { + def = new SymbolDef(this, this.variables.size(), symbol); + this.variables.set(symbol.name, def); + def.global = !this.parent_scope; + } else { + def = this.variables.get(symbol.name); + def.orig.push(symbol); + } + return symbol.thedef = def; + }); + AST_Scope.DEFMETHOD("next_mangled", function(options) { + var ext = this.enclosed; + out: while (true) { + var m = base54(++this.cname); + if (!is_identifier(m)) continue; + if (options.except.indexOf(m) >= 0) continue; + for (var i = ext.length; --i >= 0; ) { + var sym = ext[i]; + var name = sym.mangled_name || sym.unmangleable(options) && sym.name; + if (m == name) continue out; + } + return m; + } + }); + AST_Function.DEFMETHOD("next_mangled", function(options, def) { + var tricky_def = def.orig[0] instanceof AST_SymbolFunarg && this.name && this.name.definition(); + while (true) { + var name = AST_Lambda.prototype.next_mangled.call(this, options, def); + if (!(tricky_def && tricky_def.mangled_name == name)) return name; + } + }); + AST_Scope.DEFMETHOD("references", function(sym) { + if (sym instanceof AST_Symbol) sym = sym.definition(); + return this.enclosed.indexOf(sym) < 0 ? null : sym; + }); + AST_Symbol.DEFMETHOD("unmangleable", function(options) { + return this.definition().unmangleable(options); + }); + AST_SymbolAccessor.DEFMETHOD("unmangleable", function() { + return true; + }); + AST_Label.DEFMETHOD("unmangleable", function() { + return false; + }); + AST_Symbol.DEFMETHOD("unreferenced", function() { + return this.definition().references.length == 0 && !(this.scope.uses_eval || this.scope.uses_with); + }); + AST_Symbol.DEFMETHOD("undeclared", function() { + return this.definition().undeclared; + }); + AST_LabelRef.DEFMETHOD("undeclared", function() { + return false; + }); + AST_Label.DEFMETHOD("undeclared", function() { + return false; + }); + AST_Symbol.DEFMETHOD("definition", function() { + return this.thedef; + }); + AST_Symbol.DEFMETHOD("global", function() { + return this.definition().global; + }); + AST_Toplevel.DEFMETHOD("_default_mangler_options", function(options) { + return defaults(options, { + except: [], + eval: false, + sort: false, + toplevel: false, + screw_ie8: false + }); + }); + AST_Toplevel.DEFMETHOD("mangle_names", function(options) { + options = this._default_mangler_options(options); + var lname = -1; + var to_mangle = []; + var tw = new TreeWalker(function(node, descend) { + if (node instanceof AST_LabeledStatement) { + var save_nesting = lname; + descend(); + lname = save_nesting; + return true; + } + if (node instanceof AST_Scope) { + var p = tw.parent(), a = []; + node.variables.each(function(symbol) { + if (options.except.indexOf(symbol.name) < 0) { + a.push(symbol); + } + }); + if (options.sort) a.sort(function(a, b) { + return b.references.length - a.references.length; + }); + to_mangle.push.apply(to_mangle, a); + return; + } + if (node instanceof AST_Label) { + var name; + do name = base54(++lname); while (!is_identifier(name)); + node.mangled_name = name; + return true; + } + if (options.screw_ie8 && node instanceof AST_SymbolCatch) { + to_mangle.push(node.definition()); + return; + } + }); + this.walk(tw); + to_mangle.forEach(function(def) { + def.mangle(options); + }); + }); + AST_Toplevel.DEFMETHOD("compute_char_frequency", function(options) { + options = this._default_mangler_options(options); + var tw = new TreeWalker(function(node) { + if (node instanceof AST_Constant) base54.consider(node.print_to_string()); else if (node instanceof AST_Return) base54.consider("return"); else if (node instanceof AST_Throw) base54.consider("throw"); else if (node instanceof AST_Continue) base54.consider("continue"); else if (node instanceof AST_Break) base54.consider("break"); else if (node instanceof AST_Debugger) base54.consider("debugger"); else if (node instanceof AST_Directive) base54.consider(node.value); else if (node instanceof AST_While) base54.consider("while"); else if (node instanceof AST_Do) base54.consider("do while"); else if (node instanceof AST_If) { + base54.consider("if"); + if (node.alternative) base54.consider("else"); + } else if (node instanceof AST_Var) base54.consider("var"); else if (node instanceof AST_Const) base54.consider("const"); else if (node instanceof AST_Lambda) base54.consider("function"); else if (node instanceof AST_For) base54.consider("for"); else if (node instanceof AST_ForIn) base54.consider("for in"); else if (node instanceof AST_Switch) base54.consider("switch"); else if (node instanceof AST_Case) base54.consider("case"); else if (node instanceof AST_Default) base54.consider("default"); else if (node instanceof AST_With) base54.consider("with"); else if (node instanceof AST_ObjectSetter) base54.consider("set" + node.key); else if (node instanceof AST_ObjectGetter) base54.consider("get" + node.key); else if (node instanceof AST_ObjectKeyVal) base54.consider(node.key); else if (node instanceof AST_New) base54.consider("new"); else if (node instanceof AST_This) base54.consider("this"); else if (node instanceof AST_Try) base54.consider("try"); else if (node instanceof AST_Catch) base54.consider("catch"); else if (node instanceof AST_Finally) base54.consider("finally"); else if (node instanceof AST_Symbol && node.unmangleable(options)) base54.consider(node.name); else if (node instanceof AST_Unary || node instanceof AST_Binary) base54.consider(node.operator); else if (node instanceof AST_Dot) base54.consider(node.property); + }); + this.walk(tw); + base54.sort(); + }); + var base54 = function() { + var string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_0123456789"; + var chars, frequency; + function reset() { + frequency = Object.create(null); + chars = string.split("").map(function(ch) { + return ch.charCodeAt(0); + }); + chars.forEach(function(ch) { + frequency[ch] = 0; + }); + } + base54.consider = function(str) { + for (var i = str.length; --i >= 0; ) { + var code = str.charCodeAt(i); + if (code in frequency) ++frequency[code]; + } + }; + base54.sort = function() { + chars = mergeSort(chars, function(a, b) { + if (is_digit(a) && !is_digit(b)) return 1; + if (is_digit(b) && !is_digit(a)) return -1; + return frequency[b] - frequency[a]; + }); + }; + base54.reset = reset; + reset(); + base54.get = function() { + return chars; + }; + base54.freq = function() { + return frequency; + }; + function base54(num) { + var ret = "", base = 54; + do { + ret += String.fromCharCode(chars[num % base]); + num = Math.floor(num / base); + base = 64; + } while (num > 0); + return ret; + } + return base54; + }(); + AST_Toplevel.DEFMETHOD("scope_warnings", function(options) { + options = defaults(options, { + undeclared: false, + unreferenced: true, + assign_to_global: true, + func_arguments: true, + nested_defuns: true, + eval: true + }); + var tw = new TreeWalker(function(node) { + if (options.undeclared && node instanceof AST_SymbolRef && node.undeclared()) { + AST_Node.warn("Undeclared symbol: {name} [{file}:{line},{col}]", { + name: node.name, + file: node.start.file, + line: node.start.line, + col: node.start.col + }); + } + if (options.assign_to_global) { + var sym = null; + if (node instanceof AST_Assign && node.left instanceof AST_SymbolRef) sym = node.left; else if (node instanceof AST_ForIn && node.init instanceof AST_SymbolRef) sym = node.init; + if (sym && (sym.undeclared() || sym.global() && sym.scope !== sym.definition().scope)) { + AST_Node.warn("{msg}: {name} [{file}:{line},{col}]", { + msg: sym.undeclared() ? "Accidental global?" : "Assignment to global", + name: sym.name, + file: sym.start.file, + line: sym.start.line, + col: sym.start.col + }); + } + } + if (options.eval && node instanceof AST_SymbolRef && node.undeclared() && node.name == "eval") { + AST_Node.warn("Eval is used [{file}:{line},{col}]", node.start); + } + if (options.unreferenced && (node instanceof AST_SymbolDeclaration || node instanceof AST_Label) && node.unreferenced()) { + AST_Node.warn("{type} {name} is declared but not referenced [{file}:{line},{col}]", { + type: node instanceof AST_Label ? "Label" : "Symbol", + name: node.name, + file: node.start.file, + line: node.start.line, + col: node.start.col + }); + } + if (options.func_arguments && node instanceof AST_Lambda && node.uses_arguments) { + AST_Node.warn("arguments used in function {name} [{file}:{line},{col}]", { + name: node.name ? node.name.name : "anonymous", + file: node.start.file, + line: node.start.line, + col: node.start.col + }); + } + if (options.nested_defuns && node instanceof AST_Defun && !(tw.parent() instanceof AST_Scope)) { + AST_Node.warn('Function {name} declared in nested statement "{type}" [{file}:{line},{col}]', { + name: node.name.name, + type: tw.parent().TYPE, + file: node.start.file, + line: node.start.line, + col: node.start.col + }); + } + }); + this.walk(tw); + }); + "use strict"; + function OutputStream(options) { + options = defaults(options, { + indent_start: 0, + indent_level: 4, + quote_keys: false, + space_colon: true, + ascii_only: false, + unescape_regexps: false, + inline_script: false, + width: 80, + max_line_len: 32e3, + beautify: false, + source_map: null, + bracketize: false, + semicolons: true, + comments: false, + preserve_line: false, + screw_ie8: false, + preamble: null + }, true); + var indentation = 0; + var current_col = 0; + var current_line = 1; + var current_pos = 0; + var OUTPUT = ""; + function to_ascii(str, identifier) { + return str.replace(/[\u0080-\uffff]/g, function(ch) { + var code = ch.charCodeAt(0).toString(16); + if (code.length <= 2 && !identifier) { + while (code.length < 2) code = "0" + code; + return "\\x" + code; + } else { + while (code.length < 4) code = "0" + code; + return "\\u" + code; + } + }); + } + function make_string(str) { + var dq = 0, sq = 0; + str = str.replace(/[\\\b\f\n\r\t\x22\x27\u2028\u2029\0]/g, function(s) { + switch (s) { + case "\\": + return "\\\\"; + + case "\b": + return "\\b"; + + case "\f": + return "\\f"; + + case "\n": + return "\\n"; + + case "\r": + return "\\r"; + + case "\u2028": + return "\\u2028"; + + case "\u2029": + return "\\u2029"; + + case '"': + ++dq; + return '"'; + + case "'": + ++sq; + return "'"; + + case "\x00": + return "\\x00"; + } + return s; + }); + if (options.ascii_only) str = to_ascii(str); + if (dq > sq) return "'" + str.replace(/\x27/g, "\\'") + "'"; else return '"' + str.replace(/\x22/g, '\\"') + '"'; + } + function encode_string(str) { + var ret = make_string(str); + if (options.inline_script) ret = ret.replace(/<\x2fscript([>\/\t\n\f\r ])/gi, "<\\/script$1"); + return ret; + } + function make_name(name) { + name = name.toString(); + if (options.ascii_only) name = to_ascii(name, true); + return name; + } + function make_indent(back) { + return repeat_string(" ", options.indent_start + indentation - back * options.indent_level); + } + var might_need_space = false; + var might_need_semicolon = false; + var last = null; + function last_char() { + return last.charAt(last.length - 1); + } + function maybe_newline() { + if (options.max_line_len && current_col > options.max_line_len) print("\n"); + } + var requireSemicolonChars = makePredicate("( [ + * / - , ."); + function print(str) { + str = String(str); + var ch = str.charAt(0); + if (might_need_semicolon) { + if ((!ch || ";}".indexOf(ch) < 0) && !/[;]$/.test(last)) { + if (options.semicolons || requireSemicolonChars(ch)) { + OUTPUT += ";"; + current_col++; + current_pos++; + } else { + OUTPUT += "\n"; + current_pos++; + current_line++; + current_col = 0; + } + if (!options.beautify) might_need_space = false; + } + might_need_semicolon = false; + maybe_newline(); + } + if (!options.beautify && options.preserve_line && stack[stack.length - 1]) { + var target_line = stack[stack.length - 1].start.line; + while (current_line < target_line) { + OUTPUT += "\n"; + current_pos++; + current_line++; + current_col = 0; + might_need_space = false; + } + } + if (might_need_space) { + var prev = last_char(); + if (is_identifier_char(prev) && (is_identifier_char(ch) || ch == "\\") || /^[\+\-\/]$/.test(ch) && ch == prev) { + OUTPUT += " "; + current_col++; + current_pos++; + } + might_need_space = false; + } + var a = str.split(/\r?\n/), n = a.length - 1; + current_line += n; + if (n == 0) { + current_col += a[n].length; + } else { + current_col = a[n].length; + } + current_pos += str.length; + last = str; + OUTPUT += str; + } + var space = options.beautify ? function() { + print(" "); + } : function() { + might_need_space = true; + }; + var indent = options.beautify ? function(half) { + if (options.beautify) { + print(make_indent(half ? .5 : 0)); + } + } : noop; + var with_indent = options.beautify ? function(col, cont) { + if (col === true) col = next_indent(); + var save_indentation = indentation; + indentation = col; + var ret = cont(); + indentation = save_indentation; + return ret; + } : function(col, cont) { + return cont(); + }; + var newline = options.beautify ? function() { + print("\n"); + } : noop; + var semicolon = options.beautify ? function() { + print(";"); + } : function() { + might_need_semicolon = true; + }; + function force_semicolon() { + might_need_semicolon = false; + print(";"); + } + function next_indent() { + return indentation + options.indent_level; + } + function with_block(cont) { + var ret; + print("{"); + newline(); + with_indent(next_indent(), function() { + ret = cont(); + }); + indent(); + print("}"); + return ret; + } + function with_parens(cont) { + print("("); + var ret = cont(); + print(")"); + return ret; + } + function with_square(cont) { + print("["); + var ret = cont(); + print("]"); + return ret; + } + function comma() { + print(","); + space(); + } + function colon() { + print(":"); + if (options.space_colon) space(); + } + var add_mapping = options.source_map ? function(token, name) { + try { + if (token) options.source_map.add(token.file || "?", current_line, current_col, token.line, token.col, !name && token.type == "name" ? token.value : name); + } catch (ex) { + AST_Node.warn("Couldn't figure out mapping for {file}:{line},{col} → {cline},{ccol} [{name}]", { + file: token.file, + line: token.line, + col: token.col, + cline: current_line, + ccol: current_col, + name: name || "" + }); + } + } : noop; + function get() { + return OUTPUT; + } + if (options.preamble) { + print(options.preamble.replace(/\r\n?|[\n\u2028\u2029]|\s*$/g, "\n")); + } + var stack = []; + return { + get: get, + toString: get, + indent: indent, + indentation: function() { + return indentation; + }, + current_width: function() { + return current_col - indentation; + }, + should_break: function() { + return options.width && this.current_width() >= options.width; + }, + newline: newline, + print: print, + space: space, + comma: comma, + colon: colon, + last: function() { + return last; + }, + semicolon: semicolon, + force_semicolon: force_semicolon, + to_ascii: to_ascii, + print_name: function(name) { + print(make_name(name)); + }, + print_string: function(str) { + print(encode_string(str)); + }, + next_indent: next_indent, + with_indent: with_indent, + with_block: with_block, + with_parens: with_parens, + with_square: with_square, + add_mapping: add_mapping, + option: function(opt) { + return options[opt]; + }, + line: function() { + return current_line; + }, + col: function() { + return current_col; + }, + pos: function() { + return current_pos; + }, + push_node: function(node) { + stack.push(node); + }, + pop_node: function() { + return stack.pop(); + }, + stack: function() { + return stack; + }, + parent: function(n) { + return stack[stack.length - 2 - (n || 0)]; + } + }; + } + (function() { + function DEFPRINT(nodetype, generator) { + nodetype.DEFMETHOD("_codegen", generator); + } + AST_Node.DEFMETHOD("print", function(stream, force_parens) { + var self = this, generator = self._codegen; + function doit() { + self.add_comments(stream); + self.add_source_map(stream); + generator(self, stream); + } + stream.push_node(self); + if (force_parens || self.needs_parens(stream)) { + stream.with_parens(doit); + } else { + doit(); + } + stream.pop_node(); + }); + AST_Node.DEFMETHOD("print_to_string", function(options) { + var s = OutputStream(options); + this.print(s); + return s.get(); + }); + AST_Node.DEFMETHOD("add_comments", function(output) { + var c = output.option("comments"), self = this; + if (c) { + var start = self.start; + if (start && !start._comments_dumped) { + start._comments_dumped = true; + var comments = start.comments_before || []; + if (self instanceof AST_Exit && self.value) { + self.value.walk(new TreeWalker(function(node) { + if (node.start && node.start.comments_before) { + comments = comments.concat(node.start.comments_before); + node.start.comments_before = []; + } + if (node instanceof AST_Function || node instanceof AST_Array || node instanceof AST_Object) { + return true; + } + })); + } + if (c.test) { + comments = comments.filter(function(comment) { + return c.test(comment.value); + }); + } else if (typeof c == "function") { + comments = comments.filter(function(comment) { + return c(self, comment); + }); + } + comments.forEach(function(c) { + if (/comment[134]/.test(c.type)) { + output.print("//" + c.value + "\n"); + output.indent(); + } else if (c.type == "comment2") { + output.print("/*" + c.value + "*/"); + if (start.nlb) { + output.print("\n"); + output.indent(); + } else { + output.space(); + } + } + }); + } + } + }); + function PARENS(nodetype, func) { + if (Array.isArray(nodetype)) { + nodetype.forEach(function(nodetype) { + PARENS(nodetype, func); + }); + } else { + nodetype.DEFMETHOD("needs_parens", func); + } + } + PARENS(AST_Node, function() { + return false; + }); + PARENS(AST_Function, function(output) { + return first_in_statement(output); + }); + PARENS(AST_Object, function(output) { + return first_in_statement(output); + }); + PARENS([ AST_Unary, AST_Undefined ], function(output) { + var p = output.parent(); + return p instanceof AST_PropAccess && p.expression === this; + }); + PARENS(AST_Seq, function(output) { + var p = output.parent(); + return p instanceof AST_Call || p instanceof AST_Unary || p instanceof AST_Binary || p instanceof AST_VarDef || p instanceof AST_PropAccess || p instanceof AST_Array || p instanceof AST_ObjectProperty || p instanceof AST_Conditional; + }); + PARENS(AST_Binary, function(output) { + var p = output.parent(); + if (p instanceof AST_Call && p.expression === this) return true; + if (p instanceof AST_Unary) return true; + if (p instanceof AST_PropAccess && p.expression === this) return true; + if (p instanceof AST_Binary) { + var po = p.operator, pp = PRECEDENCE[po]; + var so = this.operator, sp = PRECEDENCE[so]; + if (pp > sp || pp == sp && this === p.right) { + return true; + } + } + }); + PARENS(AST_PropAccess, function(output) { + var p = output.parent(); + if (p instanceof AST_New && p.expression === this) { + try { + this.walk(new TreeWalker(function(node) { + if (node instanceof AST_Call) throw p; + })); + } catch (ex) { + if (ex !== p) throw ex; + return true; + } + } + }); + PARENS(AST_Call, function(output) { + var p = output.parent(), p1; + if (p instanceof AST_New && p.expression === this) return true; + return this.expression instanceof AST_Function && p instanceof AST_PropAccess && p.expression === this && (p1 = output.parent(1)) instanceof AST_Assign && p1.left === p; + }); + PARENS(AST_New, function(output) { + var p = output.parent(); + if (no_constructor_parens(this, output) && (p instanceof AST_PropAccess || p instanceof AST_Call && p.expression === this)) return true; + }); + PARENS(AST_Number, function(output) { + var p = output.parent(); + if (this.getValue() < 0 && p instanceof AST_PropAccess && p.expression === this) return true; + }); + PARENS(AST_NaN, function(output) { + var p = output.parent(); + if (p instanceof AST_PropAccess && p.expression === this) return true; + }); + PARENS([ AST_Assign, AST_Conditional ], function(output) { + var p = output.parent(); + if (p instanceof AST_Unary) return true; + if (p instanceof AST_Binary && !(p instanceof AST_Assign)) return true; + if (p instanceof AST_Call && p.expression === this) return true; + if (p instanceof AST_Conditional && p.condition === this) return true; + if (p instanceof AST_PropAccess && p.expression === this) return true; + }); + DEFPRINT(AST_Directive, function(self, output) { + output.print_string(self.value); + output.semicolon(); + }); + DEFPRINT(AST_Debugger, function(self, output) { + output.print("debugger"); + output.semicolon(); + }); + function display_body(body, is_toplevel, output) { + var last = body.length - 1; + body.forEach(function(stmt, i) { + if (!(stmt instanceof AST_EmptyStatement)) { + output.indent(); + stmt.print(output); + if (!(i == last && is_toplevel)) { + output.newline(); + if (is_toplevel) output.newline(); + } + } + }); + } + AST_StatementWithBody.DEFMETHOD("_do_print_body", function(output) { + force_statement(this.body, output); + }); + DEFPRINT(AST_Statement, function(self, output) { + self.body.print(output); + output.semicolon(); + }); + DEFPRINT(AST_Toplevel, function(self, output) { + display_body(self.body, true, output); + output.print(""); + }); + DEFPRINT(AST_LabeledStatement, function(self, output) { + self.label.print(output); + output.colon(); + self.body.print(output); + }); + DEFPRINT(AST_SimpleStatement, function(self, output) { + self.body.print(output); + output.semicolon(); + }); + function print_bracketed(body, output) { + if (body.length > 0) output.with_block(function() { + display_body(body, false, output); + }); else output.print("{}"); + } + DEFPRINT(AST_BlockStatement, function(self, output) { + print_bracketed(self.body, output); + }); + DEFPRINT(AST_EmptyStatement, function(self, output) { + output.semicolon(); + }); + DEFPRINT(AST_Do, function(self, output) { + output.print("do"); + output.space(); + self._do_print_body(output); + output.space(); + output.print("while"); + output.space(); + output.with_parens(function() { + self.condition.print(output); + }); + output.semicolon(); + }); + DEFPRINT(AST_While, function(self, output) { + output.print("while"); + output.space(); + output.with_parens(function() { + self.condition.print(output); + }); + output.space(); + self._do_print_body(output); + }); + DEFPRINT(AST_For, function(self, output) { + output.print("for"); + output.space(); + output.with_parens(function() { + if (self.init && !(self.init instanceof AST_EmptyStatement)) { + if (self.init instanceof AST_Definitions) { + self.init.print(output); + } else { + parenthesize_for_noin(self.init, output, true); + } + output.print(";"); + output.space(); + } else { + output.print(";"); + } + if (self.condition) { + self.condition.print(output); + output.print(";"); + output.space(); + } else { + output.print(";"); + } + if (self.step) { + self.step.print(output); + } + }); + output.space(); + self._do_print_body(output); + }); + DEFPRINT(AST_ForIn, function(self, output) { + output.print("for"); + output.space(); + output.with_parens(function() { + self.init.print(output); + output.space(); + output.print("in"); + output.space(); + self.object.print(output); + }); + output.space(); + self._do_print_body(output); + }); + DEFPRINT(AST_With, function(self, output) { + output.print("with"); + output.space(); + output.with_parens(function() { + self.expression.print(output); + }); + output.space(); + self._do_print_body(output); + }); + AST_Lambda.DEFMETHOD("_do_print", function(output, nokeyword) { + var self = this; + if (!nokeyword) { + output.print("function"); + } + if (self.name) { + output.space(); + self.name.print(output); + } + output.with_parens(function() { + self.argnames.forEach(function(arg, i) { + if (i) output.comma(); + arg.print(output); + }); + }); + output.space(); + print_bracketed(self.body, output); + }); + DEFPRINT(AST_Lambda, function(self, output) { + self._do_print(output); + }); + AST_Exit.DEFMETHOD("_do_print", function(output, kind) { + output.print(kind); + if (this.value) { + output.space(); + this.value.print(output); + } + output.semicolon(); + }); + DEFPRINT(AST_Return, function(self, output) { + self._do_print(output, "return"); + }); + DEFPRINT(AST_Throw, function(self, output) { + self._do_print(output, "throw"); + }); + AST_LoopControl.DEFMETHOD("_do_print", function(output, kind) { + output.print(kind); + if (this.label) { + output.space(); + this.label.print(output); + } + output.semicolon(); + }); + DEFPRINT(AST_Break, function(self, output) { + self._do_print(output, "break"); + }); + DEFPRINT(AST_Continue, function(self, output) { + self._do_print(output, "continue"); + }); + function make_then(self, output) { + if (output.option("bracketize")) { + make_block(self.body, output); + return; + } + if (!self.body) return output.force_semicolon(); + if (self.body instanceof AST_Do && !output.option("screw_ie8")) { + make_block(self.body, output); + return; + } + var b = self.body; + while (true) { + if (b instanceof AST_If) { + if (!b.alternative) { + make_block(self.body, output); + return; + } + b = b.alternative; + } else if (b instanceof AST_StatementWithBody) { + b = b.body; + } else break; + } + force_statement(self.body, output); + } + DEFPRINT(AST_If, function(self, output) { + output.print("if"); + output.space(); + output.with_parens(function() { + self.condition.print(output); + }); + output.space(); + if (self.alternative) { + make_then(self, output); + output.space(); + output.print("else"); + output.space(); + force_statement(self.alternative, output); + } else { + self._do_print_body(output); + } + }); + DEFPRINT(AST_Switch, function(self, output) { + output.print("switch"); + output.space(); + output.with_parens(function() { + self.expression.print(output); + }); + output.space(); + if (self.body.length > 0) output.with_block(function() { + self.body.forEach(function(stmt, i) { + if (i) output.newline(); + output.indent(true); + stmt.print(output); + }); + }); else output.print("{}"); + }); + AST_SwitchBranch.DEFMETHOD("_do_print_body", function(output) { + if (this.body.length > 0) { + output.newline(); + this.body.forEach(function(stmt) { + output.indent(); + stmt.print(output); + output.newline(); + }); + } + }); + DEFPRINT(AST_Default, function(self, output) { + output.print("default:"); + self._do_print_body(output); + }); + DEFPRINT(AST_Case, function(self, output) { + output.print("case"); + output.space(); + self.expression.print(output); + output.print(":"); + self._do_print_body(output); + }); + DEFPRINT(AST_Try, function(self, output) { + output.print("try"); + output.space(); + print_bracketed(self.body, output); + if (self.bcatch) { + output.space(); + self.bcatch.print(output); + } + if (self.bfinally) { + output.space(); + self.bfinally.print(output); + } + }); + DEFPRINT(AST_Catch, function(self, output) { + output.print("catch"); + output.space(); + output.with_parens(function() { + self.argname.print(output); + }); + output.space(); + print_bracketed(self.body, output); + }); + DEFPRINT(AST_Finally, function(self, output) { + output.print("finally"); + output.space(); + print_bracketed(self.body, output); + }); + AST_Definitions.DEFMETHOD("_do_print", function(output, kind) { + output.print(kind); + output.space(); + this.definitions.forEach(function(def, i) { + if (i) output.comma(); + def.print(output); + }); + var p = output.parent(); + var in_for = p instanceof AST_For || p instanceof AST_ForIn; + var avoid_semicolon = in_for && p.init === this; + if (!avoid_semicolon) output.semicolon(); + }); + DEFPRINT(AST_Var, function(self, output) { + self._do_print(output, "var"); + }); + DEFPRINT(AST_Const, function(self, output) { + self._do_print(output, "const"); + }); + function parenthesize_for_noin(node, output, noin) { + if (!noin) node.print(output); else try { + node.walk(new TreeWalker(function(node) { + if (node instanceof AST_Binary && node.operator == "in") throw output; + })); + node.print(output); + } catch (ex) { + if (ex !== output) throw ex; + node.print(output, true); + } + } + DEFPRINT(AST_VarDef, function(self, output) { + self.name.print(output); + if (self.value) { + output.space(); + output.print("="); + output.space(); + var p = output.parent(1); + var noin = p instanceof AST_For || p instanceof AST_ForIn; + parenthesize_for_noin(self.value, output, noin); + } + }); + DEFPRINT(AST_Call, function(self, output) { + self.expression.print(output); + if (self instanceof AST_New && no_constructor_parens(self, output)) return; + output.with_parens(function() { + self.args.forEach(function(expr, i) { + if (i) output.comma(); + expr.print(output); + }); + }); + }); + DEFPRINT(AST_New, function(self, output) { + output.print("new"); + output.space(); + AST_Call.prototype._codegen(self, output); + }); + AST_Seq.DEFMETHOD("_do_print", function(output) { + this.car.print(output); + if (this.cdr) { + output.comma(); + if (output.should_break()) { + output.newline(); + output.indent(); + } + this.cdr.print(output); + } + }); + DEFPRINT(AST_Seq, function(self, output) { + self._do_print(output); + }); + DEFPRINT(AST_Dot, function(self, output) { + var expr = self.expression; + expr.print(output); + if (expr instanceof AST_Number && expr.getValue() >= 0) { + if (!/[xa-f.]/i.test(output.last())) { + output.print("."); + } + } + output.print("."); + output.add_mapping(self.end); + output.print_name(self.property); + }); + DEFPRINT(AST_Sub, function(self, output) { + self.expression.print(output); + output.print("["); + self.property.print(output); + output.print("]"); + }); + DEFPRINT(AST_UnaryPrefix, function(self, output) { + var op = self.operator; + output.print(op); + if (/^[a-z]/i.test(op) || /[+-]$/.test(op) && self.expression instanceof AST_UnaryPrefix && /^[+-]/.test(self.expression.operator)) { + output.space(); + } + self.expression.print(output); + }); + DEFPRINT(AST_UnaryPostfix, function(self, output) { + self.expression.print(output); + output.print(self.operator); + }); + DEFPRINT(AST_Binary, function(self, output) { + self.left.print(output); + output.space(); + output.print(self.operator); + if (self.operator == "<" && self.right instanceof AST_UnaryPrefix && self.right.operator == "!" && self.right.expression instanceof AST_UnaryPrefix && self.right.expression.operator == "--") { + output.print(" "); + } else { + output.space(); + } + self.right.print(output); + }); + DEFPRINT(AST_Conditional, function(self, output) { + self.condition.print(output); + output.space(); + output.print("?"); + output.space(); + self.consequent.print(output); + output.space(); + output.colon(); + self.alternative.print(output); + }); + DEFPRINT(AST_Array, function(self, output) { + output.with_square(function() { + var a = self.elements, len = a.length; + if (len > 0) output.space(); + a.forEach(function(exp, i) { + if (i) output.comma(); + exp.print(output); + if (i === len - 1 && exp instanceof AST_Hole) output.comma(); + }); + if (len > 0) output.space(); + }); + }); + DEFPRINT(AST_Object, function(self, output) { + if (self.properties.length > 0) output.with_block(function() { + self.properties.forEach(function(prop, i) { + if (i) { + output.print(","); + output.newline(); + } + output.indent(); + prop.print(output); + }); + output.newline(); + }); else output.print("{}"); + }); + DEFPRINT(AST_ObjectKeyVal, function(self, output) { + var key = self.key; + if (output.option("quote_keys")) { + output.print_string(key + ""); + } else if ((typeof key == "number" || !output.option("beautify") && +key + "" == key) && parseFloat(key) >= 0) { + output.print(make_num(key)); + } else if (RESERVED_WORDS(key) ? output.option("screw_ie8") : is_identifier_string(key)) { + output.print_name(key); + } else { + output.print_string(key); + } + output.colon(); + self.value.print(output); + }); + DEFPRINT(AST_ObjectSetter, function(self, output) { + output.print("set"); + output.space(); + self.key.print(output); + self.value._do_print(output, true); + }); + DEFPRINT(AST_ObjectGetter, function(self, output) { + output.print("get"); + output.space(); + self.key.print(output); + self.value._do_print(output, true); + }); + DEFPRINT(AST_Symbol, function(self, output) { + var def = self.definition(); + output.print_name(def ? def.mangled_name || def.name : self.name); + }); + DEFPRINT(AST_Undefined, function(self, output) { + output.print("void 0"); + }); + DEFPRINT(AST_Hole, noop); + DEFPRINT(AST_Infinity, function(self, output) { + output.print("1/0"); + }); + DEFPRINT(AST_NaN, function(self, output) { + output.print("0/0"); + }); + DEFPRINT(AST_This, function(self, output) { + output.print("this"); + }); + DEFPRINT(AST_Constant, function(self, output) { + output.print(self.getValue()); + }); + DEFPRINT(AST_String, function(self, output) { + output.print_string(self.getValue()); + }); + DEFPRINT(AST_Number, function(self, output) { + output.print(make_num(self.getValue())); + }); + function regexp_safe_literal(code) { + return [ 92, 47, 46, 43, 42, 63, 40, 41, 91, 93, 123, 125, 36, 94, 58, 124, 33, 10, 13, 0, 65279, 8232, 8233 ].indexOf(code) < 0; + } + DEFPRINT(AST_RegExp, function(self, output) { + var str = self.getValue().toString(); + if (output.option("ascii_only")) { + str = output.to_ascii(str); + } else if (output.option("unescape_regexps")) { + str = str.split("\\\\").map(function(str) { + return str.replace(/\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{2}/g, function(s) { + var code = parseInt(s.substr(2), 16); + return regexp_safe_literal(code) ? String.fromCharCode(code) : s; + }); + }).join("\\\\"); + } + output.print(str); + var p = output.parent(); + if (p instanceof AST_Binary && /^in/.test(p.operator) && p.left === self) output.print(" "); + }); + function force_statement(stat, output) { + if (output.option("bracketize")) { + if (!stat || stat instanceof AST_EmptyStatement) output.print("{}"); else if (stat instanceof AST_BlockStatement) stat.print(output); else output.with_block(function() { + output.indent(); + stat.print(output); + output.newline(); + }); + } else { + if (!stat || stat instanceof AST_EmptyStatement) output.force_semicolon(); else stat.print(output); + } + } + function first_in_statement(output) { + var a = output.stack(), i = a.length, node = a[--i], p = a[--i]; + while (i > 0) { + if (p instanceof AST_Statement && p.body === node) return true; + if (p instanceof AST_Seq && p.car === node || p instanceof AST_Call && p.expression === node && !(p instanceof AST_New) || p instanceof AST_Dot && p.expression === node || p instanceof AST_Sub && p.expression === node || p instanceof AST_Conditional && p.condition === node || p instanceof AST_Binary && p.left === node || p instanceof AST_UnaryPostfix && p.expression === node) { + node = p; + p = a[--i]; + } else { + return false; + } + } + } + function no_constructor_parens(self, output) { + return self.args.length == 0 && !output.option("beautify"); + } + function best_of(a) { + var best = a[0], len = best.length; + for (var i = 1; i < a.length; ++i) { + if (a[i].length < len) { + best = a[i]; + len = best.length; + } + } + return best; + } + function make_num(num) { + var str = num.toString(10), a = [ str.replace(/^0\./, ".").replace("e+", "e") ], m; + if (Math.floor(num) === num) { + if (num >= 0) { + a.push("0x" + num.toString(16).toLowerCase(), "0" + num.toString(8)); + } else { + a.push("-0x" + (-num).toString(16).toLowerCase(), "-0" + (-num).toString(8)); + } + if (m = /^(.*?)(0+)$/.exec(num)) { + a.push(m[1] + "e" + m[2].length); + } + } else if (m = /^0?\.(0+)(.*)$/.exec(num)) { + a.push(m[2] + "e-" + (m[1].length + m[2].length), str.substr(str.indexOf("."))); + } + return best_of(a); + } + function make_block(stmt, output) { + if (stmt instanceof AST_BlockStatement) { + stmt.print(output); + return; + } + output.with_block(function() { + output.indent(); + stmt.print(output); + output.newline(); + }); + } + function DEFMAP(nodetype, generator) { + nodetype.DEFMETHOD("add_source_map", function(stream) { + generator(this, stream); + }); + } + DEFMAP(AST_Node, noop); + function basic_sourcemap_gen(self, output) { + output.add_mapping(self.start); + } + DEFMAP(AST_Directive, basic_sourcemap_gen); + DEFMAP(AST_Debugger, basic_sourcemap_gen); + DEFMAP(AST_Symbol, basic_sourcemap_gen); + DEFMAP(AST_Jump, basic_sourcemap_gen); + DEFMAP(AST_StatementWithBody, basic_sourcemap_gen); + DEFMAP(AST_LabeledStatement, noop); + DEFMAP(AST_Lambda, basic_sourcemap_gen); + DEFMAP(AST_Switch, basic_sourcemap_gen); + DEFMAP(AST_SwitchBranch, basic_sourcemap_gen); + DEFMAP(AST_BlockStatement, basic_sourcemap_gen); + DEFMAP(AST_Toplevel, noop); + DEFMAP(AST_New, basic_sourcemap_gen); + DEFMAP(AST_Try, basic_sourcemap_gen); + DEFMAP(AST_Catch, basic_sourcemap_gen); + DEFMAP(AST_Finally, basic_sourcemap_gen); + DEFMAP(AST_Definitions, basic_sourcemap_gen); + DEFMAP(AST_Constant, basic_sourcemap_gen); + DEFMAP(AST_ObjectProperty, function(self, output) { + output.add_mapping(self.start, self.key); + }); + })(); + "use strict"; + function Compressor(options, false_by_default) { + if (!(this instanceof Compressor)) return new Compressor(options, false_by_default); + TreeTransformer.call(this, this.before, this.after); + this.options = defaults(options, { + sequences: !false_by_default, + properties: !false_by_default, + dead_code: !false_by_default, + drop_debugger: !false_by_default, + unsafe: false, + unsafe_comps: false, + conditionals: !false_by_default, + comparisons: !false_by_default, + evaluate: !false_by_default, + booleans: !false_by_default, + loops: !false_by_default, + unused: !false_by_default, + hoist_funs: !false_by_default, + keep_fargs: false, + hoist_vars: false, + if_return: !false_by_default, + join_vars: !false_by_default, + cascade: !false_by_default, + side_effects: !false_by_default, + pure_getters: false, + pure_funcs: null, + negate_iife: !false_by_default, + screw_ie8: false, + drop_console: false, + angular: false, + warnings: true, + global_defs: {} + }, true); + } + Compressor.prototype = new TreeTransformer(); + merge(Compressor.prototype, { + option: function(key) { + return this.options[key]; + }, + warn: function() { + if (this.options.warnings) AST_Node.warn.apply(AST_Node, arguments); + }, + before: function(node, descend, in_list) { + if (node._squeezed) return node; + var was_scope = false; + if (node instanceof AST_Scope) { + node = node.hoist_declarations(this); + was_scope = true; + } + descend(node, this); + node = node.optimize(this); + if (was_scope && node instanceof AST_Scope) { + node.drop_unused(this); + descend(node, this); + } + node._squeezed = true; + return node; + } + }); + (function() { + function OPT(node, optimizer) { + node.DEFMETHOD("optimize", function(compressor) { + var self = this; + if (self._optimized) return self; + var opt = optimizer(self, compressor); + opt._optimized = true; + if (opt === self) return opt; + return opt.transform(compressor); + }); + } + OPT(AST_Node, function(self, compressor) { + return self; + }); + AST_Node.DEFMETHOD("equivalent_to", function(node) { + return this.print_to_string() == node.print_to_string(); + }); + function make_node(ctor, orig, props) { + if (!props) props = {}; + if (orig) { + if (!props.start) props.start = orig.start; + if (!props.end) props.end = orig.end; + } + return new ctor(props); + } + function make_node_from_constant(compressor, val, orig) { + if (val instanceof AST_Node) return val.transform(compressor); + switch (typeof val) { + case "string": + return make_node(AST_String, orig, { + value: val + }).optimize(compressor); + + case "number": + return make_node(isNaN(val) ? AST_NaN : AST_Number, orig, { + value: val + }).optimize(compressor); + + case "boolean": + return make_node(val ? AST_True : AST_False, orig).optimize(compressor); + + case "undefined": + return make_node(AST_Undefined, orig).optimize(compressor); + + default: + if (val === null) { + return make_node(AST_Null, orig).optimize(compressor); + } + if (val instanceof RegExp) { + return make_node(AST_RegExp, orig).optimize(compressor); + } + throw new Error(string_template("Can't handle constant of type: {type}", { + type: typeof val + })); + } + } + function as_statement_array(thing) { + if (thing === null) return []; + if (thing instanceof AST_BlockStatement) return thing.body; + if (thing instanceof AST_EmptyStatement) return []; + if (thing instanceof AST_Statement) return [ thing ]; + throw new Error("Can't convert thing to statement array"); + } + function is_empty(thing) { + if (thing === null) return true; + if (thing instanceof AST_EmptyStatement) return true; + if (thing instanceof AST_BlockStatement) return thing.body.length == 0; + return false; + } + function loop_body(x) { + if (x instanceof AST_Switch) return x; + if (x instanceof AST_For || x instanceof AST_ForIn || x instanceof AST_DWLoop) { + return x.body instanceof AST_BlockStatement ? x.body : x; + } + return x; + } + function tighten_body(statements, compressor) { + var CHANGED; + do { + CHANGED = false; + if (compressor.option("angular")) { + statements = process_for_angular(statements); + } + statements = eliminate_spurious_blocks(statements); + if (compressor.option("dead_code")) { + statements = eliminate_dead_code(statements, compressor); + } + if (compressor.option("if_return")) { + statements = handle_if_return(statements, compressor); + } + if (compressor.option("sequences")) { + statements = sequencesize(statements, compressor); + } + if (compressor.option("join_vars")) { + statements = join_consecutive_vars(statements, compressor); + } + } while (CHANGED); + if (compressor.option("negate_iife")) { + negate_iifes(statements, compressor); + } + return statements; + function process_for_angular(statements) { + function make_injector(func, name) { + return make_node(AST_SimpleStatement, func, { + body: make_node(AST_Assign, func, { + operator: "=", + left: make_node(AST_Dot, name, { + expression: make_node(AST_SymbolRef, name, name), + property: "$inject" + }), + right: make_node(AST_Array, func, { + elements: func.argnames.map(function(sym) { + return make_node(AST_String, sym, { + value: sym.name + }); + }) + }) + }) + }); + } + return statements.reduce(function(a, stat) { + a.push(stat); + var token = stat.start; + var comments = token.comments_before; + if (comments && comments.length > 0) { + var last = comments.pop(); + if (/@ngInject/.test(last.value)) { + if (stat instanceof AST_Defun) { + a.push(make_injector(stat, stat.name)); + } else if (stat instanceof AST_Definitions) { + stat.definitions.forEach(function(def) { + if (def.value && def.value instanceof AST_Lambda) { + a.push(make_injector(def.value, def.name)); + } + }); + } else { + compressor.warn("Unknown statement marked with @ngInject [{file}:{line},{col}]", token); + } + } + } + return a; + }, []); + } + function eliminate_spurious_blocks(statements) { + var seen_dirs = []; + return statements.reduce(function(a, stat) { + if (stat instanceof AST_BlockStatement) { + CHANGED = true; + a.push.apply(a, eliminate_spurious_blocks(stat.body)); + } else if (stat instanceof AST_EmptyStatement) { + CHANGED = true; + } else if (stat instanceof AST_Directive) { + if (seen_dirs.indexOf(stat.value) < 0) { + a.push(stat); + seen_dirs.push(stat.value); + } else { + CHANGED = true; + } + } else { + a.push(stat); + } + return a; + }, []); + } + function handle_if_return(statements, compressor) { + var self = compressor.self(); + var in_lambda = self instanceof AST_Lambda; + var ret = []; + loop: for (var i = statements.length; --i >= 0; ) { + var stat = statements[i]; + switch (true) { + case in_lambda && stat instanceof AST_Return && !stat.value && ret.length == 0: + CHANGED = true; + continue loop; + + case stat instanceof AST_If: + if (stat.body instanceof AST_Return) { + if ((in_lambda && ret.length == 0 || ret[0] instanceof AST_Return && !ret[0].value) && !stat.body.value && !stat.alternative) { + CHANGED = true; + var cond = make_node(AST_SimpleStatement, stat.condition, { + body: stat.condition + }); + ret.unshift(cond); + continue loop; + } + if (ret[0] instanceof AST_Return && stat.body.value && ret[0].value && !stat.alternative) { + CHANGED = true; + stat = stat.clone(); + stat.alternative = ret[0]; + ret[0] = stat.transform(compressor); + continue loop; + } + if ((ret.length == 0 || ret[0] instanceof AST_Return) && stat.body.value && !stat.alternative && in_lambda) { + CHANGED = true; + stat = stat.clone(); + stat.alternative = ret[0] || make_node(AST_Return, stat, { + value: make_node(AST_Undefined, stat) + }); + ret[0] = stat.transform(compressor); + continue loop; + } + if (!stat.body.value && in_lambda) { + CHANGED = true; + stat = stat.clone(); + stat.condition = stat.condition.negate(compressor); + stat.body = make_node(AST_BlockStatement, stat, { + body: as_statement_array(stat.alternative).concat(ret) + }); + stat.alternative = null; + ret = [ stat.transform(compressor) ]; + continue loop; + } + if (ret.length == 1 && in_lambda && ret[0] instanceof AST_SimpleStatement && (!stat.alternative || stat.alternative instanceof AST_SimpleStatement)) { + CHANGED = true; + ret.push(make_node(AST_Return, ret[0], { + value: make_node(AST_Undefined, ret[0]) + }).transform(compressor)); + ret = as_statement_array(stat.alternative).concat(ret); + ret.unshift(stat); + continue loop; + } + } + var ab = aborts(stat.body); + var lct = ab instanceof AST_LoopControl ? compressor.loopcontrol_target(ab.label) : null; + if (ab && (ab instanceof AST_Return && !ab.value && in_lambda || ab instanceof AST_Continue && self === loop_body(lct) || ab instanceof AST_Break && lct instanceof AST_BlockStatement && self === lct)) { + if (ab.label) { + remove(ab.label.thedef.references, ab); + } + CHANGED = true; + var body = as_statement_array(stat.body).slice(0, -1); + stat = stat.clone(); + stat.condition = stat.condition.negate(compressor); + stat.body = make_node(AST_BlockStatement, stat, { + body: as_statement_array(stat.alternative).concat(ret) + }); + stat.alternative = make_node(AST_BlockStatement, stat, { + body: body + }); + ret = [ stat.transform(compressor) ]; + continue loop; + } + var ab = aborts(stat.alternative); + var lct = ab instanceof AST_LoopControl ? compressor.loopcontrol_target(ab.label) : null; + if (ab && (ab instanceof AST_Return && !ab.value && in_lambda || ab instanceof AST_Continue && self === loop_body(lct) || ab instanceof AST_Break && lct instanceof AST_BlockStatement && self === lct)) { + if (ab.label) { + remove(ab.label.thedef.references, ab); + } + CHANGED = true; + stat = stat.clone(); + stat.body = make_node(AST_BlockStatement, stat.body, { + body: as_statement_array(stat.body).concat(ret) + }); + stat.alternative = make_node(AST_BlockStatement, stat.alternative, { + body: as_statement_array(stat.alternative).slice(0, -1) + }); + ret = [ stat.transform(compressor) ]; + continue loop; + } + ret.unshift(stat); + break; + + default: + ret.unshift(stat); + break; + } + } + return ret; + } + function eliminate_dead_code(statements, compressor) { + var has_quit = false; + var orig = statements.length; + var self = compressor.self(); + statements = statements.reduce(function(a, stat) { + if (has_quit) { + extract_declarations_from_unreachable_code(compressor, stat, a); + } else { + if (stat instanceof AST_LoopControl) { + var lct = compressor.loopcontrol_target(stat.label); + if (stat instanceof AST_Break && lct instanceof AST_BlockStatement && loop_body(lct) === self || stat instanceof AST_Continue && loop_body(lct) === self) { + if (stat.label) { + remove(stat.label.thedef.references, stat); + } + } else { + a.push(stat); + } + } else { + a.push(stat); + } + if (aborts(stat)) has_quit = true; + } + return a; + }, []); + CHANGED = statements.length != orig; + return statements; + } + function sequencesize(statements, compressor) { + if (statements.length < 2) return statements; + var seq = [], ret = []; + function push_seq() { + seq = AST_Seq.from_array(seq); + if (seq) ret.push(make_node(AST_SimpleStatement, seq, { + body: seq + })); + seq = []; + } + statements.forEach(function(stat) { + if (stat instanceof AST_SimpleStatement) seq.push(stat.body); else push_seq(), ret.push(stat); + }); + push_seq(); + ret = sequencesize_2(ret, compressor); + CHANGED = ret.length != statements.length; + return ret; + } + function sequencesize_2(statements, compressor) { + function cons_seq(right) { + ret.pop(); + var left = prev.body; + if (left instanceof AST_Seq) { + left.add(right); + } else { + left = AST_Seq.cons(left, right); + } + return left.transform(compressor); + } + var ret = [], prev = null; + statements.forEach(function(stat) { + if (prev) { + if (stat instanceof AST_For) { + var opera = {}; + try { + prev.body.walk(new TreeWalker(function(node) { + if (node instanceof AST_Binary && node.operator == "in") throw opera; + })); + if (stat.init && !(stat.init instanceof AST_Definitions)) { + stat.init = cons_seq(stat.init); + } else if (!stat.init) { + stat.init = prev.body; + ret.pop(); + } + } catch (ex) { + if (ex !== opera) throw ex; + } + } else if (stat instanceof AST_If) { + stat.condition = cons_seq(stat.condition); + } else if (stat instanceof AST_With) { + stat.expression = cons_seq(stat.expression); + } else if (stat instanceof AST_Exit && stat.value) { + stat.value = cons_seq(stat.value); + } else if (stat instanceof AST_Exit) { + stat.value = cons_seq(make_node(AST_Undefined, stat)); + } else if (stat instanceof AST_Switch) { + stat.expression = cons_seq(stat.expression); + } + } + ret.push(stat); + prev = stat instanceof AST_SimpleStatement ? stat : null; + }); + return ret; + } + function join_consecutive_vars(statements, compressor) { + var prev = null; + return statements.reduce(function(a, stat) { + if (stat instanceof AST_Definitions && prev && prev.TYPE == stat.TYPE) { + prev.definitions = prev.definitions.concat(stat.definitions); + CHANGED = true; + } else if (stat instanceof AST_For && prev instanceof AST_Definitions && (!stat.init || stat.init.TYPE == prev.TYPE)) { + CHANGED = true; + a.pop(); + if (stat.init) { + stat.init.definitions = prev.definitions.concat(stat.init.definitions); + } else { + stat.init = prev; + } + a.push(stat); + prev = stat; + } else { + prev = stat; + a.push(stat); + } + return a; + }, []); + } + function negate_iifes(statements, compressor) { + statements.forEach(function(stat) { + if (stat instanceof AST_SimpleStatement) { + stat.body = function transform(thing) { + return thing.transform(new TreeTransformer(function(node) { + if (node instanceof AST_Call && node.expression instanceof AST_Function) { + return make_node(AST_UnaryPrefix, node, { + operator: "!", + expression: node + }); + } else if (node instanceof AST_Call) { + node.expression = transform(node.expression); + } else if (node instanceof AST_Seq) { + node.car = transform(node.car); + } else if (node instanceof AST_Conditional) { + var expr = transform(node.condition); + if (expr !== node.condition) { + node.condition = expr; + var tmp = node.consequent; + node.consequent = node.alternative; + node.alternative = tmp; + } + } + return node; + })); + }(stat.body); + } + }); + } + } + function extract_declarations_from_unreachable_code(compressor, stat, target) { + compressor.warn("Dropping unreachable code [{file}:{line},{col}]", stat.start); + stat.walk(new TreeWalker(function(node) { + if (node instanceof AST_Definitions) { + compressor.warn("Declarations in unreachable code! [{file}:{line},{col}]", node.start); + node.remove_initializers(); + target.push(node); + return true; + } + if (node instanceof AST_Defun) { + target.push(node); + return true; + } + if (node instanceof AST_Scope) { + return true; + } + })); + } + (function(def) { + var unary_bool = [ "!", "delete" ]; + var binary_bool = [ "in", "instanceof", "==", "!=", "===", "!==", "<", "<=", ">=", ">" ]; + def(AST_Node, function() { + return false; + }); + def(AST_UnaryPrefix, function() { + return member(this.operator, unary_bool); + }); + def(AST_Binary, function() { + return member(this.operator, binary_bool) || (this.operator == "&&" || this.operator == "||") && this.left.is_boolean() && this.right.is_boolean(); + }); + def(AST_Conditional, function() { + return this.consequent.is_boolean() && this.alternative.is_boolean(); + }); + def(AST_Assign, function() { + return this.operator == "=" && this.right.is_boolean(); + }); + def(AST_Seq, function() { + return this.cdr.is_boolean(); + }); + def(AST_True, function() { + return true; + }); + def(AST_False, function() { + return true; + }); + })(function(node, func) { + node.DEFMETHOD("is_boolean", func); + }); + (function(def) { + def(AST_Node, function() { + return false; + }); + def(AST_String, function() { + return true; + }); + def(AST_UnaryPrefix, function() { + return this.operator == "typeof"; + }); + def(AST_Binary, function(compressor) { + return this.operator == "+" && (this.left.is_string(compressor) || this.right.is_string(compressor)); + }); + def(AST_Assign, function(compressor) { + return (this.operator == "=" || this.operator == "+=") && this.right.is_string(compressor); + }); + def(AST_Seq, function(compressor) { + return this.cdr.is_string(compressor); + }); + def(AST_Conditional, function(compressor) { + return this.consequent.is_string(compressor) && this.alternative.is_string(compressor); + }); + def(AST_Call, function(compressor) { + return compressor.option("unsafe") && this.expression instanceof AST_SymbolRef && this.expression.name == "String" && this.expression.undeclared(); + }); + })(function(node, func) { + node.DEFMETHOD("is_string", func); + }); + function best_of(ast1, ast2) { + return ast1.print_to_string().length > ast2.print_to_string().length ? ast2 : ast1; + } + (function(def) { + AST_Node.DEFMETHOD("evaluate", function(compressor) { + if (!compressor.option("evaluate")) return [ this ]; + try { + var val = this._eval(compressor); + return [ best_of(make_node_from_constant(compressor, val, this), this), val ]; + } catch (ex) { + if (ex !== def) throw ex; + return [ this ]; + } + }); + def(AST_Statement, function() { + throw new Error(string_template("Cannot evaluate a statement [{file}:{line},{col}]", this.start)); + }); + def(AST_Function, function() { + throw def; + }); + function ev(node, compressor) { + if (!compressor) throw new Error("Compressor must be passed"); + return node._eval(compressor); + } + def(AST_Node, function() { + throw def; + }); + def(AST_Constant, function() { + return this.getValue(); + }); + def(AST_UnaryPrefix, function(compressor) { + var e = this.expression; + switch (this.operator) { + case "!": + return !ev(e, compressor); + + case "typeof": + if (e instanceof AST_Function) return typeof function() {}; + e = ev(e, compressor); + if (e instanceof RegExp) throw def; + return typeof e; + + case "void": + return void ev(e, compressor); + + case "~": + return ~ev(e, compressor); + + case "-": + e = ev(e, compressor); + if (e === 0) throw def; + return -e; + + case "+": + return +ev(e, compressor); + } + throw def; + }); + def(AST_Binary, function(c) { + var left = this.left, right = this.right; + switch (this.operator) { + case "&&": + return ev(left, c) && ev(right, c); + + case "||": + return ev(left, c) || ev(right, c); + + case "|": + return ev(left, c) | ev(right, c); + + case "&": + return ev(left, c) & ev(right, c); + + case "^": + return ev(left, c) ^ ev(right, c); + + case "+": + return ev(left, c) + ev(right, c); + + case "*": + return ev(left, c) * ev(right, c); + + case "/": + return ev(left, c) / ev(right, c); + + case "%": + return ev(left, c) % ev(right, c); + + case "-": + return ev(left, c) - ev(right, c); + + case "<<": + return ev(left, c) << ev(right, c); + + case ">>": + return ev(left, c) >> ev(right, c); + + case ">>>": + return ev(left, c) >>> ev(right, c); + + case "==": + return ev(left, c) == ev(right, c); + + case "===": + return ev(left, c) === ev(right, c); + + case "!=": + return ev(left, c) != ev(right, c); + + case "!==": + return ev(left, c) !== ev(right, c); + + case "<": + return ev(left, c) < ev(right, c); + + case "<=": + return ev(left, c) <= ev(right, c); + + case ">": + return ev(left, c) > ev(right, c); + + case ">=": + return ev(left, c) >= ev(right, c); + + case "in": + return ev(left, c) in ev(right, c); + + case "instanceof": + return ev(left, c) instanceof ev(right, c); + } + throw def; + }); + def(AST_Conditional, function(compressor) { + return ev(this.condition, compressor) ? ev(this.consequent, compressor) : ev(this.alternative, compressor); + }); + def(AST_SymbolRef, function(compressor) { + var d = this.definition(); + if (d && d.constant && d.init) return ev(d.init, compressor); + throw def; + }); + def(AST_Dot, function(compressor) { + if (compressor.option("unsafe") && this.property == "length") { + var str = ev(this.expression, compressor); + if (typeof str == "string") return str.length; + } + throw def; + }); + })(function(node, func) { + node.DEFMETHOD("_eval", func); + }); + (function(def) { + function basic_negation(exp) { + return make_node(AST_UnaryPrefix, exp, { + operator: "!", + expression: exp + }); + } + def(AST_Node, function() { + return basic_negation(this); + }); + def(AST_Statement, function() { + throw new Error("Cannot negate a statement"); + }); + def(AST_Function, function() { + return basic_negation(this); + }); + def(AST_UnaryPrefix, function() { + if (this.operator == "!") return this.expression; + return basic_negation(this); + }); + def(AST_Seq, function(compressor) { + var self = this.clone(); + self.cdr = self.cdr.negate(compressor); + return self; + }); + def(AST_Conditional, function(compressor) { + var self = this.clone(); + self.consequent = self.consequent.negate(compressor); + self.alternative = self.alternative.negate(compressor); + return best_of(basic_negation(this), self); + }); + def(AST_Binary, function(compressor) { + var self = this.clone(), op = this.operator; + if (compressor.option("unsafe_comps")) { + switch (op) { + case "<=": + self.operator = ">"; + return self; + + case "<": + self.operator = ">="; + return self; + + case ">=": + self.operator = "<"; + return self; + + case ">": + self.operator = "<="; + return self; + } + } + switch (op) { + case "==": + self.operator = "!="; + return self; + + case "!=": + self.operator = "=="; + return self; + + case "===": + self.operator = "!=="; + return self; + + case "!==": + self.operator = "==="; + return self; + + case "&&": + self.operator = "||"; + self.left = self.left.negate(compressor); + self.right = self.right.negate(compressor); + return best_of(basic_negation(this), self); + + case "||": + self.operator = "&&"; + self.left = self.left.negate(compressor); + self.right = self.right.negate(compressor); + return best_of(basic_negation(this), self); + } + return basic_negation(this); + }); + })(function(node, func) { + node.DEFMETHOD("negate", function(compressor) { + return func.call(this, compressor); + }); + }); + (function(def) { + def(AST_Node, function(compressor) { + return true; + }); + def(AST_EmptyStatement, function(compressor) { + return false; + }); + def(AST_Constant, function(compressor) { + return false; + }); + def(AST_This, function(compressor) { + return false; + }); + def(AST_Call, function(compressor) { + var pure = compressor.option("pure_funcs"); + if (!pure) return true; + return pure.indexOf(this.expression.print_to_string()) < 0; + }); + def(AST_Block, function(compressor) { + for (var i = this.body.length; --i >= 0; ) { + if (this.body[i].has_side_effects(compressor)) return true; + } + return false; + }); + def(AST_SimpleStatement, function(compressor) { + return this.body.has_side_effects(compressor); + }); + def(AST_Defun, function(compressor) { + return true; + }); + def(AST_Function, function(compressor) { + return false; + }); + def(AST_Binary, function(compressor) { + return this.left.has_side_effects(compressor) || this.right.has_side_effects(compressor); + }); + def(AST_Assign, function(compressor) { + return true; + }); + def(AST_Conditional, function(compressor) { + return this.condition.has_side_effects(compressor) || this.consequent.has_side_effects(compressor) || this.alternative.has_side_effects(compressor); + }); + def(AST_Unary, function(compressor) { + return this.operator == "delete" || this.operator == "++" || this.operator == "--" || this.expression.has_side_effects(compressor); + }); + def(AST_SymbolRef, function(compressor) { + return false; + }); + def(AST_Object, function(compressor) { + for (var i = this.properties.length; --i >= 0; ) if (this.properties[i].has_side_effects(compressor)) return true; + return false; + }); + def(AST_ObjectProperty, function(compressor) { + return this.value.has_side_effects(compressor); + }); + def(AST_Array, function(compressor) { + for (var i = this.elements.length; --i >= 0; ) if (this.elements[i].has_side_effects(compressor)) return true; + return false; + }); + def(AST_Dot, function(compressor) { + if (!compressor.option("pure_getters")) return true; + return this.expression.has_side_effects(compressor); + }); + def(AST_Sub, function(compressor) { + if (!compressor.option("pure_getters")) return true; + return this.expression.has_side_effects(compressor) || this.property.has_side_effects(compressor); + }); + def(AST_PropAccess, function(compressor) { + return !compressor.option("pure_getters"); + }); + def(AST_Seq, function(compressor) { + return this.car.has_side_effects(compressor) || this.cdr.has_side_effects(compressor); + }); + })(function(node, func) { + node.DEFMETHOD("has_side_effects", func); + }); + function aborts(thing) { + return thing && thing.aborts(); + } + (function(def) { + def(AST_Statement, function() { + return null; + }); + def(AST_Jump, function() { + return this; + }); + function block_aborts() { + var n = this.body.length; + return n > 0 && aborts(this.body[n - 1]); + } + def(AST_BlockStatement, block_aborts); + def(AST_SwitchBranch, block_aborts); + def(AST_If, function() { + return this.alternative && aborts(this.body) && aborts(this.alternative); + }); + })(function(node, func) { + node.DEFMETHOD("aborts", func); + }); + OPT(AST_Directive, function(self, compressor) { + if (self.scope.has_directive(self.value) !== self.scope) { + return make_node(AST_EmptyStatement, self); + } + return self; + }); + OPT(AST_Debugger, function(self, compressor) { + if (compressor.option("drop_debugger")) return make_node(AST_EmptyStatement, self); + return self; + }); + OPT(AST_LabeledStatement, function(self, compressor) { + if (self.body instanceof AST_Break && compressor.loopcontrol_target(self.body.label) === self.body) { + return make_node(AST_EmptyStatement, self); + } + return self.label.references.length == 0 ? self.body : self; + }); + OPT(AST_Block, function(self, compressor) { + self.body = tighten_body(self.body, compressor); + return self; + }); + OPT(AST_BlockStatement, function(self, compressor) { + self.body = tighten_body(self.body, compressor); + switch (self.body.length) { + case 1: + return self.body[0]; + + case 0: + return make_node(AST_EmptyStatement, self); + } + return self; + }); + AST_Scope.DEFMETHOD("drop_unused", function(compressor) { + var self = this; + if (compressor.option("unused") && !(self instanceof AST_Toplevel) && !self.uses_eval) { + var in_use = []; + var initializations = new Dictionary(); + var scope = this; + var tw = new TreeWalker(function(node, descend) { + if (node !== self) { + if (node instanceof AST_Defun) { + initializations.add(node.name.name, node); + return true; + } + if (node instanceof AST_Definitions && scope === self) { + node.definitions.forEach(function(def) { + if (def.value) { + initializations.add(def.name.name, def.value); + if (def.value.has_side_effects(compressor)) { + def.value.walk(tw); + } + } + }); + return true; + } + if (node instanceof AST_SymbolRef) { + push_uniq(in_use, node.definition()); + return true; + } + if (node instanceof AST_Scope) { + var save_scope = scope; + scope = node; + descend(); + scope = save_scope; + return true; + } + } + }); + self.walk(tw); + for (var i = 0; i < in_use.length; ++i) { + in_use[i].orig.forEach(function(decl) { + var init = initializations.get(decl.name); + if (init) init.forEach(function(init) { + var tw = new TreeWalker(function(node) { + if (node instanceof AST_SymbolRef) { + push_uniq(in_use, node.definition()); + } + }); + init.walk(tw); + }); + }); + } + var tt = new TreeTransformer(function before(node, descend, in_list) { + if (node instanceof AST_Lambda && !(node instanceof AST_Accessor)) { + if (!compressor.option("keep_fargs")) { + for (var a = node.argnames, i = a.length; --i >= 0; ) { + var sym = a[i]; + if (sym.unreferenced()) { + a.pop(); + compressor.warn("Dropping unused function argument {name} [{file}:{line},{col}]", { + name: sym.name, + file: sym.start.file, + line: sym.start.line, + col: sym.start.col + }); + } else break; + } + } + } + if (node instanceof AST_Defun && node !== self) { + if (!member(node.name.definition(), in_use)) { + compressor.warn("Dropping unused function {name} [{file}:{line},{col}]", { + name: node.name.name, + file: node.name.start.file, + line: node.name.start.line, + col: node.name.start.col + }); + return make_node(AST_EmptyStatement, node); + } + return node; + } + if (node instanceof AST_Definitions && !(tt.parent() instanceof AST_ForIn)) { + var def = node.definitions.filter(function(def) { + if (member(def.name.definition(), in_use)) return true; + var w = { + name: def.name.name, + file: def.name.start.file, + line: def.name.start.line, + col: def.name.start.col + }; + if (def.value && def.value.has_side_effects(compressor)) { + def._unused_side_effects = true; + compressor.warn("Side effects in initialization of unused variable {name} [{file}:{line},{col}]", w); + return true; + } + compressor.warn("Dropping unused variable {name} [{file}:{line},{col}]", w); + return false; + }); + def = mergeSort(def, function(a, b) { + if (!a.value && b.value) return -1; + if (!b.value && a.value) return 1; + return 0; + }); + var side_effects = []; + for (var i = 0; i < def.length; ) { + var x = def[i]; + if (x._unused_side_effects) { + side_effects.push(x.value); + def.splice(i, 1); + } else { + if (side_effects.length > 0) { + side_effects.push(x.value); + x.value = AST_Seq.from_array(side_effects); + side_effects = []; + } + ++i; + } + } + if (side_effects.length > 0) { + side_effects = make_node(AST_BlockStatement, node, { + body: [ make_node(AST_SimpleStatement, node, { + body: AST_Seq.from_array(side_effects) + }) ] + }); + } else { + side_effects = null; + } + if (def.length == 0 && !side_effects) { + return make_node(AST_EmptyStatement, node); + } + if (def.length == 0) { + return side_effects; + } + node.definitions = def; + if (side_effects) { + side_effects.body.unshift(node); + node = side_effects; + } + return node; + } + if (node instanceof AST_For) { + descend(node, this); + if (node.init instanceof AST_BlockStatement) { + var body = node.init.body.slice(0, -1); + node.init = node.init.body.slice(-1)[0].body; + body.push(node); + return in_list ? MAP.splice(body) : make_node(AST_BlockStatement, node, { + body: body + }); + } + } + if (node instanceof AST_Scope && node !== self) return node; + }); + self.transform(tt); + } + }); + AST_Scope.DEFMETHOD("hoist_declarations", function(compressor) { + var hoist_funs = compressor.option("hoist_funs"); + var hoist_vars = compressor.option("hoist_vars"); + var self = this; + if (hoist_funs || hoist_vars) { + var dirs = []; + var hoisted = []; + var vars = new Dictionary(), vars_found = 0, var_decl = 0; + self.walk(new TreeWalker(function(node) { + if (node instanceof AST_Scope && node !== self) return true; + if (node instanceof AST_Var) { + ++var_decl; + return true; + } + })); + hoist_vars = hoist_vars && var_decl > 1; + var tt = new TreeTransformer(function before(node) { + if (node !== self) { + if (node instanceof AST_Directive) { + dirs.push(node); + return make_node(AST_EmptyStatement, node); + } + if (node instanceof AST_Defun && hoist_funs) { + hoisted.push(node); + return make_node(AST_EmptyStatement, node); + } + if (node instanceof AST_Var && hoist_vars) { + node.definitions.forEach(function(def) { + vars.set(def.name.name, def); + ++vars_found; + }); + var seq = node.to_assignments(); + var p = tt.parent(); + if (p instanceof AST_ForIn && p.init === node) { + if (seq == null) return node.definitions[0].name; + return seq; + } + if (p instanceof AST_For && p.init === node) { + return seq; + } + if (!seq) return make_node(AST_EmptyStatement, node); + return make_node(AST_SimpleStatement, node, { + body: seq + }); + } + if (node instanceof AST_Scope) return node; + } + }); + self = self.transform(tt); + if (vars_found > 0) { + var defs = []; + vars.each(function(def, name) { + if (self instanceof AST_Lambda && find_if(function(x) { + return x.name == def.name.name; + }, self.argnames)) { + vars.del(name); + } else { + def = def.clone(); + def.value = null; + defs.push(def); + vars.set(name, def); + } + }); + if (defs.length > 0) { + for (var i = 0; i < self.body.length; ) { + if (self.body[i] instanceof AST_SimpleStatement) { + var expr = self.body[i].body, sym, assign; + if (expr instanceof AST_Assign && expr.operator == "=" && (sym = expr.left) instanceof AST_Symbol && vars.has(sym.name)) { + var def = vars.get(sym.name); + if (def.value) break; + def.value = expr.right; + remove(defs, def); + defs.push(def); + self.body.splice(i, 1); + continue; + } + if (expr instanceof AST_Seq && (assign = expr.car) instanceof AST_Assign && assign.operator == "=" && (sym = assign.left) instanceof AST_Symbol && vars.has(sym.name)) { + var def = vars.get(sym.name); + if (def.value) break; + def.value = assign.right; + remove(defs, def); + defs.push(def); + self.body[i].body = expr.cdr; + continue; + } + } + if (self.body[i] instanceof AST_EmptyStatement) { + self.body.splice(i, 1); + continue; + } + if (self.body[i] instanceof AST_BlockStatement) { + var tmp = [ i, 1 ].concat(self.body[i].body); + self.body.splice.apply(self.body, tmp); + continue; + } + break; + } + defs = make_node(AST_Var, self, { + definitions: defs + }); + hoisted.push(defs); + } + } + self.body = dirs.concat(hoisted, self.body); + } + return self; + }); + OPT(AST_SimpleStatement, function(self, compressor) { + if (compressor.option("side_effects")) { + if (!self.body.has_side_effects(compressor)) { + compressor.warn("Dropping side-effect-free statement [{file}:{line},{col}]", self.start); + return make_node(AST_EmptyStatement, self); + } + } + return self; + }); + OPT(AST_DWLoop, function(self, compressor) { + var cond = self.condition.evaluate(compressor); + self.condition = cond[0]; + if (!compressor.option("loops")) return self; + if (cond.length > 1) { + if (cond[1]) { + return make_node(AST_For, self, { + body: self.body + }); + } else if (self instanceof AST_While) { + if (compressor.option("dead_code")) { + var a = []; + extract_declarations_from_unreachable_code(compressor, self.body, a); + return make_node(AST_BlockStatement, self, { + body: a + }); + } + } + } + return self; + }); + function if_break_in_loop(self, compressor) { + function drop_it(rest) { + rest = as_statement_array(rest); + if (self.body instanceof AST_BlockStatement) { + self.body = self.body.clone(); + self.body.body = rest.concat(self.body.body.slice(1)); + self.body = self.body.transform(compressor); + } else { + self.body = make_node(AST_BlockStatement, self.body, { + body: rest + }).transform(compressor); + } + if_break_in_loop(self, compressor); + } + var first = self.body instanceof AST_BlockStatement ? self.body.body[0] : self.body; + if (first instanceof AST_If) { + if (first.body instanceof AST_Break && compressor.loopcontrol_target(first.body.label) === self) { + if (self.condition) { + self.condition = make_node(AST_Binary, self.condition, { + left: self.condition, + operator: "&&", + right: first.condition.negate(compressor) + }); + } else { + self.condition = first.condition.negate(compressor); + } + drop_it(first.alternative); + } else if (first.alternative instanceof AST_Break && compressor.loopcontrol_target(first.alternative.label) === self) { + if (self.condition) { + self.condition = make_node(AST_Binary, self.condition, { + left: self.condition, + operator: "&&", + right: first.condition + }); + } else { + self.condition = first.condition; + } + drop_it(first.body); + } + } + } + OPT(AST_While, function(self, compressor) { + if (!compressor.option("loops")) return self; + self = AST_DWLoop.prototype.optimize.call(self, compressor); + if (self instanceof AST_While) { + if_break_in_loop(self, compressor); + self = make_node(AST_For, self, self).transform(compressor); + } + return self; + }); + OPT(AST_For, function(self, compressor) { + var cond = self.condition; + if (cond) { + cond = cond.evaluate(compressor); + self.condition = cond[0]; + } + if (!compressor.option("loops")) return self; + if (cond) { + if (cond.length > 1 && !cond[1]) { + if (compressor.option("dead_code")) { + var a = []; + if (self.init instanceof AST_Statement) { + a.push(self.init); + } else if (self.init) { + a.push(make_node(AST_SimpleStatement, self.init, { + body: self.init + })); + } + extract_declarations_from_unreachable_code(compressor, self.body, a); + return make_node(AST_BlockStatement, self, { + body: a + }); + } + } + } + if_break_in_loop(self, compressor); + return self; + }); + OPT(AST_If, function(self, compressor) { + if (!compressor.option("conditionals")) return self; + var cond = self.condition.evaluate(compressor); + self.condition = cond[0]; + if (cond.length > 1) { + if (cond[1]) { + compressor.warn("Condition always true [{file}:{line},{col}]", self.condition.start); + if (compressor.option("dead_code")) { + var a = []; + if (self.alternative) { + extract_declarations_from_unreachable_code(compressor, self.alternative, a); + } + a.push(self.body); + return make_node(AST_BlockStatement, self, { + body: a + }).transform(compressor); + } + } else { + compressor.warn("Condition always false [{file}:{line},{col}]", self.condition.start); + if (compressor.option("dead_code")) { + var a = []; + extract_declarations_from_unreachable_code(compressor, self.body, a); + if (self.alternative) a.push(self.alternative); + return make_node(AST_BlockStatement, self, { + body: a + }).transform(compressor); + } + } + } + if (is_empty(self.alternative)) self.alternative = null; + var negated = self.condition.negate(compressor); + var negated_is_best = best_of(self.condition, negated) === negated; + if (self.alternative && negated_is_best) { + negated_is_best = false; + self.condition = negated; + var tmp = self.body; + self.body = self.alternative || make_node(AST_EmptyStatement); + self.alternative = tmp; + } + if (is_empty(self.body) && is_empty(self.alternative)) { + return make_node(AST_SimpleStatement, self.condition, { + body: self.condition + }).transform(compressor); + } + if (self.body instanceof AST_SimpleStatement && self.alternative instanceof AST_SimpleStatement) { + return make_node(AST_SimpleStatement, self, { + body: make_node(AST_Conditional, self, { + condition: self.condition, + consequent: self.body.body, + alternative: self.alternative.body + }) + }).transform(compressor); + } + if (is_empty(self.alternative) && self.body instanceof AST_SimpleStatement) { + if (negated_is_best) return make_node(AST_SimpleStatement, self, { + body: make_node(AST_Binary, self, { + operator: "||", + left: negated, + right: self.body.body + }) + }).transform(compressor); + return make_node(AST_SimpleStatement, self, { + body: make_node(AST_Binary, self, { + operator: "&&", + left: self.condition, + right: self.body.body + }) + }).transform(compressor); + } + if (self.body instanceof AST_EmptyStatement && self.alternative && self.alternative instanceof AST_SimpleStatement) { + return make_node(AST_SimpleStatement, self, { + body: make_node(AST_Binary, self, { + operator: "||", + left: self.condition, + right: self.alternative.body + }) + }).transform(compressor); + } + if (self.body instanceof AST_Exit && self.alternative instanceof AST_Exit && self.body.TYPE == self.alternative.TYPE) { + return make_node(self.body.CTOR, self, { + value: make_node(AST_Conditional, self, { + condition: self.condition, + consequent: self.body.value || make_node(AST_Undefined, self.body).optimize(compressor), + alternative: self.alternative.value || make_node(AST_Undefined, self.alternative).optimize(compressor) + }) + }).transform(compressor); + } + if (self.body instanceof AST_If && !self.body.alternative && !self.alternative) { + self.condition = make_node(AST_Binary, self.condition, { + operator: "&&", + left: self.condition, + right: self.body.condition + }).transform(compressor); + self.body = self.body.body; + } + if (aborts(self.body)) { + if (self.alternative) { + var alt = self.alternative; + self.alternative = null; + return make_node(AST_BlockStatement, self, { + body: [ self, alt ] + }).transform(compressor); + } + } + if (aborts(self.alternative)) { + var body = self.body; + self.body = self.alternative; + self.condition = negated_is_best ? negated : self.condition.negate(compressor); + self.alternative = null; + return make_node(AST_BlockStatement, self, { + body: [ self, body ] + }).transform(compressor); + } + return self; + }); + OPT(AST_Switch, function(self, compressor) { + if (self.body.length == 0 && compressor.option("conditionals")) { + return make_node(AST_SimpleStatement, self, { + body: self.expression + }).transform(compressor); + } + for (;;) { + var last_branch = self.body[self.body.length - 1]; + if (last_branch) { + var stat = last_branch.body[last_branch.body.length - 1]; + if (stat instanceof AST_Break && loop_body(compressor.loopcontrol_target(stat.label)) === self) last_branch.body.pop(); + if (last_branch instanceof AST_Default && last_branch.body.length == 0) { + self.body.pop(); + continue; + } + } + break; + } + var exp = self.expression.evaluate(compressor); + out: if (exp.length == 2) try { + self.expression = exp[0]; + if (!compressor.option("dead_code")) break out; + var value = exp[1]; + var in_if = false; + var in_block = false; + var started = false; + var stopped = false; + var ruined = false; + var tt = new TreeTransformer(function(node, descend, in_list) { + if (node instanceof AST_Lambda || node instanceof AST_SimpleStatement) { + return node; + } else if (node instanceof AST_Switch && node === self) { + node = node.clone(); + descend(node, this); + return ruined ? node : make_node(AST_BlockStatement, node, { + body: node.body.reduce(function(a, branch) { + return a.concat(branch.body); + }, []) + }).transform(compressor); + } else if (node instanceof AST_If || node instanceof AST_Try) { + var save = in_if; + in_if = !in_block; + descend(node, this); + in_if = save; + return node; + } else if (node instanceof AST_StatementWithBody || node instanceof AST_Switch) { + var save = in_block; + in_block = true; + descend(node, this); + in_block = save; + return node; + } else if (node instanceof AST_Break && this.loopcontrol_target(node.label) === self) { + if (in_if) { + ruined = true; + return node; + } + if (in_block) return node; + stopped = true; + return in_list ? MAP.skip : make_node(AST_EmptyStatement, node); + } else if (node instanceof AST_SwitchBranch && this.parent() === self) { + if (stopped) return MAP.skip; + if (node instanceof AST_Case) { + var exp = node.expression.evaluate(compressor); + if (exp.length < 2) { + throw self; + } + if (exp[1] === value || started) { + started = true; + if (aborts(node)) stopped = true; + descend(node, this); + return node; + } + return MAP.skip; + } + descend(node, this); + return node; + } + }); + tt.stack = compressor.stack.slice(); + self = self.transform(tt); + } catch (ex) { + if (ex !== self) throw ex; + } + return self; + }); + OPT(AST_Case, function(self, compressor) { + self.body = tighten_body(self.body, compressor); + return self; + }); + OPT(AST_Try, function(self, compressor) { + self.body = tighten_body(self.body, compressor); + return self; + }); + AST_Definitions.DEFMETHOD("remove_initializers", function() { + this.definitions.forEach(function(def) { + def.value = null; + }); + }); + AST_Definitions.DEFMETHOD("to_assignments", function() { + var assignments = this.definitions.reduce(function(a, def) { + if (def.value) { + var name = make_node(AST_SymbolRef, def.name, def.name); + a.push(make_node(AST_Assign, def, { + operator: "=", + left: name, + right: def.value + })); + } + return a; + }, []); + if (assignments.length == 0) return null; + return AST_Seq.from_array(assignments); + }); + OPT(AST_Definitions, function(self, compressor) { + if (self.definitions.length == 0) return make_node(AST_EmptyStatement, self); + return self; + }); + OPT(AST_Function, function(self, compressor) { + self = AST_Lambda.prototype.optimize.call(self, compressor); + if (compressor.option("unused")) { + if (self.name && self.name.unreferenced()) { + self.name = null; + } + } + return self; + }); + OPT(AST_Call, function(self, compressor) { + if (compressor.option("unsafe")) { + var exp = self.expression; + if (exp instanceof AST_SymbolRef && exp.undeclared()) { + switch (exp.name) { + case "Array": + if (self.args.length != 1) { + return make_node(AST_Array, self, { + elements: self.args + }).transform(compressor); + } + break; + + case "Object": + if (self.args.length == 0) { + return make_node(AST_Object, self, { + properties: [] + }); + } + break; + + case "String": + if (self.args.length == 0) return make_node(AST_String, self, { + value: "" + }); + if (self.args.length <= 1) return make_node(AST_Binary, self, { + left: self.args[0], + operator: "+", + right: make_node(AST_String, self, { + value: "" + }) + }).transform(compressor); + break; + + case "Number": + if (self.args.length == 0) return make_node(AST_Number, self, { + value: 0 + }); + if (self.args.length == 1) return make_node(AST_UnaryPrefix, self, { + expression: self.args[0], + operator: "+" + }).transform(compressor); + + case "Boolean": + if (self.args.length == 0) return make_node(AST_False, self); + if (self.args.length == 1) return make_node(AST_UnaryPrefix, self, { + expression: make_node(AST_UnaryPrefix, null, { + expression: self.args[0], + operator: "!" + }), + operator: "!" + }).transform(compressor); + break; + + case "Function": + if (all(self.args, function(x) { + return x instanceof AST_String; + })) { + try { + var code = "(function(" + self.args.slice(0, -1).map(function(arg) { + return arg.value; + }).join(",") + "){" + self.args[self.args.length - 1].value + "})()"; + var ast = parse(code); + ast.figure_out_scope({ + screw_ie8: compressor.option("screw_ie8") + }); + var comp = new Compressor(compressor.options); + ast = ast.transform(comp); + ast.figure_out_scope({ + screw_ie8: compressor.option("screw_ie8") + }); + ast.mangle_names(); + var fun; + try { + ast.walk(new TreeWalker(function(node) { + if (node instanceof AST_Lambda) { + fun = node; + throw ast; + } + })); + } catch (ex) { + if (ex !== ast) throw ex; + } + var args = fun.argnames.map(function(arg, i) { + return make_node(AST_String, self.args[i], { + value: arg.print_to_string() + }); + }); + var code = OutputStream(); + AST_BlockStatement.prototype._codegen.call(fun, fun, code); + code = code.toString().replace(/^\{|\}$/g, ""); + args.push(make_node(AST_String, self.args[self.args.length - 1], { + value: code + })); + self.args = args; + return self; + } catch (ex) { + if (ex instanceof JS_Parse_Error) { + compressor.warn("Error parsing code passed to new Function [{file}:{line},{col}]", self.args[self.args.length - 1].start); + compressor.warn(ex.toString()); + } else { + console.log(ex); + throw ex; + } + } + } + break; + } + } else if (exp instanceof AST_Dot && exp.property == "toString" && self.args.length == 0) { + return make_node(AST_Binary, self, { + left: make_node(AST_String, self, { + value: "" + }), + operator: "+", + right: exp.expression + }).transform(compressor); + } else if (exp instanceof AST_Dot && exp.expression instanceof AST_Array && exp.property == "join") EXIT: { + var separator = self.args.length == 0 ? "," : self.args[0].evaluate(compressor)[1]; + if (separator == null) break EXIT; + var elements = exp.expression.elements.reduce(function(a, el) { + el = el.evaluate(compressor); + if (a.length == 0 || el.length == 1) { + a.push(el); + } else { + var last = a[a.length - 1]; + if (last.length == 2) { + var val = "" + last[1] + separator + el[1]; + a[a.length - 1] = [ make_node_from_constant(compressor, val, last[0]), val ]; + } else { + a.push(el); + } + } + return a; + }, []); + if (elements.length == 0) return make_node(AST_String, self, { + value: "" + }); + if (elements.length == 1) return elements[0][0]; + if (separator == "") { + var first; + if (elements[0][0] instanceof AST_String || elements[1][0] instanceof AST_String) { + first = elements.shift()[0]; + } else { + first = make_node(AST_String, self, { + value: "" + }); + } + return elements.reduce(function(prev, el) { + return make_node(AST_Binary, el[0], { + operator: "+", + left: prev, + right: el[0] + }); + }, first).transform(compressor); + } + var node = self.clone(); + node.expression = node.expression.clone(); + node.expression.expression = node.expression.expression.clone(); + node.expression.expression.elements = elements.map(function(el) { + return el[0]; + }); + return best_of(self, node); + } + } + if (compressor.option("side_effects")) { + if (self.expression instanceof AST_Function && self.args.length == 0 && !AST_Block.prototype.has_side_effects.call(self.expression, compressor)) { + return make_node(AST_Undefined, self).transform(compressor); + } + } + if (compressor.option("drop_console")) { + if (self.expression instanceof AST_PropAccess && self.expression.expression instanceof AST_SymbolRef && self.expression.expression.name == "console" && self.expression.expression.undeclared()) { + return make_node(AST_Undefined, self).transform(compressor); + } + } + return self.evaluate(compressor)[0]; + }); + OPT(AST_New, function(self, compressor) { + if (compressor.option("unsafe")) { + var exp = self.expression; + if (exp instanceof AST_SymbolRef && exp.undeclared()) { + switch (exp.name) { + case "Object": + case "RegExp": + case "Function": + case "Error": + case "Array": + return make_node(AST_Call, self, self).transform(compressor); + } + } + } + return self; + }); + OPT(AST_Seq, function(self, compressor) { + if (!compressor.option("side_effects")) return self; + if (!self.car.has_side_effects(compressor)) { + var p; + if (!(self.cdr instanceof AST_SymbolRef && self.cdr.name == "eval" && self.cdr.undeclared() && (p = compressor.parent()) instanceof AST_Call && p.expression === self)) { + return self.cdr; + } + } + if (compressor.option("cascade")) { + if (self.car instanceof AST_Assign && !self.car.left.has_side_effects(compressor)) { + if (self.car.left.equivalent_to(self.cdr)) { + return self.car; + } + if (self.cdr instanceof AST_Call && self.cdr.expression.equivalent_to(self.car.left)) { + self.cdr.expression = self.car; + return self.cdr; + } + } + if (!self.car.has_side_effects(compressor) && !self.cdr.has_side_effects(compressor) && self.car.equivalent_to(self.cdr)) { + return self.car; + } + } + if (self.cdr instanceof AST_UnaryPrefix && self.cdr.operator == "void" && !self.cdr.expression.has_side_effects(compressor)) { + self.cdr.operator = self.car; + return self.cdr; + } + if (self.cdr instanceof AST_Undefined) { + return make_node(AST_UnaryPrefix, self, { + operator: "void", + expression: self.car + }); + } + return self; + }); + AST_Unary.DEFMETHOD("lift_sequences", function(compressor) { + if (compressor.option("sequences")) { + if (this.expression instanceof AST_Seq) { + var seq = this.expression; + var x = seq.to_array(); + this.expression = x.pop(); + x.push(this); + seq = AST_Seq.from_array(x).transform(compressor); + return seq; + } + } + return this; + }); + OPT(AST_UnaryPostfix, function(self, compressor) { + return self.lift_sequences(compressor); + }); + OPT(AST_UnaryPrefix, function(self, compressor) { + self = self.lift_sequences(compressor); + var e = self.expression; + if (compressor.option("booleans") && compressor.in_boolean_context()) { + switch (self.operator) { + case "!": + if (e instanceof AST_UnaryPrefix && e.operator == "!") { + return e.expression; + } + break; + + case "typeof": + compressor.warn("Boolean expression always true [{file}:{line},{col}]", self.start); + return make_node(AST_True, self); + } + if (e instanceof AST_Binary && self.operator == "!") { + self = best_of(self, e.negate(compressor)); + } + } + return self.evaluate(compressor)[0]; + }); + function has_side_effects_or_prop_access(node, compressor) { + var save_pure_getters = compressor.option("pure_getters"); + compressor.options.pure_getters = false; + var ret = node.has_side_effects(compressor); + compressor.options.pure_getters = save_pure_getters; + return ret; + } + AST_Binary.DEFMETHOD("lift_sequences", function(compressor) { + if (compressor.option("sequences")) { + if (this.left instanceof AST_Seq) { + var seq = this.left; + var x = seq.to_array(); + this.left = x.pop(); + x.push(this); + seq = AST_Seq.from_array(x).transform(compressor); + return seq; + } + if (this.right instanceof AST_Seq && this instanceof AST_Assign && !has_side_effects_or_prop_access(this.left, compressor)) { + var seq = this.right; + var x = seq.to_array(); + this.right = x.pop(); + x.push(this); + seq = AST_Seq.from_array(x).transform(compressor); + return seq; + } + } + return this; + }); + var commutativeOperators = makePredicate("== === != !== * & | ^"); + OPT(AST_Binary, function(self, compressor) { + var reverse = compressor.has_directive("use asm") ? noop : function(op, force) { + if (force || !(self.left.has_side_effects(compressor) || self.right.has_side_effects(compressor))) { + if (op) self.operator = op; + var tmp = self.left; + self.left = self.right; + self.right = tmp; + } + }; + if (commutativeOperators(self.operator)) { + if (self.right instanceof AST_Constant && !(self.left instanceof AST_Constant)) { + if (!(self.left instanceof AST_Binary && PRECEDENCE[self.left.operator] >= PRECEDENCE[self.operator])) { + reverse(null, true); + } + } + if (/^[!=]==?$/.test(self.operator)) { + if (self.left instanceof AST_SymbolRef && self.right instanceof AST_Conditional) { + if (self.right.consequent instanceof AST_SymbolRef && self.right.consequent.definition() === self.left.definition()) { + if (/^==/.test(self.operator)) return self.right.condition; + if (/^!=/.test(self.operator)) return self.right.condition.negate(compressor); + } + if (self.right.alternative instanceof AST_SymbolRef && self.right.alternative.definition() === self.left.definition()) { + if (/^==/.test(self.operator)) return self.right.condition.negate(compressor); + if (/^!=/.test(self.operator)) return self.right.condition; + } + } + if (self.right instanceof AST_SymbolRef && self.left instanceof AST_Conditional) { + if (self.left.consequent instanceof AST_SymbolRef && self.left.consequent.definition() === self.right.definition()) { + if (/^==/.test(self.operator)) return self.left.condition; + if (/^!=/.test(self.operator)) return self.left.condition.negate(compressor); + } + if (self.left.alternative instanceof AST_SymbolRef && self.left.alternative.definition() === self.right.definition()) { + if (/^==/.test(self.operator)) return self.left.condition.negate(compressor); + if (/^!=/.test(self.operator)) return self.left.condition; + } + } + } + } + self = self.lift_sequences(compressor); + if (compressor.option("comparisons")) switch (self.operator) { + case "===": + case "!==": + if (self.left.is_string(compressor) && self.right.is_string(compressor) || self.left.is_boolean() && self.right.is_boolean()) { + self.operator = self.operator.substr(0, 2); + } + + case "==": + case "!=": + if (self.left instanceof AST_String && self.left.value == "undefined" && self.right instanceof AST_UnaryPrefix && self.right.operator == "typeof" && compressor.option("unsafe")) { + if (!(self.right.expression instanceof AST_SymbolRef) || !self.right.expression.undeclared()) { + self.right = self.right.expression; + self.left = make_node(AST_Undefined, self.left).optimize(compressor); + if (self.operator.length == 2) self.operator += "="; + } + } + break; + } + if (compressor.option("booleans") && compressor.in_boolean_context()) switch (self.operator) { + case "&&": + var ll = self.left.evaluate(compressor); + var rr = self.right.evaluate(compressor); + if (ll.length > 1 && !ll[1] || rr.length > 1 && !rr[1]) { + compressor.warn("Boolean && always false [{file}:{line},{col}]", self.start); + return make_node(AST_False, self); + } + if (ll.length > 1 && ll[1]) { + return rr[0]; + } + if (rr.length > 1 && rr[1]) { + return ll[0]; + } + break; + + case "||": + var ll = self.left.evaluate(compressor); + var rr = self.right.evaluate(compressor); + if (ll.length > 1 && ll[1] || rr.length > 1 && rr[1]) { + compressor.warn("Boolean || always true [{file}:{line},{col}]", self.start); + return make_node(AST_True, self); + } + if (ll.length > 1 && !ll[1]) { + return rr[0]; + } + if (rr.length > 1 && !rr[1]) { + return ll[0]; + } + break; + + case "+": + var ll = self.left.evaluate(compressor); + var rr = self.right.evaluate(compressor); + if (ll.length > 1 && ll[0] instanceof AST_String && ll[1] || rr.length > 1 && rr[0] instanceof AST_String && rr[1]) { + compressor.warn("+ in boolean context always true [{file}:{line},{col}]", self.start); + return make_node(AST_True, self); + } + break; + } + if (compressor.option("comparisons")) { + if (!(compressor.parent() instanceof AST_Binary) || compressor.parent() instanceof AST_Assign) { + var negated = make_node(AST_UnaryPrefix, self, { + operator: "!", + expression: self.negate(compressor) + }); + self = best_of(self, negated); + } + switch (self.operator) { + case "<": + reverse(">"); + break; + + case "<=": + reverse(">="); + break; + } + } + if (self.operator == "+" && self.right instanceof AST_String && self.right.getValue() === "" && self.left instanceof AST_Binary && self.left.operator == "+" && self.left.is_string(compressor)) { + return self.left; + } + if (compressor.option("evaluate")) { + if (self.operator == "+") { + if (self.left instanceof AST_Constant && self.right instanceof AST_Binary && self.right.operator == "+" && self.right.left instanceof AST_Constant && self.right.is_string(compressor)) { + self = make_node(AST_Binary, self, { + operator: "+", + left: make_node(AST_String, null, { + value: "" + self.left.getValue() + self.right.left.getValue(), + start: self.left.start, + end: self.right.left.end + }), + right: self.right.right + }); + } + if (self.right instanceof AST_Constant && self.left instanceof AST_Binary && self.left.operator == "+" && self.left.right instanceof AST_Constant && self.left.is_string(compressor)) { + self = make_node(AST_Binary, self, { + operator: "+", + left: self.left.left, + right: make_node(AST_String, null, { + value: "" + self.left.right.getValue() + self.right.getValue(), + start: self.left.right.start, + end: self.right.end + }) + }); + } + if (self.left instanceof AST_Binary && self.left.operator == "+" && self.left.is_string(compressor) && self.left.right instanceof AST_Constant && self.right instanceof AST_Binary && self.right.operator == "+" && self.right.left instanceof AST_Constant && self.right.is_string(compressor)) { + self = make_node(AST_Binary, self, { + operator: "+", + left: make_node(AST_Binary, self.left, { + operator: "+", + left: self.left.left, + right: make_node(AST_String, null, { + value: "" + self.left.right.getValue() + self.right.left.getValue(), + start: self.left.right.start, + end: self.right.left.end + }) + }), + right: self.right.right + }); + } + } + } + if (self.right instanceof AST_Binary && self.right.operator == self.operator && (self.operator == "*" || self.operator == "&&" || self.operator == "||")) { + self.left = make_node(AST_Binary, self.left, { + operator: self.operator, + left: self.left, + right: self.right.left + }); + self.right = self.right.right; + return self.transform(compressor); + } + return self.evaluate(compressor)[0]; + }); + OPT(AST_SymbolRef, function(self, compressor) { + if (self.undeclared()) { + var defines = compressor.option("global_defs"); + if (defines && defines.hasOwnProperty(self.name)) { + return make_node_from_constant(compressor, defines[self.name], self); + } + switch (self.name) { + case "undefined": + return make_node(AST_Undefined, self); + + case "NaN": + return make_node(AST_NaN, self); + + case "Infinity": + return make_node(AST_Infinity, self); + } + } + return self; + }); + OPT(AST_Undefined, function(self, compressor) { + if (compressor.option("unsafe")) { + var scope = compressor.find_parent(AST_Scope); + var undef = scope.find_variable("undefined"); + if (undef) { + var ref = make_node(AST_SymbolRef, self, { + name: "undefined", + scope: scope, + thedef: undef + }); + ref.reference(); + return ref; + } + } + return self; + }); + var ASSIGN_OPS = [ "+", "-", "/", "*", "%", ">>", "<<", ">>>", "|", "^", "&" ]; + OPT(AST_Assign, function(self, compressor) { + self = self.lift_sequences(compressor); + if (self.operator == "=" && self.left instanceof AST_SymbolRef && self.right instanceof AST_Binary && self.right.left instanceof AST_SymbolRef && self.right.left.name == self.left.name && member(self.right.operator, ASSIGN_OPS)) { + self.operator = self.right.operator + "="; + self.right = self.right.right; + } + return self; + }); + OPT(AST_Conditional, function(self, compressor) { + if (!compressor.option("conditionals")) return self; + if (self.condition instanceof AST_Seq) { + var car = self.condition.car; + self.condition = self.condition.cdr; + return AST_Seq.cons(car, self); + } + var cond = self.condition.evaluate(compressor); + if (cond.length > 1) { + if (cond[1]) { + compressor.warn("Condition always true [{file}:{line},{col}]", self.start); + return self.consequent; + } else { + compressor.warn("Condition always false [{file}:{line},{col}]", self.start); + return self.alternative; + } + } + var negated = cond[0].negate(compressor); + if (best_of(cond[0], negated) === negated) { + self = make_node(AST_Conditional, self, { + condition: negated, + consequent: self.alternative, + alternative: self.consequent + }); + } + var consequent = self.consequent; + var alternative = self.alternative; + if (consequent instanceof AST_Assign && alternative instanceof AST_Assign && consequent.operator == alternative.operator && consequent.left.equivalent_to(alternative.left)) { + return make_node(AST_Assign, self, { + operator: consequent.operator, + left: consequent.left, + right: make_node(AST_Conditional, self, { + condition: self.condition, + consequent: consequent.right, + alternative: alternative.right + }) + }); + } + if (consequent instanceof AST_Call && alternative.TYPE === consequent.TYPE && consequent.args.length == alternative.args.length && consequent.expression.equivalent_to(alternative.expression)) { + if (consequent.args.length == 0) { + return make_node(AST_Seq, self, { + car: self.condition, + cdr: consequent + }); + } + if (consequent.args.length == 1) { + consequent.args[0] = make_node(AST_Conditional, self, { + condition: self.condition, + consequent: consequent.args[0], + alternative: alternative.args[0] + }); + return consequent; + } + } + if (consequent instanceof AST_Conditional && consequent.alternative.equivalent_to(alternative)) { + return make_node(AST_Conditional, self, { + condition: make_node(AST_Binary, self, { + left: self.condition, + operator: "&&", + right: consequent.condition + }), + consequent: consequent.consequent, + alternative: alternative + }); + } + return self; + }); + OPT(AST_Boolean, function(self, compressor) { + if (compressor.option("booleans")) { + var p = compressor.parent(); + if (p instanceof AST_Binary && (p.operator == "==" || p.operator == "!=")) { + compressor.warn("Non-strict equality against boolean: {operator} {value} [{file}:{line},{col}]", { + operator: p.operator, + value: self.value, + file: p.start.file, + line: p.start.line, + col: p.start.col + }); + return make_node(AST_Number, self, { + value: +self.value + }); + } + return make_node(AST_UnaryPrefix, self, { + operator: "!", + expression: make_node(AST_Number, self, { + value: 1 - self.value + }) + }); + } + return self; + }); + OPT(AST_Sub, function(self, compressor) { + var prop = self.property; + if (prop instanceof AST_String && compressor.option("properties")) { + prop = prop.getValue(); + if (RESERVED_WORDS(prop) ? compressor.option("screw_ie8") : is_identifier_string(prop)) { + return make_node(AST_Dot, self, { + expression: self.expression, + property: prop + }).optimize(compressor); + } + var v = parseFloat(prop); + if (!isNaN(v) && v.toString() == prop) { + self.property = make_node(AST_Number, self.property, { + value: v + }); + } + } + return self; + }); + OPT(AST_Dot, function(self, compressor) { + return self.evaluate(compressor)[0]; + }); + function literals_in_boolean_context(self, compressor) { + if (compressor.option("booleans") && compressor.in_boolean_context()) { + return make_node(AST_True, self); + } + return self; + } + OPT(AST_Array, literals_in_boolean_context); + OPT(AST_Object, literals_in_boolean_context); + OPT(AST_RegExp, literals_in_boolean_context); + })(); + "use strict"; + function SourceMap(options) { + options = defaults(options, { + file: null, + root: null, + orig: null, + orig_line_diff: 0, + dest_line_diff: 0 + }); + var generator = new MOZ_SourceMap.SourceMapGenerator({ + file: options.file, + sourceRoot: options.root + }); + var orig_map = options.orig && new MOZ_SourceMap.SourceMapConsumer(options.orig); + function add(source, gen_line, gen_col, orig_line, orig_col, name) { + if (orig_map) { + var info = orig_map.originalPositionFor({ + line: orig_line, + column: orig_col + }); + if (info.source === null) { + return; + } + source = info.source; + orig_line = info.line; + orig_col = info.column; + name = info.name; + } + generator.addMapping({ + generated: { + line: gen_line + options.dest_line_diff, + column: gen_col + }, + original: { + line: orig_line + options.orig_line_diff, + column: orig_col + }, + source: source, + name: name + }); + } + return { + add: add, + get: function() { + return generator; + }, + toString: function() { + return generator.toString(); + } + }; + } + "use strict"; + (function() { + var MOZ_TO_ME = { + ExpressionStatement: function(M) { + var expr = M.expression; + if (expr.type === "Literal" && typeof expr.value === "string") { + return new AST_Directive({ + start: my_start_token(M), + end: my_end_token(M), + value: expr.value + }); + } + return new AST_SimpleStatement({ + start: my_start_token(M), + end: my_end_token(M), + body: from_moz(expr) + }); + }, + TryStatement: function(M) { + var handlers = M.handlers || [ M.handler ]; + if (handlers.length > 1 || M.guardedHandlers && M.guardedHandlers.length) { + throw new Error("Multiple catch clauses are not supported."); + } + return new AST_Try({ + start: my_start_token(M), + end: my_end_token(M), + body: from_moz(M.block).body, + bcatch: from_moz(handlers[0]), + bfinally: M.finalizer ? new AST_Finally(from_moz(M.finalizer)) : null + }); + }, + Property: function(M) { + var key = M.key; + var name = key.type == "Identifier" ? key.name : key.value; + var args = { + start: my_start_token(key), + end: my_end_token(M.value), + key: name, + value: from_moz(M.value) + }; + switch (M.kind) { + case "init": + return new AST_ObjectKeyVal(args); + + case "set": + args.value.name = from_moz(key); + return new AST_ObjectSetter(args); + + case "get": + args.value.name = from_moz(key); + return new AST_ObjectGetter(args); + } + }, + ObjectExpression: function(M) { + return new AST_Object({ + start: my_start_token(M), + end: my_end_token(M), + properties: M.properties.map(function(prop) { + prop.type = "Property"; + return from_moz(prop); + }) + }); + }, + SequenceExpression: function(M) { + return AST_Seq.from_array(M.expressions.map(from_moz)); + }, + MemberExpression: function(M) { + return new (M.computed ? AST_Sub : AST_Dot)({ + start: my_start_token(M), + end: my_end_token(M), + property: M.computed ? from_moz(M.property) : M.property.name, + expression: from_moz(M.object) + }); + }, + SwitchCase: function(M) { + return new (M.test ? AST_Case : AST_Default)({ + start: my_start_token(M), + end: my_end_token(M), + expression: from_moz(M.test), + body: M.consequent.map(from_moz) + }); + }, + VariableDeclaration: function(M) { + return new (M.kind === "const" ? AST_Const : AST_Var)({ + start: my_start_token(M), + end: my_end_token(M), + definitions: M.declarations.map(from_moz) + }); + }, + Literal: function(M) { + var val = M.value, args = { + start: my_start_token(M), + end: my_end_token(M) + }; + if (val === null) return new AST_Null(args); + switch (typeof val) { + case "string": + args.value = val; + return new AST_String(args); + + case "number": + args.value = val; + return new AST_Number(args); + + case "boolean": + return new (val ? AST_True : AST_False)(args); + + default: + args.value = val; + return new AST_RegExp(args); + } + }, + Identifier: function(M) { + var p = FROM_MOZ_STACK[FROM_MOZ_STACK.length - 2]; + return new (p.type == "LabeledStatement" ? AST_Label : p.type == "VariableDeclarator" && p.id === M ? p.kind == "const" ? AST_SymbolConst : AST_SymbolVar : p.type == "FunctionExpression" ? p.id === M ? AST_SymbolLambda : AST_SymbolFunarg : p.type == "FunctionDeclaration" ? p.id === M ? AST_SymbolDefun : AST_SymbolFunarg : p.type == "CatchClause" ? AST_SymbolCatch : p.type == "BreakStatement" || p.type == "ContinueStatement" ? AST_LabelRef : AST_SymbolRef)({ + start: my_start_token(M), + end: my_end_token(M), + name: M.name + }); + } + }; + MOZ_TO_ME.UpdateExpression = MOZ_TO_ME.UnaryExpression = function To_Moz_Unary(M) { + var prefix = "prefix" in M ? M.prefix : M.type == "UnaryExpression" ? true : false; + return new (prefix ? AST_UnaryPrefix : AST_UnaryPostfix)({ + start: my_start_token(M), + end: my_end_token(M), + operator: M.operator, + expression: from_moz(M.argument) + }); + }; + map("Program", AST_Toplevel, "body@body"); + map("EmptyStatement", AST_EmptyStatement); + map("BlockStatement", AST_BlockStatement, "body@body"); + map("IfStatement", AST_If, "test>condition, consequent>body, alternate>alternative"); + map("LabeledStatement", AST_LabeledStatement, "label>label, body>body"); + map("BreakStatement", AST_Break, "label>label"); + map("ContinueStatement", AST_Continue, "label>label"); + map("WithStatement", AST_With, "object>expression, body>body"); + map("SwitchStatement", AST_Switch, "discriminant>expression, cases@body"); + map("ReturnStatement", AST_Return, "argument>value"); + map("ThrowStatement", AST_Throw, "argument>value"); + map("WhileStatement", AST_While, "test>condition, body>body"); + map("DoWhileStatement", AST_Do, "test>condition, body>body"); + map("ForStatement", AST_For, "init>init, test>condition, update>step, body>body"); + map("ForInStatement", AST_ForIn, "left>init, right>object, body>body"); + map("DebuggerStatement", AST_Debugger); + map("FunctionDeclaration", AST_Defun, "id>name, params@argnames, body%body"); + map("VariableDeclarator", AST_VarDef, "id>name, init>value"); + map("CatchClause", AST_Catch, "param>argname, body%body"); + map("ThisExpression", AST_This); + map("ArrayExpression", AST_Array, "elements@elements"); + map("FunctionExpression", AST_Function, "id>name, params@argnames, body%body"); + map("BinaryExpression", AST_Binary, "operator=operator, left>left, right>right"); + map("LogicalExpression", AST_Binary, "operator=operator, left>left, right>right"); + map("AssignmentExpression", AST_Assign, "operator=operator, left>left, right>right"); + map("ConditionalExpression", AST_Conditional, "test>condition, consequent>consequent, alternate>alternative"); + map("NewExpression", AST_New, "callee>expression, arguments@args"); + map("CallExpression", AST_Call, "callee>expression, arguments@args"); + def_to_moz(AST_Directive, function To_Moz_Directive(M) { + return { + type: "ExpressionStatement", + expression: { + type: "Literal", + value: M.value + } + }; + }); + def_to_moz(AST_SimpleStatement, function To_Moz_ExpressionStatement(M) { + return { + type: "ExpressionStatement", + expression: to_moz(M.body) + }; + }); + def_to_moz(AST_SwitchBranch, function To_Moz_SwitchCase(M) { + return { + type: "SwitchCase", + test: to_moz(M.expression), + consequent: M.body.map(to_moz) + }; + }); + def_to_moz(AST_Try, function To_Moz_TryStatement(M) { + return { + type: "TryStatement", + block: to_moz_block(M), + handler: to_moz(M.bcatch), + guardedHandlers: [], + finalizer: to_moz(M.bfinally) + }; + }); + def_to_moz(AST_Catch, function To_Moz_CatchClause(M) { + return { + type: "CatchClause", + param: to_moz(M.argname), + guard: null, + body: to_moz_block(M) + }; + }); + def_to_moz(AST_Definitions, function To_Moz_VariableDeclaration(M) { + return { + type: "VariableDeclaration", + kind: M instanceof AST_Const ? "const" : "var", + declarations: M.definitions.map(to_moz) + }; + }); + def_to_moz(AST_Seq, function To_Moz_SequenceExpression(M) { + return { + type: "SequenceExpression", + expressions: M.to_array().map(to_moz) + }; + }); + def_to_moz(AST_PropAccess, function To_Moz_MemberExpression(M) { + var isComputed = M instanceof AST_Sub; + return { + type: "MemberExpression", + object: to_moz(M.expression), + computed: isComputed, + property: isComputed ? to_moz(M.property) : { + type: "Identifier", + name: M.property + } + }; + }); + def_to_moz(AST_Unary, function To_Moz_Unary(M) { + return { + type: M.operator == "++" || M.operator == "--" ? "UpdateExpression" : "UnaryExpression", + operator: M.operator, + prefix: M instanceof AST_UnaryPrefix, + argument: to_moz(M.expression) + }; + }); + def_to_moz(AST_Binary, function To_Moz_BinaryExpression(M) { + return { + type: M.operator == "&&" || M.operator == "||" ? "LogicalExpression" : "BinaryExpression", + left: to_moz(M.left), + operator: M.operator, + right: to_moz(M.right) + }; + }); + def_to_moz(AST_Object, function To_Moz_ObjectExpression(M) { + return { + type: "ObjectExpression", + properties: M.properties.map(to_moz) + }; + }); + def_to_moz(AST_ObjectProperty, function To_Moz_Property(M) { + var key = is_identifier(M.key) ? { + type: "Identifier", + name: M.key + } : { + type: "Literal", + value: M.key + }; + var kind; + if (M instanceof AST_ObjectKeyVal) { + kind = "init"; + } else if (M instanceof AST_ObjectGetter) { + kind = "get"; + } else if (M instanceof AST_ObjectSetter) { + kind = "set"; + } + return { + type: "Property", + kind: kind, + key: key, + value: to_moz(M.value) + }; + }); + def_to_moz(AST_Symbol, function To_Moz_Identifier(M) { + var def = M.definition(); + return { + type: "Identifier", + name: def ? def.mangled_name || def.name : M.name + }; + }); + def_to_moz(AST_Constant, function To_Moz_Literal(M) { + var value = M.value; + if (typeof value === "number" && (value < 0 || value === 0 && 1 / value < 0)) { + return { + type: "UnaryExpression", + operator: "-", + prefix: true, + argument: { + type: "Literal", + value: -value + } + }; + } + return { + type: "Literal", + value: value + }; + }); + def_to_moz(AST_Atom, function To_Moz_Atom(M) { + return { + type: "Identifier", + name: String(M.value) + }; + }); + AST_Boolean.DEFMETHOD("to_mozilla_ast", AST_Constant.prototype.to_mozilla_ast); + AST_Null.DEFMETHOD("to_mozilla_ast", AST_Constant.prototype.to_mozilla_ast); + AST_Hole.DEFMETHOD("to_mozilla_ast", function To_Moz_ArrayHole() { + return null; + }); + AST_Block.DEFMETHOD("to_mozilla_ast", AST_BlockStatement.prototype.to_mozilla_ast); + AST_Lambda.DEFMETHOD("to_mozilla_ast", AST_Function.prototype.to_mozilla_ast); + function my_start_token(moznode) { + var loc = moznode.loc; + var range = moznode.range; + return new AST_Token({ + file: loc && loc.source, + line: loc && loc.start.line, + col: loc && loc.start.column, + pos: range ? range[0] : moznode.start, + endpos: range ? range[0] : moznode.start + }); + } + function my_end_token(moznode) { + var loc = moznode.loc; + var range = moznode.range; + return new AST_Token({ + file: loc && loc.source, + line: loc && loc.end.line, + col: loc && loc.end.column, + pos: range ? range[1] : moznode.end, + endpos: range ? range[1] : moznode.end + }); + } + function map(moztype, mytype, propmap) { + if (typeof UglifyJS_NoUnsafeEval !== "undefined") { + var prop_list = []; + if (propmap) propmap.split(/\s*,\s*/).forEach(function(prop) { + var m = /([a-z0-9$_]+)(=|@|>|%)([a-z0-9$_]+)/i.exec(prop); + if (!m) throw new Error("Can't understand property map: " + prop); + if ("=@>%".indexOf(m[2]) < 0) { + throw new Error("Can't understand operator in propmap: " + prop); + } + prop_list.push(m); + }); + var moz_to_me = function(M) { + var props = { + start: my_start_token(M), + end: my_end_token(M) + }; + for (var i = 0; i < prop_list.length; i++) { + var m = prop_list[i]; + var moz = m[1], how = m[2], my = m[3]; + var mozProp = M[moz]; + var myProp; + switch (how) { + case "@": + myProp = mozProp.map(from_moz); + break; + + case ">": + myProp = from_moz(mozProp); + break; + + case "=": + myProp = mozProp; + break; + + case "%": + myProp = from_moz(mozProp).body; + break; + } + props[my] = myProp; + } + return new mytype(props); + }; + var me_to_moz = function(M) { + var props = { + type: moztype + }; + for (var i = 0; i < prop_list.length; i++) { + var m = prop_list[i]; + var moz = m[1], how = m[2], my = m[3]; + var myProp = M[my]; + var mozProp; + switch (how) { + case "@": + mozProp = myProp.map(to_moz); + break; + + case ">": + mozProp = to_moz(myProp); + break; + + case "=": + mozProp = myProp; + break; + + case "%": + mozProp = to_moz_block(M); + break; + } + props[moz] = mozProp; + } + return props; + }; + MOZ_TO_ME[moztype] = moz_to_me; + def_to_moz(mytype, me_to_moz); + return; + } + var moz_to_me = "function From_Moz_" + moztype + "(M){\n"; + moz_to_me += "return new " + mytype.name + "({\n" + "start: my_start_token(M),\n" + "end: my_end_token(M)"; + var me_to_moz = "function To_Moz_" + moztype + "(M){\n"; + me_to_moz += "return {\n" + "type: " + JSON.stringify(moztype); + if (propmap) propmap.split(/\s*,\s*/).forEach(function(prop) { + var m = /([a-z0-9$_]+)(=|@|>|%)([a-z0-9$_]+)/i.exec(prop); + if (!m) throw new Error("Can't understand property map: " + prop); + var moz = m[1], how = m[2], my = m[3]; + moz_to_me += ",\n" + my + ": "; + me_to_moz += ",\n" + moz + ": "; + switch (how) { + case "@": + moz_to_me += "M." + moz + ".map(from_moz)"; + me_to_moz += "M." + my + ".map(to_moz)"; + break; + + case ">": + moz_to_me += "from_moz(M." + moz + ")"; + me_to_moz += "to_moz(M." + my + ")"; + break; + + case "=": + moz_to_me += "M." + moz; + me_to_moz += "M." + my; + break; + + case "%": + moz_to_me += "from_moz(M." + moz + ").body"; + me_to_moz += "to_moz_block(M)"; + break; + + default: + throw new Error("Can't understand operator in propmap: " + prop); + } + }); + moz_to_me += "\n})\n}"; + me_to_moz += "\n}\n}"; + moz_to_me = new Function("my_start_token", "my_end_token", "from_moz", "return(" + moz_to_me + ")")(my_start_token, my_end_token, from_moz); + me_to_moz = new Function("to_moz", "to_moz_block", "return(" + me_to_moz + ")")(to_moz, to_moz_block); + MOZ_TO_ME[moztype] = moz_to_me; + def_to_moz(mytype, me_to_moz); + } + var FROM_MOZ_STACK = null; + function from_moz(node) { + FROM_MOZ_STACK.push(node); + var ret = node != null ? MOZ_TO_ME[node.type](node) : null; + FROM_MOZ_STACK.pop(); + return ret; + } + AST_Node.from_mozilla_ast = function(node) { + var save_stack = FROM_MOZ_STACK; + FROM_MOZ_STACK = []; + var ast = from_moz(node); + FROM_MOZ_STACK = save_stack; + return ast; + }; + function moz_sub_loc(token) { + return token.line ? { + line: token.line, + column: token.col + } : null; + } + function set_moz_loc(mynode, moznode) { + var start = mynode.start; + var end = mynode.end; + if (start.pos != null && end.pos != null) { + moznode.range = [ start.pos, end.pos ]; + } + if (start.line) { + moznode.loc = { + start: moz_sub_loc(start), + end: moz_sub_loc(end) + }; + if (start.file) { + moznode.loc.source = start.file; + } + } + return moznode; + } + function def_to_moz(mytype, handler) { + mytype.DEFMETHOD("to_mozilla_ast", function() { + return set_moz_loc(this, handler(this)); + }); + } + function to_moz(node) { + return node != null ? node.to_mozilla_ast() : null; + } + function to_moz_block(node) { + return { + type: "BlockStatement", + body: node.body.map(to_moz) + }; + } + })(); + exports["array_to_hash"] = array_to_hash; + exports["slice"] = slice; + exports["characters"] = characters; + exports["member"] = member; + exports["find_if"] = find_if; + exports["repeat_string"] = repeat_string; + exports["DefaultsError"] = DefaultsError; + exports["defaults"] = defaults; + exports["merge"] = merge; + exports["noop"] = noop; + exports["MAP"] = MAP; + exports["push_uniq"] = push_uniq; + exports["string_template"] = string_template; + exports["remove"] = remove; + exports["mergeSort"] = mergeSort; + exports["set_difference"] = set_difference; + exports["set_intersection"] = set_intersection; + exports["makePredicate"] = makePredicate; + exports["all"] = all; + exports["Dictionary"] = Dictionary; + exports["DEFNODE"] = DEFNODE; + exports["AST_Token"] = AST_Token; + exports["AST_Node"] = AST_Node; + exports["AST_Statement"] = AST_Statement; + exports["AST_Debugger"] = AST_Debugger; + exports["AST_Directive"] = AST_Directive; + exports["AST_SimpleStatement"] = AST_SimpleStatement; + exports["walk_body"] = walk_body; + exports["AST_Block"] = AST_Block; + exports["AST_BlockStatement"] = AST_BlockStatement; + exports["AST_EmptyStatement"] = AST_EmptyStatement; + exports["AST_StatementWithBody"] = AST_StatementWithBody; + exports["AST_LabeledStatement"] = AST_LabeledStatement; + exports["AST_IterationStatement"] = AST_IterationStatement; + exports["AST_DWLoop"] = AST_DWLoop; + exports["AST_Do"] = AST_Do; + exports["AST_While"] = AST_While; + exports["AST_For"] = AST_For; + exports["AST_ForIn"] = AST_ForIn; + exports["AST_With"] = AST_With; + exports["AST_Scope"] = AST_Scope; + exports["AST_Toplevel"] = AST_Toplevel; + exports["AST_Lambda"] = AST_Lambda; + exports["AST_Accessor"] = AST_Accessor; + exports["AST_Function"] = AST_Function; + exports["AST_Defun"] = AST_Defun; + exports["AST_Jump"] = AST_Jump; + exports["AST_Exit"] = AST_Exit; + exports["AST_Return"] = AST_Return; + exports["AST_Throw"] = AST_Throw; + exports["AST_LoopControl"] = AST_LoopControl; + exports["AST_Break"] = AST_Break; + exports["AST_Continue"] = AST_Continue; + exports["AST_If"] = AST_If; + exports["AST_Switch"] = AST_Switch; + exports["AST_SwitchBranch"] = AST_SwitchBranch; + exports["AST_Default"] = AST_Default; + exports["AST_Case"] = AST_Case; + exports["AST_Try"] = AST_Try; + exports["AST_Catch"] = AST_Catch; + exports["AST_Finally"] = AST_Finally; + exports["AST_Definitions"] = AST_Definitions; + exports["AST_Var"] = AST_Var; + exports["AST_Const"] = AST_Const; + exports["AST_VarDef"] = AST_VarDef; + exports["AST_Call"] = AST_Call; + exports["AST_New"] = AST_New; + exports["AST_Seq"] = AST_Seq; + exports["AST_PropAccess"] = AST_PropAccess; + exports["AST_Dot"] = AST_Dot; + exports["AST_Sub"] = AST_Sub; + exports["AST_Unary"] = AST_Unary; + exports["AST_UnaryPrefix"] = AST_UnaryPrefix; + exports["AST_UnaryPostfix"] = AST_UnaryPostfix; + exports["AST_Binary"] = AST_Binary; + exports["AST_Conditional"] = AST_Conditional; + exports["AST_Assign"] = AST_Assign; + exports["AST_Array"] = AST_Array; + exports["AST_Object"] = AST_Object; + exports["AST_ObjectProperty"] = AST_ObjectProperty; + exports["AST_ObjectKeyVal"] = AST_ObjectKeyVal; + exports["AST_ObjectSetter"] = AST_ObjectSetter; + exports["AST_ObjectGetter"] = AST_ObjectGetter; + exports["AST_Symbol"] = AST_Symbol; + exports["AST_SymbolAccessor"] = AST_SymbolAccessor; + exports["AST_SymbolDeclaration"] = AST_SymbolDeclaration; + exports["AST_SymbolVar"] = AST_SymbolVar; + exports["AST_SymbolConst"] = AST_SymbolConst; + exports["AST_SymbolFunarg"] = AST_SymbolFunarg; + exports["AST_SymbolDefun"] = AST_SymbolDefun; + exports["AST_SymbolLambda"] = AST_SymbolLambda; + exports["AST_SymbolCatch"] = AST_SymbolCatch; + exports["AST_Label"] = AST_Label; + exports["AST_SymbolRef"] = AST_SymbolRef; + exports["AST_LabelRef"] = AST_LabelRef; + exports["AST_This"] = AST_This; + exports["AST_Constant"] = AST_Constant; + exports["AST_String"] = AST_String; + exports["AST_Number"] = AST_Number; + exports["AST_RegExp"] = AST_RegExp; + exports["AST_Atom"] = AST_Atom; + exports["AST_Null"] = AST_Null; + exports["AST_NaN"] = AST_NaN; + exports["AST_Undefined"] = AST_Undefined; + exports["AST_Hole"] = AST_Hole; + exports["AST_Infinity"] = AST_Infinity; + exports["AST_Boolean"] = AST_Boolean; + exports["AST_False"] = AST_False; + exports["AST_True"] = AST_True; + exports["TreeWalker"] = TreeWalker; + exports["KEYWORDS"] = KEYWORDS; + exports["KEYWORDS_ATOM"] = KEYWORDS_ATOM; + exports["RESERVED_WORDS"] = RESERVED_WORDS; + exports["KEYWORDS_BEFORE_EXPRESSION"] = KEYWORDS_BEFORE_EXPRESSION; + exports["OPERATOR_CHARS"] = OPERATOR_CHARS; + exports["RE_HEX_NUMBER"] = RE_HEX_NUMBER; + exports["RE_OCT_NUMBER"] = RE_OCT_NUMBER; + exports["RE_DEC_NUMBER"] = RE_DEC_NUMBER; + exports["OPERATORS"] = OPERATORS; + exports["WHITESPACE_CHARS"] = WHITESPACE_CHARS; + exports["PUNC_BEFORE_EXPRESSION"] = PUNC_BEFORE_EXPRESSION; + exports["PUNC_CHARS"] = PUNC_CHARS; + exports["REGEXP_MODIFIERS"] = REGEXP_MODIFIERS; + exports["UNICODE"] = UNICODE; + exports["is_letter"] = is_letter; + exports["is_digit"] = is_digit; + exports["is_alphanumeric_char"] = is_alphanumeric_char; + exports["is_unicode_combining_mark"] = is_unicode_combining_mark; + exports["is_unicode_connector_punctuation"] = is_unicode_connector_punctuation; + exports["is_identifier"] = is_identifier; + exports["is_identifier_start"] = is_identifier_start; + exports["is_identifier_char"] = is_identifier_char; + exports["is_identifier_string"] = is_identifier_string; + exports["parse_js_number"] = parse_js_number; + exports["JS_Parse_Error"] = JS_Parse_Error; + exports["js_error"] = js_error; + exports["is_token"] = is_token; + exports["EX_EOF"] = EX_EOF; + exports["tokenizer"] = tokenizer; + exports["UNARY_PREFIX"] = UNARY_PREFIX; + exports["UNARY_POSTFIX"] = UNARY_POSTFIX; + exports["ASSIGNMENT"] = ASSIGNMENT; + exports["PRECEDENCE"] = PRECEDENCE; + exports["STATEMENTS_WITH_LABELS"] = STATEMENTS_WITH_LABELS; + exports["ATOMIC_START_TOKEN"] = ATOMIC_START_TOKEN; + exports["parse"] = parse; + exports["TreeTransformer"] = TreeTransformer; + exports["SymbolDef"] = SymbolDef; + exports["base54"] = base54; + exports["OutputStream"] = OutputStream; + exports["Compressor"] = Compressor; + exports["SourceMap"] = SourceMap; +})({}, function() { + return this; +}()); \ No newline at end of file diff --git a/omega-target-chromium-extension/.gitignore b/omega-target-chromium-extension/.gitignore new file mode 100644 index 0000000..e47655e --- /dev/null +++ b/omega-target-chromium-extension/.gitignore @@ -0,0 +1,4 @@ +/index.js +/omega_target_*.min.js + +/build diff --git a/omega-target-chromium-extension/Gruntfile.coffee b/omega-target-chromium-extension/Gruntfile.coffee new file mode 100644 index 0000000..522b59e --- /dev/null +++ b/omega-target-chromium-extension/Gruntfile.coffee @@ -0,0 +1 @@ +module.exports = require('load-grunt-config') diff --git a/omega-target-chromium-extension/background.coffee b/omega-target-chromium-extension/background.coffee new file mode 100644 index 0000000..c240ede --- /dev/null +++ b/omega-target-chromium-extension/background.coffee @@ -0,0 +1,196 @@ +OmegaTargetCurrent = Object.create(OmegaTargetChromium) +Promise = OmegaTargetCurrent.Promise +Promise.longStackTraces() + +OmegaTargetCurrent.Log = Object.create(OmegaTargetCurrent.Log) +Log = OmegaTargetCurrent.Log +Log.log = (args...) -> + console.log(args...) + localStorage['log'] += args.map(Log.str.bind(Log)).join(' ') + '\n' +Log.error = (args...) -> + console.error(args...) + content = args.map(Log.str.bind(Log)).join(' ') + localStorage['log'] += 'ERROR: ' + content + '\n' + +unhandledPromises = [] +unhandledPromisesId = [] +unhandledPromisesNextId = 1 +Promise.onPossiblyUnhandledRejection (reason, promise) -> + Log.error("[#{unhandledPromisesNextId}] Unhandled rejection:\n", reason) + unhandledPromises.push(promise) + unhandledPromisesId.push(unhandledPromisesNextId) + unhandledPromisesNextId++ +Promise.onUnhandledRejectionHandled (promise) -> + index = unhandledPromises.indexOf(promise) + Log.log("[#{unhandledPromisesId[index]}] Rejection handled!", promise) + unhandledPromises.splice(index, 1) + unhandledPromisesId.splice(index, 1) + +iconCache = {} +drawIcon = (resultColor, profileColor) -> + cacheKey = "omega+#{resultColor ? ''}+#{profileColor}" + icon = iconCache[cacheKey] + return icon if icon + ctx = document.getElementById('canvas-icon').getContext('2d') + if resultColor? + drawOmega ctx, resultColor, profileColor + else + drawOmega ctx, profileColor + icon = ctx.getImageData(0, 0, 19, 19) + return iconCache[cacheKey] = icon + +actionForUrl = (url) -> + options.ready.then(-> + request = OmegaPac.Conditions.requestFromUrl(url) + options.matchProfile(request) + ).then ({profile, results}) -> + current = options.currentProfile() + details = '' + direct = false + for result in results + if Array.isArray(result) + if not result[1]? + details += "(default) => #{result[0]}\n" + else if result[1].length == 0 + details += "#{result[0]}\n" + else if typeof result[1] == 'string' + details += "#{result[1]} => #{result[0]}\n" + else + condition = (result[1].condition ? result[1]).pattern ? '' + if result[0] == 'DIRECT' + direct = true + details += "#{condition} => #{result[0]}\n" + else if result.profileName + if result.isTempRule + details += chrome.i18n.getMessage('browserAction_tempRulePrefix') + condition = (result.source ? result.condition.pattern ? + result.condition.conditionType) + details += "#{condition} => #{result.profileName}\n" + + icon = + if profile.name == current.name and options.isCurrentProfileStatic() + if direct + drawIcon(options.profile('direct').color, profile.color) + else + drawIcon(profile.color) + else + drawIcon(profile.color, current.color) + return { + title: chrome.i18n.getMessage('browserAction_titleWithResult', [ + current.name + profile.name + details + ]) + icon: icon + } + + +storage = new OmegaTargetCurrent.Storage(chrome.storage.local, 'local') +state = new OmegaTargetCurrent.BrowserStorage(localStorage, 'omega.local.') +options = new OmegaTargetCurrent.Options(null, storage, state, Log) +console.log(options.log) + +tabs = new OmegaTargetCurrent.ChromeTabs(actionForUrl) +tabs.watch() + +options.setProxyNotControllable(null) +timeout = null + +options.watchProxyChange (details) -> + lastProxyChangeAt = Date.now() + switch details['levelOfControl'] + when "controllable_by_this_extension" + break + when "controlled_by_other_extensions", "not_controllable" + reason = + if details['levelOfControl'] == 'not_controllable' + 'policy' + else + 'app' + options.setProxyNotControllable(reason) + else + options.setProxyNotControllable(null) + + return if details['levelOfControl'] == 'controlled_by_this_extension' + Log.log('external proxy: ', details) + + # Chromium will send chrome.proxy.settings.onChange on extension unload, + # just after the current extension has lost control of the proxy settings. + # This is just annoying, and may change the currentProfileName state + # suprisingly. + # To workaround this issue, wait for some time before setting the proxy. + # However this will cause some delay before the settings are processed. + clearTimeout(timeout) if timeout? + timeout = setTimeout (-> + options.setExternalProfile( + options.parseExternalProfile(details), + noRevert: true) + ), 500 + return + +external = false +options.currentProfileChanged = (reason) -> + profile = options.currentProfile() + iconCache = {} + + if reason == 'external' + external = true + else if reason != 'clearBadge' + external = false + + title = + if profile.name == '' + details = profile.pacUrl ? options.printFixedProfile(profile) + details = details ? profile.profileType + else + chrome.i18n.getMessage('browserAction_titleNormal', [ + options._currentProfileName + ]) + if external and profile.profileType != 'SystemProfile' + message = chrome.i18n.getMessage('browserAction_titleExternalProxy') + title = message + '\n' + title + options.setBadge() + + tabs.resetAll( + icon: drawIcon(profile.color) + title: title + ) + +encodeError = (obj) -> + if obj instanceof Error + { + _error: 'error' + name: obj.name + message: obj.message + stack: obj.stack + original: obj + } + else + obj + +chrome.runtime.onMessage.addListener (request, sender, respond) -> + options.ready.then -> + target = options + method = target[request.method] + if typeof method != 'function' + Log.error("No such method #{request.method}!") + respond( + error: + reason: 'noSuchMethod' + ) + return + + promise = Promise.resolve().then -> method.apply(target, request.args) + + promise.then (result) -> + if request.method == 'updateProfile' + for own key, value of result + result[key] = encodeError(value) + respond(result: result) + + promise.catch (error) -> + Log.error(request.method + ' ==>', error) + respond(error: encodeError(error)) + + # Wait for my response! + return true diff --git a/omega-target-chromium-extension/grunt/aliases.coffee b/omega-target-chromium-extension/grunt/aliases.coffee new file mode 100644 index 0000000..0e7b7ec --- /dev/null +++ b/omega-target-chromium-extension/grunt/aliases.coffee @@ -0,0 +1,8 @@ +module.exports = + default: [ + 'coffeelint' + 'browserify' + 'coffee' + 'copy' + ] + test: ['mochaTest'] diff --git a/omega-target-chromium-extension/grunt/browserify.coffee b/omega-target-chromium-extension/grunt/browserify.coffee new file mode 100644 index 0000000..b402df7 --- /dev/null +++ b/omega-target-chromium-extension/grunt/browserify.coffee @@ -0,0 +1,28 @@ +module.exports = + index: + files: + 'index.js': 'index.coffee' + options: + transform: ['coffeeify'] + exclude: ['bluebird', 'omega-pac'] + browserifyOptions: + extensions: '.coffee' + builtins: [] + standalone: 'index.coffee' + debug: true + browser: + files: + 'omega_target_chromium_extension.min.js': 'index.coffee' + options: + alias: [ + './index.coffee:OmegaTargetChromium' + ] + transform: ['coffeeify'] + plugin: + if process.env.BUILD == 'release' + [['minifyify', {map: false}]] + else + [] + browserifyOptions: + extensions: '.coffee' + standalone: 'OmegaTargetChromium' diff --git a/omega-target-chromium-extension/grunt/coffee.coffee b/omega-target-chromium-extension/grunt/coffee.coffee new file mode 100644 index 0000000..80ffbd5 --- /dev/null +++ b/omega-target-chromium-extension/grunt/coffee.coffee @@ -0,0 +1,10 @@ +module.exports = + target_web: + files: + 'build/js/omega_target_web.js': 'omega_target_web.coffee' + target_web_log: + files: + 'build/js/omega_target_web_basics.js': 'omega_target_web_basics.coffee' + background: + files: + 'build/js/background.js': 'background.coffee' diff --git a/omega-target-chromium-extension/grunt/coffeelint.coffee b/omega-target-chromium-extension/grunt/coffeelint.coffee new file mode 100644 index 0000000..cde5378 --- /dev/null +++ b/omega-target-chromium-extension/grunt/coffeelint.coffee @@ -0,0 +1,20 @@ +module.exports = + options: + arrow_spacing: level: 'error' + colon_assignment_spacing: + level: 'error' + spacing: + left: 0 + right: 1 + line_endings: level: 'error' + missing_fat_arrows: level: 'warn' + newlines_after_classes: level: 'error' + no_empty_functions: level: 'error' + no_empty_param_list: level: 'error' + no_interpolation_in_single_quotes: level: 'error' + no_stand_alone_at: level: 'error' + space_operators: level: 'error' + + gruntfile: ['Gruntfile.coffee'] + tasks: ['grunt/**/*.coffee'] + src: ['*.coffee', 'src/**/*.coffee', 'test/**/*.coffee'] diff --git a/omega-target-chromium-extension/grunt/copy.coffee b/omega-target-chromium-extension/grunt/copy.coffee new file mode 100644 index 0000000..6a7fc72 --- /dev/null +++ b/omega-target-chromium-extension/grunt/copy.coffee @@ -0,0 +1,28 @@ +module.exports = + web: + expand: true + cwd: '../omega-web/build' + src: ['**/*'] + dest: 'build/' + target: + files: + 'build/js/omega_target.min.js': + 'node_modules/omega-target/omega_target.min.js' + target_self: + src: 'omega_target_chromium_extension.min.js' + dest: 'build/js/' + i18n: + expand: true + cwd: '../omega-i18n' + src: ['**/*'] + dest: 'build/_locales/' + overlay: + expand: true + cwd: 'overlay' + src: ['**/*'] + dest: 'build/' + docs: + expand: true + cwd: '..' + src: ['COPYING', 'AUTHORS'] + dest: 'build/' diff --git a/omega-target-chromium-extension/grunt/mochaTest.coffee b/omega-target-chromium-extension/grunt/mochaTest.coffee new file mode 100644 index 0000000..0bb9539 --- /dev/null +++ b/omega-target-chromium-extension/grunt/mochaTest.coffee @@ -0,0 +1,6 @@ +module.exports = + test: + options: + reporter: 'spec' + require: 'coffee-script/register' + src: ['test/**/*.coffee'] diff --git a/omega-target-chromium-extension/grunt/watch.coffee b/omega-target-chromium-extension/grunt/watch.coffee new file mode 100644 index 0000000..abbf64d --- /dev/null +++ b/omega-target-chromium-extension/grunt/watch.coffee @@ -0,0 +1,25 @@ +module.exports = + grunt: + options: + reload: true + files: + 'grunt/*' + tasks: ['coffeelint:tasks', 'default'] + copy_web: + files: ['node_modules/omega-web/build/**/*'] + tasks: ['copy:web'] + copy_i18n: + files: ['../omega-i18n/**/*'] + tasks: ['copy:i18n'] + copy_target: + files: ['node_modules/omega-target/omega_target.min.js'] + tasks: ['copy:target'] + copy_overlay: + files: ['overlay/**/*'] + tasks: ['copy:overlay'] + src: + files: ['src/**/*.coffee'] + tasks: ['coffeelint:src', 'browserify', 'copy:target_self'] + coffee: + files: ['src/**/*.coffee', '*.coffee'] + tasks: ['coffeelint:src', 'coffee', 'copy:target_self'] diff --git a/omega-target-chromium-extension/index.coffee b/omega-target-chromium-extension/index.coffee new file mode 100644 index 0000000..239d6a2 --- /dev/null +++ b/omega-target-chromium-extension/index.coffee @@ -0,0 +1,8 @@ +module.exports = + Storage: require('./src/storage') + Options: require('./src/options') + ChromeTabs: require('./src/tabs.coffee') + Url: require('url') + +for name, value of require('omega-target') + module.exports[name] ?= value diff --git a/omega-target-chromium-extension/omega_target_shim.js b/omega-target-chromium-extension/omega_target_shim.js new file mode 100644 index 0000000..d28f8b3 --- /dev/null +++ b/omega-target-chromium-extension/omega_target_shim.js @@ -0,0 +1 @@ +module.exports = OmegaTarget diff --git a/omega-target-chromium-extension/omega_target_web.coffee b/omega-target-chromium-extension/omega_target_web.coffee new file mode 100644 index 0000000..dc25b89 --- /dev/null +++ b/omega-target-chromium-extension/omega_target_web.coffee @@ -0,0 +1,105 @@ +angular.module('omegaTarget', []).factory 'omegaTarget', ($q) -> + decodeError = (obj) -> + if obj._error == 'error' + err = new Error(obj.message) + err.name = obj.name + err.stack = obj.stack + err.original = obj.original + err + else + obj + callBackground = (method, args...) -> + d = $q['defer']() + chrome.runtime.sendMessage({ + method: method + args: args + }, (response) -> + if chrome.runtime.lastError? + d.reject(chrome.runtime.lastError) + return + if response.error + d.reject(decodeError(response.error)) + else + d.resolve(response.result) + ) + return d.promise + + isChromeUrl = (url) -> url.substr(0, 6) == 'chrome' + + optionsChangeCallback = [] + urlParser = document.createElement('a') + omegaTarget = + options: null + state: (name, value) -> + prefix = 'omega.local.' + if arguments.length == 1 + getValue = (key) -> try JSON.parse(localStorage[prefix + key]) + if Array.isArray(name) + return $q.when(name.map(getValue)) + else + value = getValue(name) + else + localStorage[prefix + name] = JSON.stringify(value) + return $q.when(value) + addOptionsChangeCallback: (callback) -> + optionsChangeCallback.push(callback) + refresh: (args) -> + return callBackground('getAll').then (opt) -> + omegaTarget.options = opt + for callback in optionsChangeCallback + callback(omegaTarget.options) + return args + renameProfile: (fromName, toName) -> + callBackground('renameProfile', fromName, toName).then omegaTarget.refresh + optionsPatch: (patch) -> + callBackground('patch', patch).then omegaTarget.refresh + resetOptions: (opt) -> + callBackground('reset', opt).then omegaTarget.refresh + updateProfile: (name) -> + callBackground('updateProfile', name).then((results) -> + for own key, value of results + results[key] = decodeError(value) + results + ).then omegaTarget.refresh + getMessage: chrome.i18n.getMessage.bind(chrome.i18n) + openOptions: -> + d = $q['defer']() + options_url = chrome.extension.getURL('options.html') + chrome.tabs.query url: options_url, (tabs) -> + if tabs.length > 0 + chrome.tabs.update(tabs[0].id, {active: true}) + else + chrome.tabs.create({url: options_url}) + d.resolve() + return d.promise + applyProfile: (name) -> + callBackground('applyProfile', name) + addTempRule: (domain, profileName) -> + callBackground('addTempRule', domain, profileName) + addCondition: (condition, profileName) -> + callBackground('addCondition', condition, profileName) + addProfile: (profile) -> + callBackground('addProfile', profile).then omegaTarget.refresh + getActivePageInfo: -> + # First, try to clear badges on opening the popup. + callBackground('clearBadge') + d = $q['defer']() + chrome.tabs.query {active: true, lastFocusedWindow: true}, (tabs) -> + d.resolve(tabs[0]?.url) + return d.promise.then (url) -> + return null if not url or isChromeUrl(url) + urlParser.href = url + domain = urlParser.hostname + callBackground('queryTempRule', domain).then (profileName) -> + url: url + domain: domain + tempRuleProfileName: profileName + refreshActivePage: -> + d = $q['defer']() + chrome.tabs.query {active: true, lastFocusedWindow: true}, (tabs) -> + if tabs[0].url and not isChromeUrl(tabs[0].url) + chrome.tabs.reload(tabs[0].id) + d.resolve() + return d.promise + + return omegaTarget diff --git a/omega-target-chromium-extension/omega_target_web_basics.coffee b/omega-target-chromium-extension/omega_target_web_basics.coffee new file mode 100644 index 0000000..8df338c --- /dev/null +++ b/omega-target-chromium-extension/omega_target_web_basics.coffee @@ -0,0 +1,11 @@ +window.OmegaTargetWebBasics = + getLog: (callback) -> + callback(localStorage['log'] || '') + getEnv: (callback) -> + extensionVersion = chrome.runtime.getManifest().version + callback({ + extensionVersion: extensionVersion + projectVersion: extensionVersion + userAgent: navigator.userAgent + }) + getMessage: chrome.i18n.getMessage.bind(chrome.i18n) diff --git a/omega-target-chromium-extension/overlay/background.html b/omega-target-chromium-extension/overlay/background.html new file mode 100644 index 0000000..d4e59f1 --- /dev/null +++ b/omega-target-chromium-extension/overlay/background.html @@ -0,0 +1,17 @@ + + + + + SwitchyOmega Background + + + + + + + + + + + + diff --git a/omega-target-chromium-extension/overlay/js/background_preload.js b/omega-target-chromium-extension/overlay/js/background_preload.js new file mode 100644 index 0000000..aed9f90 --- /dev/null +++ b/omega-target-chromium-extension/overlay/js/background_preload.js @@ -0,0 +1,2 @@ +window.UglifyJS_NoUnsafeEval = true +localStorage['log'] = '' diff --git a/omega-target-chromium-extension/overlay/manifest.json b/omega-target-chromium-extension/overlay/manifest.json new file mode 100644 index 0000000..7133321 --- /dev/null +++ b/omega-target-chromium-extension/overlay/manifest.json @@ -0,0 +1,35 @@ +{ + "manifest_version": 2, + "name": "__MSG_manifest_app_name__", + "version": "2.1.1", + "description": "__MSG_manifest_app_description__", + "icons": { + "16": "img/icons/omega-16.png", + "32": "img/icons/omega-32.png", + "48": "img/icons/omega-48.png", + "64": "img/icons/omega-64.png", + "128": "img/icons/omega-128.png" + }, + "default_locale": "en", + "browser_action": { + "default_icon": "img/icons/omega-32.png", + "default_title": "__MSG_manifest_icon_default_title__", + "default_popup": "popup.html" + }, + "background": { + "page": "background.html" + }, + "homepage_url": "https://chrome.google.com/webstore/detail/dpplabbmogkhghncfbfdeeokoefdjegm", + "minimum_chrome_version": "22.0.0", + "options_page": "options.html", + "permissions": [ + "proxy", + "tabs", + "alarms", + "storage", + "http://*/*", + "https://*/*", + "ftp://*/*", + "" + ] +} diff --git a/omega-target-chromium-extension/package.json b/omega-target-chromium-extension/package.json new file mode 100644 index 0000000..e99a0fb --- /dev/null +++ b/omega-target-chromium-extension/package.json @@ -0,0 +1,31 @@ +{ + "name": "omega-target-chromium-extension", + "version": "0.0.1", + "private": true, + "main": "./index", + "devDependencies": { + "chai": "~1.9.1", + "coffee-script": "^1.7.1", + "coffeeify": "^0.7.0", + "grunt": "^0.4.5", + "grunt-browserify": "^3.0.0", + "grunt-coffeelint": "^0.0.13", + "grunt-contrib-coffee": "^0.11.1", + "grunt-contrib-copy": "^0.5.0", + "grunt-contrib-watch": "^0.6.1", + "grunt-mocha-test": "~0.11.0", + "load-grunt-config": "^0.13.1", + "minifyify": "^4.1.1" + }, + "dependencies": { + "omega-target": "../omega-target", + "omega-web": "../omega-web", + "xhr": "^1.16.0" + }, + "browser": { + "omega-target": "./omega_target_shim.js" + }, + "scripts": { + "dev": "npm link ../omega-target; npm link ../omega-web" + } +} diff --git a/omega-target-chromium-extension/src/chrome_api.coffee b/omega-target-chromium-extension/src/chrome_api.coffee new file mode 100644 index 0000000..6977bca --- /dev/null +++ b/omega-target-chromium-extension/src/chrome_api.coffee @@ -0,0 +1,19 @@ +OmegaTarget = require('omega-target') +Promise = OmegaTarget.Promise + +chromeApiPromisifer = (originalMethod) -> + return (args...) -> + new Promise (resolve, reject) => + callback = (callbackArgs...) -> + if chrome.runtime.lastError? + return reject(chrome.runtime.lastError) + if callbackArgs.length <= 1 + resolve(callbackArgs[0]) + else + resolve(callbackArgs) + + args.push(callback) + originalMethod.apply(this, args) + +module.exports = (obj) -> + Promise.promisifyAll(Object.create(obj), {promisifier: chromeApiPromisifer}) diff --git a/omega-target-chromium-extension/src/options.coffee b/omega-target-chromium-extension/src/options.coffee new file mode 100644 index 0000000..43c6cc9 --- /dev/null +++ b/omega-target-chromium-extension/src/options.coffee @@ -0,0 +1,217 @@ +OmegaTarget = require('omega-target') +OmegaPac = OmegaTarget.OmegaPac +Promise = OmegaTarget.Promise +xhr = Promise.promisify(require('xhr')) +url = require('url') +chromeApiPromisifyAll = require('./chrome_api') +proxySettings = chromeApiPromisifyAll(chrome.proxy.settings) +parseExternalProfile = require('./parse_external_profile') + +class ChromeOptions extends OmegaTarget.Options + parseExternalProfile: (details) -> + parseExternalProfile(details, @_options, @_fixedProfileConfig.bind(this)) + + fetchUrl: (dest_url, opt_bypass_cache) -> + if opt_bypass_cache + parsed = url.parse(dest_url, true) + parsed.search = undefined + parsed.query['_'] = Date.now() + dest_url = url.format(parsed) + xhr(dest_url).get(1) + + updateProfile: (args...) -> + super(args...).then (results) => + error = false + for own profileName, result of results + if result instanceof Error + error = true + break + if error + @setBadge( + text: '!' + color: '#faa732' + title: chrome.i18n.getMessage('browserAction_titleDownloadFail') + ) + return results + + _proxyNotControllable: null + proxyNotControllable: => @_proxyNotControllable + setProxyNotControllable: (reason) -> + @_proxyNotControllable = reason + if reason + @_state.set({'proxyNotControllable': reason}) + @setBadge() + else + @_state.remove(['proxyNotControllable']) + @clearBadge() + + _badgeTitle: null + setBadge: (options) -> + if not options + options = + if @_proxyNotControllable + text: '=' + color: '#da4f49' + else + text: '?' + color: '#49afcd' + chrome.browserAction.setBadgeText(text: options.text) + chrome.browserAction.setBadgeBackgroundColor(color: options.color) + if options.title + @_badgeTitle = options.title + chrome.browserAction.setTitle(title: options.title) + else + @_badgeTitle = null + clearBadge: -> + if @_badgeTitle + @currentProfileChanged('clearBadge') + if @_proxyNotControllable + @setBadge() + else + chrome.browserAction.setBadgeText(text: '') + return + + _fixedProfileConfig: (profile) -> + config = {} + config['mode'] = 'fixed_servers' + rules = {} + protocols = ['proxyForHttp', 'proxyForHttps', 'proxyForFtp'] + protocolProxySet = false + for protocol in protocols when profile[protocol]? + rules[protocol] = profile[protocol] + protocolProxySet = true + + if profile.fallbackProxy + if profile.fallbackProxy.scheme == 'http' + # Chromium does not allow HTTP proxies in 'fallbackProxy'. + if not protocolProxySet + # Use 'singleProxy' if no proxy is configured for other protocols. + rules['singleProxy'] = profile.fallbackProxy + else + # Try to set the proxies of all possible protocols. + for protocol in protocols + rules[protocol] ?= profile.fallbackProxy + else + rules['fallbackProxy'] = profile.fallbackProxy + else if not protocolProxySet + config['mode'] = 'direct' + + if config['mode'] != 'direct' + rules['bypassList'] = profile.bypassList.map((b) -> b.pattern) + config['rules'] = rules + return config + + _proxyChangeWatchers: [] + _proxyChangeListener: null + watchProxyChange: (callback) -> + if not @_proxyChangeListener? + @_proxyChangeListener = (details) => + for watcher in @_proxyChangeWatchers + watcher(details) + chrome.proxy.settings.onChange.addListener @_proxyChangeListener + @_proxyChangeWatchers.push(callback) + applyProfileProxy: (profile) -> + if profile.profileType == 'SystemProfile' + # Clear proxy settings, returning proxy control to Chromium. + return proxySettings.clearAsync({}).then => + chrome.proxy.settings.get {}, @_proxyChangeListener + return + config = {} + if profile.profileType == 'DirectProfile' + config['mode'] = 'direct' + else if profile.profileType == 'PacProfile' + config['mode'] = 'pac_script' + config['pacScript'] = if profile.pacScript + data: profile.pacScript + mandatory: true + else + url: profile.pacUrl + mandatory: true + else if profile.profileType == 'FixedProfile' + config = @_fixedProfileConfig(profile) + else + config['mode'] = 'pac_script' + config['pacScript'] = + data: null + mandatory: true + setPacScript = @pacForProfile(profile).then (script) -> + profileName = JSON.stringify(profile.name).replace(/\*/g, '\\u002a') + profileName = profileName.replace(/\\/g, '\\u002f') + prefix = "/*OmegaProfile*#{profileName}*#{profile.revision}*/" + config['pacScript'].data = prefix + script + return + setPacScript ?= Promise.resolve() + setPacScript.then(-> + proxySettings.setAsync({value: config}) + ).then => + chrome.proxy.settings.get {}, @_proxyChangeListener + return + + _quickSwitchInit: false + setQuickSwitch: (quickSwitch) -> + if quickSwitch + chrome.browserAction.setPopup({popup: ''}) + if not @_quickSwitchInit + @_quickSwitchInit = true + chrome.browserAction.onClicked.addListener (tab) => + @clearBadge() + profiles = @_options['-quickSwitchProfiles'] + index = profiles.indexOf(@_currentProfileName) + index = (index + 1) % profiles.length + @applyProfile(profiles[index]).then => + if @_options['-refreshOnProfileChange'] + if tab.url and tab.url.indexOf('chrome') != 0 + chrome.tabs.reload(tab.id) + else + chrome.browserAction.setPopup({popup: 'popup.html'}) + Promise.resolve() + + _alarms: null + schedule: (name, periodInMinutes, callback) -> + name = 'omega.' + name + if not _alarms? + @_alarms = {} + chrome.alarms.onAlarm.addListener (alarm) => + @_alarms[alarm.name]?() + if periodInMinutes < 0 + delete @_alarms[name] + chrome.alarms.clear(name) + else + @_alarms[name] = callback + chrome.alarms.create(name, { + periodInMinutes: periodInMinutes + }) + Promise.resolve() + + printFixedProfile: (profile) -> + return unless profile.profileType == 'FixedProfile' + result = '' + for scheme in OmegaPac.Profiles.schemes when profile[scheme.prop] + pacResult = OmegaPac.Profiles.pacResult(profile[scheme.prop]) + if scheme.scheme + result += "#{scheme.scheme}: #{pacResult}\n" + else + result += "#{pacResult}\n" + return result + + upgrade: (options, changes) -> + super(options).catch (err) => + if not options?['schemaVersion'] + if options?['config'] or localStorage['config'] + oldOptions = if options?['config'] then options else localStorage + try + # Upgrade from SwitchySharp. + upgraded = require('./upgrade')(oldOptions) + catch ex + OmegaTarget.Log.error(ex) + if upgraded + if localStorage['config'] + Object.getPrototypeOf(localStorage).clear.call(localStorage) + return this && super(upgraded, upgraded) + else + return Promise.reject new Error('No options set.') + + Promise.reject err + +module.exports = ChromeOptions + diff --git a/omega-target-chromium-extension/src/parse_external_profile.coffee b/omega-target-chromium-extension/src/parse_external_profile.coffee new file mode 100644 index 0000000..a2b181f --- /dev/null +++ b/omega-target-chromium-extension/src/parse_external_profile.coffee @@ -0,0 +1,110 @@ +OmegaTarget = require('omega-target') +OmegaPac = OmegaTarget.OmegaPac + +module.exports = (details, options, fixedProfileConfig) -> + if details.name + details + else + switch details.value.mode + when 'system' + OmegaPac.Profiles.byName('system') + when 'direct' + OmegaPac.Profiles.byName('direct') + when 'auto_detect' + OmegaPac.Profiles.create({ + profileType: 'PacProfile' + name: '' + pacUrl: 'http://wpad/wpad.dat' + }) + when 'pac_script' + url = details.value.pacScript.url + if url + profile = null + OmegaPac.Profiles.each options, (key, p) -> + if p.profileType == 'PacProfile' and p.pacUrl == url + profile = p + profile ? OmegaPac.Profiles.create({ + profileType: 'PacProfile' + name: '' + pacUrl: url + }) + else do -> + profile = null + script = details.value.pacScript.data + OmegaPac.Profiles.each options, (key, p) -> + if p.profileType == 'PacProfile' and p.pacScript == script + profile = p + return profile if profile + # Try to parse the prefix used by this class. + script = script.trim() + magic = '/*OmegaProfile*' + if script.substr(0, magic.length) == magic + end = script.indexOf('*/') + if end > 0 + i = magic.length + tokens = script.substring(magic.length, end).split('*') + [profileName, revision] = tokens + try + profileName = JSON.parse(profileName) + catch + profileName = null + if profileName and revision + profile = OmegaPac.Profiles.byName(profileName, options) + if OmegaPac.Revision.compare(profile.revision, revision) == 0 + return profile + return OmegaPac.Profiles.create({ + profileType: 'PacProfile' + name: '' + pacScript: script + }) + when 'fixed_servers' + props = ['proxyForHttp', 'proxyForHttps', 'proxyForFtp', + 'fallbackProxy', 'singleProxy'] + proxies = {} + for prop in props + result = OmegaPac.Profiles.pacResult(details.value.rules[prop]) + if prop == 'singleProxy' + proxies['fallbackProxy'] = result + else + proxies[prop] = result + bypassSet = {} + bypassCount = 0 + if details.value.rules.bypassList + for pattern in details.value.rules.bypassList + bypassSet[pattern] = true + bypassCount++ + if bypassSet[''] + for host in OmegaPac.Conditions.localHosts when bypassSet[host] + delete bypassSet[host] + bypassCount-- + profile = null + OmegaPac.Profiles.each options, (key, p) -> + return if p.profileType != 'FixedProfile' + return if p.bypassList.length != bypassCount + for condition in p.bypassList + return unless bypassSet[condition.pattern] + rules = fixedProfileConfig(p).rules + if rules['singleProxy'] + rules['fallbackProxy'] = rules['singleProxy'] + delete rules['singleProxy'] + return unless rules? + for prop in props when rules[prop] or proxies[prop] + if OmegaPac.Profiles.pacResult(rules[prop]) != proxies[prop] + return + profile = p + if profile + profile + else + profile = OmegaPac.Profiles.create({ + profileType: 'FixedProfile' + name: '' + }) + for prop in props when details.value.rules[prop] + if prop == 'singleProxy' + profile['fallbackProxy'] = details.value.rules[prop] + else + profile[prop] = details.value.rules[prop] + profile.bypassList = + for own pattern of bypassSet + {conditionType: 'BypassCondition', pattern: pattern} + profile diff --git a/omega-target-chromium-extension/src/storage.coffee b/omega-target-chromium-extension/src/storage.coffee new file mode 100644 index 0000000..354caf2 --- /dev/null +++ b/omega-target-chromium-extension/src/storage.coffee @@ -0,0 +1,63 @@ +chromeApiPromisifyAll = require('./chrome_api') +OmegaTarget = require('omega-target') +Promise = OmegaTarget.Promise + +class ChromeStorage extends OmegaTarget.Storage + constructor: (storage, @areaName) -> + @storage = chromeApiPromisifyAll(storage) + + get: (keys) -> + keys ?= null + @storage.getAsync(keys) + + set: (items) -> + if Object.keys(items).length == 0 + return Promise.resolve({}) + @storage.setAsync(items) + + remove: (keys) -> + if not keys? + return @storage.clearAsync() + if Array.isArray(keys) and keys.length == 0 + return Promise.resolve({}) + @storage.removeAsync(keys) + + watch: (keys, callback) -> + ChromeStorage.watchers[@areaName] ?= {} + area = ChromeStorage.watchers[@areaName] + watcher = {keys: keys, callback: callback} + id = Date.now().toString() + while area[id] + id = Date.now().toString() + + if Array.isArray(keys) + keyMap = {} + for key in keys + keyMap[key] = true + keys = keyMap + area[id] = {keys: keys, callback: callback} + if not ChromeStorage.onChangedListenerInstalled + chrome.storage.onChanged.addListener(ChromeStorage.onChangedListener) + ChromeStorage.onChangedListenerInstalled = true + return -> delete area[id] + + @onChangedListener: (changes, areaName) -> + map = null + for _, watcher of ChromeStorage.watchers[areaName] + match = watcher.keys == null + if not match + for own key of changes + if watcher.keys[key] + match = true + break + if match + if not map? + map = {} + for own key, change of changes + map[key] = change.newValue + watcher.callback(map) + + @onChangedListenerInstalled: false + @watchers: {} + +module.exports = ChromeStorage diff --git a/omega-target-chromium-extension/src/tabs.coffee b/omega-target-chromium-extension/src/tabs.coffee new file mode 100644 index 0000000..2325aac --- /dev/null +++ b/omega-target-chromium-extension/src/tabs.coffee @@ -0,0 +1,60 @@ +class ChromeTabs + _dirtyTabs: {} + _defaultAction: null + + constructor: (@actionForUrl) -> return + + ignoreError: -> + chrome.runtime.lastError + return + + watch: -> + chrome.tabs.onUpdated.addListener @onUpdated.bind(this) + chrome.tabs.onActivated.addListener (info) => + chrome.tabs.get info.tabId, (tab) => + if @_dirtyTabs.hasOwnProperty(info.tabId) + @onUpdated tab.id, {}, tab + + resetAll: (action) -> + @_defaultAction = action + chrome.tabs.query {}, (tabs) => + @_dirtyTabs = {} + tabs.forEach (tab) => + @_dirtyTabs[tab.id] = tab.id + @onUpdated tab.id, {}, tab if tab.active + chrome.browserAction.setTitle({title: action.title}) + @setIcon(action.icon) + + onUpdated: (tabId, changeInfo, tab) -> + if @_dirtyTabs.hasOwnProperty(tab.id) + delete @_dirtyTabs[tab.id] + else if not changeInfo.url? + if changeInfo.status? and changeInfo.status != 'loading' + return + @processTab(tab, changeInfo) + + processTab: (tab, changeInfo) -> + if not tab.url? or tab.url.indexOf("chrome") == 0 + chrome.browserAction.setTitle(title: @_defaultAction.title, tabId: tab.id) + @clearIcon tab.id + return + @actionForUrl(tab.url).then (action) => + @setIcon(action.icon, tab.id) + chrome.browserAction.setTitle(title: action.title, tabId: tab.id) + + setIcon: (icon, tabId) -> + if tabId? + chrome.browserAction.setIcon({ + imageData: icon + tabId: tabId + }, @ignoreError) + else + chrome.browserAction.setIcon({imageData: icon}, @ignoreError) + + clearIcon: (tabId) -> + chrome.browserAction.setIcon({ + imageData: @_defaultAction.icon + tabId: tabId + }, @ignoreError) + +module.exports = ChromeTabs diff --git a/omega-target-chromium-extension/src/upgrade.coffee b/omega-target-chromium-extension/src/upgrade.coffee new file mode 100644 index 0000000..3db7aea --- /dev/null +++ b/omega-target-chromium-extension/src/upgrade.coffee @@ -0,0 +1,146 @@ +OmegaTarget = require('omega-target') +OmegaPac = OmegaTarget.OmegaPac + +module.exports = (oldOptions) -> + config = try JSON.parse(oldOptions['config']) + if config + options = changes ? {} + options['schemaVersion'] = 2 + boolItems = + '-confirmDeletion': 'confirmDeletion' + '-refreshOnProfileChange': 'refreshTab' + '-enableQuickSwitch': 'quickSwitch' + '-revertProxyChanges': 'preventProxyChanges' + for own key, oldKey of boolItems + options[key] = !!config[oldKey] + options['-downloadInterval'] = + parseInt(config['ruleListReload']) || 15 + + profile = OmegaPac.Profiles.create( + profileType: 'SwitchProfile' + name: 'auto' + color: '#55bb55' + defaultProfileName: 'rulelist' + ) + OmegaPac.Profiles.updateRevision(profile) + options[OmegaPac.Profiles.nameAsKey(profile.name)] = profile + + profile = OmegaPac.Profiles.create( + profileType: 'RuleListProfile' + name: 'rulelist' + color: '#dd6633' + format: + if config['ruleListAutoProxy'] then 'AutoProxy' else 'Switchy' + defaultProfileName: 'direct' + sourceUrl: config['ruleListUrl'] || '' + ) + options[OmegaPac.Profiles.nameAsKey(profile.name)] = profile + + nameMap = {'auto': 'auto', 'direct': 'direct'} + oldProfiles = (try JSON.parse(oldOptions['profiles'])) || {} + colorTranslations = + 'blue': '#99ccee' + 'green': '#99dd99' + 'red': '#ffaa88' + 'yellow': '#ffee99' + 'purple': '#d497ee' + '': '#99ccee' + + for own _, oldProfile of oldProfiles + profile = null + switch oldProfile['proxyMode'] + when 'auto' + profile = OmegaPac.Profiles.create( + profileType: 'PacProfile' + ) + url = oldProfile['proxyConfigUrl'] + if url.substr(0, 5) == 'data:' + text = url.substr(url.indexOf(',') + 1) + Buffer = require('buffer').Buffer + text = new Buffer(text, 'base64').toString('utf8') + profile.pacScript = text + else + profile.pacUrl = url + when 'manual' + profile = OmegaPac.Profiles.create( + profileType: 'FixedProfile' + ) + if !!oldProfile['useSameProxy'] + profile.fallbackProxy = OmegaPac.Profiles.parseHostPort( + oldProfile['proxyHttp'], 'http') + else if oldProfile['proxySocks'] + protocol = + if oldProfile['socksVersion'] == 5 + 'socks5' + else + 'socks4' + profile.fallbackProxy = OmegaPac.Profiles.parseHostPort( + oldProfile['proxySocks'], + protocol + ) + else + profile.proxyForHttp = OmegaPac.Profiles.parseHostPort( + oldProfile['proxyHttp'], 'http') + profile.proxyForHttps = OmegaPac.Profiles.parseHostPort( + oldProfile['proxyHttps'], 'http') + profile.proxyForFtp = OmegaPac.Profiles.parseHostPort( + oldProfile['proxyFtp'], 'http') + if oldProfile['proxyExceptions']? + haslocalPattern = false + profile.bypassList = [] + oldProfile['proxyExceptions'].split(';').forEach (line) -> + line = line.trim() + return unless line + haslocalPattern = true if line == '' + profile.bypassList.push( + conditionType: 'BypassCondition' + pattern: line + ) + if haslocalPattern + profile.bypassList = profile.bypassList.filter (cond) -> + OmegaPac.Conditions.localHosts.indexOf(cond.pattern) < 0 + if profile + color = oldProfile['color'] + profile.color = colorTranslations[color] ? colorTranslations[''] + name = oldProfile['name'] ? oldProfile['id'] + profile.name = name + num = 1 + while OmegaPac.Profiles.byName(profile.name, options) + profile.name = name + num + num++ + nameMap[oldProfile['id']] = profile.name + OmegaPac.Profiles.updateRevision(profile) + options[OmegaPac.Profiles.nameAsKey(profile.name)] = profile + + startupId = config['startupProfileId'] + options['-startupProfileName'] = nameMap[startupId] || '' + + quickSwitch = try JSON.parse(oldOptions['quickSwitchProfiles']) + options['-quickSwitchProfiles'] = if not quickSwitch? then [] else + quickSwitch.map (p) -> nameMap[p] + + profile = OmegaPac.Profiles.byName('rulelist', options) + if config['ruleListProfileId'] + profile.matchProfileName = + nameMap[config['ruleListProfileId']] || 'direct' + + defaultRule = try JSON.parse(oldOptions['defaultRule']) + if defaultRule + profile.defaultProfileName = + nameMap[defaultRule.profileId] || 'direct' + + rules = try JSON.parse(oldOptions['rules']) + if rules + profile = OmegaPac.Profiles.byName('auto', options) + profile.rules = for own _, rule of rules + profileName: nameMap[rule['profileId']] || 'direct' + condition: + conditionType: + if rule['patternType'] == 'wildcard' + # TODO(catus): Recognize HostWildcardCondition. + 'UrlWildcardCondition' + else + 'UrlRegexCondition' + pattern: rule['urlPattern'] + return options + return diff --git a/omega-target/.gitignore b/omega-target/.gitignore new file mode 100644 index 0000000..99c51a8 --- /dev/null +++ b/omega-target/.gitignore @@ -0,0 +1,2 @@ +/index.js +/omega_target.min.js diff --git a/omega-target/Gruntfile.coffee b/omega-target/Gruntfile.coffee new file mode 100644 index 0000000..522b59e --- /dev/null +++ b/omega-target/Gruntfile.coffee @@ -0,0 +1 @@ +module.exports = require('load-grunt-config') diff --git a/omega-target/grunt/aliases.coffee b/omega-target/grunt/aliases.coffee new file mode 100644 index 0000000..650fde5 --- /dev/null +++ b/omega-target/grunt/aliases.coffee @@ -0,0 +1,6 @@ +module.exports = + default: [ + 'coffeelint' + 'browserify' + ] + test: ['mochaTest'] diff --git a/omega-target/grunt/browserify.coffee b/omega-target/grunt/browserify.coffee new file mode 100644 index 0000000..05a581a --- /dev/null +++ b/omega-target/grunt/browserify.coffee @@ -0,0 +1,28 @@ +module.exports = + index: + files: + 'index.js': 'index.coffee' + options: + transform: ['coffeeify'] + exclude: ['bluebird', 'jsondiffpatch', 'omega-pac'] + browserifyOptions: + extensions: '.coffee' + builtins: [] + standalone: 'index.coffee' + debug: true + browser: + files: + 'omega_target.min.js': 'index.coffee' + options: + alias: [ + './index.coffee:OmegaTarget' + ] + transform: ['coffeeify'] + plugin: + if process.env.BUILD == 'release' + [['minifyify', {map: false}]] + else + [] + browserifyOptions: + extensions: '.coffee' + standalone: 'OmegaTarget' diff --git a/omega-target/grunt/coffeelint.coffee b/omega-target/grunt/coffeelint.coffee new file mode 100644 index 0000000..518657b --- /dev/null +++ b/omega-target/grunt/coffeelint.coffee @@ -0,0 +1,20 @@ +module.exports = + options: + arrow_spacing: level: 'error' + colon_assignment_spacing: + level: 'error' + spacing: + left: 0 + right: 1 + line_endings: level: 'error' + missing_fat_arrows: level: 'warn' + newlines_after_classes: level: 'error' + no_empty_functions: level: 'error' + no_empty_param_list: level: 'error' + no_interpolation_in_single_quotes: level: 'error' + no_stand_alone_at: level: 'error' + space_operators: level: 'error' + + gruntfile: ['Gruntfile.coffee'] + tasks: ['grunt/**/*.coffee'] + src: ['src/**/*.coffee', 'test/**/*.coffee'] diff --git a/omega-target/grunt/mochaTest.coffee b/omega-target/grunt/mochaTest.coffee new file mode 100644 index 0000000..0bb9539 --- /dev/null +++ b/omega-target/grunt/mochaTest.coffee @@ -0,0 +1,6 @@ +module.exports = + test: + options: + reporter: 'spec' + require: 'coffee-script/register' + src: ['test/**/*.coffee'] diff --git a/omega-target/grunt/watch.coffee b/omega-target/grunt/watch.coffee new file mode 100644 index 0000000..fdc8573 --- /dev/null +++ b/omega-target/grunt/watch.coffee @@ -0,0 +1,10 @@ +module.exports = + grunt: + options: + reload: true + files: + 'grunt/*' + tasks: ['coffeelint:tasks', 'default'] + src: + files: ['src/**/*.coffee', 'test/**/*.coffee'] + tasks: ['default'] diff --git a/omega-target/index.coffee b/omega-target/index.coffee new file mode 100644 index 0000000..ddb0037 --- /dev/null +++ b/omega-target/index.coffee @@ -0,0 +1,9 @@ +module.exports = + Log: require('./src/log') + Storage: require('./src/storage') + BrowserStorage: require('./src/browser_storage') + Options: require('./src/options') + OmegaPac: require('omega-pac') + +for name, value of require('./src/utils.coffee') + module.exports[name] = value diff --git a/omega-target/omega_pac_shim.js b/omega-target/omega_pac_shim.js new file mode 100644 index 0000000..d790c2a --- /dev/null +++ b/omega-target/omega_pac_shim.js @@ -0,0 +1 @@ +module.exports = OmegaPac; diff --git a/omega-target/package.json b/omega-target/package.json new file mode 100644 index 0000000..eb464c0 --- /dev/null +++ b/omega-target/package.json @@ -0,0 +1,30 @@ +{ + "name": "omega-target", + "version": "0.0.1", + "private": true, + "main": "./index.js", + "devDependencies": { + "chai": "~1.9.1", + "coffee-script": "^1.8.0", + "coffeeify": "^0.7.0", + "grunt": "^0.4.5", + "grunt-browserify": "^3.0.0", + "grunt-coffeelint": "^0.0.13", + "grunt-contrib-coffee": "^0.11.1", + "grunt-contrib-watch": "^0.6.1", + "grunt-mocha-test": "~0.11.0", + "load-grunt-config": "^0.13.1", + "minifyify": "^4.1.1" + }, + "dependencies": { + "bluebird": "^2.3.2", + "jsondiffpatch": "^0.1.8", + "omega-pac": "../omega-pac" + }, + "browser": { + "omega-pac": "./omega_pac_shim.js" + }, + "scripts": { + "dev": "npm link ../omega-pac" + } +} diff --git a/omega-target/src/browser_storage.coffee b/omega-target/src/browser_storage.coffee new file mode 100644 index 0000000..f2b400f --- /dev/null +++ b/omega-target/src/browser_storage.coffee @@ -0,0 +1,50 @@ +Storage = require('./storage') +Promise = require('bluebird') + +class BrowserStorage extends Storage + constructor: (@storage, @prefix = '') -> + @proto = Object.getPrototypeOf(@storage) + + get: (keys) -> + map = {} + if typeof keys == 'string' + map[keys] = undefined + else if Array.isArray(keys) + for key in keys + map[key] = undefined + else if typeof keys == 'object' + map = keys + for own key, value of map + try + map[key] = JSON.parse(@proto.getItem.call(@storage, @prefix + key)) + if typeof map[key] == 'undefined' + delete map[key] + Promise.resolve map + + set: (items) -> + for own key, value of items + value = JSON.stringify(value) + @proto.setItem.call(@storage, @prefix + key, value) + Promise.resolve items + + remove: (keys) -> + if not keys? + if not @prefix + @proto.clear.call(@storage) + else + index = 0 + while true + key = @proto.key.call(index) + break if key == null + if @key.substr(0, @prefix.length) == @prefix + @proto.removeItem.call(@storage, @prefix + keys) + else + index++ + if typeof keys == 'string' + @proto.removeItem.call(@storage, @prefix + keys) + for key in keys + @proto.removeItem.call(@storage, @prefix + key) + + Promise.resolve() + +module.exports = BrowserStorage diff --git a/omega-target/src/default_options.coffee b/omega-target/src/default_options.coffee new file mode 100644 index 0000000..4a093f8 --- /dev/null +++ b/omega-target/src/default_options.coffee @@ -0,0 +1,43 @@ +module.exports = -> + schemaVersion: 2 + "-enableQuickSwitch": false + "-refreshOnProfileChange": true + "-startupProfileName": "" + "-quickSwitchProfiles": [] + "-revertProxyChanges": false + "-confirmDeletion": true + "-downloadInterval": 1440 + "+proxy": + bypassList: [ + pattern: "" + conditionType: "BypassCondition" + ] + profileType: "FixedProfile" + name: "proxy" + color: "#99ccee" + fallbackProxy: + port: 8080 + scheme: "http" + host: "proxy.example.com" + + "+auto switch": + profileType: "SwitchProfile" + rules: [ + { + condition: + pattern: "internal.example.com" + conditionType: "HostWildcardCondition" + + profileName: "direct" + } + { + condition: + pattern: "*.example.com" + conditionType: "HostWildcardCondition" + + profileName: "proxy" + } + ] + name: "auto switch" + color: "#99dd99" + defaultProfileName: "direct" diff --git a/omega-target/src/log.coffee b/omega-target/src/log.coffee new file mode 100644 index 0000000..1e99d58 --- /dev/null +++ b/omega-target/src/log.coffee @@ -0,0 +1,61 @@ +### @module omega-target/log ### +Log = require './log' + +# Log is used as singleton. +# coffeelint: disable=missing_fat_arrows +module.exports = Log = + ###* + # Pretty-print an object and return the result string. + # @param {{}} obj The object to format + # @returns {String} the formatted object in string + ### + str: (obj) -> + # TODO(catus): This can be improved to print things more friendly. + if typeof obj == 'object' and obj != null + if obj.debugStr? + if typeof obj.debugStr == 'function' + obj.debugStr() + else + obj.debugStr + else if obj instanceof Error + obj.stack || obj.message + else + JSON.stringify(obj, null, 4) + else if typeof obj == 'function' + if obj.name + "" + else + obj.toString() + else + '' + obj + + ###* + # Print something to the log. + # @param {...{}} args The objects to log + ### + log: console.log.bind(console) + + ###* + # Print something to the error log. + # @param {...{}} args The objects to log + ### + error: console.error.bind(console) + + ###* + # Log a function call with target and arguments + # @param {string} name The name of the method + # @param {Array} args The arguments to the method call + ### + func: (name, args) -> + this.log(name, '(', [].slice.call(args), ')') + + ###* + # Log a method call with target and arguments + # @param {string} name The name of the method + # @param {{}} self The target of the method call + # @param {Array} args The arguments to the method call + ### + method: (name, self, args) -> + this.log(this.str(self), '<<', name, [].slice.call(args)) + +# coffeelint: enable=missing_fat_arrows diff --git a/omega-target/src/options.coffee b/omega-target/src/options.coffee new file mode 100644 index 0000000..111b6a0 --- /dev/null +++ b/omega-target/src/options.coffee @@ -0,0 +1,619 @@ +### @module omega-target/options ### +Promise = require 'bluebird' +Log = require './log' +Storage = require './storage' +OmegaPac = require 'omega-pac' +jsondiffpatch = require 'jsondiffpatch' + +class Options + ###* + # The entire set of options including profiles and other settings. + # @typedef OmegaOptions + # @type {object} + ### + + ###* + # All the options, in a map from key to value. + # @type OmegaOptions + ### + _options: {} + _storage: null + _state: null + _currentProfileName: null + _watchingProfiles: {} + _tempProfile: null + _tempProfileRules: {} + fallbackProfileName: 'system' + _isSystem: false + debugStr: 'Options' + + ready: null + + ProfileNotExistError: class ProfileNotExistError extends Error + constructor: (@profileName) -> + super.constructor("Profile #{@profileName} does not exist!") + + constructor: (@_options, @_storage, @_state, @log) -> + @_storage ?= Storage() + @_state ?= Storage() + @log ?= Log + if @_options? + @ready = Promise.resolve(@_options) + else + @ready = @_storage.get(null) + @ready = @ready.then((options) => + @upgrade(options).then(([options, changes]) => + modified = {} + removed = [] + for own key, value of changes + if typeof value == 'undefined' + removed.push(value) + else + modified[key] = value + @_storage.set(modified).then(=> + @_storage.remove(removed) + ).return(options) + ).catch (ex) => + @log.error(ex.stack) + @reset() + ).then((options) => + @_options = options + @_watch() + ).then(=> + if @_options['-startupProfileName'] + @applyProfile(@_options['-startupProfileName']) + else + @_state.get({ + 'currentProfileName': @fallbackProfileName + 'isSystemProfile': false + }).then (st) => + if st['isSystemProfile'] + @applyProfile('system') + else + @applyProfile(st['currentProfileName'] || @fallbackProfileName) + ).catch(ProfileNotExistError, => + @applyProfile(@fallbackProfileName) + ).then => @getAll() + + @ready.then => + if @_options['-downloadInterval'] > 0 + @updateProfile() + + toString: -> "" + + ###* + # Upgrade options from previous versions. + # For now, this method only supports schemaVersion 1 and 2. If so, it upgrades + # the options to version 2 (the latest version). Otherwise it rejects. + # It is recommended for the derived classes to call super() two times in the + # beginning and in the end of the implementation to check the schemaVersion + # and to apply future upgrades, respectively. + # Example: super(options).catch -> super(doCustomUpgrades(options), changes) + # @param {?OmegaOptions} options The legacy options to upgrade + # @param {{}={}} changes Previous pending changes to be applied. Default to + # an empty dictionary. Please provide this argument when calling super(). + # @returns {Promise<[OmegaOptions, {}]>} The new options and the changes. + ### + upgrade: (options, changes) -> + changes ?= {} + version = options?['schemaVersion'] + if version == 1 + autoDetectUsed = false + OmegaPac.Profiles.each options, (key, profile) -> + if not autoDetectUsed + refs = OmegaPac.Profiles.directReferenceSet(profile) + if refs['+auto_detect'] + autoDetectUsed = true + if autoDetectUsed + options['+auto_detect'] = OmegaPac.Profiles.create( + name: 'auto_detect' + profileType: 'PacProfile' + pacUrl: 'http://wpad/wpad.dat' + color: '#00cccc' + ) + version = changes['schemaVersion'] = options['schemaVersion'] = 2 + if version == 2 + # Current schemaVersion. + Promise.resolve([options, changes]) + else + Promise.reject new Error("Invalid schemaVerion #{version}!") + + ###* + # Reset the options to the given options or initial options. + # @param {?OmegaOptions} options The options to set. Defaults to initial. + # @returns {Promise} The options just applied + ### + reset: (options) -> + @log.method('Options#reset', this, arguments) + if not options + options = @getDefaultOptions() + if typeof options == 'string' + if options[0] != '{' + try + Buffer = require('buffer').Buffer + options = new Buffer(options, 'base64').toString('utf8') + catch + options = null + options = try JSON.parse(options) + if not options + return Promise.reject new Error('Invalid options!') + @upgrade(options).then ([opt]) => + @_storage.remove().then(=> + @_storage.set(opt) + ).then -> opt + + ###* + # Return the default options used initially and on resets. + # @returns {?OmegaOptions} The default options. + ### + getDefaultOptions: -> require('./default_options')() + + ###* + # Return all options. + # @returns {?OmegaOptions} The options. + ### + getAll: -> @_options + + ###* + # Get profile by name. + # @returns {?{}} The profile, or undefined if no such profile. + ### + profile: (name) -> OmegaPac.Profiles.byName(name, @_options) + + ###* + # Apply the patch to the current options. + # @param {jsondiffpatch} patch The patch to apply + # @returns {Promise} The updated options + ### + patch: (patch) -> + return unless patch + @log.method('Options#patch', this, arguments) + + @_options = jsondiffpatch.patch(@_options, patch) + # Only set the keys whose values have changed. + changes = {} + removed = [] + for own key, delta of patch + if delta.length == 3 and delta[1] == 0 and delta[2] == 0 + # [previousValue, 0, 0] indicates that the key was removed. + changes[key] = undefined + else + changes[key] = @_options[key] + + @_setOptions(changes) + + _setOptions: (changes, args) => + removed = [] + checkRev = args?.checkRevision ? false + currentProfileAffected = false + for own key, value of changes + if typeof value == 'undefined' + delete @_options[key] + removed.push(key) + if key == '+' + @_currentProfileName + currentProfileAffected = 'removed' + else + if checkRev and key[0] == '+' and @_options[key] + result = OmegaPac.Revision.compare(@_options[key].revision, + value.revision) + continue if result >= 0 + @_options[key] = value + if not currentProfileAffected and @_watchingProfiles[key] + currentProfileAffected = 'changed' + switch currentProfileAffected + when 'removed' + @applyProfile(@fallbackProfileName) + when 'changed' + @applyProfile(@_currentProfileName) + if args?.persist ? true + for key in removed + delete changes[key] + @_storage.set(changes).then => + @_storage.remove(removed) + return @_options + + _watch: -> + handler = (changes) => + if changes + @_setOptions(changes, {checkRev: true, persist: false}) + else + # Initial update. + changes = @_options + + refresh = changes['-refreshOnProfileChange'] + if refresh? + @_state.set({'refreshOnProfileChange': refresh}) + + if changes['-enableQuickSwitch']? or changes['-quickSwitchProfiles']? + if @_options['-enableQuickSwitch'] + profiles = @_options['-quickSwitchProfiles'] + if profiles.length >= 2 + @setQuickSwitch(profiles) + else + @setQuickSwitch(null) + else + @setQuickSwitch(null) + if changes['-downloadInterval']? + @schedule 'updateProfile', @_options['-downloadInterval'], => + @updateProfile() + + handler() + @_storage.watch null, handler + + ###* + # @callback watchCallback + # @param {Object.} changes A map from keys to values. + ### + + ###* + # Watch for any changes to the options + # @param {watchCallback} callback Called everytime the value of a key changes + # @returns {function} Calling the returned function will stop watching. + ### + watch: (callback) -> @_storage.watch null, callback + + ###* + # Get PAC script for profile. + # @param {?string|Object} profile The name of the profile, or the profile. + # @param {bool=false} compress Compress the script if true. + # @returns {String} The compiled + ### + pacForProfile: (profile, compress = false) -> + ast = OmegaPac.PacGenerator.script(@_options, profile) + if compress + ast = OmegaPac.PacGenerator.compress(ast) + Promise.resolve ast.print_to_string() + + ###* + # Apply the profile by name. + # @param {?string} name The name of the profile, or null for default. + # @param {?{}} options Some options + # @param {bool=true} options.proxy Set proxy for the applied profile if true + # @param {bool=false} options.system Whether options is in system mode. + # @param {{}=undefined} options.reason will be passed to currentProfileChanged + # @returns {Promise} A promise which is fulfilled when the profile is applied. + ### + applyProfile: (name, options) -> + @log.method('Options#applyProfile', this, arguments) + profile = OmegaPac.Profiles.byName(name, @_options) + if not profile + return Promise.reject new ProfileNotExistError(name) + + @_currentProfileName = profile.name + @_isSystem = options?.system || (profile.profileType == 'SystemProfile') + @_watchingProfiles = OmegaPac.Profiles.allReferenceSet(profile, @_options) + + if not OmegaPac.Profiles.isInclusive(profile) + results = [] + profiles = {} + OmegaPac.Profiles.each @_options, (key, profile) -> + profiles[key] = + name: profile.name + profileType: profile.profileType + color: profile.color + builtin: !!profile.builtin + if results? and OmegaPac.Profiles.isIncludable(profile) + results.push(profile.name) + if OmegaPac.Profiles.isInclusive(profile) + results = OmegaPac.Profiles.validResultProfilesFor(profile, @_options) + results = results.map (profile) -> profile.name + @_state.set({ + 'currentProfileName': @_currentProfileName + 'isSystemProfile': @_isSystem + 'availableProfiles': profiles + 'validResultProfiles': results + 'currentProfileCanAddRule': profile.rules? + }) + @currentProfileChanged(options?.reason) + if options? and options.proxy == false + return Promise.resolve() + if @_tempProfile? + if @_tempProfile.defaultProfileName != profile.name + @_tempProfile.defaultProfileName = profile.name + @_tempProfile.color = profile.color + OmegaPac.Profiles.updateRevision(@_tempProfile) + @applyProfileProxy(@_tempProfile) + else + @applyProfileProxy(profile) + + ###* + # Get the current applied profile. + # @returns {{}} The current profile + ### + currentProfile: -> + if @_currentProfileName + OmegaPac.Profiles.byName(@_currentProfileName, @_options) + else + @_externalProfile + + ###* + # Return true if in system mode. + # @returns {boolean} True if system mode is activated + ### + isSystem: -> @_isSystem + + ###* + # Set proxy settings based on the given profile. + # In base class, this method is not implemented and will always reject. + # @param {{}} profile The profile to apply + # @returns {Promise} A promise which is fulfilled when the proxy is set. + ### + applyProfileProxy: (profile) -> + Promise.reject new Error('not implemented') + + ###* + # Called when current profile has changed. + # In base class, this method is not implemented and will not do anything. + ### + currentProfileChanged: -> null + + ###* + # Set or disable the quick switch profiles. + # In base class, this method is not implemented and will not do anything. + # @param {string[]|null} quickSwitch The profile names, or null to disable + # @returns {Promise} A promise which is fulfilled when the quick switch is set + ### + setQuickSwitch: (quickSwitch) -> + Promise.resolve() + + ###* + # Schedule a task that runs every periodInMinutes. + # In base class, this method is not implemented and will not do anything. + # @param {string} name The name of the schedule. If there is a previous + # schedule with the same name, it will be replaced by the new one. + # @param {number} periodInMinutes The interval of the schedule + # @param {function} callback The callback to call when the task runs + # @returns {Promise} A promise which is fulfilled when the schedule is set + ### + schedule: (name, periodInMinutes, callback) -> + Promise.resolve() + + ###* + # Return true if the match result of current profile does not change with URLs + # @returns {bool} Whether @match always return the same result for requests + ### + isCurrentProfileStatic: -> + return true if not @_currentProfileName + return false if @_tempProfile + currentProfile = @currentProfile() + return false if OmegaPac.Profiles.isInclusive(currentProfile) + return true + + ###* + # Update the profile by name. + # @param {(string|string[]|null)} name The name of the profiles, + # or null for all. + # @param {?bool} opt_bypass_cache Do not read from the cache if true + # @returns {Promise>} A map from keys to updated + # profiles or errors. + # A value is an error if `value instanceof Error`. Otherwise the value is an + # updated profile. + ### + updateProfile: (name, opt_bypass_cache) -> + @log.method('Options#updateProfile', this, arguments) + results = {} + OmegaPac.Profiles.each @_options, (key, profile) => + return if name? and profile.name != name + url = OmegaPac.Profiles.updateUrl(profile) + if url + results[key] = @fetchUrl(url, opt_bypass_cache).then((data) => + profile = OmegaPac.Profiles.byKey(key, @_options) + OmegaPac.Profiles.update(profile, data) + changes = {} + changes[key] = profile + @_setOptions(changes).return(profile) + ).catch (reason) -> + if reason instanceof Error then reason else new Error(reason) + + Promise.props(results) + + ###* + # Make an HTTP GET request to fetch the content of the url. + # In base class, this method is not implemented and will always reject. + # @param {string} url The name of the profiles, + # @param {?bool} opt_bypass_cache Do not read from the cache if true + # @returns {Promise} The text content fetched from the url + ### + fetchUrl: (url, opt_bypass_cache) -> + Promise.reject new Error('not implemented') + + ###* + # Rename a profile and update references and options + # @param {String} fromName The original profile name + # @param {String} toname The target profile name + # @returns {Promise} The updated options + ### + renameProfile: (fromName, toName) -> + @log.method('Options#renameProfile', this, arguments) + if OmegaPac.Profiles.byName(toName, @_options) + return Promise.reject new Error("Target name #{name} already taken!") + profile = OmegaPac.Profiles.byName(fromName, @_options) + if not profile + return Promise.reject new ProfileNotExistError(name) + + profile.name = toName + changes = {} + changes[OmegaPac.Profiles.nameAsKey(profile)] = profile + + OmegaPac.Profiles.each @_options, (key, p) -> + if OmegaPac.Profiles.replaceRef(p, fromName, toName) + OmegaPac.Profiles.updateRevision(p) + changes[OmegaPac.Profiles.nameAsKey(p)] = p + + if @_options['-startupProfileName'] == fromName + changes['-startupProfileName'] = toName + quickSwitch = @_options['-quickSwitchProfiles'] + for i in [0...quickSwitch.length] + if quickSwitch[i] == fromName + quickSwitch[i] = toName + changes['-quickSwitchProfiles'] = quickSwitch + + for own key, value of changes + @_options[key] = value + + fromKey = OmegaPac.Profiles.nameAsKey(fromName) + changes[fromKey] = undefined + delete @_options[fromKey] + + if @_watchingProfiles[fromKey] + if @_currentProfileName == fromName + @_currentProfileName = toName + @applyProfile(@_currentProfileName) + + @_setOptions(changes) + + ###* + # Add a temp rule. + # @param {String} domain The domain for the temp rule. + # @param {String} profileName The profile to apply for the domain. + # @returns {Promise} A promise which is fulfilled when the rule is applied. + ### + addTempRule: (domain, profileName) -> + @log.method('Options#addTempRule', this, arguments) + return Profile.resolve() if not @_currentProfileName + profile = OmegaPac.Profiles.byName(profileName, @_options) + if not profile + return Promise.reject new ProfileNotExistError(profileName) + if not @_tempProfile? + @_tempProfile = OmegaPac.Profiles.create('', 'SwitchProfile') + currentProfile = @currentProfile() + @_tempProfile.color = currentProfile.color + @_tempProfile.defaultProfileName = currentProfile.name + + changed = false + rule = @_tempProfileRules[domain] + if rule + if rule.profileName != profileName + rule.profileName = profileName + changed = true + else + rule = + condition: + conditionType: 'HostWildcardCondition' + pattern: '*.' + domain + profileName: profileName + isTempRule: true + @_tempProfile.rules.push(rule) + @_tempProfileRules[domain] = rule + changed = true + if changed + OmegaPac.Profiles.updateRevision(@_tempProfile) + @applyProfile(@_currentProfileName) + else + Promise.resolve() + + ###* + # Find a temp rule by domain. + # @param {String} domain The domain of the temp rule. + # @returns {Promise} The profile name for the domain, or null if such + # rule does not exist. + ### + queryTempRule: (domain) -> + rule = @_tempProfileRules[domain] + if rule + rule.profileName + else + null + + ###* + # Add a condition to the current active switch profile. + # @param {Object.} cond The condition to add + # @param {string>} profileName The name of the profile to add the rule to. + # @returns {Promise} A promise which is fulfilled when the condition is saved. + ### + addCondition: (condition, profileName) -> + @log.method('Options#addCondition', this, arguments) + return Profile.resolve() if not @_currentProfileName + profile = OmegaPac.Profiles.byName(@_currentProfileName, @_options) + if not profile?.rules? + return Promise.reject new Error( + "Cannot add condition to Profile #{@profile.name} (@{profile.type})") + # Try to remove rules with the same condition first. + tag = OmegaPac.Conditions.tag(condition) + for i in [0...profile.rules.length] + if OmegaPac.Conditions.tag(profile.rules[i].condition) == tag + profile.rules.splice(i, 1) + break + + # Add the new rule to the beginning so that it won't be shadowed by others. + profile.rules.unshift({ + condition: condition + profileName: profileName + }) + OmegaPac.Profiles.updateRevision(profile) + changes = {} + changes[OmegaPac.Profiles.nameAsKey(profile)] = profile + @_setOptions(changes) + + ###* + # Add a profile to the options + # @param {{}} profile The profile to create + # @returns {Promise<{}>} The saved profile + ### + addProfile: (profile) -> + @log.method('Options#addProfile', this, arguments) + if OmegaPac.Profiles.byName(profile.name, @_options) + return Promise.reject( + new Error("Target name #{profile.name} already taken!")) + else + changes = {} + changes[OmegaPac.Profiles.nameAsKey(profile)] = profile + @_setOptions(changes) + + ###* + # Get the matching results of a request + # @param {{}} request The request to test + # @returns {Promise<{profile: {}, results: {}[]}>} The last matched profile + # and the matching details + ### + matchProfile: (request) -> + if not @_currentProfileName + return Profile.resolve({profile: @_externalProfile, results: []}) + results = [] + profile = @_tempProfile + profile ?= OmegaPac.Profiles.byName(@_currentProfileName, @_options) + while profile + lastProfile = profile + result = OmegaPac.Profiles.match(profile, request) + break unless result? + results.push(result) + if Array.isArray(result) + next = result[0] + else if result.profileName + next = OmegaPac.Profiles.nameAsKey(result.profileName) + else + break + profile = OmegaPac.Profiles.byKey(next, @_options) + Promise.resolve(profile: lastProfile, results: results) + + ###* + # Notify Options that the proxy settings are set externally. + # @param {{}} profile The external profile + # @param {?{}} args Extra arguments + # @param {boolean=false} args.noRevert If true, do not revert changes. + # @returns {Promise} A promise which is fulfilled when the profile is set + ### + setExternalProfile: (profile, args) -> + if not args?.noRevert and @_options['-revertProxyChanges'] + if profile.name != @_currentProfileName and @_currentProfileName + if not @_isSystem + @applyProfile(@_currentProfileName) + return + p = OmegaPac.Profiles.byName(profile.name, @_options) + if p + @applyProfile(p.name, + {proxy: false, system: @_isSystem, reason: 'external'}) + else + @_currentProfileName = null + @_externalProfile = profile + profile.color ?= '#49afcd' + @_state.set({ + 'currentProfileName': '' + 'externalProfile': profile + 'validResultProfiles': [] + 'currentProfileCanAddRule': false + }) + @currentProfileChanged('external') + return + +module.exports = Options diff --git a/omega-target/src/storage.coffee b/omega-target/src/storage.coffee new file mode 100644 index 0000000..b2814a0 --- /dev/null +++ b/omega-target/src/storage.coffee @@ -0,0 +1,59 @@ +### @module omega-target/storage ### +Promise = require 'bluebird' +Log = require './log' + +class Storage + ###* + # Get the requested values by keys from the storage. + # @param {(string|string[]|null|Object.)} keys The keys to retrive, + # or null for all. + # @returns {Promise<(Object.)>} A map from keys to values + ### + get: (keys) -> + Log.method('Storage#get', this, arguments) + if not keys? + keys = ['a', 'b', 'c'] + map = {} + if typeof keys == 'string' + map[keys] = 42 + else if Array.isArray(keys) + for key in keys + map[key] = 42 + else if typeof keys == 'object' + map = keys + Promise.resolve(map) + + ###* + # Set multiple values by keys in the storage. + # @param {(string|Object.)} items A map from key to value to set. + # @returns {Promise<(Object.)>} A map of key-value pairs just set. + ### + set: (items) -> + Log.method('Storage#set', this, arguments) + Promise.resolve(items) + + ###* + # Remove items by keys from the storage. + # @param {(string|string[]|null)} keys The keys to remove, or null for all. + # @returns {Promise} A promise that fulfills on successful removal. + ### + remove: (keys) -> + Log.method('Storage#remove', this, arguments) + Promise.resolve() + + ###* + # @callback watchCallback + # @param {Object.} map A map of key-value pairs just changed. + ### + + ###* + # Watch for any changes to the storage. + # @param {(string|string[]|null)} keys The keys to watch, or null for all. + # @param {watchCallback} callback Called everytime something changes. + # @returns {function} Calling the returned function will stop watching. + ### + watch: (keys, callback) -> + Log.method('Storage#watch', this, arguments) + return (-> null) + +module.exports = Storage diff --git a/omega-target/src/utils.coffee b/omega-target/src/utils.coffee new file mode 100644 index 0000000..91e2e74 --- /dev/null +++ b/omega-target/src/utils.coffee @@ -0,0 +1 @@ +exports.Promise = require('bluebird') diff --git a/omega-target/test/conditions.coffee b/omega-target/test/conditions.coffee new file mode 100644 index 0000000..c4a808e --- /dev/null +++ b/omega-target/test/conditions.coffee @@ -0,0 +1,193 @@ +chai = require 'chai' +should = chai.should() + +describe 'Conditions', -> + Conditions = require '../src/conditions' + url = require 'url' + + requestFromUri = (uri) -> + if typeof uri == 'string' + uri = url.parse uri + req = + url: url.format(uri) + host: uri.host + scheme: uri.protocol.replace(':', '') + + U2 = require 'uglify-js' + testCond = (condition, request, should_match) -> + o_request = request + should_match = !!should_match + if typeof request == 'string' + request = requestFromUri(request) + + matchResult = Conditions.match(condition, request) + condExpr = Conditions.compile(condition, request) + testFunc = new U2.AST_Function( + argnames: [ + new U2.AST_SymbolFunarg name: 'url' + new U2.AST_SymbolFunarg name: 'host' + new U2.AST_SymbolFunarg name: 'scheme' + ] + body: [ + new U2.AST_Return value: condExpr + ] + ) + testFunc = eval '(' + testFunc.print_to_string() + ')' + compileResult = testFunc(request.url, request.host, request.scheme) + + friendlyError = (compiled) -> + # Try to give friendly assert messages instead of something like + # "expect true to be false". + printCond = JSON.stringify(condition) + printCompiled = if compiled then 'COMPILED ' else '' + printMatch = if should_match then 'to match' else 'not to match' + msg = ("expect #{printCompiled}condition #{printCond} " + + "#{printMatch} request #{o_request}") + chai.assert(false, msg) + + if matchResult != should_match + friendlyError() + + if compileResult != should_match + friendlyError('compiled') + + return matchResult + + describe 'TrueCondition', -> + it 'should always return true', -> + testCond({conditionType: 'TrueCondition'}, {}, 'match') + describe 'FalseCondition', -> + it 'should always return false', -> + testCond({conditionType: 'FalseCondition'}, {}, not 'match') + describe 'UrlRegexCondition', -> + cond = + conditionType: 'UrlRegexCondition' + pattern: 'example\\.com' + it 'should match requests based on regex pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should not match requests not matching the pattern', -> + testCond(cond, 'http://www.example.net/', not 'match') + it 'should support regex meta chars', -> + con = + conditionType: 'UrlRegexCondition' + pattern: 'exam.*\\.com' + testCond(cond, 'http://www.example.com/', 'match') + describe 'UrlWildcardCondition', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '*example.com*' + it 'should match requests based on wildcard pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should not match requests not matching the pattern', -> + testCond(cond, 'http://www.example.net/', not 'match') + it 'should support wildcard question marks', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '*exam???.com*' + testCond(cond, 'http://www.example.com/', 'match') + it 'should not support regex meta chars', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '.*example.com.*' + testCond(cond, 'http://example.com/', not 'match') + it 'should support multiple patterns in one condition', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '*.example.com/*|*.example.net/*' + testCond(cond, 'http://a.example.com/abc', 'match') + testCond(cond, 'http://b.example.net/def', 'match') + testCond(cond, 'http://c.example.org/ghi', not 'match') + describe 'HostRegexCondition', -> + cond = + conditionType: 'HostRegexCondition' + pattern: '.*\\.example\\.com' + it 'should match requests based on regex pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should not match requests not matching the pattern', -> + testCond(cond, 'http://example.com/', not 'match') + it 'should not match URL parts other than the host', -> + testCond(cond, 'http://example.net/www.example.com') + .should.be.false + + describe 'HostWildcardCondition', -> + cond = + conditionType: 'HostWildcardCondition' + pattern: '*.example.com' + it 'should match requests based on wildcard pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should also match hostname without the optional level', -> + # https://github.com/FelisCatus/SwitchyOmega/wiki/Host-wildcard-condition + testCond(cond, 'http://example.com/', 'match') + it 'should allow override of the magical behavior', -> + con = + conditionType: 'HostWildcardCondition' + pattern: '**.example.com' + testCond(con, 'http://www.example.com/', 'match') + testCond(con, 'http://example.com/', not 'match') + it 'should not match URL parts other than the host', -> + testCond(cond, 'http://example.net/www.example.com') + .should.be.false + it 'should support multiple patterns in one condition', -> + cond = + conditionType: 'HostWildcardCondition' + pattern: '*.example.com|*.example.net' + testCond(cond, 'http://a.example.com/abc', 'match') + testCond(cond, 'http://example.net/def', 'match') + testCond(cond, 'http://c.example.org/ghi', not 'match') + + describe 'BypassCondition', -> + # See https://developer.chrome.com/extensions/proxy#bypass_list + it 'should correctly support patterns containing hosts', -> + cond = + conditionType: 'BypassCondition' + pattern: '.example.com' + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://example.com/', not 'match') + cond.pattern = '*.example.com' + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://example.com/', not 'match') + cond.pattern = 'example.com' + testCond(cond, 'http://example.com/', 'match') + testCond(cond, 'http://www.example.com/', not 'match') + cond.pattern = '*example.com' + testCond(cond, 'http://example.com/', 'match') + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://anotherexample.com/', 'match') + it 'should match the scheme specified in the pattern', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://example.com' + testCond(cond, 'http://example.com/', 'match') + testCond(cond, 'https://example.com/', not 'match') + it 'should match the port specified in the pattern', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://example.com:8080' + testCond(cond, 'http://example.com:8080/', 'match') + testCond(cond, 'http://example.com:888/', not 'match') + it 'should correctly support patterns using IPv4 literals', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://127.0.0.1:8080' + testCond(cond, 'http://127.0.0.1:8080/', 'match') + testCond(cond, 'http://127.0.0.2:8080/', not 'match') + # TODO(felis): Not yet supported. See the code for BypassCondition. + it.skip 'should correctly support IPv6 canonicalization', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://[0:0::1]:8080' + Conditions.analyze(cond) + cond._analyzed().url.should.equal '999' + testCond(cond, 'http://[::1]:8080/', 'match') + testCond(cond, 'http://[1::1]:8080/', not 'match') + + describe 'KeywordCondition', -> + cond = + conditionType: 'KeywordCondition' + pattern: 'example.com' + it 'should match requests based on substring', -> + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://www.example.net/', not 'match') + it 'should not match HTTPS requests', -> + testCond(cond, 'https://example.com/', not 'match') + testCond(cond, 'https://example.net/', not 'match') diff --git a/omega-target/test/pac_generator.coffee b/omega-target/test/pac_generator.coffee new file mode 100644 index 0000000..cd84f47 --- /dev/null +++ b/omega-target/test/pac_generator.coffee @@ -0,0 +1,56 @@ +chai = require 'chai' +should = chai.should() + +describe 'PacGenerator', -> + PacGenerator = require '../src/pac_generator.coffee' + + options = + '+auto': + name: 'auto' + profileType: 'SwitchProfile' + revision: 'test' + defaultProfileName: 'direct' + rules: [ + {profileName: 'proxy', condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://(www|www2)\\.example\\.com/' + } + {profileName: 'direct', condition: + conditionType: 'HostLevelsCondition' + minValue: 3 + maxValue: 8 + } + { + profileName: 'proxy' + condition: {conditionType: 'KeywordCondition', pattern: 'keyword'} + } + {profileName: 'proxy', condition: + conditionType: 'UrlWildcardCondition' + pattern: 'https://ssl.example.com/*' + } + ] + '+proxy': + name: 'proxy' + profileType: 'FixedProfile' + revision: 'test' + fallbackProxy: {scheme: 'http', host: '127.0.0.1', port: 8888} + bypassList: [ + {conditionType: 'BypassCondition', pattern: '127.0.0.1:8080'} + {conditionType: 'BypassCondition', pattern: '127.0.0.1'} + {conditionType: 'BypassCondition', pattern: ''} + ] + + it 'should generate pac scripts from options', -> + ast = PacGenerator.script(options, 'auto') + pac = ast.print_to_string(beautify: true, comments: true) + pac.should.not.be.empty + func = eval("(function () { #{pac}\n return FindProxyForURL; })()") + result = func('http://www.example.com/', 'www.example.com') + result.should.equal('PROXY 127.0.0.1:8888') + it 'should be able to compress pac scripts', -> + ast = PacGenerator.script(options, 'auto') + pac = PacGenerator.compress(ast).print_to_string() + pac.should.not.be.empty + func = eval("(function () { #{pac}\n return FindProxyForURL; })()") + result = func('http://www.example.com/', 'www.example.com') + result.should.equal('PROXY 127.0.0.1:8888') diff --git a/omega-target/test/profiles.coffee b/omega-target/test/profiles.coffee new file mode 100644 index 0000000..27cc11c --- /dev/null +++ b/omega-target/test/profiles.coffee @@ -0,0 +1,198 @@ +chai = require 'chai' +should = chai.should() + +describe 'Profiles', -> + Profiles = require '../src/profiles' + url = require 'url' + + requestFromUri = (uri) -> + if typeof uri == 'string' + uri = url.parse uri + req = + url: url.format(uri) + host: uri.host + scheme: uri.protocol.replace(':', '') + + U2 = require 'uglify-js' + testProfile = (profile, request, expected) -> + o_request = request + if typeof request == 'string' + request = requestFromUri(request) + + matchResult = Profiles.match(profile, request) + compiled = Profiles.compile(profile, request) + compileResult = eval '(' + compiled.print_to_string() + ')' + if typeof compileResult == 'function' + compileResult = compileResult(request.url, request.host, request.scheme) + + friendlyError = (compiled) -> + # Try to give friendly assert messages. + printProfile = JSON.stringify(printProfile) + printCompiled = if compiled then 'COMPILED ' else '' + printMatch = if should_match then 'to match' else 'not to match' + msg = ("expect #{printCompiled} #{printProfile} #{printMatch} " + + "request #{o_request}") + chai.assert(false, msg) + + if expected[0] == '+' and matchResult != expected + friendlyError() + + if compileResult != expected #TODO + friendlyError('compiled') + + return matchResult + + describe '#pacResult', -> + it 'should return DIRECT for no proxy', -> + Profiles.pacResult().should.equal("DIRECT") + it 'should return a valid PAC result for a proxy', -> + proxy = {scheme: "http", host: "127.0.0.1", port: 8888} + Profiles.pacResult(proxy).should.equal("PROXY 127.0.0.1:8888") + describe '#byName', -> + it 'should get profiles from builtin profiles', -> + profile = Profiles.byName('direct') + profile.should.be.an('object') + profile.profileType.should.equal('DirectProfile') + it 'should get profiles from given options', -> + profile = {} + profile = Profiles.byName('profile', {"+profile": profile}) + profile.should.equal(profile) + describe 'SystemProfile', -> + it 'should be builtin with the name "system"', -> + profile = Profiles.byName('system') + profile.should.be.an('object') + profile.profileType.should.equal('SystemProfile') + it 'should not match request to profiles', -> + profile = Profiles.byName('system') + should.not.exist Profiles.match(profile, {}) + it 'should throw when trying to compile', -> + profile = Profiles.byName('system') + should.throw(-> Profiles.compile(profile)) + describe 'DirectProfile', -> + it 'should be builtin with the name "direct"', -> + profile = Profiles.byName('direct') + profile.should.be.an('object') + profile.profileType.should.equal('DirectProfile') + it 'should return "DIRECT" when compiled', -> + profile = Profiles.byName('direct') + testProfile(profile, {}, 'DIRECT') + return + describe 'UrlWildcardCondition', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '*example.com*' + it 'should match requests based on wildcard pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should not match requests not matching the pattern', -> + testCond(cond, 'http://www.example.net/', not 'match') + it 'should support wildcard question marks', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '*exam???.com*' + testCond(cond, 'http://www.example.com/', 'match') + it 'should not support regex meta chars', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '.*example.com.*' + testCond(cond, 'http://example.com/', not 'match') + it 'should support multiple patterns in one condition', -> + cond = + conditionType: 'UrlWildcardCondition' + pattern: '*.example.com/*|*.example.net/*' + testCond(cond, 'http://a.example.com/abc', 'match') + testCond(cond, 'http://b.example.net/def', 'match') + testCond(cond, 'http://c.example.org/ghi', not 'match') + describe 'HostRegexCondition', -> + cond = + conditionType: 'HostRegexCondition' + pattern: '.*\\.example\\.com' + it 'should match requests based on regex pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should not match requests not matching the pattern', -> + testCond(cond, 'http://example.com/', not 'match') + it 'should not match URL parts other than the host', -> + testCond(cond, 'http://example.net/www.example.com') + .should.be.false + + describe 'HostWildcardCondition', -> + cond = + conditionType: 'HostWildcardCondition' + pattern: '*.example.com' + it 'should match requests based on wildcard pattern', -> + testCond(cond, 'http://www.example.com/', 'match') + it 'should also match hostname without the optional level', -> + # https://github.com/FelisCatus/SwitchyOmega/wiki/Host-wildcard-condition + testCond(cond, 'http://example.com/', 'match') + it 'should allow override of the magical behavior', -> + con = + conditionType: 'HostWildcardCondition' + pattern: '**.example.com' + testCond(con, 'http://www.example.com/', 'match') + testCond(con, 'http://example.com/', not 'match') + it 'should not match URL parts other than the host', -> + testCond(cond, 'http://example.net/www.example.com') + .should.be.false + it 'should support multiple patterns in one condition', -> + cond = + conditionType: 'HostWildcardCondition' + pattern: '*.example.com|*.example.net' + testCond(cond, 'http://a.example.com/abc', 'match') + testCond(cond, 'http://example.net/def', 'match') + testCond(cond, 'http://c.example.org/ghi', not 'match') + + describe 'BypassCondition', -> + # See https://developer.chrome.com/extensions/proxy#bypass_list + it 'should correctly support patterns containing hosts', -> + cond = + conditionType: 'BypassCondition' + pattern: '.example.com' + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://example.com/', not 'match') + cond.pattern = '*.example.com' + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://example.com/', not 'match') + cond.pattern = 'example.com' + testCond(cond, 'http://example.com/', 'match') + testCond(cond, 'http://www.example.com/', not 'match') + cond.pattern = '*example.com' + testCond(cond, 'http://example.com/', 'match') + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://anotherexample.com/', 'match') + it 'should match the scheme specified in the pattern', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://example.com' + testCond(cond, 'http://example.com/', 'match') + testCond(cond, 'https://example.com/', not 'match') + it 'should match the port specified in the pattern', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://example.com:8080' + testCond(cond, 'http://example.com:8080/', 'match') + testCond(cond, 'http://example.com:888/', not 'match') + it 'should correctly support patterns using IPv4 literals', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://127.0.0.1:8080' + testCond(cond, 'http://127.0.0.1:8080/', 'match') + testCond(cond, 'http://127.0.0.2:8080/', not 'match') + # TODO(felis): Not yet supported. See the code for BypassCondition. + it.skip 'should correctly support IPv6 canonicalization', -> + cond = + conditionType: 'BypassCondition' + pattern: 'http://[0:0::1]:8080' + Conditions.analyze(cond) + cond._analyzed().url.should.equal '999' + testCond(cond, 'http://[::1]:8080/', 'match') + testCond(cond, 'http://[1::1]:8080/', not 'match') + + describe 'KeywordCondition', -> + cond = + conditionType: 'KeywordCondition' + pattern: 'example.com' + it 'should match requests based on substring', -> + testCond(cond, 'http://www.example.com/', 'match') + testCond(cond, 'http://www.example.net/', not 'match') + it 'should not match HTTPS requests', -> + testCond(cond, 'https://example.com/', not 'match') + testCond(cond, 'https://example.net/', not 'match') diff --git a/omega-target/test/rule_list.coffee b/omega-target/test/rule_list.coffee new file mode 100644 index 0000000..99141da --- /dev/null +++ b/omega-target/test/rule_list.coffee @@ -0,0 +1,211 @@ +chai = require 'chai' +should = chai.should() + +describe 'RuleList', -> + RuleList = require '../src/rule_list' + describe 'AutoProxy', -> + parse = RuleList['AutoProxy'] + it 'should parse keyword conditions', -> + result = parse('example.com', 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + profileName: 'match' + condition: + conditionType: 'KeywordCondition' + pattern: 'example.com' + ) + it 'should parse keyword conditions with asterisks', -> + result = parse('example*.com', 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'http://*example*.com*' + ) + it 'should parse host conditions', -> + result = parse('||example.com', 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + profileName: 'match' + condition: + conditionType: 'HostWildcardCondition' + pattern: '*.example.com' + ) + it 'should parse "starts-with" conditions', -> + result = parse('|https://ssl.example.com', 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'https://ssl.example.com*' + ) + it 'should parse "starts-with" conditions for the HTTP scheme', -> + result = parse('|http://example.com', 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'http://example.com*' + ) + it 'should parse url regex conditions', -> + result = parse('/^https?:\\/\\/[^\\/]+example\.com/', 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^https?:\\/\\/[^\\/]+example\.com' + ) + it 'should ignore comment lines', -> + result = parse('!example.com', 'match', 'notmatch') + result.should.have.length(0) + it 'should parse multiple lines', -> + result = parse 'example.com\n!comment\n||example.com', 'match', 'notmatch' + result.should.have.length(2) + result[0].should.eql( + profileName: 'match' + condition: + conditionType: 'KeywordCondition' + pattern: 'example.com' + ) + result[1].should.eql( + profileName: 'match' + condition: + conditionType: 'HostWildcardCondition' + pattern: '*.example.com' + ) + it 'should put exclusive rules first', -> + result = parse 'example.com\n@@||example.com', 'match', 'notmatch' + result.should.have.length(2) + result[0].should.eql( + profileName: 'notmatch' + condition: + conditionType: 'HostWildcardCondition' + pattern: '*.example.com' + ) + result[1].should.eql( + profileName: 'match' + condition: + conditionType: 'KeywordCondition' + pattern: 'example.com' + ) + + describe 'Switchy', -> + parse = RuleList['Switchy'] + compose = (sections) -> + list = '#BEGIN\r\n\r\n' + for sec, rules of sections + list += "[#{sec}]\r\n" + for rule in rules + list += rule + list += '\r\n' + list += '\r\n\r\n#END\r\n' + it 'should parse empty rule lists', -> + list = compose {} + result = parse(list, 'match', 'notmatch') + result.should.have.length(0) + it 'should ignore stuff before #BEGIN or after #END.', -> + list = compose {} + list += '[RegExp]\r\ntest\r\n' + list = '[Wildcard]\r\ntest\r\n' + list + result = parse(list, 'match', 'notmatch') + result.should.have.length(0) + it 'should parse wildcard rules', -> + list = compose 'Wildcard': [ + '*://example.com/*' + ] + result = parse(list, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: '*://example.com/*' + ) + it 'should parse RegExp rules', -> + list = compose 'RegExp': [ + '^http://www\.example\.com/.*' + ] + result = parse(list, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://www\.example\.com/.*' + ) + it 'should parse exclusive rules', -> + list = compose 'RegExp': [ + '!^http://www\.example\.com/.*' + ] + result = parse(list, 'match', 'notmatch') + result.should.have.length(1) + result[0].should.eql( + profileName: 'notmatch' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://www\.example\.com/.*' + ) + it 'should parse multiple rules in multiple sections', -> + list = compose { + 'Wildcard': [ + 'http://www\.example\.com/*' + 'http://example\.com/*' + ] + 'RegExp': [ + '^http://www\.example\.com/.*' + '^http://example\.com/.*' + ] + } + result = parse(list, 'match', 'notmatch') + result.should.have.length(4) + result[0].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'http://www.example.com/*' + ) + result[1].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'http://example.com/*' + ) + result[2].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://www\.example\.com/.*' + ) + result[3].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://example\.com/.*' + ) + it 'should put exclusive rules first', -> + list = compose { + 'Wildcard': [ + 'http://www\.example\.com/*' + ] + 'RegExp': [ + '!^http://www\.example\.com/.*' + ] + } + result = parse(list, 'match', 'notmatch') + result.should.have.length(2) + result[0].should.eql( + profileName: 'notmatch' + condition: + conditionType: 'UrlRegexCondition' + pattern: '^http://www.example\.com/.*' + ) + result[1].should.eql( + profileName: 'match' + condition: + conditionType: 'UrlWildcardCondition' + pattern: 'http://www.example.com/*' + ) diff --git a/omega-target/test/shexp_utils.coffee b/omega-target/test/shexp_utils.coffee new file mode 100644 index 0000000..56452cc --- /dev/null +++ b/omega-target/test/shexp_utils.coffee @@ -0,0 +1,15 @@ +chai = require 'chai' +should = chai.should() + +describe 'ShexpUtils', -> + ShexpUtils = require '../src/shexp_utils' + describe '#escapeSlash', -> + it 'should escape all forward slashes', -> + regex = ShexpUtils.escapeSlash '/test/' + regex.should.equal '\\/test\\/' + it 'should not escape slashes that are already escaped', -> + regex = ShexpUtils.escapeSlash '\\/test\\/' + regex.should.equal '\\/test\\/' + it 'should know the difference between escaped and unescaped slashes', -> + regex = ShexpUtils.escapeSlash '\\\\/\\/test\\/' + regex.should.equal '\\\\\\/\\/test\\/' diff --git a/omega-web/.gitignore b/omega-web/.gitignore new file mode 100644 index 0000000..3d16579 --- /dev/null +++ b/omega-web/.gitignore @@ -0,0 +1,2 @@ +/build +/tmp diff --git a/omega-web/Gruntfile.coffee b/omega-web/Gruntfile.coffee new file mode 100644 index 0000000..522b59e --- /dev/null +++ b/omega-web/Gruntfile.coffee @@ -0,0 +1 @@ +module.exports = require('load-grunt-config') diff --git a/omega-web/bower.json b/omega-web/bower.json new file mode 100644 index 0000000..2e12ea8 --- /dev/null +++ b/omega-web/bower.json @@ -0,0 +1,107 @@ +{ + "name": "switchyomega", + "version": "0.0.1", + "authors": [ + "FelisCatus " + ], + "description": "The ultimate proxy switcher.", + "keywords": [ + "proxy", + "pac", + "extension" + ], + "license": "GPL", + "homepage": "https://github.com/FelisCatus/SwitchyOmega", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "angular": "~1.2.16", + "angular-bootstrap": "~0.11.0", + "angular-animate": "~1.2.16", + "angular-ui-router": "~0.2.10", + "angular-loader": "~1.2.16", + "angular-i18n": "~1.2.16", + "bootstrap": "~3.1.1", + "script.js": "~2.5.3", + "ngprogress": "~1.0.4", + "angular-ui-sortable": "~0.12.6", + "jsondiffpatch": "~0.1.7", + "angular-spectrum-colorpicker": "~1.0.13", + "blob": "*", + "FileSaver": "*", + "angular-ui-utils": "bower-validate", + "angular-ladda": "~0.1.6" + }, + "exportsOverride": { + "script.js": { + "": "dist/*.min.js" + }, + "jquery": { + "": "dist/jquery.min.js" + }, + "jquery-ui": { + "": [] + }, + "angular": { + "": "*.js" + }, + "angular-animate": { + "": "*.min.*" + }, + "angular-bootstrap": { + "": "*.min.js" + }, + "angular-i18n": { + "": [ + "angular-locale_en-us.js", + "angular-locale_zh-cn.js" + ] + }, + "angular-loader": { + "": "*.min.js" + }, + "angular-spectrum-colorpicker": { + "": "dist/*.min.js" + }, + "angular-ui-router": { + "": "release/*.js" + }, + "angular-ui-sortable": { + "": "*.js" + }, + "angular-ui-utils": { + "": "*.js" + }, + "bootstrap": { + "css": "dist/css/*.min.*", + "fonts": "dist/fonts/*" + }, + "ngprogress": { + "": "build/*.min.js" + }, + "jsondiffpatch": { + "": "build/*.min.js" + }, + "spectrum": { + "": [ + "*.js", + "*.css" + ] + }, + "FileSaver": { + "": "*.js" + }, + "blob": { + "": "*.js" + }, + "ladda": { + "": ["dist/ladda-themeless.min.css", "dist/ladda.min.js"] + } + } +} diff --git a/omega-web/grunt/aliases.coffee b/omega-web/grunt/aliases.coffee new file mode 100644 index 0000000..6b30259 --- /dev/null +++ b/omega-web/grunt/aliases.coffee @@ -0,0 +1,12 @@ +module.exports = + default: [ + 'copy' + 'jade' + 'less' + 'autoprefixer' + 'coffeelint' + 'coffee' + 'ngAnnotate' + 'bower' + ] + test: ['mochaTest'] diff --git a/omega-web/grunt/autoprefixer.coffee b/omega-web/grunt/autoprefixer.coffee new file mode 100644 index 0000000..6c7a41f --- /dev/null +++ b/omega-web/grunt/autoprefixer.coffee @@ -0,0 +1,8 @@ +module.exports = + options: + map: true + unprefixed: + expand: true + cwd: 'tmp/css' + src: '**/*.css' + dest: 'build/css/' diff --git a/omega-web/grunt/bower.coffee b/omega-web/grunt/bower.coffee new file mode 100644 index 0000000..b8d4feb --- /dev/null +++ b/omega-web/grunt/bower.coffee @@ -0,0 +1,5 @@ +module.exports = + install: + options: + targetDir: 'build/lib/' + layout: 'byComponent' diff --git a/omega-web/grunt/coffee.coffee b/omega-web/grunt/coffee.coffee new file mode 100644 index 0000000..33d30df --- /dev/null +++ b/omega-web/grunt/coffee.coffee @@ -0,0 +1,10 @@ +module.exports = + web: + expand: true + cwd: 'src/coffee' + src: ['**/*.coffee'] + dest: 'build/js/' + ext: '.js' + web_omega: + files: + 'build/js/omega.js': 'src/omega/**/*.coffee' diff --git a/omega-web/grunt/coffeelint.coffee b/omega-web/grunt/coffeelint.coffee new file mode 100644 index 0000000..47ce66b --- /dev/null +++ b/omega-web/grunt/coffeelint.coffee @@ -0,0 +1,20 @@ +module.exports = + options: + arrow_spacing: level: 'error' + colon_assignment_spacing: + level: 'error' + spacing: + left: 0 + right: 1 + line_endings: level: 'error' + missing_fat_arrows: level: 'warn' + newlines_after_classes: level: 'error' + no_empty_functions: level: 'error' + no_empty_param_list: level: 'error' + no_interpolation_in_single_quotes: level: 'error' + no_stand_alone_at: level: 'error' + space_operators: level: 'error' + + gruntfile: ['Gruntfile.coffee'] + tasks: ['grunt/**/*.coffee'] + src: ['src/**/*.coffee'] diff --git a/omega-web/grunt/copy.coffee b/omega-web/grunt/copy.coffee new file mode 100644 index 0000000..ba0cd18 --- /dev/null +++ b/omega-web/grunt/copy.coffee @@ -0,0 +1,14 @@ +module.exports = + pac: + files: + 'build/js/omega_pac.min.js': 'node_modules/omega-pac/omega_pac.min.js' + lib: + expand: true + cwd: 'lib' + src: ['**/*'] + dest: 'build/lib/' + img: + expand: true + cwd: 'img' + src: ['**/*'] + dest: 'build/img/' diff --git a/omega-web/grunt/jade.coffee b/omega-web/grunt/jade.coffee new file mode 100644 index 0000000..0c9483e --- /dev/null +++ b/omega-web/grunt/jade.coffee @@ -0,0 +1,18 @@ +module.exports = + web: + files: [ + { + expand: true + dest: 'build/' + cwd: 'src/' + ext: '.html' + src: '*.jade' + } + { + expand: true + dest: 'build/partials/' + cwd: 'src/partials' + ext: '.html' + src: ['*.jade'] + } + ] diff --git a/omega-web/grunt/less.coffee b/omega-web/grunt/less.coffee new file mode 100644 index 0000000..df7b5eb --- /dev/null +++ b/omega-web/grunt/less.coffee @@ -0,0 +1,7 @@ +module.exports = + web_options: + files: + 'tmp/css/options.css': 'src/less/options.less' + web_popup: + files: + 'tmp/css/popup.css': 'src/less/popup.less' diff --git a/omega-web/grunt/mochaTest.coffee b/omega-web/grunt/mochaTest.coffee new file mode 100644 index 0000000..0bb9539 --- /dev/null +++ b/omega-web/grunt/mochaTest.coffee @@ -0,0 +1,6 @@ +module.exports = + test: + options: + reporter: 'spec' + require: 'coffee-script/register' + src: ['test/**/*.coffee'] diff --git a/omega-web/grunt/ngAnnotate.coffee b/omega-web/grunt/ngAnnotate.coffee new file mode 100644 index 0000000..ab30d33 --- /dev/null +++ b/omega-web/grunt/ngAnnotate.coffee @@ -0,0 +1,6 @@ +module.exports = + options: + singleQuotes: true + app: + files: + 'build/js/omega.ngmin.js': 'build/js/omega.js' diff --git a/omega-web/grunt/watch.coffee b/omega-web/grunt/watch.coffee new file mode 100644 index 0000000..532f26f --- /dev/null +++ b/omega-web/grunt/watch.coffee @@ -0,0 +1,35 @@ +module.exports = + grunt: + options: + reload: true + files: + 'grunt/*' + tasks: ['coffeelint:tasks', 'default'] + copy_pac: + files: + 'node_modules/omega-pac/omega_pac.min.js' + tasks: 'copy:pac' + copy_lib: + files: + 'lib/**/*' + tasks: 'copy:lib' + copy_img: + files: + 'img/**/*' + tasks: 'copy:img' + jade: + files: ['src/**/*.jade'] + tasks: 'jade' + less: + files: + 'src/less/**/*.less' + tasks: ['less', 'autoprefixer'] + coffeelint: + files: 'src/**/*.coffee' + tasks: ['coffeelint'] + coffee: + files: [ + 'src/coffee/**/*.coffee' + 'src/omega/**/*.coffee' + ] + tasks: ['coffee'] diff --git a/omega-web/img/icons/omega-128.png b/omega-web/img/icons/omega-128.png new file mode 100644 index 0000000000000000000000000000000000000000..fbf53064cf432c41a1d8546447bd3185602828ab GIT binary patch literal 2818 zcmV+d3;pzoP)Tz}0w(o~+NLJ;i$E}KlNt%AA8cZ68op@ck5LFy1tT>I#VW+xPKap*B!Pmo){xPb zf3?9|`eEjV6TIh~-9582d(PP>*+9tIdEa;6AG5o&Gw+N#bLI>+{mbao4%DQj7C@g< z3!u-b1<>cz0_byU0rWZNrFI}DQrioF@xX-*2f&Gj6^14wNz1Ev7i z0FxVzD}XT>n;8P$1NJo>dx1ShiT6YMoRY2=fYf#(FuURSO35Y<0^5fVqr~Bo?UhPb z3P5T*9as#^qx^)o;~=mFSZkEnmDrcubd3O{wo`CPUkF^8*qvq^0M_HguqU-MS*WxC zq_(4hTY#m&w8ZY@We`{eY%)sxFSRqxsH6a-w&w$jfMvLcxo&1Juo75jlsKK*nRBU> z0Q~>>yMg7v#Ki7|<}k1VSY4$ap)vxH+Rg-?00vU~QZl=N`-~EAq;>=&WaD6uo9(WtiosqGSA2X*pCViK^!U!!P5=>ep+V}Re_Uh!($ zvl{rZQDUfUL!}EqYC9Hq4mWOJlbtQV4~!D0$~01{0Hn5`#|6@D!?;yp5SV9_I9is0 z(gYy2y%cyIxT+Kb{lk7>mQmt^G8!-CAy8_25%4l~@CP-sFNR$ali(D?K>jG zGr+HbYXh`XlaFh0*S;e-)f5&aNe_5?A7Kv8>3+g?a#~?e)N)sovt!t-#~JHlxIULpCh6{R}V% zxF08mD)9+0)hO|H-V=)o;5FREs0wca%Z(C$h+#}>`%T;w?;B&-%g8pP#JBRElv@B& z+i&2$@>PMifd`Een^PE<+Rg_a2Ch$Ge*|V3CH|P_gd(qjKQC}jXohgh#RgI(AHdrl z@V=BjdnW_AbCatF@T+p8PFfs+k8qPG3%~l5fqx}l4_utsl^JB@`2gO>+i^apB6F@X^#Fd=?jhjv4xB2=yTEj##77Bi7e;D(F|Z4` zI)V8kz@#jzkhAsx-zH7+KLqA?Tk-+C&+~CH#O88b&f1by0CyF3r8uX7Ta6O?6W9)d z_x)B}EU~#O%a7XL0{jr@hrrm*eagi$qr|GnHcLTjyA)R?BqpbTOWF;IXXycMB!2S$ zh8xrG6&}aM6qB)J7^==9fK2z2z$_2_F()NHmgNadx6NG@1@I@}*~m6y@GLI2n6%AZ z%Vz+o?F8KYKjx%}*|EOr#m0X{+#b>75binPShF9r>j7pFH~HIoHTeKOwwMD%V{kca zN;?6>{>-@{vb_|?cc(eE0Lo#4&*g};>H%Wb;Xh`ScsHWGr1)5yzM7Rl(_FR^ zKq(a|n$Yw2{t<{7bJJY55K>q6n&z^V01lSmNHZo#ZBwkENrI0x=4*D-Ts9Se-_9)NZQz!Owo(%_)?=-OT;9`qO+uT)A001j|U4ov#$Fd@U=^z4l6IW~?CSS(Y&-pWiANo9HLfQ} z@xQ1L-q($!sSyb9OqLpfkZ)4x(*>Og!5rXuUxu~Pcpsl9?S2ZGbCp>DzV_@(h1?0l z0$`Wa_NxhfXo>f27isGRzm&C3Fyx;Wp5ke?k*@)RzBGHK@IDRVd`wBM92MCyIgdz(=ep%q0(2N0A0lWRSElKdU zcY9w_Cm#yrrrtEF0DZk_6x7?s(eK;F0Z46E;xBI1*r$Q6k>Pkx*|P zTOm5yI=0?Cy8G#9^XQJ2hkL^*M+p0!r#sz2Eku2Qp8)R!Xs0G0@8GU|M|3Ozqr^$v zOxKg0wpEjvlfZ5M+IK{d3L!>`J-|I7+N#OKJ^q?^Ot2C$Mu~O6lcCzHY08uSS_dI? zji8?bF9vS2<}5Gbu5mCrdJP;2sqF>0L71C@He9m<< z-XA^O4cupxcq6qVP@xSY`~{f`{0KOl*qt&sjEik*Rgw=C>H!+@t7{hl%ZR&sX&LMV zRs!pc5~ou;b1sz>z%WwV(YUgzOMz*L-O0-!ZpF~1tV^|1P-y`)BDI|YECv=*-KppY zaAO>6L(SMJo30T+BU0PxI5Es4?iRNraS%6dzt$+RE3q%R=}G}KBDI}}``|Qulr4yZ zxVaA7f$c_#u6|(cdI6k^-zj?v?%{EA!*K=KSNRM9?*aQ7j=jL1R<-xLk)9JkEBth| z@whd9!w0yvgu}=2(9^D}OkWA0=F?PWAgCq{wE+5@S^#}cEr33!7C@g<3!u;WKbG`z Ugg>GY^Z)<=07*qoM6N<$f-4#@+5i9m literal 0 HcmV?d00001 diff --git a/omega-web/img/icons/omega-16.png b/omega-web/img/icons/omega-16.png new file mode 100644 index 0000000000000000000000000000000000000000..f121c50c6b8e1a199c3f1801845bc43317d1277a GIT binary patch literal 444 zcmV;t0YmYxBa+!!Wlo zc4{i@0d6FBMS1>m#?9Bm?7;Sm-9gb0Peh~%Y%Vx(q^h;R%iR1aP`v**6XkN1wpBO} zD(O~Spm_JbEsl8sQjuo@T&ZY#YLYT=3YaMWspH9RaJ2O{-GL()7W|FsLD$Q4tV3^I zKfFfd5m*OO5p!$+1lj+cNg8u709`NB5tT;lix^Yz2U#F*dD~0Iw{!V|KggPe))m9_ zyxb7zTUvRE-_G@WUT$ar6n&bhDq45_uryD)epph`dhCms#MhYkA;n7!D(RN0R)NtH mcBHD+861ybN21U4pZEbNmz|}bZ_*|J0000kwg&^O+D z@BjPf<-2>|iK=p(VO7!`8l^TgN^KdYL#xoF{%sc{TsscFMz0TCOR`Do0fnY2%j9_dkE0mE$#2@uE&>2hL z5s}fV!t)+f2a}P&n9)G!ZRVm=!vf&*(8>Y}ip&8BdKqX3>OA^#))wvYTmH}a2m3%y z4Z|Mr4OE7*xu)lz`p6b0E{#)H=Ucsx=wqPX(C^b4o&b^jgWU;qPGq{oV#WoYa8&!} zBY{Pi8aiU>HcR9U5H!`OLwB;*KMRF#fu#d3x@$g>uooO)J`z~;iG+bQV^INX*MK0s zW?8*E)6=x#1<%J!Pt%Hu-ZSM3S)!_R+H2>8t#5|sgizV)F1>l?_5{@W zVOG(uvJJTP?l3KYWVm4oST>Zl2JLkJQPG`~=>ZIH0IB6GJ@k-$H#slIrJe)Mgn6fBh{xfau8nB06-$*k0L|?rwV8XAzJJ{2>!oJ+00000 LNkvXXu0mjfAOd$K literal 0 HcmV?d00001 diff --git a/omega-web/img/icons/omega-48.png b/omega-web/img/icons/omega-48.png new file mode 100644 index 0000000000000000000000000000000000000000..f4a689df5a7475a477ab97cdb65cb53a59f57882 GIT binary patch literal 1262 zcmVxTKBXeJ7#xnK48F=s9bAEsBdw1?VPgIpgmi3?ks#RSLgeDi-LX!(^^{gSa zc!c^Ues6U$O$P-zjObGcT_|0^qi)Dw2=gfOsQjYnW!t}cH8JqS{c3A(+rXIs1=<@eVQB$zOz-pmNO8Jr%v+V-~mp! z3}sbz2KL9(FR17QVAq4pgB(#An2H3hl)72cKq$Vj(-z;)Kzo(Ne|3NNRGH_FquHlt zBb|$dH+-d{ZTugAZU_Dj zq8aGywad!#8wTsItB;a{!7W#bN(zsqE&%TuhHK8p zlQRRs+iMp#&44((j48Y@*PK~CV+vYM&6(vh2y=$v;xevlvjHVym?(?r@$1pu%ZBk~ zbv?R!8PVfF)+D}=aos>)?9X=KfKgT9eDX*~!UN})N*?J*Ae=W0Kj5+gc?Q()>;?QL z8Pu^Rydl;!r{84-@(oz*GECs)9$B670%syOr_rG%pLOFMaOg6sq|D78>b&QXucBrT zb>2e_U*Vd6QXgDP^ERwP8S+HypFSJ}4e?=do@Wuni3e-Sz18geDi- z5W^;cUBZCmxLrEBc)0ai%5rlW?ZNU{Hwu4wyBHlUF3GmCuurK;=U{G*(-sc_vkp9$_u2ZvYK0ED%@#%m-#=Pa6SlDcp46CNPM|h3Z+HxwJNT zZzAiP5CfV{|NVkKbek2Lfz?2@v8gn0UZ7X1oZh~5&-~_Fky(5-ps8nQg{yP{Z~4|} zF5fxA-oBRFAN*?27X!jQsaHg`OQF?=n#M&SrXqV2Ey175ZmS#vUhVy>zS<3Z25e#S z?VN_Od11^s9wqLt7-kg^G&@ z5Ee>+3*p$%7E`#F9O;sFxt=iiC9hB`EzUIZL0vxeDOEk!BX1D5sPS?;(_91vsD z2cYjKT0_3vIl{5zCXlbN{~+o*ZsbaHU4Q<8qCG%dXD4>?_XJ(#mw13gYv`Dus{rfE z5y@&Qpum8pLsSc_GbYN2$cA`pC~k}&m3V6?E+QMC^l9#Y5m?uBsPM&1fdN{ddJ~vu zOCr1CEx~h^*iwkM1kVYw%NRZ{t1X8D16-xenBZz+Aoztbwq&w7u3BP?Y0EJ{I&lPT zu{16!yR{wH66*zR#}$>`mRLbsq%&7@&L7Jf?<@uCy*EsMJQ_S}Dat}T8ayjda78bH z`mDw$Nf6+0kj=LZGzP%bnEQM7j&u)osqjge1w5t_ zi=&A0X*^o%eQ^|~$MX1?E?;X7-FJj2!m$6fO)$c+BSgm%4<&T7zLwe_aCNJHl}!?^ zZY^a?ZAs%S5e*#|w6pxGCI>+~OW7X;9`^}`ds4@y{7x~E>Lw@QMM&oYmayio!KmHq`$59aypQS0Ec1X zSA~n<0*BLqOvEZ4W + + + diff --git a/omega-web/img/icons/omega_svg.js b/omega-web/img/icons/omega_svg.js new file mode 100644 index 0000000..3488566 --- /dev/null +++ b/omega-web/img/icons/omega_svg.js @@ -0,0 +1,37 @@ +var drawOmega = function (ctx, outerCircleColor, innerCircleColor) { + ctx.clearRect(0,0,19,19) + + if (innerCircleColor != null) { + ctx.save(); + ctx.fillStyle = innerCircleColor; + ctx.beginPath(); + ctx.moveTo(14.05,9.50); + ctx.bezierCurveTo(14.05,11.95,12.01,13.94,9.50,13.94); + ctx.bezierCurveTo(6.99,13.94,4.95,11.95,4.95,9.50); + ctx.bezierCurveTo(4.95,7.05,6.99,5.06,9.50,5.06); + ctx.bezierCurveTo(12.01,5.06,14.05,7.05,14.05,9.50); + ctx.closePath(); + ctx.fill('evenodd'); + ctx.restore(); + } + + ctx.save(); + ctx.fillStyle = outerCircleColor; + ctx.beginPath(); + ctx.moveTo(14.05,9.50); + ctx.bezierCurveTo(14.05,11.95,12.01,13.94,9.50,13.94); + ctx.bezierCurveTo(6.99,13.94,4.95,11.95,4.95,9.50); + ctx.bezierCurveTo(4.95,7.05,6.99,5.06,9.50,5.06); + ctx.bezierCurveTo(12.01,5.06,14.05,7.05,14.05,9.50); + ctx.closePath(); + ctx.moveTo(18.04,9.50); + ctx.bezierCurveTo(18.04,14.11,14.22,17.85,9.50,17.85); + ctx.bezierCurveTo(4.78,17.85,0.96,14.11,0.96,9.50); + ctx.bezierCurveTo(0.96,4.89,4.78,1.15,9.50,1.15); + ctx.bezierCurveTo(14.22,1.15,18.04,4.89,18.04,9.50); + ctx.closePath(); + ctx.fill('evenodd'); + ctx.restore(); + ctx.save(); + ctx.fillStyle = outerCircleColor; +}; diff --git a/omega-web/lib/jquery-ui-1.10.4.custom.min.js b/omega-web/lib/jquery-ui-1.10.4.custom.min.js new file mode 100644 index 0000000..4553312 --- /dev/null +++ b/omega-web/lib/jquery-ui-1.10.4.custom.min.js @@ -0,0 +1,6 @@ +/*! jQuery UI - v1.10.4 - 2014-05-25 +* http://jqueryui.com +* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.sortable.js +* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ + +(function(t,e){function i(e,i){var s,o,a,u=e.nodeName.toLowerCase();return"area"===u?(s=e.parentNode,o=s.name,e.href&&o&&"map"===s.nodeName.toLowerCase()?(a=t("img[usemap=#"+o+"]")[0],!!a&&n(a)):!1):(/input|select|textarea|button|object/.test(u)?!e.disabled:"a"===u?e.href||i:i)&&n(e)}function n(e){return t.expr.filters.visible(e)&&!t(e).parents().addBack().filter(function(){return"hidden"===t.css(this,"visibility")}).length}var s=0,o=/^ui-id-\d+$/;t.ui=t.ui||{},t.extend(t.ui,{version:"1.10.4",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),t.fn.extend({focus:function(e){return function(i,n){return"number"==typeof i?this.each(function(){var e=this;setTimeout(function(){t(e).focus(),n&&n.call(e)},i)}):e.apply(this,arguments)}}(t.fn.focus),scrollParent:function(){var e;return e=t.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(t.css(this,"position"))&&/(auto|scroll)/.test(t.css(this,"overflow")+t.css(this,"overflow-y")+t.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(t.css(this,"overflow")+t.css(this,"overflow-y")+t.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!e.length?t(document):e},zIndex:function(i){if(i!==e)return this.css("zIndex",i);if(this.length)for(var n,s,o=t(this[0]);o.length&&o[0]!==document;){if(n=o.css("position"),("absolute"===n||"relative"===n||"fixed"===n)&&(s=parseInt(o.css("zIndex"),10),!isNaN(s)&&0!==s))return s;o=o.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++s)})},removeUniqueId:function(){return this.each(function(){o.test(this.id)&&t(this).removeAttr("id")})}}),t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(i){return!!t.data(i,e)}}):function(e,i,n){return!!t.data(e,n[3])},focusable:function(e){return i(e,!isNaN(t.attr(e,"tabindex")))},tabbable:function(e){var n=t.attr(e,"tabindex"),s=isNaN(n);return(s||n>=0)&&i(e,!s)}}),t("").outerWidth(1).jquery||t.each(["Width","Height"],function(i,n){function s(e,i,n,s){return t.each(o,function(){i-=parseFloat(t.css(e,"padding"+this))||0,n&&(i-=parseFloat(t.css(e,"border"+this+"Width"))||0),s&&(i-=parseFloat(t.css(e,"margin"+this))||0)}),i}var o="Width"===n?["Left","Right"]:["Top","Bottom"],a=n.toLowerCase(),u={innerWidth:t.fn.innerWidth,innerHeight:t.fn.innerHeight,outerWidth:t.fn.outerWidth,outerHeight:t.fn.outerHeight};t.fn["inner"+n]=function(i){return i===e?u["inner"+n].call(this):this.each(function(){t(this).css(a,s(this,i)+"px")})},t.fn["outer"+n]=function(e,i){return"number"!=typeof e?u["outer"+n].call(this,e):this.each(function(){t(this).css(a,s(this,e,!0,i)+"px")})}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t("").data("a-b","a").removeData("a-b").data("a-b")&&(t.fn.removeData=function(e){return function(i){return arguments.length?e.call(this,t.camelCase(i)):e.call(this)}}(t.fn.removeData)),t.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),t.support.selectstart="onselectstart"in document.createElement("div"),t.fn.extend({disableSelection:function(){return this.bind((t.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(t){t.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),t.extend(t.ui,{plugin:{add:function(e,i,n){var s,o=t.ui[e].prototype;for(s in n)o.plugins[s]=o.plugins[s]||[],o.plugins[s].push([i,n[s]])},call:function(t,e,i){var n,s=t.plugins[e];if(s&&t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType)for(n=0;s.length>n;n++)t.options[s[n][0]]&&s[n][1].apply(t.element,i)}},hasScroll:function(e,i){if("hidden"===t(e).css("overflow"))return!1;var n=i&&"left"===i?"scrollLeft":"scrollTop",s=!1;return e[n]>0?!0:(e[n]=1,s=e[n]>0,e[n]=0,s)}})})(jQuery);(function(t,e){var i=0,s=Array.prototype.slice,n=t.cleanData;t.cleanData=function(e){for(var i,s=0;null!=(i=e[s]);s++)try{t(i).triggerHandler("remove")}catch(o){}n(e)},t.widget=function(i,s,n){var o,a,r,l,h={},u=i.split(".")[0];i=i.split(".")[1],o=u+"-"+i,n||(n=s,s=t.Widget),t.expr[":"][o.toLowerCase()]=function(e){return!!t.data(e,o)},t[u]=t[u]||{},a=t[u][i],r=t[u][i]=function(t,i){return this._createWidget?(arguments.length&&this._createWidget(t,i),e):new r(t,i)},t.extend(r,a,{version:n.version,_proto:t.extend({},n),_childConstructors:[]}),l=new s,l.options=t.widget.extend({},l.options),t.each(n,function(i,n){return t.isFunction(n)?(h[i]=function(){var t=function(){return s.prototype[i].apply(this,arguments)},e=function(t){return s.prototype[i].apply(this,t)};return function(){var i,s=this._super,o=this._superApply;return this._super=t,this._superApply=e,i=n.apply(this,arguments),this._super=s,this._superApply=o,i}}(),e):(h[i]=n,e)}),r.prototype=t.widget.extend(l,{widgetEventPrefix:a?l.widgetEventPrefix||i:i},h,{constructor:r,namespace:u,widgetName:i,widgetFullName:o}),a?(t.each(a._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,r,i._proto)}),delete a._childConstructors):s._childConstructors.push(r),t.widget.bridge(i,r)},t.widget.extend=function(i){for(var n,o,a=s.call(arguments,1),r=0,l=a.length;l>r;r++)for(n in a[r])o=a[r][n],a[r].hasOwnProperty(n)&&o!==e&&(i[n]=t.isPlainObject(o)?t.isPlainObject(i[n])?t.widget.extend({},i[n],o):t.widget.extend({},o):o);return i},t.widget.bridge=function(i,n){var o=n.prototype.widgetFullName||i;t.fn[i]=function(a){var r="string"==typeof a,l=s.call(arguments,1),h=this;return a=!r&&l.length?t.widget.extend.apply(null,[a].concat(l)):a,r?this.each(function(){var s,n=t.data(this,o);return n?t.isFunction(n[a])&&"_"!==a.charAt(0)?(s=n[a].apply(n,l),s!==n&&s!==e?(h=s&&s.jquery?h.pushStack(s.get()):s,!1):e):t.error("no such method '"+a+"' for "+i+" widget instance"):t.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var e=t.data(this,o);e?e.option(a||{})._init():t.data(this,o,new n(a,this))}),h}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this.bindings=t(),this.hoverable=t(),this.focusable=t(),s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(i,s){var n,o,a,r=i;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof i)if(r={},n=i.split("."),i=n.shift(),n.length){for(o=r[i]=t.widget.extend({},this.options[i]),a=0;n.length-1>a;a++)o[n[a]]=o[n[a]]||{},o=o[n[a]];if(i=n.pop(),1===arguments.length)return o[i]===e?null:o[i];o[i]=s}else{if(1===arguments.length)return this.options[i]===e?null:this.options[i];r[i]=s}return this._setOptions(r),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!e).attr("aria-disabled",e),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var o,a=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=o=t(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,o=this.widget()),t.each(n,function(n,r){function l(){return i||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof r?a[r]:r).apply(a,arguments):e}"string"!=typeof r&&(l.guid=r.guid=r.guid||l.guid||t.guid++);var h=n.match(/^(\w+)\s*(.*)$/),u=h[1]+a.eventNamespace,p=h[2];p?o.delegate(p,u,l):s.bind(u,l)})},_off:function(t,e){e=(e||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(e).undelegate(e)},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}})})(jQuery);(function(t){var e=!1;t(document).mouseup(function(){e=!1}),t.widget("ui.mouse",{version:"1.10.4",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.bind("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).bind("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):undefined}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(i){if(!e){this._mouseStarted&&this._mouseUp(i),this._mouseDownEvent=i;var s=this,n=1===i.which,o="string"==typeof this.options.cancel&&i.target.nodeName?t(i.target).closest(this.options.cancel).length:!1;return n&&!o&&this._mouseCapture(i)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){s.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(i)&&this._mouseDelayMet(i)&&(this._mouseStarted=this._mouseStart(i)!==!1,!this._mouseStarted)?(i.preventDefault(),!0):(!0===t.data(i.target,this.widgetName+".preventClickEvent")&&t.removeData(i.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return s._mouseMove(t)},this._mouseUpDelegate=function(t){return s._mouseUp(t)},t(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),i.preventDefault(),e=!0,!0)):!0}},_mouseMove:function(e){return t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button?this._mouseUp(e):this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){return t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),!1},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}})})(jQuery);(function(t){function e(t,e,i){return t>e&&e+i>t}function i(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))}t.widget("ui.sortable",t.ui.mouse,{version:"1.10.4",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_create:function(){var t=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?"x"===t.axis||i(this.items[0].item):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},_destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_setOption:function(e,i){"disabled"===e?(this.options[e]=i,this.widget().toggleClass("ui-sortable-disabled",!!i)):t.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(e,i){var s=null,n=!1,o=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,o.widgetName+"-item")===o?(s=t(this),!1):undefined}),t.data(e.target,o.widgetName+"-item")===o&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,o,a=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,a.cursorAt&&this._adjustOffsetFromHelper(a.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),a.containment&&this._setContainment(),a.cursor&&"auto"!==a.cursor&&(o=this.document.find("body"),this.storedCursor=o.css("cursor"),o.css("cursor",a.cursor),this.storedStylesheet=t("").appendTo(o)),a.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",a.opacity)),a.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",a.zIndex)),this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!a.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,o,a=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==document&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY=0;i--)if(s=this.items[i],n=s.item[0],o=this._intersectsWithPointer(s),o&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===o?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===o?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),o=this.options.axis,a={};o&&"x"!==o||(a.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollLeft)),o&&"y"!==o||(a.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===document.body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(a,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,o=t.left,a=o+t.width,r=t.top,h=r+t.height,l=this.offset.click.top,c=this.offset.click.left,u="x"===this.options.axis||s+l>r&&h>s+l,d="y"===this.options.axis||e+c>o&&a>e+c,p=u&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>o&&a>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&h>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var i="x"===this.options.axis||e(this.positionAbs.top+this.offset.click.top,t.top,t.height),s="y"===this.options.axis||e(this.positionAbs.left+this.offset.click.left,t.left,t.width),n=i&&s,o=this._getDragVerticalDirection(),a=this._getDragHorizontalDirection();return n?this.floating?a&&"right"===a||"down"===o?2:1:o&&("down"===o?2:1):!1},_intersectsWithSides:function(t){var i=e(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),s=e(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),n=this._getDragVerticalDirection(),o=this._getDragHorizontalDirection();return this.floating&&o?"right"===o&&s||"left"===o&&!s:n&&("down"===n&&i||"up"===n&&!i)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,o,a,r=[],h=[],l=this._connectWith();if(l&&e)for(s=l.length-1;s>=0;s--)for(o=t(l[s]),n=o.length-1;n>=0;n--)a=t.data(o[n],this.widgetFullName),a&&a!==this&&!a.options.disabled&&h.push([t.isFunction(a.options.items)?a.options.items.call(a.element):t(a.options.items,a.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),a]);for(h.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=h.length-1;s>=0;s--)h[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,o,a,r,h,l,c=this.items,u=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i]),s=n.length-1;s>=0;s--)o=t.data(n[s],this.widgetFullName),o&&o!==this&&!o.options.disabled&&(u.push([t.isFunction(o.options.items)?o.options.items.call(o.element[0],e,{item:this.currentItem}):t(o.options.items,o.element),o]),this.containers.push(o));for(i=u.length-1;i>=0;i--)for(a=u[i][1],r=u[i][0],s=0,l=r.length;l>s;s++)h=t(r[s]),h.data(this.widgetName+"-item",a),c.push({item:h,instance:a,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,o;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),o=n.offset(),s.left=o.left,s.top=o.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)o=this.containers[i].element.offset(),this.containers[i].containerCache.left=o.left,this.containers[i].containerCache.top=o.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]).addClass(i||e.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper");return"tr"===s?e.currentItem.children().each(function(){t(" ",e.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(n)}):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_contactContainers:function(s){var n,o,a,r,h,l,c,u,d,p,f=null,g=null;for(n=this.containers.length-1;n>=0;n--)if(!t.contains(this.currentItem[0],this.containers[n].element[0]))if(this._intersectsWith(this.containers[n].containerCache)){if(f&&t.contains(this.containers[n].element[0],f.element[0]))continue;f=this.containers[n],g=n}else this.containers[n].containerCache.over&&(this.containers[n]._trigger("out",s,this._uiHash(this)),this.containers[n].containerCache.over=0);if(f)if(1===this.containers.length)this.containers[g].containerCache.over||(this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1);else{for(a=1e4,r=null,p=f.floating||i(this.currentItem),h=p?"left":"top",l=p?"width":"height",c=this.positionAbs[h]+this.offset.click[h],o=this.items.length-1;o>=0;o--)t.contains(this.containers[g].element[0],this.items[o].item[0])&&this.items[o].item[0]!==this.currentItem[0]&&(!p||e(this.positionAbs.top+this.offset.click.top,this.items[o].top,this.items[o].height))&&(u=this.items[o].item.offset()[h],d=!1,Math.abs(u-c)>Math.abs(u+this.items[o][l]-c)&&(d=!0,u+=this.items[o][l]),a>Math.abs(u-c)&&(a=Math.abs(u-c),r=this.items[o],this.direction=d?"up":"down"));if(!r&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[g])return;r?this._rearrange(s,r,null,!0):this._rearrange(s,null,this.containers[g].element,!0),this._trigger("change",s,this._uiHash()),this.containers[g]._trigger("change",s,this._uiHash(this)),this.currentContainer=this.containers[g],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[g]._trigger("over",s,this._uiHash(this)),this.containers[g].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===document.body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,t("document"===n.containment?document:window).width()-this.helperProportions.width-this.margins.left,(t("document"===n.containment?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,o=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():o?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():o?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,o=e.pageX,a=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==document&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==document&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.leftthis.containment[2]&&(o=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(a=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((a-this.originalPageY)/n.grid[1])*n.grid[1],a=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((o-this.originalPageX)/n.grid[0])*n.grid[0],o=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:a-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:o-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,this.cancelHelperRemoval){if(!e){for(this._trigger("beforeStop",t,this._uiHash()),s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!1}if(e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null,!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})})(jQuery); \ No newline at end of file diff --git a/omega-web/package.json b/omega-web/package.json new file mode 100644 index 0000000..15e0a9e --- /dev/null +++ b/omega-web/package.json @@ -0,0 +1,25 @@ +{ + "name": "omega-web", + "version": "0.0.1", + "private": true, + "devDependencies": { + "chai": "~1.9.1", + "grunt": "^0.4.5", + "grunt-autoprefixer": "^1.0.1", + "grunt-bower-task": "^0.4.0", + "grunt-coffeelint": "^0.0.13", + "grunt-contrib-coffee": "^0.11.1", + "grunt-contrib-concat": "^0.5.0", + "grunt-contrib-copy": "^0.5.0", + "grunt-contrib-jade": "^0.12.0", + "grunt-contrib-less": "^0.11.4", + "grunt-contrib-watch": "^0.6.1", + "grunt-mocha-test": "~0.11.0", + "grunt-ng-annotate": "^0.3.2", + "load-grunt-config": "^0.13.1", + "omega-pac": "../omega-pac" + }, + "scripts": { + "dev": "npm link ../omega-pac" + } +} diff --git a/omega-web/src/coffee/log_error.coffee b/omega-web/src/coffee/log_error.coffee new file mode 100644 index 0000000..056f11c --- /dev/null +++ b/omega-web/src/coffee/log_error.coffee @@ -0,0 +1,8 @@ +window.onerror = (message, url, line, col, err) -> + log = localStorage['log'] || '' + if err.stack + log += err.stack + '\n\n' + else + log += "#{url}:#{line}:#{col}:\t#{message}\n\n" + localStorage['log'] = log + return diff --git a/omega-web/src/coffee/omega_decoration.coffee b/omega-web/src/coffee/omega_decoration.coffee new file mode 100644 index 0000000..e316b85 --- /dev/null +++ b/omega-web/src/coffee/omega_decoration.coffee @@ -0,0 +1,10 @@ +angular.module('omegaDecoration', []).value('profileIcons', { + 'DirectProfile': 'glyphicon-transfer', + 'SystemProfile': 'glyphicon-off', + 'AutoDetectProfile': 'glyphicon-file', + 'FixedProfile': 'glyphicon-globe', + 'PacProfile': 'glyphicon-file', + 'RulelistProfile': 'glyphicon-list', + 'SwitchProfile': 'glyphicon-retweet', + 'RuleListProfile': 'glyphicon-list', +}) diff --git a/omega-web/src/coffee/options.coffee b/omega-web/src/coffee/options.coffee new file mode 100644 index 0000000..604f260 --- /dev/null +++ b/omega-web/src/coffee/options.coffee @@ -0,0 +1,43 @@ +this.UglifyJS_NoUnsafeEval = true +$script 'lib/angular-loader/angular-loader.min.js', + 'angular-loader' +$script 'lib/jquery/jquery.min.js', 'jquery' +$script 'js/omega_pac.min.js', 'omega-pac' +$script 'lib/FileSaver/FileSaver.js', 'filesaver' +$script 'lib/blob/Blob.js', 'blob' +$script 'lib/spin.js/spin.js', -> + $script 'lib/ladda/ladda.min.js', -> + $script.ready ['angular-loader'], -> + $script 'lib/angular-ladda/angular-ladda.min.js', 'angular-ladda' + +$script.ready ['angular-loader'], -> + angular.module 'omega', ['ngLocale', 'ui.bootstrap', 'ui.router', + 'ngProgress', 'ui.sortable', 'angularSpectrumColorpicker', 'ui.validate', + 'angular-ladda', 'omegaTarget', 'omegaDecoration'] + $script.ready ['omega-pac'], -> + $script 'js/omega.js', 'omega' + $script([ + 'js/omega_target_web.js' + 'js/omega_decoration.js' + 'lib/angular-animate/angular-animate.min.js' + 'lib/angular-bootstrap/ui-bootstrap-tpls.min.js' + 'lib/angular-i18n/angular-locale_en-us.js' + 'lib/ngprogress/ngProgress.min.js' + 'lib/angular-ui-sortable/sortable.min.js' + 'lib/angular-ui-utils/validate.min.js' + 'lib/jsondiffpatch/bundle.min.js' + 'lib/angular-spectrum-colorpicker/angular-spectrum-colorpicker.min.js' + ], 'omega-deps') +$script.ready ['jquery'], -> + $script 'lib/jquery-ui-1.10.4.custom.min.js', 'jquery-ui' + $script 'lib/spectrum/spectrum.js', 'spectrum' + +$script.ready ['angular-loader', 'jquery'], -> + $script 'lib/angular/angular.js', 'angular' + +$script.ready ['angular'], -> + $script 'lib/angular-ui-router/angular-ui-router.js', 'angular-ui-router' + +$script.ready ['angular', 'omega', 'omega-deps', 'angular-ui-router', + 'jquery-ui', 'spectrum', 'filesaver', 'blob', 'angular-ladda'], -> + angular.bootstrap document, ['omega'] diff --git a/omega-web/src/coffee/popup.coffee b/omega-web/src/coffee/popup.coffee new file mode 100644 index 0000000..1d7509f --- /dev/null +++ b/omega-web/src/coffee/popup.coffee @@ -0,0 +1,109 @@ +module = angular.module('omegaPopup', ['omegaTarget', 'omegaDecoration', + 'ui.bootstrap', 'ui.validate']) + +module.filter 'tr', (omegaTarget) -> omegaTarget.getMessage +module.filter 'dispName', (omegaTarget) -> + (name) -> omegaTarget.getMessage('profile_' + name) || name + +module.controller 'PopupCtrl', ($scope, $window, $q, omegaTarget, + profileIcons) -> + + refreshOnProfileChange = false + refresh = -> + if refreshOnProfileChange + omegaTarget.refreshActivePage().then -> + $window.close() + else + $window.close() + $scope.profileIcons = profileIcons + $scope.isActive = (profileName) -> + if $scope.isSystemProfile + profileName == 'system' + else + $scope.currentProfileName == profileName + $scope.isEffective = (profileName) -> + $scope.isSystemProfile and $scope.currentProfileName == profileName + $scope.getIcon = (profile, normal) -> + return unless profile + if not normal and $scope.isEffective(profile.name) + 'glyphicon-ok' + else + profileIcons[profile.profileType] + $scope.openOptions = -> + omegaTarget.openOptions().then -> + $window.close() + $scope.applyProfile = (profile) -> + omegaTarget.applyProfile(profile.name).then -> + refresh() + + $scope.tempRuleMenu = {open: false} + $scope.nameExternal = {open: false} + $scope.addTempRule = (domain, profileName) -> + $scope.tempRuleMenu.open = false + omegaTarget.addTempRule(domain, profileName).then -> + refresh() + + $scope.addCondition = (condition, profileName) -> + omegaTarget.addCondition(condition, profileName).then -> + refresh() + + $scope.notConflict = (name) -> not $scope.availableProfiles?['+' + name] + $scope.saveExternal = -> + $scope.nameExternal.open = false + name = $scope.externalProfile.name + if name + omegaTarget.addProfile($scope.externalProfile).then -> + omegaTarget.applyProfile(name).then -> + refresh() + + omegaTarget.state([ + 'availableProfiles', 'currentProfileName', 'isSystemProfile', + 'validResultProfiles', 'refreshOnProfileChange', 'externalProfile', + 'proxyNotControllable' + ]).then ([availableProfiles, currentProfileName, isSystemProfile, + validResultProfiles, refreshOnProfileChange, externalProfile, + proxyNotControllable]) -> + $scope.proxyNotControllable = proxyNotControllable + return if proxyNotControllable + $scope.builtinProfiles = [] + $scope.customProfiles = [] + $scope.availableProfiles = availableProfiles + for own key, profile of availableProfiles + if profile.builtin + $scope.builtinProfiles.push(profile) + else + $scope.customProfiles.push(profile) + $scope.currentProfile = availableProfiles['+' + currentProfileName] + $scope.currentProfileName = currentProfileName + $scope.isSystemProfile = isSystemProfile + $scope.externalProfile = externalProfile + refreshOnProfileChange = refreshOnProfileChange + $scope.validResultProfiles = validResultProfiles.map (name) -> + availableProfiles['+' + name] + + omegaTarget.getActivePageInfo().then((info) -> + if info + $scope.currentTempRuleProfile = info.tempRuleProfileName + $scope.currentDomain = info.domain + else + $q.reject() + ).then(-> + omegaTarget.state('currentProfileCanAddRule') + ).then (value) -> + $scope.currentProfileCanAddRule = value + if $scope.currentProfileCanAddRule + currentDomain = $scope.currentDomain + currentDomainEscaped = currentDomain.replace('.', '\\.') + conditionSuggestion = + 'HostWildcardCondition': '*.' + currentDomain + 'HostRegexCondition': '(^|\\.)' + currentDomainEscaped + '$' + 'UrlWildcardCondition': '*://*.' + currentDomain + '/*' + 'UrlRegexCondition': '://([^/.]+\\.)*' + currentDomainEscaped + '/' + 'KeywordCondition': currentDomain + $scope.rule = + condition: + conditionType: 'HostWildcardCondition' + pattern: conditionSuggestion['HostWildcardCondition'] + profileName: $scope.currentTempRuleProfile ? 'direct' + $scope.$watch 'rule.condition.conditionType', (type) -> + $scope.rule.condition.pattern = conditionSuggestion[type] diff --git a/omega-web/src/coffee/popup_basics.coffee b/omega-web/src/coffee/popup_basics.coffee new file mode 100644 index 0000000..911dbce --- /dev/null +++ b/omega-web/src/coffee/popup_basics.coffee @@ -0,0 +1,18 @@ +# Use events to ensure that the log can be downloaded even if everything else +# fails to load. +document.querySelector('.error-log').addEventListener 'click', (-> + window.OmegaTargetWebBasics.getLog (log) -> + blob = new Blob [log], {type: "text/plain;charset=utf-8"} + saveAs(blob, "OmegaLog_#{Date.now()}.txt") +), false + +window.OmegaTargetWebBasics.getEnv (env) -> + url = 'https://github.com/FelisCatus/SwitchyOmega/issues/new?title=&body=' + body = window.OmegaTargetWebBasics.getMessage('popup_issueTemplate', env) + body ||= """ + \n\n + + SwitchyOmega #{env.projectVersion} + #{env.userAgent} + """ + document.querySelector('.report-issue').href = url + encodeURIComponent(body) diff --git a/omega-web/src/less/options.less b/omega-web/src/less/options.less new file mode 100644 index 0000000..e0376e4 --- /dev/null +++ b/omega-web/src/less/options.less @@ -0,0 +1,288 @@ +/* angular */ +.ng-hide { + display: none !important; +} + +a[role="button"] { + cursor: pointer; +} + +.width-initial { + width: auto !important; + width: initial !important; +} + +.width-limit { + max-width: 600px; +} + +.width-limit-lg { + max-width: 800px; +} + +.width-limit-xl { + max-width: 1000px; +} + +.inline-form-control { + display: inline-block; + margin-right: 20px; + margin-left: 20px; + min-width: 50%; + width: 50% !important; + width: initial !important; +} + +.no-min-width { + min-width: 0 !important; +} + +ul.list-style-none, li.list-style-none { + list-style: none; + padding-left: 0; +} + +.help-inline { + margin-left: 10px; + color: #595959; +} + +.settings-group { + > * { + margin-left: 30px; + } + > h3 { + margin-left: 0; + } +} + +.form-control.ng-invalid { + border-color: #a94442; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + + &:hover { + border-color: #843534; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #CE8483; + } +} + +/* alert */ + +.alert-top-wrapper { + z-index: 100; + position: fixed; + top: 0; + left: 0; + right: 0; + width: 100%; + display: inline-block !important; + + transition: transform .5s, opacity .5s; + transition: transform cubic-bezier(0.19, 1, 0.22, 1) .5s, + opacity cubic-bezier(0.19, 1, 0.22, 1) .5s; + + &.ng-hide { + transform: translateY(-100%); + opacity: 0; + } + + pointer-events: none; + text-align: center; + + * { + pointer-events: auto; + pointer-events: initial; + text-align: left; + text-align: initial; + } + + .alert { + display: inline-block; + min-width: 400px; + margin-bottom: 0; + + @media (max-width: 767px) { + min-width: 80%; + } + } +} + +/* body */ + +h1 { + color: #5c6166; + font-size: 1.7em; + font-weight: bold; + margin-bottom: 1.5em; +} + +.side-nav { + position: fixed; + height: 100%; + overflow: auto; + + @media (max-width: 767px) { + position: static; + height: auto; + height: initial; + } +} + +main { + padding-top: 85px; + padding-bottom: 20px; + + .page-header { + margin: 0; + padding: 20px 60px 20px 0; + position: fixed; + top: 0; + width: inherit; + background-color: rgba(255, 255, 255, 0.6); + background-image: linear-gradient(to bottom, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.6)); + max-height: 85px; + z-index: 3; + + h2 { + margin: 0; + } + } + + @media (max-width: 767px) { + padding-top: 0; + .page-header { + position: static; + max-height: none; + } + } +} + +#ngProgress-container { + position: fixed; + top: 0; + width: 100%; +} + +.divider { + height: 1px; + margin: 9px 1px; + overflow: hidden; + background-color: #e5e5e5; +} + +.nav-header { + margin-right: -15px !important; + margin-left: -15px !important; + text-shadow: 0 1px 0 rgba(255,255,255,0.5); + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + color: #999; + text-shadow: 0 1px 0 rgba(255,255,255,0.5); + text-transform: uppercase; +} + +#restore-local-file { + position: absolute; + visibility: hidden; +} + +.profile-actions { + float: right; +} + +.profile-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.profile-color-editor { + display: inline-block; + float: left; + margin-right: 10px; + + input[type="color"] { + height: 30px; + width: 30px; + padding: 0; + border: none; + background: none; + + &::-webkit-color-swatch-wrapper { + padding: 0; + } + } + + .sp-replacer { + padding: 0; + border: none; + } + + .sp-preview { + margin-right: 0; + height: 30px; + width: 30px; + } + + .sp-dd { + display: none; + } +} + +.fixed-servers, +.switch-rules { + td { + vertical-align: middle !important; + } +} + +.fixed-show-advanced { + td { + background-color: transparent !important; + } + .btn-link { + padding: 0; + } +} + +.host-levels-details { + input { + .width-initial(); + display: inline-block; + } +} + +.sort-bar { + width: 1em; + text-align: center; + cursor: move; +} + +.cycle-profile-container { + list-style-type: none; + min-height: 20px; + display: inline-block; + min-width: 40%; + padding: 0; + border: solid 1px #abc; + + li { + display: block; + margin: 10px; + padding: 5px; + cursor: move; + background-color: #ddd; + + } + + &.cycle-enabled li { + background-color: #e4ffcd; + } +} + +.modal-body .well:last-child { + margin-bottom: 0; +} diff --git a/omega-web/src/less/popup.less b/omega-web/src/less/popup.less new file mode 100644 index 0000000..b73b5f1 --- /dev/null +++ b/omega-web/src/less/popup.less @@ -0,0 +1,130 @@ +/* angular */ + +.ng-hide { + display: none !important; +} + +/* popup */ + +body { + margin: 0; + padding: 0; + min-width: 180px; +} + +body.with-condition-form { +} + +.condition-form { + min-width: 360px; +} + +.profile-inline { + background-color: #eee; + padding: 0 5px; +} + +.nav { + margin-bottom: 0; +} + +li > a { + text-overflow: ellipsis; + overflow: hidden; + max-width: 20em; +} + +.nav-pills.nav-stacked { + > li > a { + padding-left: 8px; + padding-right: 25px; + white-space: nowrap; + cursor: pointer; + + .glyphicon { + margin-right: 6px; + } + } +} + +.divider { + height: 1px; + overflow: hidden; + background-color: #E5E5E5; +} + +.temp-rule a { + padding-left: 8px !important; + box-shadow: none !important; +} + +.dropdown-menu { + position: static; + top: initial; + top: -moz-initial; + margin: 0 5px !important; + float: none; + + a { + cursor: pointer; + } +} + +.current-domain { + color: #08C; +} + +select, textarea, input { + margin-bottom: 0px !important; +} + +form { + margin: 10px; +} + +legend, +.well { + margin-bottom: 10px; +} + +.well { + padding: 10px; +} + +.condition-controls .btn-primary { + float: right; +} + +.external-profile { + a { + padding-right: 10px !important; + } + + form { + margin: 0; + padding: 0; + display: inline-block; + } + + .form-control { + display: inline-block; + padding: 0; + height: 2em; + width: ~"calc(100% - 1em - 8px)"; + } +} + +.form-control.ng-invalid { + border-color: #a94442; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + + &:hover { + border-color: #843534; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 6px #CE8483; + } +} + +.proxy-not-controllable { + min-width: 400px; + padding: 10px 5px; +} diff --git a/omega-web/src/omega/app.coffee b/omega-web/src/omega/app.coffee new file mode 100644 index 0000000..4931a5f --- /dev/null +++ b/omega-web/src/omega/app.coffee @@ -0,0 +1,37 @@ +angular.module('omega').constant('builtinProfiles', + OmegaPac.Profiles.builtinProfiles) + +profileColors = [ + '#9ce', '#9d9', '#fa8', '#fe9', '#d497ee', '#47b', '#5b5', '#d63', '#ca0' +] +colors = [].concat(profileColors) +profileColorPalette = (colors.splice(0, 3) while colors.length) + +angular.module('omega').constant('profileColors', profileColors) +angular.module('omega').constant('profileColorPalette', profileColorPalette) + +angular.module('omega').config ($stateProvider, $urlRouterProvider, + $httpProvider) -> + $urlRouterProvider.otherwise '/ui' + + $stateProvider + .state('ui', + url: '/ui' + templateUrl: 'partials/ui.html' + #controller: 'UiCtrl' + ).state('general', + url: '/general' + templateUrl: 'partials/general.html' + #controller: 'GeneralCtrl' + ).state('io', + url: '/io' + templateUrl: 'partials/io.html' + controller: 'IoCtrl' + ).state('profile', + url: '/profile/:name' + templateUrl: 'partials/profile.html' + controller: 'ProfileCtrl' + ).state('about', + url: '/about' + templateUrl: 'partials/about.html' + ) diff --git a/omega-web/src/omega/controllers/fixed_profile.coffee b/omega-web/src/omega/controllers/fixed_profile.coffee new file mode 100644 index 0000000..371b03f --- /dev/null +++ b/omega-web/src/omega/controllers/fixed_profile.coffee @@ -0,0 +1,58 @@ +angular.module('omega').controller 'FixedProfileCtrl', ($scope) -> + $scope.urlSchemes = ['', 'http', 'https', 'ftp'] + $scope.urlSchemeDefault = 'fallbackProxy' + proxyProperties = + '': 'fallbackProxy' + 'http': 'proxyForHttp' + 'https': 'proxyForHttps' + 'ftp': 'proxyForFtp' + $scope.schemeDisp = + '': null + 'http': 'http://' + 'https': 'https://' + 'ftp': 'ftp://' + + defaultPort = + 'http': 80 + 'https': 443 + 'socks4': 1080 + 'socks5': 1080 + + $scope.showAdvanced = false + + $scope.proxyEditors = {} + + onProxyChange = (proxyEditors, oldProxyEditors) -> + return unless proxyEditors + for scheme in $scope.urlSchemes + proxy = proxyEditors[scheme] + if not proxy.scheme + if not scheme + proxyEditors[scheme] = {} + delete $scope.profile[proxyProperties[scheme]] + continue + else if not oldProxyEditors[scheme].scheme + if proxy.scheme == proxyEditors[''].scheme + proxy.port ?= proxyEditors[''].port + proxy.port ?= defaultPort[proxy.scheme] + proxy.host ?= proxyEditors[''].host ? 'example.com' + $scope.profile[proxyProperties[scheme]] ?= proxy + for scheme in $scope.urlSchemes + do (scheme) -> + $scope.$watch (-> $scope.profile[proxyProperties[scheme]]), (proxy) -> + if scheme and proxy + $scope.showAdvanced = true + $scope.proxyEditors[scheme] = proxy ? {} + $scope.$watch 'proxyEditors', onProxyChange, true + + onBypassListChange = (list) -> + $scope.bypassList = (item.pattern for item in list).join('\n') + + $scope.$watch 'profile.bypassList', onBypassListChange, true + + $scope.$watch 'bypassList', (bypassList, oldList) -> + return if not bypassList? or bypassList == oldList + $scope.profile.bypassList = + for entry in bypassList.split(/\r?\n/) when entry + conditionType: "BypassCondition" + pattern: entry diff --git a/omega-web/src/omega/controllers/io.coffee b/omega-web/src/omega/controllers/io.coffee new file mode 100644 index 0000000..86834b3 --- /dev/null +++ b/omega-web/src/omega/controllers/io.coffee @@ -0,0 +1,41 @@ +angular.module('omega').controller 'IoCtrl', ($scope, $rootScope) -> + $scope.exportOptions = -> + $rootScope.applyOptionsConfirm().then -> + plainOptions = angular.fromJson(angular.toJson($rootScope.options)) + content = JSON.stringify(plainOptions) + blob = new Blob [content], {type: "text/plain;charset=utf-8"} + saveAs(blob, "OmegaOptions.bak") + + $scope.restoreLocal = (content) -> + $rootScope.resetOptions(content).then ( -> + $rootScope.showAlert( + type: 'success' + i18n: 'options_importSuccess' + message: 'Options imported.' + ) + ), -> $scope.restoreLocalError() + $scope.restoreLocalError = -> + $rootScope.showAlert( + type: 'error' + i18n: 'options_importFormatError' + message: 'Invalid backup file!' + ) + $scope.downloadError = -> + $rootScope.showAlert( + type: 'error' + i18n: 'options_importDownloadError' + message: 'Error downloading backup file!' + ) + $scope.triggerFileInput = -> + angular.element('#restore-local-file').click() + return + $scope.restoreOnline = -> + $.ajax( + url: $scope.restoreOnlineUrl, + success: (content) -> $scope.$apply -> + $scope.restoreLocal(content) + error: $scope.downloadError, + dataType: "text", + cache: false, + timeout: 10000 + ) diff --git a/omega-web/src/omega/controllers/master.coffee b/omega-web/src/omega/controllers/master.coffee new file mode 100644 index 0000000..c442ba5 --- /dev/null +++ b/omega-web/src/omega/controllers/master.coffee @@ -0,0 +1,222 @@ +angular.module('omega').controller 'MasterCtrl', ($scope, $rootScope, $window, + $modal, $state, builtinProfiles, profileColors, profileIcons, omegaTarget, $q, + $timeout, $location, $filter) -> + + tr = $filter('tr') + + # This method allows watchers to change the value without recursively firing + # itself. Usage: $scope.omegaWatchAndChange + # coffeelint: disable=missing_fat_arrows + $rootScope.omegaWatchAndChange = (expression, listener, objectEquality) -> + scope = this + # coffeelint: enable=missing_fat_arrows + handler = (newValue, oldValue) -> + modified = listener(newValue, oldValue) + return if newValue == oldValue and newValue == modified + for watcher in scope.$$watchers + if watcher.exp == expression + watcher.last = modified + return scope.$watch(expression, handler, objectEquality) + + $rootScope.options = null + + omegaTarget.addOptionsChangeCallback (newOptions) -> + $rootScope.options = angular.copy(newOptions) + $rootScope.optionsOld = angular.copy(newOptions) + $timeout -> + $rootScope.optionsDirty = false + + $rootScope.revertOptions = -> + $window.location.reload() + + $rootScope.exportScript = (name) -> + getProfileName = + if name + $q.when(name) + else + omegaTarget.state('currentProfileName') + + getProfileName.then (profileName) -> + return unless profileName + profile = $rootScope.profileByName(profileName) + return if profile.profileType in ['DirectProfile', 'SystemProfile'] + ast = OmegaPac.PacGenerator.script($rootScope.options, profileName) + pac = ast.print_to_string(beautify: true, comments: true) + blob = new Blob [pac], {type: "text/plain;charset=utf-8"} + fileName = profileName.replace(/\W+/g, '_') + saveAs(blob, "OmegaProfile_#{fileName}.pac") + + diff = jsondiffpatch.create( + objectHash: (obj) -> JSON.stringify(obj) + textDiff: minLength: 1 / 0 + ) + + $rootScope.showAlert = (alert) -> $timeout -> + $scope.alert = alert + $scope.alertShown = true + $scope.alertShownAt = Date.now() + $timeout $rootScope.hideAlert, 3000 + return + + $rootScope.hideAlert = -> $timeout -> + if Date.now() - $scope.alertShownAt >= 1000 + $scope.alertShown = false + + checkFormValid = -> + fields = angular.element('.ng-invalid') + if fields.length > 0 + fields[0].focus() + $rootScope.showAlert( + type: 'error' + i18n: 'options_formInvalid' + ) + return false + return true + + $rootScope.applyOptions = -> + return unless checkFormValid() + plainOptions = angular.fromJson(angular.toJson($rootScope.options)) + patch = diff.diff($rootScope.optionsOld, plainOptions) + omegaTarget.optionsPatch(patch).then -> + $rootScope.showAlert( + type: 'success' + i18n: 'options_saveSuccess' + ) + + $rootScope.resetOptions = (options) -> + omegaTarget.resetOptions(options).then(-> + $rootScope.showAlert( + type: 'success' + i18n: 'options_resetSuccess' + ) + ).catch (err) -> + $rootScope.showAlert( + type: 'error' + message: err + ) + $q.reject err + + $rootScope.profileByName = (name) -> + OmegaPac.Profiles.byName(name, $rootScope.options) + + $rootScope.applyOptionsConfirm = -> + return $q.reject 'form_invalid' unless checkFormValid() + return $q.when(true) unless $rootScope.optionsDirty + $modal.open(templateUrl: 'partials/apply_options_confirm.html').result + .then -> $rootScope.applyOptions() + + $rootScope.newProfile = -> + scope = $rootScope.$new('isolate') + scope.options = $rootScope.options + scope.notConflict = (name) -> not $rootScope.profileByName(name) + scope.profileIcons = profileIcons + $modal.open( + templateUrl: 'partials/new_profile.html' + scope: scope + ).result.then (profile) -> + profile = OmegaPac.Profiles.create(profile) + choice = Math.floor(Math.random() * profileColors.length) + profile.color ?= profileColors[choice] + OmegaPac.Profiles.updateRevision(profile) + $rootScope.options[OmegaPac.Profiles.nameAsKey(profile)] = profile + $state.go('profile', {name: profile.name}) + + $rootScope.renameProfile = (fromName) -> + $rootScope.applyOptionsConfirm().then -> + profile = $rootScope.profileByName(fromName) + scope = $rootScope.$new('isolate') + scope.options = $rootScope.options + scope.fromName = fromName + scope.notConflict = (name) -> + name == fromName or not $rootScope.profileByName(name) + scope.profileIcons = profileIcons + $modal.open( + templateUrl: 'partials/rename_profile.html' + scope: scope + ).result.then (toName) -> + if toName != fromName + omegaTarget.renameProfile(fromName, toName).then(-> + $state.go('profile', {name: toName}) + ).catch (err) -> + $rootScope.showAlert( + type: 'error' + message: err + ) + + $scope.updatingProfile = {} + + $rootScope.updateProfile = (name) -> + $rootScope.applyOptionsConfirm().then(-> + $scope.updatingProfile[name] = true + omegaTarget.updateProfile(name).then((results) -> + success = 0 + error = 0 + for own profileName, result of results + if result instanceof Error + error++ + else + success++ + if error == 0 + $rootScope.showAlert( + type: 'success' + i18n: 'options_profileDownloadSuccess' + ) + else + $q.reject(results) + ).catch((err) -> + $rootScope.showAlert( + type: 'error' + i18n: 'options_profileDownloadError' + ) + ).finally -> + $scope.updatingProfile[name] = false + ) + + onOptionChange = (options, oldOptions) -> + return if options == oldOptions or not oldOptions? + plainOptions = angular.fromJson(angular.toJson(options)) + $rootScope.optionsDirty = true + $rootScope.$watch 'options', onOptionChange, true + + $rootScope.$on '$stateChangeStart', (event, _, __, fromState) -> + if not checkFormValid() + event.preventDefault() + + $rootScope.$on '$stateChangeSuccess', -> + omegaTarget.state('web.last_url', $location.url()) + + $window.onbeforeunload = -> + if $rootScope.optionsDirty + return tr('options_optionsNotSaved') + else + null + + document.addEventListener 'click', (-> + $rootScope.hideAlert() + ), false + + $scope.profileIcons = profileIcons + + for own type of OmegaPac.Profiles.formatByType + $scope.profileIcons[type] = $scope.profileIcons['RuleListProfile'] + + $scope.alertIcons = + 'success': 'glyphicon-ok', + 'warning': 'glyphicon-warning-sign', + 'error': 'glyphicon-remove', + 'danger': 'glyphicon-danger', + + $scope.alertClassForType = (type) -> + return '' if not type + if type == 'error' + type = 'danger' + return 'alert-' + type + + $scope.downloadIntervals = [15, 60, 180, 360, 720, 1440, -1] + $scope.downloadIntervalI18n = (interval) -> + "options_downloadInterval_" + (if interval < 0 then "never" else interval) + + omegaTarget.refresh() + + omegaTarget.state('web.last_url').then (lastUrl) -> + $location.url(lastUrl) if lastUrl? diff --git a/omega-web/src/omega/controllers/profile.coffee b/omega-web/src/omega/controllers/profile.coffee new file mode 100644 index 0000000..996fb9a --- /dev/null +++ b/omega-web/src/omega/controllers/profile.coffee @@ -0,0 +1,77 @@ +angular.module('omega').controller 'ProfileCtrl', ($scope, $stateParams, + $location, $rootScope, $modal, $state, profileColorPalette) -> + name = $stateParams.name + profileTemplates = + 'FixedProfile': 'profile_fixed.html' + 'PacProfile': 'profile_pac.html' + 'SwitchProfile': 'profile_switch.html' + 'RuleListProfile': 'profile_rule_list.html' + $scope.spectrumOptions = + localStorageKey: 'spectrum.profileColor' + palette: profileColorPalette + preferredFormat: 'hex' + showButtons: false + showInitial: true + showInput: true + showPalette: true + showSelectionPalette: true + + $scope.deleteProfile = -> + profileName = $scope.profile.name + refs = OmegaPac.Profiles.referencedBySet(profileName, $rootScope.options) + refs = Object.keys(refs) + + scope = $rootScope.$new('isolate') + scope.profile = $scope.profile + scope.profileIcons = $scope.profileIcons + + if refs.length > 0 + scope.refs = refs.map (p) -> OmegaPac.Profiles.byKey(p, + $rootScope.options) + $modal.open( + templateUrl: 'partials/cannot_delete_profile.html' + scope: scope + ) + else + $modal.open( + templateUrl: 'partials/delete_profile.html' + scope: scope + ).result.then -> + delete $rootScope.options[OmegaPac.Profiles.nameAsKey(profileName)] + if $rootScope.options['-startupProfileName'] == profileName + $rootScope.options['-startupProfileName'] = "" + quickSwitch = $rootScope.options['-quickSwitchProfiles'] + for i in [0...quickSwitch.length] + if profileName == quickSwitch[i] + quickSwitch.splice i, 1 + break + $state.go('ui') + + unwatch = $scope.$watch (-> $scope.options?['+' + name]), (profile) -> + if not profile + if $scope.options + unwatch() + $location.path '/' + else + unwatch2 = $scope.$watch 'options', -> + if $scope.options + unwatch2() + if not $scope.options['+' + name] + unwatch() + $location.path '/' + return + if OmegaPac.Profiles.formatByType[profile.profileType] + profile.format = OmegaPac.Profiles.formatByType[profile.profileType] + profile.profileType = 'RuleListProfile' + $scope.profile = profile + type = $scope.profile.profileType + templ = profileTemplates[type] ? 'profile_unsupported.html' + $scope.profileTemplate = 'partials/' + templ + $scope.scriptable = true + + onProfileChange = (profile, oldProfile) -> + return profile if profile == oldProfile + OmegaPac.Profiles.updateRevision(profile) + return profile + + $scope.omegaWatchAndChange 'profile', onProfileChange, true diff --git a/omega-web/src/omega/controllers/quick_switch.coffee b/omega-web/src/omega/controllers/quick_switch.coffee new file mode 100644 index 0000000..870e136 --- /dev/null +++ b/omega-web/src/omega/controllers/quick_switch.coffee @@ -0,0 +1,15 @@ +angular.module('omega').controller 'QuickSwitchCtrl', ($scope, $filter) -> + $scope.sortableOptions = + tolerance: 'pointer' + axis: 'y' + forceHelperSize: true + forcePlaceholderSize: true + connectWith: '.cycle-profile-container' + containment: '#quick-switch-settings' + + $scope.$watchCollection 'options', (options) -> + return unless options? + $scope.notCycledProfiles = + for profile in $filter('profiles')(options, 'all') when ( + options["-quickSwitchProfiles"].indexOf(profile.name) < 0) + profile.name diff --git a/omega-web/src/omega/controllers/rule_list_profile.coffee b/omega-web/src/omega/controllers/rule_list_profile.coffee new file mode 100644 index 0000000..f72a727 --- /dev/null +++ b/omega-web/src/omega/controllers/rule_list_profile.coffee @@ -0,0 +1,2 @@ +angular.module('omega').controller 'RuleListProfileCtrl', ($scope) -> + $scope.ruleListFormats = OmegaPac.Profiles.ruleListFormats diff --git a/omega-web/src/omega/controllers/switch_profile.coffee b/omega-web/src/omega/controllers/switch_profile.coffee new file mode 100644 index 0000000..0c15586 --- /dev/null +++ b/omega-web/src/omega/controllers/switch_profile.coffee @@ -0,0 +1,57 @@ +angular.module('omega').controller 'SwitchProfileCtrl', ($scope, $modal) -> + $scope.conditionI18n = + 'HostWildcardCondition': 'condition_hostWildcard' + 'HostRegexCondition': 'condition_hostRegex' + 'HostLevelsCondition': 'condition_hostLevels' + 'UrlWildcardCondition': 'condition_urlWildcard' + 'UrlRegexCondition': 'condition_urlRegex' + 'KeywordCondition': 'condition_keyword' + 'AlwaysCondition': 'condition_always' + 'NeverCondition': 'condition_never' + + $scope.addRule = -> + rule = + if $scope.profile.rules.length > 0 + [..., templ] = $scope.profile.rules + angular.copy(templ) + else + condition: {conditionType: 'HostWildcardCondition', pattern: ''} + profileName: $scope.profile.defaultProfileName + if rule.condition.pattern + rule.condition.pattern = '' + $scope.profile.rules.push rule + + $scope.removeRule = (index) -> + removeForReal = -> + $scope.profile.rules.splice index, 1 + if $scope.options['-confirmDeletion'] + scope = $scope.$new('isolate') + scope.rule = $scope.profile.rules[index] + scope.conditionI18n = $scope.conditionI18n + scope.ruleProfile = $scope.profileByName(scope.rule.profileName) + scope.profileIcons = $scope.profileIcons + $modal.open( + templateUrl: 'partials/rule_remove_confirm.html' + scope: scope + ).result.then removeForReal + else + removeForReal() + + $scope.resetRules = -> + scope = $scope.$new('isolate') + scope.ruleProfile = $scope.profileByName($scope.profile.defaultProfileName) + scope.profileIcons = $scope.profileIcons + $modal.open( + templateUrl: 'partials/rule_reset_confirm.html' + scope: scope + ).result.then -> + for rule in $scope.profile.rules + rule.profileName = $scope.profile.defaultProfileName + + $scope.sortableOptions = + handle: '.sort-bar' + tolerance: 'pointer' + axis: 'y' + forceHelperSize: true + forcePlaceholderSize: true + containment: 'parent' diff --git a/omega-web/src/omega/directives.coffee b/omega-web/src/omega/directives.coffee new file mode 100644 index 0000000..f0ff3cd --- /dev/null +++ b/omega-web/src/omega/directives.coffee @@ -0,0 +1,32 @@ +angular.module('omega').directive 'inputGroupClear', -> + restrict: 'A' + templateUrl: 'partials/input_group_clear.html' + scope: + 'model': '=model' + 'type': '@type' + 'placeholder': '@placeholder' + link: (scope, element, attrs) -> + scope.oldModel = '' + scope.modelChange = -> + if scope.model + scope.oldModel = '' + scope.toggleClear = -> + [scope.model, scope.oldModel] = [scope.oldModel, scope.model] +angular.module('omega').directive 'omegaUpload', -> + restrict: 'A' + scope: + success: '&omegaUpload' + error: '&omegaError' + link: (scope, element, attrs) -> + input = element[0] + element.on 'change', -> + if input.files.length > 0 and input.files[0].name.length > 0 + reader = new FileReader() + reader.addEventListener 'load', (e) -> + scope.$apply -> + scope.success({'$content': e.target.result}) + reader.addEventListener 'error', (e) -> + scope.$apply -> + scope.error({'$error': e.target.error}) + reader.readAsText(input.files[0]) + input.value = '' diff --git a/omega-web/src/omega/filters.coffee b/omega-web/src/omega/filters.coffee new file mode 100644 index 0000000..3934a8c --- /dev/null +++ b/omega-web/src/omega/filters.coffee @@ -0,0 +1,19 @@ +angular.module('omega').filter 'profiles', (builtinProfiles) -> + charCodePlus = '+'.charCodeAt(0) + builtinProfileList = (profile for _, profile of builtinProfiles) + (options, filter) -> + result = [] + for name, value of options when name.charCodeAt(0) == charCodePlus + result.push value + if (typeof filter == 'object' or ( + typeof filter == 'string' and filter.charCodeAt(0) == charCodePlus)) + if typeof filter == 'string' + filter = filter.substr(1) + result = OmegaPac.Profiles.validResultProfilesFor(filter, options) + if filter == 'all' + result = result.concat builtinProfileList + result + +angular.module('omega').filter 'tr', (omegaTarget) -> omegaTarget.getMessage +angular.module('omega').filter 'dispName', (omegaTarget) -> + (name) -> omegaTarget.getMessage('profile_' + name) || name diff --git a/omega-web/src/options.jade b/omega-web/src/options.jade new file mode 100644 index 0000000..01df462 --- /dev/null +++ b/omega-web/src/options.jade @@ -0,0 +1,68 @@ +doctype html +html(lang='en' ng-controller='MasterCtrl' ng-csp) + head + meta(charset='utf-8') + title(ng-bind="'options_title' | tr") SwitchyOmega Options + meta(name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no') + link(rel='stylesheet' href='lib/bootstrap/css/bootstrap.min.css') + link(rel='stylesheet' href='lib/spectrum/spectrum.css') + link(rel='stylesheet' href='lib/ladda/ladda-themeless.min.css') + link(rel='stylesheet' href='css/options.css') + body(style='display: none;' ng-style='{display: options ? "block" : "none"}') + .container-fluid + header.col-lg-2.col-sm-3.side-nav + h1 {{'appNameShort' | tr}} + nav.nav.nav-pills.nav-stacked + li.nav-header {{'options_navHeader_setting' | tr}} + li(ui-sref-active='active'): a(ui-sref='ui') + span.glyphicon.glyphicon-wrench + = ' ' + | {{'options_tab_ui' | tr}} + li(ui-sref-active='active'): a(ui-sref='general') + span.glyphicon.glyphicon-cog + = ' ' + | {{'options_tab_general' | tr}} + li(ui-sref-active='active'): a(ui-sref='io') + span.glyphicon.glyphicon-floppy-save + = ' ' + | {{'options_tab_importExport' | tr}} + li.divider + li.nav-header {{'options_navHeader_profiles' | tr}} + li(ng-repeat='profile in options | profiles' ui-sref-active='active') + a(ui-sref='profile({name: profile.name})') + span.glyphicon(class='{{profileIcons[profile.profileType]}}' + ng-style='{color: profile.color}') + = ' ' + | {{profile.name}} + li + a(role='button' ng-click='newProfile()') + span.glyphicon.glyphicon-plus + = ' ' + span {{'options_newProfile' | tr}} + li.divider + li.nav-header Actions + li(ng-class='{"bg-warning": optionsDirty}') + a.text-success(role='button' ng-click='applyOptions()') + span.glyphicon.glyphicon-ok-circle + = ' ' + | {{'options_apply' | tr}} + li(ng-class='{disabled: !optionsDirty}') + a.text-danger(role='button' ng-click='revertOptions()') + span.glyphicon.glyphicon-remove-circle + = ' ' + | {{'options_discard' | tr}} + main.col-lg-10.col-sm-9.col-lg-offset-2.col-sm-offset-3(ui-view) + + //- + Note: Alert type classes cannot be changed in angular-bootstrap. + This is already fixed but they don't have time for a new release: + https://github.com/angular-ui/bootstrap/issues/2641 + .alert-top-wrapper(ng-show='alertShown') + div(alert type='workaround' close='hideAlert()' + class='{{alertClassForType(alert.type)}}') + span.glyphicon(class="{{alertIcons[alert.type]}}") + = ' ' + | {{alert.i18n ? (alert.i18n | tr) : alert.message}} + script(src='js/log_error.js') + script(src='lib/script.js/script.min.js') + script(src='js/options.js') diff --git a/omega-web/src/partials/apply_options_confirm.jade b/omega-web/src/partials/apply_options_confirm.jade new file mode 100644 index 0000000..f1caff3 --- /dev/null +++ b/omega-web/src/partials/apply_options_confirm.jade @@ -0,0 +1,11 @@ +.modal-header + button.close(type='button', data-dismiss='modal') + span(aria-hidden='true') × + span.sr-only {{'dialog_close' | tr}} + h4.modal-title {{'options_modalHeader_applyOptions' | tr}} +.modal-body + p {{'options_applyOptionsRequired' | tr}} + p {{'options_applyOptionsConfirm' | tr}} +.modal-footer + button.btn.btn-default(ng-click='$dismiss()') {{'dialog_cancel' | tr}} + button.btn.btn-primary(type='button' ng-click='$close("ok")') {{'options_apply' | tr}} diff --git a/omega-web/src/partials/cannot_delete_profile.jade b/omega-web/src/partials/cannot_delete_profile.jade new file mode 100644 index 0000000..7514c78 --- /dev/null +++ b/omega-web/src/partials/cannot_delete_profile.jade @@ -0,0 +1,17 @@ +.modal-header + button.close(type='button', data-dismiss='modal') + span(aria-hidden='true') × + span.sr-only {{'dialog_close' | tr}} + h4.modal-title {{'options_modalHeader_cannotDeleteProfile' | tr}} +.modal-body + p {{'options_profileReferredBy' | tr}} + .well + ul.list-style-none + li(ng-repeat='p in refs') + span.glyphicon(class='{{profileIcons[p.profileType]}}' + ng-style='{color: p.color}') + = ' ' + | {{p.name | dispName}} + p {{'options_modifyReferringProfiles' | tr}} +.modal-footer + button.btn.btn-default(ng-click='$dismiss()') {{'dialog_cancel' | tr}} diff --git a/omega-web/src/partials/delete_profile.jade b/omega-web/src/partials/delete_profile.jade new file mode 100644 index 0000000..dc8f289 --- /dev/null +++ b/omega-web/src/partials/delete_profile.jade @@ -0,0 +1,15 @@ +.modal-header + button.close(type='button', data-dismiss='modal') + span(aria-hidden='true') × + span.sr-only {{'dialog_close' | tr}} + h4.modal-title {{'options_modalHeader_deleteProfile' | tr}} +.modal-body + p {{'options_deleteProfileConfirm' | tr}} + .well + span.glyphicon(class='{{profileIcons[profile.profileType]}}' + ng-style='{color: profile.color}') + = ' ' + | {{profile.name}} +.modal-footer + button.btn.btn-default(ng-click='$dismiss()') {{'dialog_cancel' | tr}} + button.btn.btn-danger(type='button' ng-click='$close("ok")') {{'options_deleteProfile' | tr}} diff --git a/omega-web/src/partials/general.jade b/omega-web/src/partials/general.jade new file mode 100644 index 0000000..685f74f --- /dev/null +++ b/omega-web/src/partials/general.jade @@ -0,0 +1,15 @@ +.page-header + h2 {{'options_tab_general' | tr}} +section.settings-group + h3 {{'options_group_proxyChanges' | tr}} + div.checkbox + label + input#revert-proxy-changes(type='checkbox' ng-model='options["-revertProxyChanges"]') + span {{'options_revertProxyChanges' | tr}} +section.settings-group + h3 {{'options_downloadOptions' | tr}} + p.help-block {{'options_downloadOptionsHelp' | tr}} + .form-group + label(for='download-interval') {{'options_downloadInterval' | tr}} + select#download-interval.form-control.inline-form-control(ng-model='options["-downloadInterval"]' + ng-options='interval as (downloadIntervalI18n(interval) | tr) for interval in downloadIntervals') diff --git a/omega-web/src/partials/input_group_clear.jade b/omega-web/src/partials/input_group_clear.jade new file mode 100644 index 0000000..14ba1da --- /dev/null +++ b/omega-web/src/partials/input_group_clear.jade @@ -0,0 +1,7 @@ +.input-group + input.form-control(ng-model='model' ng-attr-type='{{type}}' + placeholder='{{placeholder}}' ng-change='modelChange()') + span.input-group-btn + button.btn.btn-default.input-group-clear-btn(ng-click='toggleClear()' ng-disabled='!model && !oldModel' + title="{{'inputClear_' + (oldModel ? 'restore' : 'clear') | tr}}") + span.glyphicon(ng-class='{"glyphicon-remove": !oldModel, "glyphicon-repeat": !!oldModel}') diff --git a/omega-web/src/partials/io.jade b/omega-web/src/partials/io.jade new file mode 100644 index 0000000..5ce9fe4 --- /dev/null +++ b/omega-web/src/partials/io.jade @@ -0,0 +1,31 @@ +.page-header + h2 {{'options_tab_importExport' | tr}} +section.settings-group + h3 {{'options_group_importExportProfile' | tr}} + p + button.btn.btn-default(ng-click='exportScript()') + span.glyphicon.glyphicon-download + = ' ' + | {{'options_exportPacFile' | tr}} + span.help-inline {{'options_exportPacFileHelp' | tr}} +section.settings-group + h3 {{'options_group_importExportSettings' | tr}} + p + button.btn.btn-default(ng-click='exportOptions()') + span.glyphicon.glyphicon-floppy-save + = ' ' + | {{'options_makeBackup' | tr}} + span.help-inline {{'options_makeBackupHelp' | tr}} + p + input#restore-local-file(type='file' omega-upload='restoreLocal($content)' omega-error='restoreLocalError($error)') + button.btn.btn-default(ng-click='triggerFileInput()') + span.glyphicon.glyphicon-folder-open + = ' ' + | {{'options_restoreLocal' | tr}} + span.help-inline {{'options_restoreLocalHelp' | tr}} + div + label {{'options_restoreOnline' | tr}} + .input-group.width-limit + input.form-control(type='url' ng-model='restoreOnlineUrl' placeholder="{{'options_restoreOnlinePlaceholder' | tr}}") + span.input-group-btn + button.btn.btn-default(ng-click='restoreOnline()') {{'options_restoreOnlineSubmit' | tr}} diff --git a/omega-web/src/partials/new_profile.jade b/omega-web/src/partials/new_profile.jade new file mode 100644 index 0000000..6fdfd70 --- /dev/null +++ b/omega-web/src/partials/new_profile.jade @@ -0,0 +1,49 @@ +form(ng-submit='newProfile.$valid && $close(profile)' name='newProfile') + .modal-header + button.close(type='button', data-dismiss='modal') + span(aria-hidden='true') × + span.sr-only Close + h4.modal-title {{'options_modalHeader_newProfile' | tr}} + .modal-body + .form-group(ng-class='{"has-error": !newProfile.profileNewName.$valid}') + label(for='profile-new-name') {{'options_newProfileName' | tr}} + input#profile-new-name.form-control(type='text' name='profileNewName' required ng-model='profile.name' + ui-validate='{conflict: "notConflict($value)"}') + .help-block(ng-show='newProfile.profileNewName.$error.required') {{'options_profileNameEmpty' | tr}} + .help-block(ng-show='newProfile.profileNewName.$error.conflict') {{'options_profileNameConflict' | tr}} + label {{'options_profileType' | tr}} + .radio + label + input(type='radio' name='profile-new-type' value='FixedProfile' ng-model='profile.profileType' ng-init='profile.profileType = "FixedProfile"') + span.profile-type + span.glyphicon.glyphicon-globe + = ' ' + span {{'options_profileTypeFixedProfile' | tr}} + .help-block {{'options_profileDescFixedProfile' | tr}} + .radio + label + input(type='radio', name='profile-new-type', value='SwitchProfile' ng-model='profile.profileType') + span.profile-type + span.glyphicon.glyphicon-retweet + = ' ' + span {{'options_profileTypeSwitchProfile' | tr}} + .help-block {{'options_profileDescSwitchProfile' | tr}} + .radio + label + input(type='radio', name='profile-new-type', value='PacProfile' ng-model='profile.profileType') + span.profile-type + span.glyphicon.glyphicon-file + = ' ' + span {{'options_profileTypePacProfile' | tr}} + .help-block {{'options_profileDescPacProfile' | tr}} + .radio + label + input(type='radio', name='profile-new-type', value='SwitchyRuleListProfile' ng-model='profile.profileType') + span.profile-type + span.glyphicon.glyphicon-list + = ' ' + span {{'options_profileTypeRuleListProfile' | tr}} + .help-block {{'options_profileDescRuleListProfile' | tr}} + .modal-footer + button.btn.btn-default(type='button' ng-click='$dismiss()') {{'dialog_cancel' | tr}} + button.btn.btn-primary(type='submit' ng-disabled='!newProfile.$valid') {{'options_createProfile' | tr}} diff --git a/omega-web/src/partials/profile.jade b/omega-web/src/partials/profile.jade new file mode 100644 index 0000000..67e0e78 --- /dev/null +++ b/omega-web/src/partials/profile.jade @@ -0,0 +1,20 @@ +.page-header + .profile-actions + button.btn.btn-default(ng-show='scriptable' ng-click='exportScript(profile.name)' title="{{'options_exportPacFileHelp' | tr}}") + span.glyphicon.glyphicon-download + = ' ' + | {{'options_profileExportPac' | tr}} + = ' ' + button.btn.btn-default(ng-click='renameProfile(profile.name)') + span.glyphicon.glyphicon-edit + = ' ' + | {{'options_renameProfile' | tr}} + = ' ' + button.btn.btn-danger(ng-click='deleteProfile(profile.name)') + span.glyphicon.glyphicon-trash + = ' ' + | {{'options_deleteProfile' | tr}} + span.profile-color-editor + x-spectrum-colorpicker(ng-model='profile.color' options='spectrumOptions') + h2.profile-name {{'options_profileTabPrefix' | tr}}{{profile.name}} +div(ng-include='profileTemplate') diff --git a/omega-web/src/partials/profile_fixed.jade b/omega-web/src/partials/profile_fixed.jade new file mode 100644 index 0000000..6ea6e7b --- /dev/null +++ b/omega-web/src/partials/profile_fixed.jade @@ -0,0 +1,43 @@ +div(ng-controller='FixedProfileCtrl') + section.settings-group + h3 {{'options_group_proxyServers' | tr}} + .table-responsive + table.fixed-servers.table.table-bordered.table-striped.width-limit-lg + thead + tr + th {{'options_proxy_scheme' | tr}} + th {{'options_proxy_protocol' | tr}} + th {{'options_proxy_server' | tr}} + th {{'options_proxy_port' | tr}} + tbody + tr(ng-repeat='scheme in urlSchemes' ng-show='scheme == "" || showAdvanced') + td {{schemeDisp[scheme] || ('options_scheme_default' | tr)}} + td + select.form-control(ng-model='proxyEditors[scheme].scheme') + option(value='') + | {{(scheme ? 'options_protocol_useDefault' : 'options_protocol_direct') | tr}} + option(value='http') HTTP + option(value='https') HTTPS + option(value='socks4') SOCKS4 + option(value='socks5') SOCKS5 + td(ng-if='proxyEditors[scheme].scheme') + input.form-control(type='text' ng-model='proxyEditors[scheme].host' required) + td(ng-if='!proxyEditors[scheme].scheme') + input.form-control(type='text' value='' placeholder='{{proxyEditors[""].host}}' disabled) + + td(ng-if='proxyEditors[scheme].scheme') + input.form-control(type='number' min='1' ng-model='proxyEditors[scheme].port' required) + td(ng-if='!proxyEditors[scheme].scheme') + input.form-control(type='number' value='' placeholder='{{proxyEditors[""].port}}' disabled) + tbody(ng-show='!showAdvanced') + tr.fixed-show-advanced + td(colspan='6') + button.btn.btn-link(ng-click='showAdvanced = true') + | #[span.glyphicon.glyphicon-chevron-down] {{'options_proxy_expand' | tr}} + section.settings-group + h3 {{'options_group_bypassList' | tr}} + p.help-block {{'options_bypassListHelp' | tr}} + p.help-block + a(href='https://developer.chrome.com/extensions/proxy#bypass_list' target='_blank') + | {{'options_bypassListHelpLinkText' | tr}} + textarea.form-control.width-limit(rows='10' ng-model='bypassList' ng-model-options="{updateOn:'blur'}") diff --git a/omega-web/src/partials/profile_pac.jade b/omega-web/src/partials/profile_pac.jade new file mode 100644 index 0000000..3df4033 --- /dev/null +++ b/omega-web/src/partials/profile_pac.jade @@ -0,0 +1,12 @@ +section.settings-group + h3 {{'options_group_pacUrl' | tr}} + .width-limit(input-group-clear type='url' model='profile.pacUrl') + p.help-block {{'options_pacUrlHelp' | tr}} +section.settings-group + h3 {{'options_group_pacScript' | tr}} + p + button.btn.btn-default(ng-disabled='!profile.pacUrl' ng-click='updateProfile(profile.name)' + ladda='updatingProfile[profile.name]' data-spinner-color="#000000") + | #[span.glyphicon.glyphicon-download-alt] {{'options_downloadProfileNow' | tr}} + textarea.form-control.width-limit(ng-model='profile.pacScript' rows=20 + ng-disabled='!!profile.pacUrl') diff --git a/omega-web/src/partials/profile_rule_list.jade b/omega-web/src/partials/profile_rule_list.jade new file mode 100644 index 0000000..f552117 --- /dev/null +++ b/omega-web/src/partials/profile_rule_list.jade @@ -0,0 +1,30 @@ +div(ng-controller='RuleListProfileCtrl') + section.settings-group + h3 {{'options_group_ruleListConfig' | tr}} + .form-group + label {{'options_ruleListMatchProfile' | tr}} + select.form-control.inline-form-control(ng-model='profile.matchProfileName' + ng-options='p.name as (p.name | dispName) for p in options | profiles:profile') + .form-group + label {{'options_ruleListDefaultProfile' | tr}} + select.form-control.inline-form-control(ng-model='profile.defaultProfileName' + ng-options='p.name as (p.name | dispName) for p in options | profiles:profile') + form.form-group + label {{'options_ruleListFormat' | tr}} + .radio.inline-form-control.no-min-width(ng-repeat='format in ruleListFormats') + label + input(type='radio' name='formatInput' value='{{format}}' ng-model='profile.format') + | {{'rulelistFormat_' + format | tr}} + section.settings-group + h3 {{'options_group_ruleListUrl' | tr}} + .width-limit(input-group-clear type='url' model='profile.sourceUrl') + p.help-block {{'options_ruleListUrlHelp' | tr}} + section.settings-group + h3 {{'options_group_ruleListText' | tr}} + p + button.btn.btn-default(ng-disabled='!profile.sourceUrl' ng-click='updateProfile(profile.name)' + ladda='updatingProfile[profile.name]' data-spinner-color="#000000") + | #[span.glyphicon.glyphicon-download-alt] {{'options_downloadProfileNow' | tr}} + textarea.form-control.width-limit(ng-model='profile.ruleList' rows=20 + ng-disabled='!!profile.sourceUrl') + diff --git a/omega-web/src/partials/profile_switch.jade b/omega-web/src/partials/profile_switch.jade new file mode 100644 index 0000000..b04daad --- /dev/null +++ b/omega-web/src/partials/profile_switch.jade @@ -0,0 +1,52 @@ +section.settings-group(ng-controller='SwitchProfileCtrl') + h3 {{'options_group_switchRules' | tr}} + .table-responsive + table.switch-rules.table.table-bordered.table-condensed.width-limit-xl + thead + tr + th(style='white-space: nowrap') {{'options_sort' | tr}} + th {{'options_conditionType' | tr}} + th {{'options_conditionDetails' | tr}} + th {{'options_resultProfile' | tr}} + th {{'options_conditionActions' | tr}} + tbody(ui-sortable='sortableOptions' ng-model='profile.rules') + tr(ng-repeat='rule in profile.rules') + td.sort-bar + span.glyphicon.glyphicon-sort + td + select.form-control(ng-model='rule.condition.conditionType' + ng-options='cond as (i18n | tr) for (cond, i18n) in conditionI18n') + td(ng-switch='rule.condition.conditionType') + span(ng-switch-when='AlwaysCondition') {{'condition_always_details' | tr}} + span(ng-switch-when='NeverCondition') {{'condition_never_details' | tr}} + span.host-levels-details(ng-switch-when='HostLevelsCondition') + input.form-control(type='number' min='1' max='99' ng-model='rule.condition.minValue' required) + = ' ' + span {{'options_hostLevelsBetween' | tr}} + = ' ' + input.form-control(type='number' max='99' min='1' ng-model='rule.condition.maxValue' required) + input.form-control(ng-model='rule.condition.pattern' ng-switch-default required) + td + select.form-control(ng-model='rule.profileName' + ng-options='p.name as (p.name | dispName) for p in options | profiles:profile') + td + button.btn.btn-danger.btn-sm(title="{{'options_deleteRule' | tr}}" ng-click='removeRule($index)') + span.glyphicon.glyphicon-trash + tbody + tr + td(style='border-right: none;') + td(style='border-left: none;', colspan='4') + button.btn.btn-default.btn-sm(ng-click='addRule()') + span.glyphicon.glyphicon-plus + = ' ' + span {{'options_addCondition' | tr}} + tbody + tr + td + td(colspan='2') {{'options_switchDefaultProfile' | tr}} + td + select.form-control(ng-model='profile.defaultProfileName' + ng-options='p.name as (p.name | dispName) for p in options | profiles:profile') + td + button.btn.btn-info.btn-sm(title="{{'options_resetRules_help' | tr}}" ng-click='resetRules()') + span.glyphicon.glyphicon-chevron-up diff --git a/omega-web/src/partials/profile_unsupported.jade b/omega-web/src/partials/profile_unsupported.jade new file mode 100644 index 0000000..3e6bf4d --- /dev/null +++ b/omega-web/src/partials/profile_unsupported.jade @@ -0,0 +1,3 @@ +.lead + | {{'options_profileUnsupported' | tr:profile.profileType}} +p {{'options_profileUnsupportedHelp' | tr}} diff --git a/omega-web/src/partials/rename_profile.jade b/omega-web/src/partials/rename_profile.jade new file mode 100644 index 0000000..8de645b --- /dev/null +++ b/omega-web/src/partials/rename_profile.jade @@ -0,0 +1,16 @@ +form(ng-submit='renameProfile.$valid && $close(newName)' name='renameProfile') + .modal-header + button.close(type='button', data-dismiss='modal') + span(aria-hidden='true') × + span.sr-only {{'dialog_close' | tr}} + h4.modal-title {{'options_modalHeader_renameProfile' | tr}} + .modal-body + .form-group(ng-class='{"has-error": !renameProfile.profileNewName.$valid}') + label(for='profile-new-name') {{'options_renameProfileName' | tr}} + input#profile-new-name.form-control(type='text' name='profileNewName' required ng-model='newName' + ui-validate='{conflict: "notConflict($value)"}' ng-init='newName = fromName') + .help-block(ng-show='renameProfile.profileNewName.$error.required') {{'options_profileNameEmpty' | tr}} + .help-block(ng-show='renameProfile.profileNewName.$error.conflict') {{'options_profileNameConflict' | tr}} + .modal-footer + button.btn.btn-default(type='button' ng-click='$dismiss()') {{'dialog_cancel' | tr}} + button.btn.btn-primary(type='submit' ng-disabled='!renameProfile.$valid') {{'options_renameProfile' | tr}} diff --git a/omega-web/src/partials/rule_remove_confirm.jade b/omega-web/src/partials/rule_remove_confirm.jade new file mode 100644 index 0000000..1437522 --- /dev/null +++ b/omega-web/src/partials/rule_remove_confirm.jade @@ -0,0 +1,19 @@ +.modal-header + button.close(type='button', data-dismiss='modal') + span(aria-hidden='true') × + span.sr-only {{'dialog_close' | tr}} + h4.modal-title {{'options_modalHeader_deleteRule' | tr}} +.modal-body + p {{'options_deleteRuleConfirm' | tr}} + div.well + span.label.label-info {{conditionI18n[rule.condition.conditionType]}} + = ' ' + | {{rule.condition.pattern}} + span.pull-right + span.glyphicon(class='{{profileIcons[ruleProfile.profileType]}}' + ng-style='{color: ruleProfile.color}') + = ' ' + | {{ruleProfile.name | dispName}} +.modal-footer + button.btn.btn-default(ng-click='$dismiss()') {{'dialog_cancel' | tr}} + button.btn.btn-danger(type='button' ng-click='$close("ok")') {{'options_deleteRule' | tr}} diff --git a/omega-web/src/partials/rule_reset_confirm.jade b/omega-web/src/partials/rule_reset_confirm.jade new file mode 100644 index 0000000..2f33d7c --- /dev/null +++ b/omega-web/src/partials/rule_reset_confirm.jade @@ -0,0 +1,15 @@ +.modal-header + button.close(type='button', data-dismiss='modal') + span(aria-hidden='true') × + span.sr-only {{'dialog_close' | tr}} + h4.modal-title {{'options_modalHeader_resetRules' | tr}} +.modal-body + p {{'options_resetRulesConfirm' | tr}} + .well + span.glyphicon(class='{{profileIcons[ruleProfile.profileType]}}' + ng-style='{color: ruleProfile.color}') + = ' ' + | {{ruleProfile.name | dispName}} +.modal-footer + button.btn.btn-default(ng-click='$dismiss()') {{'dialog_cancel' | tr}} + button.btn.btn-warning(type='button' ng-click='$close("ok")') {{'options_resetRules' | tr}} diff --git a/omega-web/src/partials/ui.jade b/omega-web/src/partials/ui.jade new file mode 100644 index 0000000..9faa8e4 --- /dev/null +++ b/omega-web/src/partials/ui.jade @@ -0,0 +1,42 @@ +.page-header + h2 {{'options_tab_ui' | tr}} +section.settings-group + h3 {{'options_group_miscOptions' | tr}} + div.checkbox + label + input(type='checkbox' ng-model='options["-confirmDeletion"]') + span {{'options_confirmDeletion' | tr}} + div.checkbox + label + input#refresh-on-profile-change(type='checkbox' ng-model='options["-refreshOnProfileChange"]') + span {{'options_refreshOnProfileChange' | tr}} +section.settings-group + h3 {{'options_group_switchOptions' | tr}} + div.form-group + label {{'options_startupProfile' | tr}} + select.form-control.inline-form-control(ng-model='options["-startupProfileName"]' + ng-options='p.name as (p.name | dispName) for p in options | profiles:"all"') + option(value='') {{'options_startupProfile_none' | tr}} + div.checkbox + label + input(type='checkbox' ng-model='options["-enableQuickSwitch"]') + span {{'options_quickSwitch' | tr}} + #quick-switch-settings.settings-group(ng-show='options["-enableQuickSwitch"]' ng-controller='QuickSwitchCtrl') + h4 {{'options_cycledProfiles' | tr}} + p.help-block {{'options_cycledProfilesHelp' | tr}} + div.has-error(ng-show='options["-quickSwitchProfiles"].length < 2') + p.help-block {{'options_cycledProfilesTooFew' | tr}} + ul.cycle-profile-container.cycle-enabled(ui-sortable="sortableOptions" + ng-model='options["-quickSwitchProfiles"]') + li(ng-repeat='name in options["-quickSwitchProfiles"]') + span.glyphicon(class='{{profileIcons[profileByName(name).profileType]}}' + ng-style='{color: profileByName(name).color}') + = ' ' + | {{name | dispName}} + h4 {{'options_notCycledProfiles' | tr}} + ul.cycle-profile-container(ui-sortable="sortableOptions" ng-model='notCycledProfiles') + li.bg-success(ng-repeat='name in notCycledProfiles') + span.glyphicon(class='{{profileIcons[profileByName(name).profileType]}}' + ng-style='{color: profileByName(name).color}') + = ' ' + | {{name | dispName}} diff --git a/omega-web/src/popup.jade b/omega-web/src/popup.jade new file mode 100644 index 0000000..da1fd36 --- /dev/null +++ b/omega-web/src/popup.jade @@ -0,0 +1,133 @@ +doctype html +// + Copyright (C) 2012-2013, The SwitchyOmega Authors. Please see the AUTHORS file + for details. + + This file is part of SwitchyOmega. + + SwitchyOmega is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SwitchyOmega is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SwitchyOmega. If not, see . +html(lang='en' ng-app='omegaPopup' ng-controller='PopupCtrl' ng-csp) + head + meta(charset='utf-8') + title {{'popup_title' | tr}} + link(rel='stylesheet' href='lib/bootstrap/css/bootstrap.min.css') + link(rel='stylesheet' href='css/popup.css') + body(ng-class='{"with-condition-form": showConditionForm}') + ul.nav.nav-pills.nav-stacked(ng-hide='showConditionForm || proxyNotControllable') + li.profile(ng-repeat='profile in builtinProfiles' ng-class='{active: isActive(profile.name), "bg-info": isEffective(profile.name)}') + a(ng-click='applyProfile(profile)') + span.glyphicon(ng-style='{color: profile.color}' class='{{getIcon(profile)}}') + = ' ' + | {{profile.name | dispName}} + li.profile.external-profile(ng-show='!!externalProfile' ng-class='{active: isActive(""), "bg-info": isEffective("")}') + a(ng-click='nameExternal.open = true') + form(name='nameExternalForm' ng-submit='nameExternalForm.$valid && saveExternal()') + span.glyphicon(ng-style='{color: externalProfile.color}' class='{{getIcon(externalProfile, "normal")}}') + = ' ' + span(ng-show='!nameExternal.open') {{'popup_externalProfile' | tr}} + input.form-control(ng-show='!!nameExternal.open' ng-model='externalProfile.name' ng-blur='nameExternalForm.submit()' + placeholder='{{"popup_externalProfileName" | tr}}' ui-validate='{conflict: "notConflict($value)"}') + li.divider + li.profile(ng-repeat='profile in customProfiles' ng-class='{active: isActive(profile.name), "bg-info": isEffective(profile.name)}') + a(ng-click='applyProfile(profile)') + span.glyphicon(ng-style='{color: profile.color}' class='{{getIcon(profile)}}') + = ' ' + | {{profile.name | dispName}} + li.external-profile(style='display: none') + a.form-group + span.glyphicon.glyphicon-question-sign + = ' ' + span {{'popup_externalProfile' | tr}} + input.external-profile-name.input-small(type='text' style='display: none' + placeholder='{{"popup_externalProfileName" | tr}}') + li.divider(ng-show='!!currentDomain') + li(ng-show='!!currentProfileCanAddRule') + a(ng-click='showConditionForm = true') + span.glyphicon.glyphicon-plus + = ' ' + span {{'popup_addCondition' | tr}} + li(ng-show='!!currentDomain' dropdown is-open="tempRuleMenu.open") + a.dropdown-toggle + span.glyphicon.glyphicon-filter + = ' ' + span.current-domain {{currentDomain}} + span.caret + ul.dropdown-menu + li(ng-repeat='profile in validResultProfiles' ng-class='{active: profile.name == currentTempRuleProfile}') + a(ng-click='addTempRule(currentDomain, profile.name)') + span.glyphicon(ng-style='{color: profile.color}' + class='{{profileIcons[profile.profileType]}}') + = ' ' + | {{profile.name | dispName}} + li.divider + li + a(ng-click='openOptions()') + span.glyphicon.glyphicon-wrench + = ' ' + span {{'popup_showOptions' | tr}} + li + a.report-issue(href='https://github.com/FelisCatus/SwitchyOmega/issues' target='_blank') + span.glyphicon.glyphicon-comment + = ' ' + span {{'popup_reportIssues' | tr}} + li + a.error-log + span.glyphicon.glyphicon-download + = ' ' + span {{'popup_errorLog' | tr}} + form.condition-form(name='conditionForm' style='display: none;' + ng-style='{display: showConditionForm ? "block" : "none"}' + ng-submit='addCondition(rule.condition, rule.profileName)') + fieldset + legend + | {{'popup_addConditionTo' | tr}} + = ' ' + span.profile-inline + span.glyphicon(ng-style='{color: currentProfile.color}' + class='{{profileIcons[currentProfile.profileType]}}') + = ' ' + | {{currentProfileName | dispName}} + div.form-group + label {{'options_conditionType' | tr}} + select.form-control(ng-model='rule.condition.conditionType') + option(value='HostWildcardCondition') {{'condition_hostWildcard' | tr}} + option(value='HostRegexCondition') {{'condition_hostRegex' | tr}} + option(value='UrlWildcardCondition') {{'condition_urlWildcard' | tr}} + option(value='UrlRegexCondition') {{'condition_urlRegex' | tr}} + option(value='KeywordCondition') {{'condition_keyword' | tr}} + div.form-group + label {{'options_conditionDetails' | tr}} + input.form-control.condition-details(type='text' required ng-model='rule.condition.pattern') + div.form-group + label {{'options_resultProfile' | tr}} + select.form-control(required ng-model="rule.profileName" + ng-options='profile.name as (profile.name | dispName) for profile in validResultProfiles') + div.condition-controls + button.btn.btn-default(type='button' ng-click='showConditionForm = false') + | {{'dialog_cancel' | tr}} + button.btn.btn-primary(type='submit' ng-disabled='conditionForm.$invalid') {{'popup_addCondition' | tr}} + div.proxy-not-controllable(ng-show='proxyNotControllable') + p.text-danger {{'popup_proxyNotControllable_' + proxyNotControllable | tr}} + p.help-block {{'popup_proxyNotControllableDetails' | tr}} + + script(src='js/log_error.js') + script(src='lib/FileSaver/FileSaver.js') + script(src='js/omega_target_web_basics.js') + script(src='js/popup_basics.js') + script(src='lib/angular/angular.min.js') + script(src='lib/angular-bootstrap/ui-bootstrap-tpls.min.js') + script(src='lib/angular-ui-utils/validate.min.js') + script(src='js/omega_target_web.js') + script(src='js/omega_decoration.js') + script(src='js/popup.js')