Browse Source

Initial commit.

FelisCatus 11 years ago
commit
f67fe00aef
100 changed files with 7990 additions and 0 deletions
  1. 2 0
      .gitignore
  2. 9 0
      .tern-project
  3. 12 0
      AUTHORS
  4. 674 0
      COPYING
  5. 22 0
      omega-build/Gruntfile.coffee
  6. 13 0
      omega-build/package.json
  7. 551 0
      omega-i18n/en/messages.json
  8. 551 0
      omega-i18n/zh_CN/messages.json
  9. 2 0
      omega-pac/.gitignore
  10. 1 0
      omega-pac/Gruntfile.coffee
  11. 6 0
      omega-pac/grunt/aliases.coffee
  12. 28 0
      omega-pac/grunt/browserify.coffee
  13. 20 0
      omega-pac/grunt/coffeelint.coffee
  14. 6 0
      omega-pac/grunt/mochaTest.coffee
  15. 10 0
      omega-pac/grunt/watch.coffee
  16. 9 0
      omega-pac/index.coffee
  17. 27 0
      omega-pac/package.json
  18. 440 0
      omega-pac/src/conditions.coffee
  19. 141 0
      omega-pac/src/pac_generator.coffee
  20. 389 0
      omega-pac/src/profiles.coffee
  21. 91 0
      omega-pac/src/rule_list.coffee
  22. 51 0
      omega-pac/src/shexp_utils.coffee
  23. 34 0
      omega-pac/src/utils.coffee
  24. 183 0
      omega-pac/test/conditions.coffee
  25. 56 0
      omega-pac/test/pac_generator.coffee
  26. 199 0
      omega-pac/test/profiles.coffee
  27. 236 0
      omega-pac/test/rule_list.coffee
  28. 15 0
      omega-pac/test/shexp_utils.coffee
  29. 2 0
      omega-pac/uglifyjs-shim.js
  30. 1090 0
      omega-pac/uglifyjs.js
  31. 4 0
      omega-target-chromium-extension/.gitignore
  32. 1 0
      omega-target-chromium-extension/Gruntfile.coffee
  33. 196 0
      omega-target-chromium-extension/background.coffee
  34. 8 0
      omega-target-chromium-extension/grunt/aliases.coffee
  35. 28 0
      omega-target-chromium-extension/grunt/browserify.coffee
  36. 10 0
      omega-target-chromium-extension/grunt/coffee.coffee
  37. 20 0
      omega-target-chromium-extension/grunt/coffeelint.coffee
  38. 28 0
      omega-target-chromium-extension/grunt/copy.coffee
  39. 6 0
      omega-target-chromium-extension/grunt/mochaTest.coffee
  40. 25 0
      omega-target-chromium-extension/grunt/watch.coffee
  41. 8 0
      omega-target-chromium-extension/index.coffee
  42. 1 0
      omega-target-chromium-extension/omega_target_shim.js
  43. 105 0
      omega-target-chromium-extension/omega_target_web.coffee
  44. 11 0
      omega-target-chromium-extension/omega_target_web_basics.coffee
  45. 17 0
      omega-target-chromium-extension/overlay/background.html
  46. 2 0
      omega-target-chromium-extension/overlay/js/background_preload.js
  47. 35 0
      omega-target-chromium-extension/overlay/manifest.json
  48. 31 0
      omega-target-chromium-extension/package.json
  49. 19 0
      omega-target-chromium-extension/src/chrome_api.coffee
  50. 217 0
      omega-target-chromium-extension/src/options.coffee
  51. 110 0
      omega-target-chromium-extension/src/parse_external_profile.coffee
  52. 63 0
      omega-target-chromium-extension/src/storage.coffee
  53. 60 0
      omega-target-chromium-extension/src/tabs.coffee
  54. 146 0
      omega-target-chromium-extension/src/upgrade.coffee
  55. 2 0
      omega-target/.gitignore
  56. 1 0
      omega-target/Gruntfile.coffee
  57. 6 0
      omega-target/grunt/aliases.coffee
  58. 28 0
      omega-target/grunt/browserify.coffee
  59. 20 0
      omega-target/grunt/coffeelint.coffee
  60. 6 0
      omega-target/grunt/mochaTest.coffee
  61. 10 0
      omega-target/grunt/watch.coffee
  62. 9 0
      omega-target/index.coffee
  63. 1 0
      omega-target/omega_pac_shim.js
  64. 30 0
      omega-target/package.json
  65. 50 0
      omega-target/src/browser_storage.coffee
  66. 43 0
      omega-target/src/default_options.coffee
  67. 61 0
      omega-target/src/log.coffee
  68. 619 0
      omega-target/src/options.coffee
  69. 59 0
      omega-target/src/storage.coffee
  70. 1 0
      omega-target/src/utils.coffee
  71. 193 0
      omega-target/test/conditions.coffee
  72. 56 0
      omega-target/test/pac_generator.coffee
  73. 198 0
      omega-target/test/profiles.coffee
  74. 211 0
      omega-target/test/rule_list.coffee
  75. 15 0
      omega-target/test/shexp_utils.coffee
  76. 2 0
      omega-web/.gitignore
  77. 1 0
      omega-web/Gruntfile.coffee
  78. 107 0
      omega-web/bower.json
  79. 12 0
      omega-web/grunt/aliases.coffee
  80. 8 0
      omega-web/grunt/autoprefixer.coffee
  81. 5 0
      omega-web/grunt/bower.coffee
  82. 10 0
      omega-web/grunt/coffee.coffee
  83. 20 0
      omega-web/grunt/coffeelint.coffee
  84. 14 0
      omega-web/grunt/copy.coffee
  85. 18 0
      omega-web/grunt/jade.coffee
  86. 7 0
      omega-web/grunt/less.coffee
  87. 6 0
      omega-web/grunt/mochaTest.coffee
  88. 6 0
      omega-web/grunt/ngAnnotate.coffee
  89. 35 0
      omega-web/grunt/watch.coffee
  90. BIN
      omega-web/img/icons/omega-128.png
  91. BIN
      omega-web/img/icons/omega-16.png
  92. BIN
      omega-web/img/icons/omega-32.png
  93. BIN
      omega-web/img/icons/omega-48.png
  94. BIN
      omega-web/img/icons/omega-64.png
  95. 14 0
      omega-web/img/icons/omega.svg
  96. 37 0
      omega-web/img/icons/omega_svg.js
  97. 5 0
      omega-web/lib/jquery-ui-1.10.4.custom.min.js
  98. 25 0
      omega-web/package.json
  99. 8 0
      omega-web/src/coffee/log_error.coffee
  100. 10 0
      omega-web/src/coffee/omega_decoration.coffee

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+node_modules
+bower_components

+ 9 - 0
.tern-project

@@ -0,0 +1,9 @@
+{
+  "libs": [
+    "chai"
+  ],
+  "plugins": {
+    "node": {},
+    "coffee": {}
+  }
+}

+ 12 - 0
AUTHORS

@@ -0,0 +1,12 @@
+# SwitchyOmega authors:
+
+FelisCatus <[email protected]>
+
+# 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.

+ 674 - 0
COPYING

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ 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.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 <http://www.gnu.org/licenses/>.
+
+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:
+
+    <program>  Copyright (C) <year>  <name of author>
+    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
+<http://www.gnu.org/licenses/>.
+
+  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
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.

+ 22 - 0
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']

+ 13 - 0
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);"
+  }
+}

+ 551 - 0
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."
+  }
+}

+ 551 - 0
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": "在图标悬停提示上显示临时规则的前缀。文字应该非常短。"
+  }
+}

+ 2 - 0
omega-pac/.gitignore

@@ -0,0 +1,2 @@
+/index.js
+/omega_pac.min.js

+ 1 - 0
omega-pac/Gruntfile.coffee

@@ -0,0 +1 @@
+module.exports = require('load-grunt-config')

+ 6 - 0
omega-pac/grunt/aliases.coffee

@@ -0,0 +1,6 @@
+module.exports =
+  default: [
+    'coffeelint'
+    'browserify'
+  ]
+  test: ['mochaTest']

+ 28 - 0
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'

+ 20 - 0
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']

+ 6 - 0
omega-pac/grunt/mochaTest.coffee

@@ -0,0 +1,6 @@
+module.exports =
+  test:
+    options:
+      reporter: 'spec'
+      require: 'coffee-script/register'
+    src: ['test/**/*.coffee']

+ 10 - 0
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']

+ 9 - 0
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

+ 27 - 0
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"
+  }
+}

+ 440 - 0
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 == '<local>'
+          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 == '<local>'
+            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 == '<local>'
+          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

+ 141 - 0
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

+ 389 - 0
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: '<local>'
+        }]
+      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

+ 91 - 0
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

+ 51 - 0
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

+ 34 - 0
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

+ 183 - 0
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')

+ 56 - 0
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: '<local>'}
+      ]
+
+  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')

+ 199 - 0
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: '<local>'
+      }]
+      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'))

+ 236 - 0
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/*'
+      )

+ 15 - 0
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\\/'

+ 2 - 0
omega-pac/uglifyjs-shim.js

@@ -0,0 +1,2 @@
+require('uglify-js-real');
+module.exports = UglifyJS;

File diff suppressed because it is too large
+ 1090 - 0
omega-pac/uglifyjs.js


+ 4 - 0
omega-target-chromium-extension/.gitignore

@@ -0,0 +1,4 @@
+/index.js
+/omega_target_*.min.js
+
+/build

+ 1 - 0
omega-target-chromium-extension/Gruntfile.coffee

@@ -0,0 +1 @@
+module.exports = require('load-grunt-config')

+ 196 - 0
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

+ 8 - 0
omega-target-chromium-extension/grunt/aliases.coffee

@@ -0,0 +1,8 @@
+module.exports =
+  default: [
+    'coffeelint'
+    'browserify'
+    'coffee'
+    'copy'
+  ]
+  test: ['mochaTest']

+ 28 - 0
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'

+ 10 - 0
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'

+ 20 - 0
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']

+ 28 - 0
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/'

+ 6 - 0
omega-target-chromium-extension/grunt/mochaTest.coffee

@@ -0,0 +1,6 @@
+module.exports =
+  test:
+    options:
+      reporter: 'spec'
+      require: 'coffee-script/register'
+    src: ['test/**/*.coffee']

+ 25 - 0
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']

+ 8 - 0
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

+ 1 - 0
omega-target-chromium-extension/omega_target_shim.js

@@ -0,0 +1 @@
+module.exports = OmegaTarget

+ 105 - 0
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

+ 11 - 0
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)

+ 17 - 0
omega-target-chromium-extension/overlay/background.html

@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8" />
+  <title>SwitchyOmega Background</title>
+</head>
+<body>
+  <canvas id="canvas-icon"></canvas>
+  <script src="js/log_error.js"></script>
+  <script src="js/background_preload.js"></script>
+  <script src="js/omega_pac.min.js"></script>
+  <script src="js/omega_target.min.js"></script>
+  <script src="js/omega_target_chromium_extension.min.js"></script>
+  <script src="img/icons/omega_svg.js"></script>
+  <script src="js/background.js"></script>
+</body>
+</html>

+ 2 - 0
omega-target-chromium-extension/overlay/js/background_preload.js

@@ -0,0 +1,2 @@
+window.UglifyJS_NoUnsafeEval = true
+localStorage['log'] = ''

+ 35 - 0
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://*/*",
+    "<all_urls>"
+  ]
+}

+ 31 - 0
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"
+  }
+}

+ 19 - 0
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})

+ 217 - 0
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
+

+ 110 - 0
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['<local>']
+          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

+ 63 - 0
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

+ 60 - 0
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

+ 146 - 0
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 == '<local>'
+              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

+ 2 - 0
omega-target/.gitignore

@@ -0,0 +1,2 @@
+/index.js
+/omega_target.min.js

+ 1 - 0
omega-target/Gruntfile.coffee

@@ -0,0 +1 @@
+module.exports = require('load-grunt-config')

+ 6 - 0
omega-target/grunt/aliases.coffee

@@ -0,0 +1,6 @@
+module.exports =
+  default: [
+    'coffeelint'
+    'browserify'
+  ]
+  test: ['mochaTest']

+ 28 - 0
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'

+ 20 - 0
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']

+ 6 - 0
omega-target/grunt/mochaTest.coffee

@@ -0,0 +1,6 @@
+module.exports =
+  test:
+    options:
+      reporter: 'spec'
+      require: 'coffee-script/register'
+    src: ['test/**/*.coffee']

+ 10 - 0
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']

+ 9 - 0
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

+ 1 - 0
omega-target/omega_pac_shim.js

@@ -0,0 +1 @@
+module.exports = OmegaPac;

+ 30 - 0
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"
+  }
+}

+ 50 - 0
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

+ 43 - 0
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: "<local>"
+      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"

+ 61 - 0
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
+        "<f: #{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

+ 619 - 0
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: -> "<Options>"
+
+  ###*
+  # 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<OmegaOptions>} 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<OmegaOptions>} 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.<string, {}>} 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<Object.<string,({}|Error)>>} 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<String>} 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<OmegaOptions>} 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<?String>} 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.<String,{}>} 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

+ 59 - 0
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.<string,{}>)} keys The keys to retrive,
+  # or null for all.
+  # @returns {Promise<(Object.<string, {}>)>} 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.<string,{}>)} items A map from key to value to set.
+  # @returns {Promise<(Object.<string, {}>)>} 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.<string, {}>} 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

+ 1 - 0
omega-target/src/utils.coffee

@@ -0,0 +1 @@
+exports.Promise = require('bluebird')

+ 193 - 0
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')

+ 56 - 0
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: '<local>'}
+      ]
+
+  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')

+ 198 - 0
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')

+ 211 - 0
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/*'
+      )

+ 15 - 0
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\\/'

+ 2 - 0
omega-web/.gitignore

@@ -0,0 +1,2 @@
+/build
+/tmp

+ 1 - 0
omega-web/Gruntfile.coffee

@@ -0,0 +1 @@
+module.exports = require('load-grunt-config')

+ 107 - 0
omega-web/bower.json

@@ -0,0 +1,107 @@
+{
+  "name": "switchyomega",
+  "version": "0.0.1",
+  "authors": [
+    "FelisCatus <[email protected]>"
+  ],
+  "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"]
+    }
+  }
+}

+ 12 - 0
omega-web/grunt/aliases.coffee

@@ -0,0 +1,12 @@
+module.exports =
+  default: [
+    'copy'
+    'jade'
+    'less'
+    'autoprefixer'
+    'coffeelint'
+    'coffee'
+    'ngAnnotate'
+    'bower'
+  ]
+  test: ['mochaTest']

+ 8 - 0
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/'

+ 5 - 0
omega-web/grunt/bower.coffee

@@ -0,0 +1,5 @@
+module.exports =
+  install:
+    options:
+      targetDir: 'build/lib/'
+      layout: 'byComponent'

+ 10 - 0
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'

+ 20 - 0
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']

+ 14 - 0
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/'

+ 18 - 0
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']
+      }
+    ]

+ 7 - 0
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'

+ 6 - 0
omega-web/grunt/mochaTest.coffee

@@ -0,0 +1,6 @@
+module.exports =
+  test:
+    options:
+      reporter: 'spec'
+      require: 'coffee-script/register'
+    src: ['test/**/*.coffee']

+ 6 - 0
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'

+ 35 - 0
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']

BIN
omega-web/img/icons/omega-128.png


BIN
omega-web/img/icons/omega-16.png


BIN
omega-web/img/icons/omega-32.png


BIN
omega-web/img/icons/omega-48.png


BIN
omega-web/img/icons/omega-64.png


+ 14 - 0
omega-web/img/icons/omega.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg
+   xmlns="http://www.w3.org/2000/svg"
+   width="19"
+   height="19"
+   version="1.1">
+  <path
+     d="m 14.045687,9.5001002 c 0,2.4548268 -2.035174,4.4448578 -4.5456868,4.4448578 -2.5105136,0 -4.545687,-1.990031 -4.545687,-4.4448578 0,-2.4548275 2.0351734,-4.4448583 4.545687,-4.4448583 2.5105128,0 4.5456868,1.9900308 4.5456868,4.4448583 z m 3.993259,-1.001e-4 c 0,4.6113259 -3.823017,8.3495419 -8.5389459,8.3495419 -4.7159298,0 -8.5389462,-3.738217 -8.5389462,-8.3495419 0,-4.6113251 3.8230164,-8.3495426 8.5389462,-8.3495426 4.7159289,0 8.5389459,3.7382175 8.5389459,8.3495426 z"
+     fill-rule="evenodd"
+     class="profile-color"
+     fill="#31afec"
+     stroke="none"
+     id="profile-circle" />
+</svg>

+ 37 - 0
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;
+};

File diff suppressed because it is too large
+ 5 - 0
omega-web/lib/jquery-ui-1.10.4.custom.min.js


+ 25 - 0
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"
+  }
+}

+ 8 - 0
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

+ 10 - 0
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',
+})

Some files were not shown because too many files changed in this diff