Bug 550569 - Integrate BrowserScope's HTML editor test suite with Mozilla's mochitest framework; r=bzbarsky a=NPOTB
authorEhsan Akhgari <ehsan@mozilla.com>
Mon, 08 Mar 2010 17:36:42 -0500
changeset 26834 abdff2dd72b4c1b71f892f21ffca011bc33a44bb
parent 26833 5d3418e69ab0f0964b17531c80859ce904f70d68
child 26835 0e49d81085b510cf1ec6d6986036f40a3d2ddfc8
push id2307
push usereakhgari@mozilla.com
push dateTue, 09 Mar 2010 19:20:10 +0000
reviewersbzbarsky, NPOTB
bugs550569
milestone1.9.1.9pre
Bug 550569 - Integrate BrowserScope's HTML editor test suite with Mozilla's mochitest framework; r=bzbarsky a=NPOTB
editor/libeditor/html/tests/Makefile.in
editor/libeditor/html/tests/browserscope/lib/browserscope/LICENSE
editor/libeditor/html/tests/browserscope/lib/browserscope/README
editor/libeditor/html/tests/browserscope/lib/browserscope/README.Mozilla
editor/libeditor/html/tests/browserscope/lib/browserscope/currentStatus.js
editor/libeditor/html/tests/browserscope/lib/browserscope/current_revision
editor/libeditor/html/tests/browserscope/lib/browserscope/richtext/editable.html
editor/libeditor/html/tests/browserscope/lib/browserscope/richtext/js/range.js
editor/libeditor/html/tests/browserscope/lib/browserscope/richtext/richtext.html
editor/libeditor/html/tests/browserscope/lib/browserscope/update_from_upstream
editor/libeditor/html/tests/browserscope/test_browserscope.html
--- a/editor/libeditor/html/tests/Makefile.in
+++ b/editor/libeditor/html/tests/Makefile.in
@@ -48,9 +48,10 @@ include $(topsrcdir)/config/rules.mk
 		test_bug366682.html \
 		test_bug432225.html \
 		test_bug456244.html \
 		test_bug487524.html \
 		$(NULL)
 
 libs:: $(_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
+	(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - browserscope) | (cd $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir) && tar -xf -)
 
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/html/tests/browserscope/lib/browserscope/LICENSE
@@ -0,0 +1,202 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/html/tests/browserscope/lib/browserscope/README
@@ -0,0 +1,58 @@
+README FOR BROWSERSCOPE
+-----------------------
+
+Hey there - thanks for downloading the code. This file has instructions
+for getting setup so that you can run the codebase locally.
+
+This project is built on Google App Engine using the
+Django web application framework and written in Python.
+
+To get started, you'll need to first download the App Engine SDK at:
+http://code.google.com/appengine/downloads.html
+
+For local development, just startup the server:
+./pathto/google_appengine/dev_appserver.py --port=8080 browserscope
+
+You should then be able to access the local application at:
+http://localhost:8080/
+
+Note: the first time you hit the homepage it may take a little
+while - that's because it's trying to read out median times for all
+of the tests from a non-existent datastore and write to memcache.
+Just be a lil patient.
+
+You can run the unit tests at:
+  http://localhost:8080/test
+
+
+CONTRIBUTING
+------------------
+
+Most likely you are interested in adding new tests or creating
+a new test category. If you are interested in adding tests to an existing
+"category" you may want to get in touch with the maintainer for that
+branch of the tree. We are really looking forward to receiving your
+code in patch format. Currently the category maintainers are:
+Network: Steve Souders <souders@gmail.com>
+Reflow: Lindsey Simon <elsigh@gmail.com>
+Security: Adam Barth <adam@adambarth.com> and Collin Jackson <collin@collinjackson.com>
+
+
+To create a completely new test category:
+  * Copy one of the existing directories in categories/
+  * Edit your test_set.py, handlers.py
+  * Add your files in templates/ and static/
+  * Update urls.py and settings.CATEGORIES
+  * Follow the examples of other tests re:
+      * beaconing using/testdriver_base
+      * your GetScoreAndDisplayValue method
+      * your GetRowScoreAndDisplayValue method
+
+References:
+  * App Engine Docs - http://code.google.com/appengine/docs/python/overview.html
+  * App Engine Group - http://groups.google.com/group/google-appengine
+  * Python Docs - http://www.python.org/doc/
+  * Django - http://www.djangoproject.com/
+
+
+
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/html/tests/browserscope/lib/browserscope/README.Mozilla
@@ -0,0 +1,17 @@
+The BrowserScope project provides a set of cross-browser HTML editor tests,
+which we import in our test suite in order to run them as part of our
+continuous integration system.
+
+We pull tests occasionally from their Subversion repository using the pull
+script which can be found in this directory.  We also record the revision ID
+which we've used in the current_revision file inside this directory.
+
+Using the pull script is quite easy, just switch to this directory, and say:
+
+sh update_from_upstream
+
+There are tests which we're currently failing on, and there will probably be
+more of those in the future.  We should maintain a list of the failing tests
+manually in currentStatus.js (which can also be found in this directory), to
+make sure that the suite passes entirely, with failing tests marked as todo
+items.
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/html/tests/browserscope/lib/browserscope/currentStatus.js
@@ -0,0 +1,64 @@
+/**
+ * This file lists the tests in the BrowserScope suite which we are currently
+ * failing.  We mark them as todo items to keep track of them.
+ */
+
+var knownFailures = {
+  // Dummy result items.  There is one for each category.
+  'apply' : {
+    '0-undefined' : true
+  },
+  'unapply' : {
+    '0-undefined' : true
+  },
+  'change' : {
+    '0-undefined' : true
+  },
+  'query' : {
+    '0-undefined' : true
+  },
+  'a' : {
+    'backcolor-0' : true,
+    'backcolor-1' : true,
+    'createbookmark-0' : true,
+    'fontsize-1' : true,
+    'hilitecolor-0' : true,
+    'subscript-1' : true,
+    'superscript-1' : true,
+  },
+  'u': {
+    'bold-1' : true,
+    'italic-1' : true,
+    'removeformat-1' : true,
+    'removeformat-2' : true,
+    'strikethrough-1' : true,
+    'strikethrough-2' : true,
+    'subscript-1' : true,
+    'superscript-1' : true,
+    'unbookmark-0' : true,
+  },
+  'q': {
+    'backcolor-0' : true,
+    'backcolor-1' : true,
+    'backcolor-2' : true,
+    'fontsize-1' : true,
+    'fontsize-2' : true,
+    'underline-5' : true,
+  },
+  'c': {
+    'backcolor-0' : true,
+    'backcolor-1' : true,
+    'backcolor-2' : true,
+    'fontname-0' : true,
+    'fontname-2' : true,
+    'fontname-3' : true,
+    'fontsize-1' : true,
+    'fontsize-2' : true,
+    'forecolor-0' : true,
+    'forecolor-2' : true,
+  },
+};
+
+function isKnownFailure(type, test, param) {
+  return (type in knownFailures) && ((test + "-" + param) in knownFailures[type]);
+}
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/html/tests/browserscope/lib/browserscope/current_revision
@@ -0,0 +1,1 @@
+547
new file mode 100755
--- /dev/null
+++ b/editor/libeditor/html/tests/browserscope/lib/browserscope/richtext/editable.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+  <script>
+    function load(){
+      window.document.designMode = "On";
+    }
+  </script>
+</head>
+<body contentEditable="true" onload="load()">
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/html/tests/browserscope/lib/browserscope/richtext/js/range.js
@@ -0,0 +1,1069 @@
+var goog$global = this, goog$isString = function(val) {
+  return typeof val == "string"
+};
+Math.floor(Math.random() * 2147483648).toString(36);
+var goog$now = Date.now || function() {
+  return(new Date).getTime()
+}, goog$inherits = function(childCtor, parentCtor) {
+  function tempCtor() {
+  }
+  tempCtor.prototype = parentCtor.prototype;
+  childCtor.superClass_ = parentCtor.prototype;
+  childCtor.prototype = new tempCtor
+};var goog$array$peek = function(array) {
+  return array[array.length - 1]
+}, goog$array$indexOf = function(arr, obj, opt_fromIndex) {
+  if(arr.indexOf)return arr.indexOf(obj, opt_fromIndex);
+  if(Array.indexOf)return Array.indexOf(arr, obj, opt_fromIndex);
+  for(var fromIndex = opt_fromIndex == null ? 0 : opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex, i = fromIndex;i < arr.length;i++)if(i in arr && arr[i] === obj)return i;
+  return-1
+}, goog$array$map = function(arr, f, opt_obj) {
+  if(arr.map)return arr.map(f, opt_obj);
+  if(Array.map)return Array.map(arr, f, opt_obj);
+  for(var l = arr.length, res = [], resLength = 0, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2)res[resLength++] = f.call(opt_obj, arr2[i], i, arr);
+  return res
+}, goog$array$some = function(arr, f, opt_obj) {
+  if(arr.some)return arr.some(f, opt_obj);
+  if(Array.some)return Array.some(arr, f, opt_obj);
+  for(var l = arr.length, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2 && f.call(opt_obj, arr2[i], i, arr))return true;
+  return false
+}, goog$array$every = function(arr, f, opt_obj) {
+  if(arr.every)return arr.every(f, opt_obj);
+  if(Array.every)return Array.every(arr, f, opt_obj);
+  for(var l = arr.length, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2 && !f.call(opt_obj, arr2[i], i, arr))return false;
+  return true
+}, goog$array$find = function(arr, f, opt_obj) {
+  var i;
+  JSCompiler_inline_label_goog$array$findIndex_12: {
+    for(var JSCompiler_inline_l = arr.length, JSCompiler_inline_arr2 = goog$isString(arr) ? arr.split("") : arr, JSCompiler_inline_i = 0;JSCompiler_inline_i < JSCompiler_inline_l;JSCompiler_inline_i++)if(JSCompiler_inline_i in JSCompiler_inline_arr2 && f.call(opt_obj, JSCompiler_inline_arr2[JSCompiler_inline_i], JSCompiler_inline_i, arr)) {
+      i = JSCompiler_inline_i;
+      break JSCompiler_inline_label_goog$array$findIndex_12
+    }i = -1
+  }return i < 0 ? null : goog$isString(arr) ? arr.charAt(i) : arr[i]
+};var goog$string$trim = function(str) {
+  return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, "")
+}, goog$string$htmlEscape = function(str, opt_isLikelyToContainHtmlChars) {
+  if(opt_isLikelyToContainHtmlChars)return str.replace(goog$string$amperRe_, "&amp;").replace(goog$string$ltRe_, "&lt;").replace(goog$string$gtRe_, "&gt;").replace(goog$string$quotRe_, "&quot;");
+  else {
+    if(!goog$string$allRe_.test(str))return str;
+    if(str.indexOf("&") != -1)str = str.replace(goog$string$amperRe_, "&amp;");
+    if(str.indexOf("<") != -1)str = str.replace(goog$string$ltRe_, "&lt;");
+    if(str.indexOf(">") != -1)str = str.replace(goog$string$gtRe_, "&gt;");
+    if(str.indexOf('"') != -1)str = str.replace(goog$string$quotRe_, "&quot;");
+    return str
+  }
+}, goog$string$amperRe_ = /&/g, goog$string$ltRe_ = /</g, goog$string$gtRe_ = />/g, goog$string$quotRe_ = /\"/g, goog$string$allRe_ = /[&<>\"]/, goog$string$contains = function(s, ss) {
+  return s.indexOf(ss) != -1
+}, goog$string$compareVersions = function(version1, version2) {
+  for(var order = 0, v1Subs = goog$string$trim(String(version1)).split("."), v2Subs = goog$string$trim(String(version2)).split("."), subCount = Math.max(v1Subs.length, v2Subs.length), subIdx = 0;order == 0 && subIdx < subCount;subIdx++) {
+    var v1Sub = v1Subs[subIdx] || "", v2Sub = v2Subs[subIdx] || "", v1CompParser = new RegExp("(\\d*)(\\D*)", "g"), v2CompParser = new RegExp("(\\d*)(\\D*)", "g");
+    do {
+      var v1Comp = v1CompParser.exec(v1Sub) || ["", "", ""], v2Comp = v2CompParser.exec(v2Sub) || ["", "", ""];
+      if(v1Comp[0].length == 0 && v2Comp[0].length == 0)break;
+      var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10), v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10);
+      order = goog$string$compareElements_(v1CompNum, v2CompNum) || goog$string$compareElements_(v1Comp[2].length == 0, v2Comp[2].length == 0) || goog$string$compareElements_(v1Comp[2], v2Comp[2])
+    }while(order == 0)
+  }return order
+}, goog$string$compareElements_ = function(left, right) {
+  if(left < right)return-1;
+  else if(left > right)return 1;
+  return 0
+};
+goog$now();var goog$userAgent$detectedOpera_, goog$userAgent$detectedIe_, goog$userAgent$detectedWebkit_, goog$userAgent$detectedMobile_, goog$userAgent$detectedGecko_, goog$userAgent$detectedCamino_, goog$userAgent$detectedMac_, goog$userAgent$detectedWindows_, goog$userAgent$detectedLinux_, goog$userAgent$detectedX11_, goog$userAgent$getUserAgentString = function() {
+  return goog$global.navigator ? goog$global.navigator.userAgent : null
+}, goog$userAgent$getNavigator = function() {
+  return goog$global.navigator
+};
+goog$userAgent$detectedCamino_ = goog$userAgent$detectedGecko_ = goog$userAgent$detectedMobile_ = goog$userAgent$detectedWebkit_ = goog$userAgent$detectedIe_ = goog$userAgent$detectedOpera_ = false;
+var JSCompiler_inline_ua_15;
+if(JSCompiler_inline_ua_15 = goog$userAgent$getUserAgentString()) {
+  var JSCompiler_inline_navigator$$1_16 = goog$userAgent$getNavigator();
+  goog$userAgent$detectedOpera_ = JSCompiler_inline_ua_15.indexOf("Opera") == 0;
+  goog$userAgent$detectedIe_ = !goog$userAgent$detectedOpera_ && JSCompiler_inline_ua_15.indexOf("MSIE") != -1;
+  goog$userAgent$detectedMobile_ = (goog$userAgent$detectedWebkit_ = !goog$userAgent$detectedOpera_ && JSCompiler_inline_ua_15.indexOf("WebKit") != -1) && JSCompiler_inline_ua_15.indexOf("Mobile") != -1;
+  goog$userAgent$detectedCamino_ = (goog$userAgent$detectedGecko_ = !goog$userAgent$detectedOpera_ && !goog$userAgent$detectedWebkit_ && JSCompiler_inline_navigator$$1_16.product == "Gecko") && JSCompiler_inline_navigator$$1_16.vendor == "Camino"
+}var goog$userAgent$OPERA = goog$userAgent$detectedOpera_, goog$userAgent$IE = goog$userAgent$detectedIe_, goog$userAgent$GECKO = goog$userAgent$detectedGecko_, goog$userAgent$WEBKIT = goog$userAgent$detectedWebkit_, goog$userAgent$MOBILE = goog$userAgent$detectedMobile_, goog$userAgent$PLATFORM, JSCompiler_inline_navigator$$2_19 = goog$userAgent$getNavigator();
+goog$userAgent$PLATFORM = JSCompiler_inline_navigator$$2_19 && JSCompiler_inline_navigator$$2_19.platform || "";
+goog$userAgent$detectedMac_ = goog$string$contains(goog$userAgent$PLATFORM, "Mac");
+goog$userAgent$detectedWindows_ = goog$string$contains(goog$userAgent$PLATFORM, "Win");
+goog$userAgent$detectedLinux_ = goog$string$contains(goog$userAgent$PLATFORM, "Linux");
+goog$userAgent$detectedX11_ = !!goog$userAgent$getNavigator() && goog$string$contains(goog$userAgent$getNavigator().appVersion || "", "X11");
+var goog$userAgent$VERSION, JSCompiler_inline_version$$6_26 = "", JSCompiler_inline_re$$2_27;
+if(goog$userAgent$OPERA && goog$global.opera) {
+  var JSCompiler_inline_operaVersion_28 = goog$global.opera.version;
+  JSCompiler_inline_version$$6_26 = typeof JSCompiler_inline_operaVersion_28 == "function" ? JSCompiler_inline_operaVersion_28() : JSCompiler_inline_operaVersion_28
+}else {
+  if(goog$userAgent$GECKO)JSCompiler_inline_re$$2_27 = /rv\:([^\);]+)(\)|;)/;
+  else if(goog$userAgent$IE)JSCompiler_inline_re$$2_27 = /MSIE\s+([^\);]+)(\)|;)/;
+  else if(goog$userAgent$WEBKIT)JSCompiler_inline_re$$2_27 = /WebKit\/(\S+)/;
+  if(JSCompiler_inline_re$$2_27) {
+    var JSCompiler_inline_arr$$41_29 = JSCompiler_inline_re$$2_27.exec(goog$userAgent$getUserAgentString());
+    JSCompiler_inline_version$$6_26 = JSCompiler_inline_arr$$41_29 ? JSCompiler_inline_arr$$41_29[1] : ""
+  }
+}goog$userAgent$VERSION = JSCompiler_inline_version$$6_26;
+var goog$userAgent$isVersionCache_ = {}, goog$userAgent$isVersion = function(version) {
+  return goog$userAgent$isVersionCache_[version] || (goog$userAgent$isVersionCache_[version] = goog$string$compareVersions(goog$userAgent$VERSION, version) >= 0)
+};var goog$dom$getWindow = function(opt_doc) {
+  return opt_doc ? goog$dom$getWindow_(opt_doc) : window
+}, goog$dom$getWindow_ = function(doc) {
+  if(doc.parentWindow)return doc.parentWindow;
+  if(goog$userAgent$WEBKIT && !goog$userAgent$isVersion("500") && !goog$userAgent$MOBILE) {
+    var scriptElement = doc.createElement("script");
+    scriptElement.innerHTML = "document.parentWindow=window";
+    var parentElement = doc.documentElement;
+    parentElement.appendChild(scriptElement);
+    parentElement.removeChild(scriptElement);
+    return doc.parentWindow
+  }return doc.defaultView
+}, goog$dom$appendChild = function(parent, child) {
+  parent.appendChild(child)
+}, goog$dom$BAD_CONTAINS_WEBKIT_ = goog$userAgent$WEBKIT && goog$userAgent$isVersion("522"), goog$dom$contains = function(parent, descendant) {
+  if(typeof parent.contains != "undefined" && !goog$dom$BAD_CONTAINS_WEBKIT_ && descendant.nodeType == 1)return parent == descendant || parent.contains(descendant);
+  if(typeof parent.compareDocumentPosition != "undefined")return parent == descendant || Boolean(parent.compareDocumentPosition(descendant) & 16);
+  for(;descendant && parent != descendant;)descendant = descendant.parentNode;
+  return descendant == parent
+}, goog$dom$compareNodeOrder = function(node1, node2) {
+  if(node1 == node2)return 0;
+  if(node1.compareDocumentPosition)return node1.compareDocumentPosition(node2) & 2 ? 1 : -1;
+  if("sourceIndex" in node1 || node1.parentNode && "sourceIndex" in node1.parentNode) {
+    var isElement1 = node1.nodeType == 1, isElement2 = node2.nodeType == 1;
+    if(isElement1 && isElement2)return node1.sourceIndex - node2.sourceIndex;
+    else {
+      var parent1 = node1.parentNode, parent2 = node2.parentNode;
+      if(parent1 == parent2)return goog$dom$compareSiblingOrder_(node1, node2);
+      if(!isElement1 && goog$dom$contains(parent1, node2))return-1 * goog$dom$compareParentsDescendantNodeIe_(node1, node2);
+      if(!isElement2 && goog$dom$contains(parent2, node1))return goog$dom$compareParentsDescendantNodeIe_(node2, node1);
+      return(isElement1 ? node1.sourceIndex : parent1.sourceIndex) - (isElement2 ? node2.sourceIndex : parent2.sourceIndex)
+    }
+  }var doc = goog$dom$getOwnerDocument(node1), range1, range2;
+  range1 = doc.createRange();
+  range1.selectNode(node1);
+  range1.collapse(true);
+  range2 = doc.createRange();
+  range2.selectNode(node2);
+  range2.collapse(true);
+  return range1.compareBoundaryPoints(goog$global.Range.START_TO_END, range2)
+}, goog$dom$compareParentsDescendantNodeIe_ = function(textNode, node) {
+  var parent = textNode.parentNode;
+  if(parent == node)return-1;
+  for(var sibling = node;sibling.parentNode != parent;)sibling = sibling.parentNode;
+  return goog$dom$compareSiblingOrder_(sibling, textNode)
+}, goog$dom$compareSiblingOrder_ = function(node1, node2) {
+  for(var s = node2;s = s.previousSibling;)if(s == node1)return-1;
+  return 1
+}, goog$dom$findCommonAncestor = function() {
+  var i, count = arguments.length;
+  if(count) {
+    if(count == 1)return arguments[0]
+  }else return null;
+  var paths = [], minLength = Infinity;
+  for(i = 0;i < count;i++) {
+    for(var ancestors = [], node = arguments[i];node;) {
+      ancestors.unshift(node);
+      node = node.parentNode
+    }paths.push(ancestors);
+    minLength = Math.min(minLength, ancestors.length)
+  }var output = null;
+  for(i = 0;i < minLength;i++) {
+    for(var first = paths[0][i], j = 1;j < count;j++)if(first != paths[j][i])return output;
+    output = first
+  }return output
+}, goog$dom$getOwnerDocument = function(node) {
+  // Added 'editorDoc' as hack for browsers that don't support node.ownerDocument
+  return node.nodeType == 9 ? node : node.ownerDocument || node.document || editorDoc
+}, goog$dom$DomHelper = function(opt_document) {
+  this.document_ = opt_document || goog$global.document || document
+};
+goog$dom$DomHelper.prototype.getDocument = function() {
+  return this.document_
+};
+goog$dom$DomHelper.prototype.createElement = function(name) {
+  return this.document_.createElement(name)
+};
+goog$dom$DomHelper.prototype.getWindow = function() {
+  return goog$dom$getWindow_(this.document_)
+};
+goog$dom$DomHelper.prototype.appendChild = goog$dom$appendChild;
+goog$dom$DomHelper.prototype.contains = goog$dom$contains;var goog$Disposable = function() {
+};if("StopIteration" in goog$global)var goog$iter$StopIteration = goog$global.StopIteration;
+else goog$iter$StopIteration = Error("StopIteration");
+var goog$iter$Iterator = function() {
+};
+goog$iter$Iterator.prototype.next = function() {
+  throw goog$iter$StopIteration;
+};
+goog$iter$Iterator.prototype.__iterator__ = function() {
+  return this
+};var goog$debug$exposeException = function(err, opt_fn) {
+  try {
+    var e, JSCompiler_inline_href_34;
+    JSCompiler_inline_label_goog$getObjectByName_61: {
+      for(var JSCompiler_inline_parts = "window.location.href".split("."), JSCompiler_inline_cur = goog$global, JSCompiler_inline_part;JSCompiler_inline_part = JSCompiler_inline_parts.shift();)if(JSCompiler_inline_cur[JSCompiler_inline_part])JSCompiler_inline_cur = JSCompiler_inline_cur[JSCompiler_inline_part];
+      else {
+        JSCompiler_inline_href_34 = null;
+        break JSCompiler_inline_label_goog$getObjectByName_61
+      }JSCompiler_inline_href_34 = JSCompiler_inline_cur
+    }e = typeof err == "string" ? {message:err, name:"Unknown error", lineNumber:"Not available", fileName:JSCompiler_inline_href_34, stack:"Not available"} : !err.lineNumber || !err.fileName || !err.stack ? {message:err.message, name:err.name, lineNumber:err.lineNumber || err.line || "Not available", fileName:err.fileName || err.filename || err.sourceURL || JSCompiler_inline_href_34, stack:err.stack || "Not available"} : err;
+    var error = "Message: " + goog$string$htmlEscape(e.message) + '\nUrl: <a href="view-source:' + e.fileName + '" target="_new">' + e.fileName + "</a>\nLine: " + e.lineNumber + "\n\nBrowser stack:\n" + goog$string$htmlEscape(e.stack + "-> ") + "[end]\n\nJS stack traversal:\n" + goog$string$htmlEscape(goog$debug$getStacktrace(opt_fn) + "-> ");
+    return error
+  }catch(e2) {
+    return"Exception trying to expose exception! You win, we lose. " + e2
+  }
+}, goog$debug$getStacktrace = function(opt_fn) {
+  return goog$debug$getStacktraceHelper_(opt_fn || arguments.callee.caller, [])
+}, goog$debug$getStacktraceHelper_ = function(fn, visited) {
+  var sb = [], JSCompiler_inline_result_36;
+  JSCompiler_inline_label_goog$array$contains_41:JSCompiler_inline_result_36 = visited.contains ? visited.contains(fn) : goog$array$indexOf(visited, fn) > -1;
+  if(JSCompiler_inline_result_36)sb.push("[...circular reference...]");
+  else if(fn && visited.length < 50) {
+    sb.push(goog$debug$getFunctionName(fn) + "(");
+    for(var args = fn.arguments, i = 0;i < args.length;i++) {
+      i > 0 && sb.push(", ");
+      var argDesc, arg = args[i];
+      switch(typeof arg) {
+        case "object":
+          argDesc = arg ? "object" : "null";
+          break;
+        case "string":
+          argDesc = arg;
+          break;
+        case "number":
+          argDesc = String(arg);
+          break;
+        case "boolean":
+          argDesc = arg ? "true" : "false";
+          break;
+        case "function":
+          argDesc = (argDesc = goog$debug$getFunctionName(arg)) ? argDesc : "[fn]";
+          break;
+        case "undefined":
+        ;
+        default:
+          argDesc = typeof arg;
+          break
+      }
+      if(argDesc.length > 40)argDesc = argDesc.substr(0, 40) + "...";
+      sb.push(argDesc)
+    }visited.push(fn);
+    sb.push(")\n");
+    try {
+      sb.push(goog$debug$getStacktraceHelper_(fn.caller, visited))
+    }catch(e) {
+      sb.push("[exception trying to get caller]\n")
+    }
+  }else fn ? sb.push("[...long stack...]") : sb.push("[end]");
+  return sb.join("")
+}, goog$debug$getFunctionName = function(fn) {
+  var functionSource = String(fn);
+  if(!goog$debug$fnNameCache_[functionSource]) {
+    var matches = /function ([^\(]+)/.exec(functionSource);
+    if(matches) {
+      var method = matches[1];
+      goog$debug$fnNameCache_[functionSource] = method
+    }else goog$debug$fnNameCache_[functionSource] = "[Anonymous]"
+  }return goog$debug$fnNameCache_[functionSource]
+}, goog$debug$fnNameCache_ = {};var goog$debug$LogRecord = function(level, msg, loggerName, opt_time, opt_sequenceNumber) {
+  this.sequenceNumber_ = typeof opt_sequenceNumber == "number" ? opt_sequenceNumber : goog$debug$LogRecord$nextSequenceNumber_++;
+  this.time_ = opt_time || goog$now();
+  this.level_ = level;
+  this.msg_ = msg;
+  this.loggerName_ = loggerName
+};
+goog$debug$LogRecord.prototype.exception_ = null;
+goog$debug$LogRecord.prototype.exceptionText_ = null;
+var goog$debug$LogRecord$nextSequenceNumber_ = 0;
+goog$debug$LogRecord.prototype.setException = function(exception) {
+  this.exception_ = exception
+};
+goog$debug$LogRecord.prototype.setExceptionText = function(text) {
+  this.exceptionText_ = text
+};
+goog$debug$LogRecord.prototype.setLevel = function(level) {
+  this.level_ = level
+};var goog$debug$Logger = function(name) {
+  this.name_ = name;
+  this.parent_ = null;
+  this.children_ = {};
+  this.handlers_ = []
+};
+goog$debug$Logger.prototype.level_ = null;
+var goog$debug$Logger$Level = function(name, value) {
+  this.name = name;
+  this.value = value
+};
+goog$debug$Logger$Level.prototype.toString = function() {
+  return this.name
+};
+new goog$debug$Logger$Level("OFF", Infinity);
+new goog$debug$Logger$Level("SHOUT", 1200);
+var goog$debug$Logger$Level$SEVERE = new goog$debug$Logger$Level("SEVERE", 1000), goog$debug$Logger$Level$WARNING = new goog$debug$Logger$Level("WARNING", 900);
+new goog$debug$Logger$Level("INFO", 800);
+var goog$debug$Logger$Level$CONFIG = new goog$debug$Logger$Level("CONFIG", 700);
+new goog$debug$Logger$Level("FINE", 500);
+new goog$debug$Logger$Level("FINER", 400);
+new goog$debug$Logger$Level("FINEST", 300);
+new goog$debug$Logger$Level("ALL", 0);
+goog$debug$Logger.prototype.setLevel = function(level) {
+  this.level_ = level
+};
+goog$debug$Logger.prototype.isLoggable = function(level) {
+  if(this.level_)return level.value >= this.level_.value;
+  if(this.parent_)return this.parent_.isLoggable(level);
+  return false
+};
+goog$debug$Logger.prototype.log = function(level, msg, opt_exception) {
+  this.isLoggable(level) && this.logRecord(this.getLogRecord(level, msg, opt_exception))
+};
+goog$debug$Logger.prototype.getLogRecord = function(level, msg, opt_exception) {
+  var logRecord = new goog$debug$LogRecord(level, String(msg), this.name_);
+  if(opt_exception) {
+    logRecord.setException(opt_exception);
+    logRecord.setExceptionText(goog$debug$exposeException(opt_exception, arguments.callee.caller))
+  }return logRecord
+};
+goog$debug$Logger.prototype.severe = function(msg, opt_exception) {
+  this.log(goog$debug$Logger$Level$SEVERE, msg, opt_exception)
+};
+goog$debug$Logger.prototype.warning = function(msg, opt_exception) {
+  this.log(goog$debug$Logger$Level$WARNING, msg, opt_exception)
+};
+goog$debug$Logger.prototype.logRecord = function(logRecord) {
+  if(this.isLoggable(logRecord.level_))for(var target = this;target;) {
+    target.callPublish_(logRecord);
+    target = target.parent_
+  }
+};
+goog$debug$Logger.prototype.callPublish_ = function(logRecord) {
+  for(var i = 0;i < this.handlers_.length;i++)this.handlers_[i](logRecord)
+};
+goog$debug$Logger.prototype.setParent_ = function(parent) {
+  this.parent_ = parent
+};
+goog$debug$Logger.prototype.addChild_ = function(name, logger) {
+  this.children_[name] = logger
+};
+var goog$debug$LogManager$loggers_ = {}, goog$debug$LogManager$rootLogger_ = null, goog$debug$LogManager$getLogger = function(name) {
+  if(!goog$debug$LogManager$rootLogger_) {
+    goog$debug$LogManager$rootLogger_ = new goog$debug$Logger("");
+    goog$debug$LogManager$loggers_[""] = goog$debug$LogManager$rootLogger_;
+    goog$debug$LogManager$rootLogger_.setLevel(goog$debug$Logger$Level$CONFIG)
+  }return name in goog$debug$LogManager$loggers_ ? goog$debug$LogManager$loggers_[name] : goog$debug$LogManager$createLogger_(name)
+}, goog$debug$LogManager$createLogger_ = function(name) {
+  var logger = new goog$debug$Logger(name), parts = name.split("."), leafName = parts[parts.length - 1];
+  parts.length = parts.length - 1;
+  var parentName = parts.join("."), parentLogger = goog$debug$LogManager$getLogger(parentName);
+  parentLogger.addChild_(leafName, logger);
+  logger.setParent_(parentLogger);
+  return goog$debug$LogManager$loggers_[name] = logger
+};var goog$dom$SavedRange = function() {
+  goog$Disposable.call(this)
+};
+goog$inherits(goog$dom$SavedRange, goog$Disposable);
+goog$debug$LogManager$getLogger("goog.dom.SavedRange");var goog$dom$TagIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_tagType, opt_depth) {
+  this.reversed = !!opt_reversed;
+  opt_node && this.setPosition(opt_node, opt_tagType);
+  this.depth = opt_depth != undefined ? opt_depth : this.tagType || 0;
+  if(this.reversed)this.depth *= -1;
+  this.constrained = !opt_unconstrained
+};
+goog$inherits(goog$dom$TagIterator, goog$iter$Iterator);
+goog$dom$TagIterator.prototype.node = null;
+goog$dom$TagIterator.prototype.tagType = null;
+goog$dom$TagIterator.prototype.started_ = false;
+goog$dom$TagIterator.prototype.setPosition = function(node, opt_tagType, opt_depth) {
+  if(this.node = node)this.tagType = typeof opt_tagType == "number" ? opt_tagType : this.node.nodeType != 1 ? 0 : this.reversed ? -1 : 1;
+  if(typeof opt_depth == "number")this.depth = opt_depth
+};
+goog$dom$TagIterator.prototype.next = function() {
+  var node;
+  if(this.started_) {
+    if(!this.node || this.constrained && this.depth == 0)throw goog$iter$StopIteration;node = this.node;
+    var startType = this.reversed ? -1 : 1;
+    if(this.tagType == startType) {
+      var child = this.reversed ? node.lastChild : node.firstChild;
+      child ? this.setPosition(child) : this.setPosition(node, startType * -1)
+    }else {
+      var sibling = this.reversed ? node.previousSibling : node.nextSibling;
+      sibling ? this.setPosition(sibling) : this.setPosition(node.parentNode, startType * -1)
+    }this.depth += this.tagType * (this.reversed ? -1 : 1)
+  }else this.started_ = true;
+  node = this.node;
+  if(!this.node)throw goog$iter$StopIteration;return node
+};
+goog$dom$TagIterator.prototype.isStartTag = function() {
+  return this.tagType == 1
+};var goog$dom$AbstractRange = function() {
+};
+goog$dom$AbstractRange.prototype.getTextRanges = function() {
+  for(var output = [], i = 0, len = this.getTextRangeCount();i < len;i++)output.push(this.getTextRange(i));
+  return output
+};
+goog$dom$AbstractRange.prototype.getAnchorNode = function() {
+  return this.isReversed() ? this.getEndNode() : this.getStartNode()
+};
+goog$dom$AbstractRange.prototype.getAnchorOffset = function() {
+  return this.isReversed() ? this.getEndOffset() : this.getStartOffset()
+};
+goog$dom$AbstractRange.prototype.getFocusNode = function() {
+  return this.isReversed() ? this.getStartNode() : this.getEndNode()
+};
+goog$dom$AbstractRange.prototype.getFocusOffset = function() {
+  return this.isReversed() ? this.getStartOffset() : this.getEndOffset()
+};
+goog$dom$AbstractRange.prototype.isReversed = function() {
+  return false
+};
+goog$dom$AbstractRange.prototype.getDocument = function() {
+  return goog$dom$getOwnerDocument(goog$userAgent$IE ? this.getContainer() : this.getStartNode())
+};
+goog$dom$AbstractRange.prototype.getWindow = function() {
+  return goog$dom$getWindow(this.getDocument())
+};
+goog$dom$AbstractRange.prototype.containsNode = function(node, opt_allowPartial) {
+  return this.containsRange(goog$dom$TextRange$createFromNodeContents(node, undefined), opt_allowPartial)
+};
+var goog$dom$RangeIterator = function(node, opt_reverse) {
+  goog$dom$TagIterator.call(this, node, opt_reverse, true)
+};
+goog$inherits(goog$dom$RangeIterator, goog$dom$TagIterator);var goog$dom$AbstractMultiRange = function() {
+};
+goog$inherits(goog$dom$AbstractMultiRange, goog$dom$AbstractRange);
+goog$dom$AbstractMultiRange.prototype.containsRange = function(otherRange, opt_allowPartial) {
+  var ranges = this.getTextRanges(), otherRanges = otherRange.getTextRanges(), fn = opt_allowPartial ? goog$array$some : goog$array$every;
+  return fn(otherRanges, function(otherRange) {
+    return goog$array$some(ranges, function(range) {
+      return range.containsRange(otherRange, opt_allowPartial)
+    })
+  })
+};var goog$dom$TextRangeIterator = function(startNode, startOffset, endNode, endOffset, opt_reverse) {
+  var goNext;
+  if(startNode) {
+    this.startNode_ = startNode;
+    this.startOffset_ = startOffset;
+    this.endNode_ = endNode;
+    this.endOffset_ = endOffset;
+    if(startNode.nodeType == 1 && startNode.tagName != "BR") {
+      var startChildren = startNode.childNodes, candidate = startChildren[startOffset];
+      if(candidate) {
+        this.startNode_ = candidate;
+        this.startOffset_ = 0
+      }else {
+        if(startChildren.length)this.startNode_ = goog$array$peek(startChildren);
+        goNext = true
+      }
+    }if(endNode.nodeType == 1)if(this.endNode_ = endNode.childNodes[endOffset])this.endOffset_ = 0;
+    else this.endNode_ = endNode
+  }goog$dom$RangeIterator.call(this, opt_reverse ? this.endNode_ : this.startNode_, opt_reverse);
+  if(goNext)try {
+    this.next()
+  }catch(e) {
+    if(e != goog$iter$StopIteration)throw e;
+  }
+};
+goog$inherits(goog$dom$TextRangeIterator, goog$dom$RangeIterator);
+goog$dom$TextRangeIterator.prototype.startNode_ = null;
+goog$dom$TextRangeIterator.prototype.endNode_ = null;
+goog$dom$TextRangeIterator.prototype.startOffset_ = 0;
+goog$dom$TextRangeIterator.prototype.endOffset_ = 0;
+goog$dom$TextRangeIterator.prototype.getStartNode = function() {
+  return this.startNode_
+};
+goog$dom$TextRangeIterator.prototype.getEndNode = function() {
+  return this.endNode_
+};
+goog$dom$TextRangeIterator.prototype.isLast = function() {
+  return this.started_ && this.node == this.endNode_ && (!this.endOffset_ || !this.isStartTag())
+};
+goog$dom$TextRangeIterator.prototype.next = function() {
+  if(this.isLast())throw goog$iter$StopIteration;return goog$dom$TextRangeIterator.superClass_.next.call(this)
+};var goog$userAgent$jscript$DETECTED_HAS_JSCRIPT_, goog$userAgent$jscript$DETECTED_VERSION_, JSCompiler_inline_hasScriptEngine_44 = "ScriptEngine" in goog$global;
+goog$userAgent$jscript$DETECTED_VERSION_ = (goog$userAgent$jscript$DETECTED_HAS_JSCRIPT_ = JSCompiler_inline_hasScriptEngine_44 && goog$global.ScriptEngine() == "JScript") ? goog$global.ScriptEngineMajorVersion() + "." + goog$global.ScriptEngineMinorVersion() + "." + goog$global.ScriptEngineBuildVersion() : "0";var goog$dom$browserrange$AbstractRange = function() {
+};
+goog$dom$browserrange$AbstractRange.prototype.containsRange = function(range, opt_allowPartial) {
+  return this.containsBrowserRange(range.range_, opt_allowPartial)
+};
+goog$dom$browserrange$AbstractRange.prototype.containsBrowserRange = function(range, opt_allowPartial) {
+  try {
+    return opt_allowPartial ? this.compareBrowserRangeEndpoints(range, 0, 1) >= 0 && this.compareBrowserRangeEndpoints(range, 1, 0) <= 0 : this.compareBrowserRangeEndpoints(range, 0, 0) >= 0 && this.compareBrowserRangeEndpoints(range, 1, 1) <= 0
+  }catch(e) {
+    if(!goog$userAgent$IE)throw e;return false
+  }
+};
+goog$dom$browserrange$AbstractRange.prototype.containsNode = function(node, opt_allowPartial) {
+  return this.containsRange(goog$userAgent$IE ? goog$dom$browserrange$IeRange$createFromNodeContents(node) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)), opt_allowPartial)
+};
+goog$dom$browserrange$AbstractRange.prototype.__iterator__ = function() {
+  return new goog$dom$TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};var goog$dom$browserrange$W3cRange = function(range) {
+  this.range_ = range
+};
+goog$inherits(goog$dom$browserrange$W3cRange, goog$dom$browserrange$AbstractRange);
+var goog$dom$browserrange$W3cRange$getBrowserRangeForNode = function(node) {
+  var nodeRange = goog$dom$getOwnerDocument(node).createRange();
+  if(node.nodeType == 3) {
+    nodeRange.setStart(node, 0);
+    nodeRange.setEnd(node, node.length)
+  }else {
+    for(var tempNode, leaf = node;tempNode = leaf.firstChild;)leaf = tempNode;
+    nodeRange.setStart(leaf, 0);
+    for(leaf = node;tempNode = leaf.lastChild;)leaf = tempNode;
+    nodeRange.setEnd(leaf, leaf.nodeType == 1 ? leaf.childNodes.length : leaf.length)
+  }return nodeRange
+}, goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) {
+  var nodeRange = goog$dom$getOwnerDocument(startNode).createRange();
+  nodeRange.setStart(startNode, startOffset);
+  nodeRange.setEnd(endNode, endOffset);
+  return nodeRange
+};
+goog$dom$browserrange$W3cRange.prototype.getContainer = function() {
+  return this.range_.commonAncestorContainer
+};
+goog$dom$browserrange$W3cRange.prototype.getStartNode = function() {
+  return this.range_.startContainer
+};
+goog$dom$browserrange$W3cRange.prototype.getStartOffset = function() {
+  return this.range_.startOffset
+};
+goog$dom$browserrange$W3cRange.prototype.getEndNode = function() {
+  return this.range_.endContainer
+};
+goog$dom$browserrange$W3cRange.prototype.getEndOffset = function() {
+  return this.range_.endOffset
+};
+goog$dom$browserrange$W3cRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+  return this.range_.compareBoundaryPoints(otherEndpoint == 1 ? thisEndpoint == 1 ? goog$global.Range.START_TO_START : goog$global.Range.START_TO_END : thisEndpoint == 1 ? goog$global.Range.END_TO_START : goog$global.Range.END_TO_END, range)
+};
+goog$dom$browserrange$W3cRange.prototype.isCollapsed = function() {
+  return this.range_.collapsed
+};
+goog$dom$browserrange$W3cRange.prototype.select = function(reverse) {
+  var win = goog$dom$getWindow(goog$dom$getOwnerDocument(this.getStartNode()));
+  this.selectInternal(win.getSelection(), reverse)
+};
+goog$dom$browserrange$W3cRange.prototype.selectInternal = function(selection) {
+  selection.addRange(this.range_)
+};
+goog$dom$browserrange$W3cRange.prototype.collapse = function(toStart) {
+  this.range_.collapse(toStart)
+};var goog$dom$browserrange$GeckoRange = function(range) {
+  goog$dom$browserrange$W3cRange.call(this, range)
+};
+goog$inherits(goog$dom$browserrange$GeckoRange, goog$dom$browserrange$W3cRange);
+goog$dom$browserrange$GeckoRange.prototype.selectInternal = function(selection, reversed) {
+  var anchorNode = reversed ? this.getEndNode() : this.getStartNode(), anchorOffset = reversed ? this.getEndOffset() : this.getStartOffset(), focusNode = reversed ? this.getStartNode() : this.getEndNode(), focusOffset = reversed ? this.getStartOffset() : this.getEndOffset();
+  selection.collapse(anchorNode, anchorOffset);
+  if(anchorNode != focusNode || anchorOffset != focusOffset)selection.extend(focusNode, focusOffset)
+};var goog$dom$browserrange$IeRange = function(range, doc) {
+  this.range_ = range;
+  this.doc_ = doc
+};
+goog$inherits(goog$dom$browserrange$IeRange, goog$dom$browserrange$AbstractRange);
+var goog$dom$browserrange$IeRange$logger_ = goog$debug$LogManager$getLogger("goog.dom.browserrange.IeRange"), goog$dom$browserrange$IeRange$getBrowserRangeForNode_ = function(node) {
+  var nodeRange = goog$dom$getOwnerDocument(node).body.createTextRange();
+  if(node.nodeType == 1)nodeRange.moveToElementText(node);
+  else {
+    for(var offset = 0, sibling = node;sibling = sibling.previousSibling;) {
+      var nodeType = sibling.nodeType;
+      if(nodeType == 3)offset += sibling.length;
+      else if(nodeType == 1) {
+        nodeRange.moveToElementText(sibling);
+        break
+      }
+    }sibling || nodeRange.moveToElementText(node.parentNode);
+    nodeRange.collapse(!sibling);
+    offset && nodeRange.move("character", offset);
+    nodeRange.moveEnd("character", node.length)
+  }return nodeRange
+}, goog$dom$browserrange$IeRange$getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) {
+  var child, collapse = false;
+  if(startNode.nodeType == 1) {
+    startOffset > startNode.childNodes.length && goog$dom$browserrange$IeRange$logger_.severe("Cannot have startOffset > startNode child count");
+    child = startNode.childNodes[startOffset];
+    collapse = !child;
+    startNode = child || startNode;
+    startOffset = 0
+  }var leftRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(startNode);
+  startOffset && leftRange.move("character", startOffset);
+  collapse && leftRange.collapse(false);
+  collapse = false;
+  if(endNode.nodeType == 1) {
+    startOffset > startNode.childNodes.length && goog$dom$browserrange$IeRange$logger_.severe("Cannot have endOffset > endNode child count");
+    endNode = (child = endNode.childNodes[endOffset]) || endNode;
+    if(endNode.tagName == "BR")endOffset = 1;
+    else {
+      endOffset = 0;
+      collapse = !child
+    }
+  }var rightRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(endNode);
+  rightRange.collapse(!collapse);
+  endOffset && rightRange.moveEnd("character", endOffset);
+  leftRange.setEndPoint("EndToEnd", rightRange);
+  return leftRange
+}, goog$dom$browserrange$IeRange$createFromNodeContents = function(node) {
+  var range = new goog$dom$browserrange$IeRange(goog$dom$browserrange$IeRange$getBrowserRangeForNode_(node), goog$dom$getOwnerDocument(node));
+  range.parentNode_ = node;
+  return range
+};
+goog$dom$browserrange$IeRange.prototype.parentNode_ = null;
+goog$dom$browserrange$IeRange.prototype.startNode_ = null;
+goog$dom$browserrange$IeRange.prototype.endNode_ = null;
+goog$dom$browserrange$IeRange.prototype.clearCachedValues_ = function() {
+  this.parentNode_ = this.startNode_ = this.endNode_ = null
+};
+goog$dom$browserrange$IeRange.prototype.getContainer = function() {
+  if(!this.parentNode_) {
+    for(var selectText = this.range_.text, i = 1;selectText.charAt(selectText.length - i) == " ";i++)this.range_.moveEnd("character", -1);
+    for(var parent = this.range_.parentElement(), htmlText = this.range_.htmlText.replace(/(\r\n|\r|\n)+/g, " ");htmlText.length > parent.outerHTML.replace(/(\r\n|\r|\n)+/g, " ").length;)parent = parent.parentNode;
+    for(;parent.childNodes.length == 1 && parent.innerText == (parent.firstChild.nodeType == 3 ? parent.firstChild.nodeValue : parent.firstChild.innerText);) {
+      if(parent.firstChild.tagName == "IMG")break;
+      parent = parent.firstChild
+    }if(selectText.length == 0)parent = this.findDeepestContainer_(parent);
+    this.parentNode_ = parent
+  }return this.parentNode_
+};
+goog$dom$browserrange$IeRange.prototype.findDeepestContainer_ = function(node) {
+  for(var childNodes = node.childNodes, i = 0, len = childNodes.length;i < len;i++) {
+    var child = childNodes[i];
+    if(child.nodeType == 1)if(this.range_.inRange(goog$dom$browserrange$IeRange$getBrowserRangeForNode_(child)))return this.findDeepestContainer_(child)
+  }return node
+};
+goog$dom$browserrange$IeRange.prototype.getStartNode = function() {
+  return this.startNode_ || (this.startNode_ = this.getEndpointNode_(1))
+};
+goog$dom$browserrange$IeRange.prototype.getStartOffset = function() {
+  return this.getOffset_(1)
+};
+goog$dom$browserrange$IeRange.prototype.getEndNode = function() {
+  return this.endNode_ || (this.endNode_ = this.getEndpointNode_(0))
+};
+goog$dom$browserrange$IeRange.prototype.getEndOffset = function() {
+  return this.getOffset_(0)
+};
+goog$dom$browserrange$IeRange.prototype.containsRange = function(range, opt_allowPartial) {
+  return this.containsBrowserRange(range.range_, opt_allowPartial)
+};
+goog$dom$browserrange$IeRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+  return this.range_.compareEndPoints((thisEndpoint == 1 ? "Start" : "End") + "To" + (otherEndpoint == 1 ? "Start" : "End"), range)
+};
+goog$dom$browserrange$IeRange.prototype.getEndpointNode_ = function(endpoint, opt_node) {
+  var node = opt_node || this.getContainer();
+  if(!node || !node.firstChild) {
+    if(endpoint == 0 && node.previousSibling && node.previousSibling.tagName == "BR" && this.getOffset_(endpoint, node) == 0)node = node.previousSibling;
+    return node.tagName == "BR" ? node.parentNode : node
+  }for(var child = endpoint == 1 ? node.firstChild : node.lastChild;child;) {
+    if(this.containsNode(child, true))return this.getEndpointNode_(endpoint, child);
+    child = endpoint == 1 ? child.nextSibling : child.previousSibling
+  }return node
+};
+goog$dom$browserrange$IeRange.prototype.getOffset_ = function(endpoint, opt_container) {
+  var container = opt_container || (endpoint == 1 ? this.getStartNode() : this.getEndNode());
+  if(container.nodeType == 1) {
+    for(var children = container.childNodes, len = children.length, i = endpoint == 1 ? 0 : len - 1;i >= 0 && i < len;) {
+      var child = children[i];
+      if(this.containsNode(child, true)) {
+        endpoint == 0 && child.previousSibling && child.previousSibling.tagName == "BR" && this.getOffset_(endpoint, child) == 0 && i--;
+        break
+      }i += endpoint == 1 ? 1 : -1
+    }return i == -1 ? 0 : i
+  }else {
+    var range = this.range_.duplicate(), nodeRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(container);
+    range.setEndPoint(endpoint == 1 ? "EndToEnd" : "StartToStart", nodeRange);
+    var rangeLength = range.text.length;
+    return endpoint == 0 ? rangeLength : container.length - rangeLength
+  }
+};
+goog$dom$browserrange$IeRange.prototype.isCollapsed = function() {
+  return this.range_.text == ""
+};
+goog$dom$browserrange$IeRange.prototype.select = function() {
+  this.range_.select()
+};
+goog$dom$browserrange$IeRange.prototype.collapse = function(toStart) {
+  this.range_.collapse(toStart);
+  if(toStart)this.endNode_ = this.startNode_;
+  else this.startNode_ = this.endNode_
+};var goog$dom$browserrange$WebKitRange = function(range) {
+  goog$dom$browserrange$W3cRange.call(this, range)
+};
+goog$inherits(goog$dom$browserrange$WebKitRange, goog$dom$browserrange$W3cRange);
+goog$dom$browserrange$WebKitRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) {
+  if(goog$userAgent$isVersion("528"))return goog$dom$browserrange$WebKitRange.superClass_.compareBrowserRangeEndpoints.call(this, range, thisEndpoint, otherEndpoint);
+  return this.range_.compareBoundaryPoints(otherEndpoint == 1 ? thisEndpoint == 1 ? goog$global.Range.START_TO_START : goog$global.Range.END_TO_START : thisEndpoint == 1 ? goog$global.Range.START_TO_END : goog$global.Range.END_TO_END, range)
+};
+goog$dom$browserrange$WebKitRange.prototype.selectInternal = function(selection, reversed) {
+  selection.removeAllRanges();
+  reversed ? selection.setBaseAndExtent(this.getEndNode(), this.getEndOffset(), this.getStartNode(), this.getStartOffset()) : selection.setBaseAndExtent(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};var goog$dom$browserrange$createRangeFromNodes = function(startNode, startOffset, endNode, endOffset) {
+  return goog$userAgent$IE ? new goog$dom$browserrange$IeRange(goog$dom$browserrange$IeRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset), goog$dom$getOwnerDocument(startNode)) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, 
+  endNode, endOffset)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset))
+};var goog$dom$TextRange = function() {
+};
+goog$inherits(goog$dom$TextRange, goog$dom$AbstractRange);
+var goog$dom$TextRange$createFromBrowserRangeWrapper_ = function(browserRange, opt_isReversed) {
+  var range = new goog$dom$TextRange;
+  range.browserRangeWrapper_ = browserRange;
+  range.isReversed_ = !!opt_isReversed;
+  return range
+}, goog$dom$TextRange$createFromNodeContents = function(node, opt_isReversed) {
+  return goog$dom$TextRange$createFromBrowserRangeWrapper_(goog$userAgent$IE ? goog$dom$browserrange$IeRange$createFromNodeContents(node) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)), opt_isReversed)
+}, goog$dom$TextRange$createFromNodes = function(anchorNode, anchorOffset, focusNode, focusOffset) {
+  var range = new goog$dom$TextRange;
+  range.isReversed_ = goog$dom$Range$isReversed(anchorNode, anchorOffset, focusNode, focusOffset);
+  if(anchorNode.tagName == "BR") {
+    var parent = anchorNode.parentNode;
+    anchorOffset = goog$array$indexOf(parent.childNodes, anchorNode);
+    anchorNode = parent
+  }if(focusNode.tagName == "BR") {
+    parent = focusNode.parentNode;
+    focusOffset = goog$array$indexOf(parent.childNodes, focusNode);
+    focusNode = parent
+  }if(range.isReversed_) {
+    range.startNode_ = focusNode;
+    range.startOffset_ = focusOffset;
+    range.endNode_ = anchorNode;
+    range.endOffset_ = anchorOffset
+  }else {
+    range.startNode_ = anchorNode;
+    range.startOffset_ = anchorOffset;
+    range.endNode_ = focusNode;
+    range.endOffset_ = focusOffset
+  }return range
+};
+goog$dom$TextRange.prototype.browserRangeWrapper_ = null;
+goog$dom$TextRange.prototype.startNode_ = null;
+goog$dom$TextRange.prototype.startOffset_ = null;
+goog$dom$TextRange.prototype.endNode_ = null;
+goog$dom$TextRange.prototype.endOffset_ = null;
+goog$dom$TextRange.prototype.isReversed_ = false;
+goog$dom$TextRange.prototype.getType = function() {
+  return"text"
+};
+goog$dom$TextRange.prototype.getBrowserRangeObject = function() {
+  return this.getBrowserRangeWrapper_().range_
+};
+goog$dom$TextRange.prototype.clearCachedValues_ = function() {
+  this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null
+};
+goog$dom$TextRange.prototype.getTextRangeCount = function() {
+  return 1
+};
+goog$dom$TextRange.prototype.getTextRange = function() {
+  return this
+};
+goog$dom$TextRange.prototype.getBrowserRangeWrapper_ = function() {
+  return this.browserRangeWrapper_ || (this.browserRangeWrapper_ = goog$dom$browserrange$createRangeFromNodes(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()))
+};
+goog$dom$TextRange.prototype.getContainer = function() {
+  return this.getBrowserRangeWrapper_().getContainer()
+};
+goog$dom$TextRange.prototype.getStartNode = function() {
+  return this.startNode_ || (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode())
+};
+goog$dom$TextRange.prototype.getStartOffset = function() {
+  return this.startOffset_ != null ? this.startOffset_ : (this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset())
+};
+goog$dom$TextRange.prototype.getEndNode = function() {
+  return this.endNode_ || (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode())
+};
+goog$dom$TextRange.prototype.getEndOffset = function() {
+  return this.endOffset_ != null ? this.endOffset_ : (this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset())
+};
+goog$dom$TextRange.prototype.isReversed = function() {
+  return this.isReversed_
+};
+goog$dom$TextRange.prototype.containsRange = function(otherRange, opt_allowPartial) {
+  var otherRangeType = otherRange.getType();
+  if(otherRangeType == "text")return this.getBrowserRangeWrapper_().containsRange(otherRange.getBrowserRangeWrapper_(), opt_allowPartial);
+  else if(otherRangeType == "control") {
+    var elements = otherRange.getElements(), fn = opt_allowPartial ? goog$array$some : goog$array$every;
+    return fn(elements, function(el) {
+      return this.containsNode(el, opt_allowPartial)
+    }, this)
+  }
+};
+goog$dom$TextRange.prototype.isCollapsed = function() {
+  return this.getBrowserRangeWrapper_().isCollapsed()
+};
+goog$dom$TextRange.prototype.__iterator__ = function() {
+  return new goog$dom$TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())
+};
+goog$dom$TextRange.prototype.select = function() {
+  this.getBrowserRangeWrapper_().select(this.isReversed_)
+};
+goog$dom$TextRange.prototype.saveUsingDom = function() {
+  return new goog$dom$DomSavedTextRange_(this)
+};
+goog$dom$TextRange.prototype.collapse = function(toAnchor) {
+  var toStart = this.isReversed() ? !toAnchor : toAnchor;
+  this.browserRangeWrapper_ && this.browserRangeWrapper_.collapse(toStart);
+  if(toStart) {
+    this.endNode_ = this.startNode_;
+    this.endOffset_ = this.startOffset_
+  }else {
+    this.startNode_ = this.endNode_;
+    this.startOffset_ = this.endOffset_
+  }this.isReversed_ = false
+};
+var goog$dom$DomSavedTextRange_ = function(range) {
+  this.anchorNode_ = range.getAnchorNode();
+  this.anchorOffset_ = range.getAnchorOffset();
+  this.focusNode_ = range.getFocusNode();
+  this.focusOffset_ = range.getFocusOffset()
+};
+goog$inherits(goog$dom$DomSavedTextRange_, goog$dom$SavedRange);var goog$dom$ControlRange = function() {
+};
+goog$inherits(goog$dom$ControlRange, goog$dom$AbstractMultiRange);
+goog$dom$ControlRange.prototype.range_ = null;
+goog$dom$ControlRange.prototype.elements_ = null;
+goog$dom$ControlRange.prototype.sortedElements_ = null;
+goog$dom$ControlRange.prototype.clearCachedValues_ = function() {
+  this.sortedElements_ = this.elements_ = null
+};
+goog$dom$ControlRange.prototype.getType = function() {
+  return"control"
+};
+goog$dom$ControlRange.prototype.getBrowserRangeObject = function() {
+  return this.range_ || document.body.createControlRange()
+};
+goog$dom$ControlRange.prototype.getTextRangeCount = function() {
+  return this.range_ ? this.range_.length : 0
+};
+goog$dom$ControlRange.prototype.getTextRange = function(i) {
+  return goog$dom$TextRange$createFromNodeContents(this.range_.item(i))
+};
+goog$dom$ControlRange.prototype.getContainer = function() {
+  return goog$dom$findCommonAncestor.apply(null, this.getElements())
+};
+goog$dom$ControlRange.prototype.getStartNode = function() {
+  return this.getSortedElements()[0]
+};
+goog$dom$ControlRange.prototype.getStartOffset = function() {
+  return 0
+};
+goog$dom$ControlRange.prototype.getEndNode = function() {
+  var sorted = this.getSortedElements(), startsLast = goog$array$peek(sorted);
+  return goog$array$find(sorted, function(el) {
+    return goog$dom$contains(el, startsLast)
+  })
+};
+goog$dom$ControlRange.prototype.getEndOffset = function() {
+  return this.getEndNode().childNodes.length
+};
+goog$dom$ControlRange.prototype.getElements = function() {
+  if(!this.elements_) {
+    this.elements_ = [];
+    if(this.range_)for(var i = 0;i < this.range_.length;i++)this.elements_.push(this.range_.item(i))
+  }return this.elements_
+};
+goog$dom$ControlRange.prototype.getSortedElements = function() {
+  if(!this.sortedElements_) {
+    this.sortedElements_ = this.getElements().concat();
+    this.sortedElements_.sort(function(a, b) {
+      return a.sourceIndex - b.sourceIndex
+    })
+  }return this.sortedElements_
+};
+goog$dom$ControlRange.prototype.isCollapsed = function() {
+  return!this.range_ || !this.range_.length
+};
+goog$dom$ControlRange.prototype.__iterator__ = function() {
+  return new goog$dom$ControlRangeIterator(this)
+};
+goog$dom$ControlRange.prototype.select = function() {
+  this.range_ && this.range_.select()
+};
+goog$dom$ControlRange.prototype.saveUsingDom = function() {
+  return new goog$dom$DomSavedControlRange_(this)
+};
+goog$dom$ControlRange.prototype.collapse = function() {
+  this.range_ = null;
+  this.clearCachedValues_()
+};
+var goog$dom$DomSavedControlRange_ = function(range) {
+  this.elements_ = range.getElements()
+};
+goog$inherits(goog$dom$DomSavedControlRange_, goog$dom$SavedRange);
+var goog$dom$ControlRangeIterator = function(range) {
+  if(range) {
+    this.elements_ = range.getSortedElements();
+    this.startNode_ = this.elements_.shift();
+    this.endNode_ = goog$array$peek(this.elements_) || this.startNode_
+  }goog$dom$RangeIterator.call(this, this.startNode_, false)
+};
+goog$inherits(goog$dom$ControlRangeIterator, goog$dom$RangeIterator);
+goog$dom$ControlRangeIterator.prototype.startNode_ = null;
+goog$dom$ControlRangeIterator.prototype.endNode_ = null;
+goog$dom$ControlRangeIterator.prototype.elements_ = null;
+goog$dom$ControlRangeIterator.prototype.getStartNode = function() {
+  return this.startNode_
+};
+goog$dom$ControlRangeIterator.prototype.getEndNode = function() {
+  return this.endNode_
+};
+goog$dom$ControlRangeIterator.prototype.isLast = function() {
+  return!this.depth && !this.elements_.length
+};
+goog$dom$ControlRangeIterator.prototype.next = function() {
+  if(this.isLast())throw goog$iter$StopIteration;else if(!this.depth) {
+    var el = this.elements_.shift();
+    this.setPosition(el, 1, 1);
+    return el
+  }return goog$dom$ControlRangeIterator.superClass_.next.call(this)
+};var goog$dom$MultiRange = function() {
+  this.browserRanges_ = [];
+  this.ranges_ = [];
+  this.container_ = this.sortedRanges_ = null
+};
+goog$inherits(goog$dom$MultiRange, goog$dom$AbstractMultiRange);
+goog$dom$MultiRange.prototype.logger_ = goog$debug$LogManager$getLogger("goog.dom.MultiRange");
+goog$dom$MultiRange.prototype.clearCachedValues_ = function() {
+  this.ranges_ = [];
+  this.container_ = this.sortedRanges_ = null
+};
+goog$dom$MultiRange.prototype.getType = function() {
+  return"mutli"
+};
+goog$dom$MultiRange.prototype.getBrowserRangeObject = function() {
+  this.browserRanges_.length > 1 && this.logger_.warning("getBrowserRangeObject called on MultiRange with more than 1 range");
+  return this.browserRanges_[0]
+};
+goog$dom$MultiRange.prototype.getTextRangeCount = function() {
+  return this.browserRanges_.length
+};
+goog$dom$MultiRange.prototype.getTextRange = function(i) {
+  this.ranges_[i] || (this.ranges_[i] = goog$dom$TextRange$createFromBrowserRangeWrapper_(goog$userAgent$IE ? new goog$dom$browserrange$IeRange(this.browserRanges_[i], goog$dom$getOwnerDocument(this.browserRanges_[i].parentElement())) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(this.browserRanges_[i]) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(this.browserRanges_[i]) : new goog$dom$browserrange$W3cRange(this.browserRanges_[i]), undefined));
+  return this.ranges_[i]
+};
+goog$dom$MultiRange.prototype.getContainer = function() {
+  if(!this.container_) {
+    for(var nodes = [], i = 0, len = this.getTextRangeCount();i < len;i++)nodes.push(this.getTextRange(i).getContainer());
+    this.container_ = goog$dom$findCommonAncestor.apply(null, nodes)
+  }return this.container_
+};
+goog$dom$MultiRange.prototype.getSortedRanges = function() {
+  if(!this.sortedRanges_) {
+    this.sortedRanges_ = this.getTextRanges();
+    this.sortedRanges_.sort(function(a, b) {
+      var aStartNode = a.getStartNode(), aStartOffset = a.getStartOffset(), bStartNode = b.getStartNode(), bStartOffset = b.getStartOffset();
+      if(aStartNode == bStartNode && aStartOffset == bStartOffset)return 0;
+      return goog$dom$Range$isReversed(aStartNode, aStartOffset, bStartNode, bStartOffset) ? 1 : -1
+    })
+  }return this.sortedRanges_
+};
+goog$dom$MultiRange.prototype.getStartNode = function() {
+  return this.getSortedRanges()[0].getStartNode()
+};
+goog$dom$MultiRange.prototype.getStartOffset = function() {
+  return this.getSortedRanges()[0].getStartOffset()
+};
+goog$dom$MultiRange.prototype.getEndNode = function() {
+  return goog$array$peek(this.getSortedRanges()).getEndNode()
+};
+goog$dom$MultiRange.prototype.getEndOffset = function() {
+  return goog$array$peek(this.getSortedRanges()).getEndOffset()
+};
+goog$dom$MultiRange.prototype.isCollapsed = function() {
+  return this.browserRanges_.length == 0 || this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed()
+};
+goog$dom$MultiRange.prototype.__iterator__ = function() {
+  return new goog$dom$MultiRangeIterator(this)
+};
+goog$dom$MultiRange.prototype.select = function() {
+  var selection;
+  JSCompiler_inline_label_goog$dom$AbstractRange$getBrowserSelectionForWindow_50: {
+    var JSCompiler_inline_win = this.getWindow();
+    if(JSCompiler_inline_win.getSelection)selection = JSCompiler_inline_win.getSelection();
+    else {
+      var JSCompiler_inline_doc = JSCompiler_inline_win.document;
+      selection = JSCompiler_inline_doc.selection || JSCompiler_inline_doc.getSelection && JSCompiler_inline_doc.getSelection()
+    }
+  }selection.removeAllRanges();
+  for(var i = 0, len = this.getTextRangeCount();i < len;i++)selection.addRange(this.getTextRange(i).getBrowserRangeObject())
+};
+goog$dom$MultiRange.prototype.saveUsingDom = function() {
+  return new goog$dom$DomSavedMultiRange_(this)
+};
+goog$dom$MultiRange.prototype.collapse = function(toAnchor) {
+  if(!this.isCollapsed()) {
+    var range = toAnchor ? this.getTextRange(0) : this.getTextRange(this.getTextRangeCount() - 1);
+    this.clearCachedValues_();
+    range.collapse(toAnchor);
+    this.ranges_ = [range];
+    this.sortedRanges_ = [range];
+    this.browserRanges_ = [range.getBrowserRangeObject()]
+  }
+};
+var goog$dom$DomSavedMultiRange_ = function(range) {
+  this.savedRanges_ = goog$array$map(range.getTextRanges(), function(range) {
+    return range.saveUsingDom()
+  })
+};
+goog$inherits(goog$dom$DomSavedMultiRange_, goog$dom$SavedRange);
+var goog$dom$MultiRangeIterator = function(range) {
+  if(range) {
+    this.ranges_ = range.getSortedRanges();
+    if(this.ranges_.length) {
+      this.startNode_ = this.ranges_[0].getStartNode();
+      this.endNode_ = goog$array$peek(this.ranges_).getEndNode()
+    }
+  }goog$dom$RangeIterator.call(this, this.startNode_, false)
+};
+goog$inherits(goog$dom$MultiRangeIterator, goog$dom$RangeIterator);
+goog$dom$MultiRangeIterator.prototype.startNode_ = null;
+goog$dom$MultiRangeIterator.prototype.endNode_ = null;
+goog$dom$MultiRangeIterator.prototype.ranges_ = null;
+goog$dom$MultiRangeIterator.prototype.getStartNode = function() {
+  return this.startNode_
+};
+goog$dom$MultiRangeIterator.prototype.getEndNode = function() {
+  return this.endNode_
+};
+goog$dom$MultiRangeIterator.prototype.isLast = function() {
+  return this.ranges_.length == 1 && this.ranges_[0].isLast()
+};
+goog$dom$MultiRangeIterator.prototype.next = function() {
+  do try {
+    this.ranges_[0].next();
+    break
+  }catch(ex) {
+    if(ex != goog$iter$StopIteration)throw ex;this.ranges_.shift()
+  }while(this.ranges_.length);
+  if(this.ranges_.length) {
+    var range = this.ranges_[0];
+    this.setPosition(range.node, range.tagType, range.depth);
+    return range.node
+  }else throw goog$iter$StopIteration;
+};var goog$dom$Range$createCaret = function(node, offset) {
+  return goog$dom$TextRange$createFromNodes(node, offset, node, offset)
+}, goog$dom$Range$createFromNodes = function(startNode, startOffset, endNode, endOffset) {
+  return goog$dom$TextRange$createFromNodes(startNode, startOffset, endNode, endOffset)
+}, goog$dom$Range$isReversed = function(anchorNode, anchorOffset, focusNode, focusOffset) {
+  if(anchorNode == focusNode)return focusOffset < anchorOffset;
+  var child;
+  if(anchorNode.nodeType == 1 && anchorOffset)if(child = anchorNode.childNodes[anchorOffset]) {
+    anchorNode = child;
+    anchorOffset = 0
+  }else if(goog$dom$contains(anchorNode, focusNode))return true;
+  if(focusNode.nodeType == 1 && focusOffset)if(child = focusNode.childNodes[focusOffset]) {
+    focusNode = child;
+    focusOffset = 0
+  }else if(goog$dom$contains(focusNode, anchorNode))return false;
+  return(goog$dom$compareNodeOrder(anchorNode, focusNode) || anchorOffset - focusOffset) > 0
+};window.createCaret = goog$dom$Range$createCaret;
+window.createFromNodes = goog$dom$Range$createFromNodes;
+try {
+  goog$dom$Range$createCaret(document.body, 0).select()
+}catch(e$$13) {
+};
+
+/**************************************************
+  Trace:
+      56.427 Start        Handling request
+    0 56.427 Start        Building cUnit
+    1 56.428 Done    1 ms Building cUnit
+    0 56.428 Start        Checking memcacheg
+    0 56.428 Start        Connecting to memcacheg
+    8 56.436 Done    8 ms Connecting to memcacheg
+    1 56.437 Done    9 ms Checking memcacheg
+    0 56.437 Done   10 ms Handling request
+**************************************************/
new file mode 100755
--- /dev/null
+++ b/editor/libeditor/html/tests/browserscope/lib/browserscope/richtext/richtext.html
@@ -0,0 +1,1079 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+  <title>Rich Text Tests</title>
+  <script src="js/range.js"></script>
+  <script>
+    /**
+     * Color class allows cross-browser comparison of values, which can
+     * be returned from queryCommandValue in several formats:
+     *   0xff00ff
+     *   rgb(255, 0, 0)
+     *   Number containing the hex value
+     */
+    function Color(value) {
+      this.compare = function(other) {
+        if (!this.valid || !other.valid) {
+          return false;
+        }
+        return this.red == other.red && this.green == other.green && this.blue == other.blue;
+      }
+      this.parse = function(value) {
+        var hexMatch = String(value).match(/#([0-9a-f]{6})/i);
+        if (hexMatch) {
+          this.red = parseInt(hexMatch[1].substring(0, 2), 16);
+          this.green = parseInt(hexMatch[1].substring(2, 4), 16);
+          this.blue = parseInt(hexMatch[1].substring(4, 6), 16);
+          return true;
+        }
+        var rgbMatch = String(value).match(/rgb\(([0-9]{1,3}),\s*([0-9]{1,3}),\s*([0-9]{1,3})\)/i);
+        if (rgbMatch) {
+          this.red = Number(rgbMatch[1]);
+          this.green = Number(rgbMatch[2]);
+          this.blue = Number(rgbMatch[3]);
+          return true;
+        }
+        if (Number(value)) {
+          this.red = value & 0xFF;
+          this.green = (value & 0xFF00) >> 8;
+          this.blue = (value & 0xFF0000) >> 16;
+          return true;
+        }
+        return false;
+      }
+      this.toString = function() {
+        return this.red + ',' + this.green + ',' + this.blue;
+      }
+      this.valid = this.parse(value);
+    }
+
+    /**
+     * Utility class for converting font sizes to the size
+     * attribute in a font tag. Currently only converts px because
+     * only the sizes and px ever come from queryCommandValue.
+     */
+    function Size(value) {
+      var pxMatch = String(value).match(/([0-9]+)px/);
+      if (pxMatch) {
+        var px = Number(pxMatch[1]);
+        if (px <= 10) {
+          this.size = 1;
+        } else if (px <= 13) {
+          this.size = 2;
+        } else if (px <= 16) {
+          this.size = 3;
+        } else if (px <= 18) {
+          this.size = 4;
+        } else if (px <= 24) {
+          this.size = 5;
+        } else if (px <= 32) {
+          this.size = 6;
+        } else if (px <= 47) {
+          this.size = 7;
+        } else {
+          this.size = NaN;
+        }
+      } else if (Number(value)) {
+           this.size = Number(value);
+      } else {
+        switch (value) {
+          case 'x-small':
+            this.size = 1;
+            break;
+          case 'small':
+            this.size = 2;
+            break;
+          case 'medium':
+            this.size = 3;
+            break;
+          case 'large':
+            this.size = 4;
+            break;
+          case 'x-large':
+            this.size = 5;
+            break;
+          case 'xx-large':
+            this.size = 6;
+            break;
+          case 'xxx-large':
+          case '-webkit-xxx-large':
+            this.size = 7;
+            break;
+          default:
+            this.size = null;
+        }
+      }
+      this.compare = function(other) {
+        return this.size == other.size;
+      }
+      this.toString = function() {
+        return String(this.size);
+      }
+    }
+
+    var APPLY_TESTS = {
+      'backcolor' : {
+        opt_arg: '#FF0000',
+        styleWithCSS: 'background-color'},
+      'bold' : {
+        opt_arg: null,
+        styleWithCSS: 'font-weight'},
+      'createbookmark' : {
+        opt_arg: 'bookmark_name'},
+      'createlink' : {
+        opt_arg: 'http://www.openweb.org'},
+      'decreasefontsize' : {
+        opt_arg: null},
+      'fontname' : {
+        opt_arg: 'Arial',
+        styleWithCSS: 'font-family'},
+      'fontsize' : {
+        opt_arg: 4,
+        styleWithCSS: 'font-size'},
+      'forecolor' : {
+        opt_arg: '#FF0000',
+        styleWithCSS: 'color'},
+      'formatblock' : {
+        opt_arg: 'h1',
+        wholeline: true},
+      'hilitecolor' : {
+        opt_arg: '#FF0000',
+        styleWithCSS: 'background-color'},
+      'indent' : {
+        opt_arg: null,
+        wholeline: true,
+        styleWithCSS: 'margin'},
+      'inserthorizontalrule' : {
+        opt_arg: null,
+        collapse: true},
+      'inserthtml': {
+        opt_arg: '<br>',
+        collapse: true},
+      'insertimage': {
+        opt_arg: 'http://www.google.com/intl/en_ALL/images/logo.gif',
+        collapse: true},
+      'insertorderedlist' : {
+        opt_arg: null,
+        wholeline: true},
+      'insertunorderedlist' : {
+        opt_arg: null,
+        wholeline: true},
+      'italic' : {
+        opt_arg: null,
+        styleWithCSS: 'font-style'},
+      'justifycenter' : {
+        opt_arg: null,
+        wholeline: true,
+        styleWithCSS: 'text-align'},
+      'justifyfull' : {
+        opt_arg: null,
+        wholeline: true,
+        styleWithCSS: 'text-align'},
+      'justifyleft' : {
+        opt_arg: null,
+        wholeline: true,
+        styleWithCSS: 'text-align'},
+      'justifyright' : {
+        opt_arg: null,
+        wholeline: true,
+        styleWithCSS: 'text-align'},
+      'strikethrough' : {
+        opt_arg: null,
+        styleWithCSS: 'text-decoration'},
+      'subscript' : {
+        opt_arg: null,
+        styleWithCSS: 'vertical-align'},
+      'superscript' : {
+        opt_arg: null,
+        styleWithCSS: 'vertical-align'},
+      'underline' : {
+        opt_arg: null,
+        styleWithCSS: 'text-decoration'}};
+
+    var UNAPPLY_TESTS = {
+      'bold' : {
+        tags: [
+          ['<b>', '</b>'],
+          ['<STRONG>', '</STRONG>'],
+          ['<span style="font-weight: bold;">', '</span>']]},
+      'italic' : {
+        tags: [
+          ['<i>', '</i>'],
+          ['<EM>', '</EM>'],
+          ['<span style="font-style: italic;">', '</span>']]},
+      'outdent' : {
+        unapply: 'indent',
+        block: true,
+        tags: [
+          ['<blockquote>', '</blockquote>'],
+          ['<blockquote class="webkit-indent-blockquote" style="margin: 0 0 0 40px; border: none; padding: 0px;">', '</blockquote>'],
+          ['<ul><li>', '</li></ul>'],
+          ['<ol><li>', '</li></ol>'],
+          ['<div style="margin-left: 40px;">', '</div>']]},
+      'removeformat' : {
+        unapply: '*',
+        block: true,
+        tags: [
+          ['<b>', '</b>'],
+          ['<a href="http://www.foo.com">', '</a>'],
+          ['<table><tr><td>', '</td></tr></table>']]},
+      'strikethrough' : {
+        tags: [
+          ['<strike>', '</strike>'],
+          ['<s>', '</s>'],
+          ['<del>', '</del>'],
+          ['<span style="text-decoration: line-through;">', '</span>']]},
+      'subscript' : {
+        tags: [
+          ['<sub>', '</sub>'],
+          ['<span style="vertical-align: sub;">', '</span>']]},
+      'superscript' : {
+        tags: [
+          ['<sup>', '</sup>'],
+          ['<span style="vertical-align: super;">', '</span>']]},
+      'unbookmark' : {
+        unapply: 'createbookmark',
+        tags: [
+          ['<a name="bookmark">', '</a>']]},
+      'underline' : {
+        tags: [
+          ['<u>', '</u>'],
+          ['<span style="text-decoration: underline;">', '</span>']]},
+      'unlink' : {
+        unapply: 'createbookmark',
+        tags: [
+          ['<a href="http://www.foo.com">', '</a>']]}};
+
+    var QUERY_TESTS = {
+      'backcolor' : {
+        type: 'value',
+        tests: [
+          {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', expected: new Color('#ffccaa')},
+          {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', expected: new Color('#ff0000')},
+          {html: '<span style="background-color: #ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+        ]
+      },
+      'bold' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<b>foo bar baz</b>', expected: true},
+          {html: '<STRONG>foo bar baz</STRONG>', expected: true},
+          {html: '<span style="font-weight:bold">foo bar baz</span>', expected: true},
+          {html: '<b style="font-weight:normal">foo bar baz</b>', expected: false},
+          {html: '<b><span style="font-weight:normal;">foo bar baz</span>', expected: false}
+         ]
+      },
+      'fontname' : {
+        type: 'value',
+        tests: [
+          {html: '<font face="Arial">foo bar baz</font>', expected: 'Arial'},
+          {html: '<span style="font-family:Arial">foo bar baz</span>', expected: 'Arial'},
+          {html: '<font face="Arial" style="font-family:Courier">foo bar baz</font>', expected: 'Courier'},
+          {html: '<font face="Courier"><font face="Arial">foo bar baz</font></font>', expected: 'Arial'},
+          {html: '<span style="font-family:Courier"><font face="Arial">foo bar baz</font></span>', expected: 'Arial'}
+        ]
+      },
+      'fontsize' : {
+        type: 'value',
+        tests: [
+          {html: '<font size=4>foo bar baz</font>', expected: new Size(4)},
+          // IE adds +1 to font size from font-size style attributes.
+          // This is hard to correct for since it does NOT add +1 to size attribute from font tag.
+          {html: '<span class="Apple-style-span" style="font-size: large;">foo bar baz</span>', expected: new Size(4)},
+          {html: '<font size=1 style="font-size:x-large;">foo bar baz</font>', expected: new Size(5)}
+        ]
+      },
+      'forecolor' : {
+        type: 'value',
+        tests: [
+          {html: '<font color="#ff0000">foo bar baz</font>', expected: new Color('#ff0000')},
+          {html: '<span style="color:#ff0000">foo bar baz</span>', expected: new Color('#ff0000')},
+          {html: '<font color="#0000ff" style="color:#ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+        ]
+      },
+      'hilitecolor' : {
+        type: 'value',
+        tests: [
+          {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', expected: new Color('#ffccaa')},
+          {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', expected: new Color('#ff0000')},
+          {html: '<span style="background-color: #ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+        ]
+      },
+      'insertorderedlist' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<ol><li>foo bar baz</li></ol>', expected: true},
+          {html: '<ul><li>foo bar baz</li></ul>', expected: false}
+        ]
+      },
+      'insertunorderedlist' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<ol><li>foo bar baz</li></ol>', expected: false},
+          {html: '<ul><li>foo bar baz</li></ul>', expected: true}
+        ]
+      },
+      'italic' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<i>foo bar baz</i>', expected: true},
+          {html: '<EM>foo bar baz</EM>', expected: true},
+          {html: '<span style="font-style:italic">foo bar baz</span>', expected: true},
+          {html: '<i><span style="font-style:normal">foo bar baz</span></i>', expected: false}
+        ]
+      },
+      'justifycenter' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<div align="center">foo bar baz</div>', expected: true},
+          {html: '<p align="center">foo bar baz</p>', expected: true},
+          {html: '<div style="text-align: center;">foo bar baz</div>', expected: true}
+        ]
+      },
+      'justifyfull' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<div align="justify">foo bar baz</div>', expected: true},
+          {html: '<p align="justify">foo bar baz</p>', expected: true},
+          {html: '<div style="text-align: justify;">foo bar baz</div>', expected: true}
+        ]
+      },
+      'justifyleft' : {
+        type: 'state',
+        tests: [
+          {html: '<div align="left">foo bar baz</div>', expected: true},
+          {html: '<p align="left">foo bar baz</p>', expected: true},
+          {html: '<div style="text-align: left;">foo bar baz</div>', expected: true}
+        ]
+      },
+      'justifyright' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<div align="right">foo bar baz</div>', expected: true},
+          {html: '<p align="right">foo bar baz</p>', expected: true},
+          {html: '<div style="text-align: right;">foo bar baz</div>', expected: true}
+        ]
+      },
+      'strikethrough' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<strike>foo bar baz</strike>', expected: true},
+          {html: '<strike style="text-decoration: none">foo bar baz</strike>', expected: false},
+          {html: '<s>foo bar baz</s>', expected: true},
+          {html: '<del>foo bar baz</del>', expected: true},
+          {html: '<span style="text-decoration:line-through">foo bar baz</span>', expected: true}
+        ]
+      },
+      'subscript' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<sub>foo bar baz</sub>', expected: true}
+        ]
+      },
+      'superscript' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<sup>foo bar baz</sup>', expected: true}
+        ]
+      },
+      'underline' : {
+        type: 'state',
+        tests: [
+          {html: 'foo bar baz', expected: false},
+          {html: '<u>foo bar baz</u>', expected: true},
+          {html: '<a href="http://www.foo.com">foo bar baz</a>', expected: true},
+          {html: '<span style="text-decoration:underline">foo bar baz</span>', expected: true},
+          {html: '<u style="text-decoration:none">foo bar baz</u>', expected: false},
+          {html: '<a style="text-decoration:none" href="http://www.foo.com">foo bar baz</a>', expected: false}
+        ]
+      }
+    };
+
+    var CHANGE_TESTS = {
+      'backcolor' : {
+        type: 'value',
+        tests: [
+          {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', opt_arg: '#884422'},
+          {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', opt_arg: '#0000ff'},
+          {html: '<span style="background-color: #ff0000">foo bar baz</span>', opt_arg: '#0000ff'}
+        ]
+      },
+      'fontname' : {
+        type: 'value',
+        tests: [
+          {html: '<font face="Arial">foo bar baz</font>', opt_arg: 'Courier'},
+          {html: '<span style="font-family:Arial">foo bar baz</span>', opt_arg: 'Courier'},
+          {html: '<font face="Arial" style="font-family:Verdana">foo bar baz</font>', opt_arg: 'Courier'},
+          {html: '<font face="Verdana"><font face="Arial">foo bar baz</font></font>', opt_arg: 'Courier'},
+          {html: '<span style="font-family:Verdana"><font face="Arial">foo bar baz</font></span>', opt_arg: 'Courier'}
+        ]
+      },
+      'fontsize' : {
+        type: 'value',
+        tests: [
+          {html: '<font size=4>foo bar baz</font>', opt_arg: 1},
+          {html: '<span class="Apple-style-span" style="font-size: large;">foo bar baz</span>', opt_arg: 1},
+          {html: '<font size=1 style="font-size:x-small;">foo bar baz</font>', opt_arg: 5}
+        ]
+      },
+      'forecolor' : {
+        type: 'value',
+        tests: [
+          {html: '<font color="#ff0000">foo bar baz</font>', opt_arg: '#00ff00'},
+          {html: '<span style="color:#ff0000">foo bar baz</span>', opt_arg: '#00ff00'},
+          {html: '<font color="#0000ff" style="color:#ff0000">foo bar baz</span>', opt_arg: '#00ff00'}
+        ]
+      },
+      'hilitecolor' : {
+        type: 'value',
+        tests: [
+          {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', opt_arg: '#884422'},
+          {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', opt_arg: '#00ff00'},
+          {html: '<span style="background-color: #ff0000">foo bar baz</span>', opt_arg: '#00ff00'}
+        ]
+      }
+    };
+
+    /** The document of the editable iframe */
+    var editorDoc = null;
+    /** Dummy text to apply and unapply formatting to */
+    var TEST_CONTENT = 'foo bar baz';
+    /**
+     * Word in dummy text that should change. Formatting is sometimes applied
+     * to a single word instead of the entire text node because sometimes a
+     * style might get applied to the body node instead of wrapped around
+     * the text, and that's not what's being tested.
+     */
+    var TEST_WORD = 'bar';
+    /** Constant for indicating an action is unsupported (threw exception) */
+    var UNSUPPORTED = 'UNSUPPORTED';
+    /** <br> and <p> are acceptable HTML to be left over from block elements */
+    var BLOCK_REMOVE_TAGS = [/\s*<br>\s*/i, /\s*<p>\s*/i];
+    /** Array used to accumulate test results */
+    // Tack on the actual display tests with bogus data
+    // otherwise the beacon will fail.
+    var results = ['apply=0', 'unapply=0', 'change=0', 'query=0'];
+
+    /**
+     *
+     */
+    function resetIframe(newHtml) {
+      // These attributes can get set on the iframe by some errant execCommands
+      editorDoc.body.setAttribute('style', '');
+      editorDoc.body.setAttribute('bgcolor', '');
+      editorDoc.body.innerHTML = newHtml;
+    }
+
+    /**
+     * Finds the text node in the given node containing the given word.
+     * Returns null if not found.
+     */
+    function findTextNode(word, node) {
+      if (node.nodeType == 3) {
+        // Text node, check value.
+        if (node.data.indexOf(word) != -1) {
+          return node;
+        }
+      } else if (node.nodeType == 1) {
+        // Element node, check children.
+        for (var i = 0; i < node.childNodes.length; i++) {
+          var result = findTextNode(word, node.childNodes[i]);
+          if (result) {
+            return result;
+          }
+        }
+      }
+      return null;
+    }
+
+    /**
+     * Sets the selection to be collapsed at the start of the word,
+     * or the start of the editor if no word is passed in.
+     */
+     function selectStart(word) {
+       var textNode = findTextNode(word || '', editorDoc.body);
+       var startOffset = 0;
+       if (word) {
+         startOffset = textNode.data.indexOf(word);
+       }
+       var range = createCaret(textNode, startOffset);
+       range.select();
+     }
+
+    /**
+     * Selects the given word in the editor iframe.
+     */
+    function selectWord(word) {
+      var textNode = findTextNode(word, editorDoc.body);
+      if (!textNode) {
+        return;
+      }
+      var start = textNode.data.indexOf(word);
+      var range = createFromNodes(textNode, start, textNode, start + word.length);
+      range.select();
+    }
+
+    /**
+     * Gets the HTML before the text, so that we know how the browser
+     * applied a style
+     */
+    function getSurroundingTags(text) {
+      var html = editorDoc.body.innerHTML;
+      var tagStart = html.indexOf('<');
+      var index = editorDoc.body.innerHTML.indexOf(text);
+      if (tagStart == -1 || index == -1) {
+        return '';
+      }
+      return editorDoc.body.innerHTML.substring(tagStart, index);
+    }
+
+    /**
+     * Does the test for an apply execCommand.
+     */
+    function doApplyTest(command, styleWithCSS) {
+      try {
+        // Set styleWithCSS
+        try {
+          editorDoc.execCommand('styleWithCSS', false, styleWithCSS);
+        } catch (ex) {
+          // Ignore errors
+        }
+        resetIframe(TEST_CONTENT);
+        if (APPLY_TESTS[command].collapse) {
+          selectStart(TEST_WORD);
+        } else {
+          selectWord(TEST_WORD);
+        }
+        try {
+          editorDoc.execCommand(command, false, APPLY_TESTS[command].opt_arg);
+        } catch (ex) {
+          return UNSUPPORTED;
+        }
+        return getSurroundingTags(APPLY_TESTS[command].wholeline? TEST_CONTENT : TEST_WORD);
+      } catch (ex) {
+        return UNSUPPORTED;
+      }
+    }
+
+    /**
+     * Outputs the result of the apply command to a table.
+     * @return {boolean} success
+     */
+    function outputApplyResult(command, result, styleWithCSS) {
+      // The apply command "succeeded" if HTML was generated.
+      var success = (result != UNSUPPORTED) && result;
+      // Except for styleWithCSS commands, which only succeed if the
+      // expected style was applied.
+      if (styleWithCSS) {
+        success = result && result.toLowerCase().indexOf(APPLY_TESTS[command].styleWithCSS) != -1;
+      }
+      results.push('a-' + command + '-' + (styleWithCSS ? 1 : 0) + '=' + (success ? '1' : '0'));
+
+      // Each command is displayed as a table row with 3 columns
+      var tr = document.createElement('TR');
+      tr.className = success ? 'success' : 'fail';
+
+      // Column 1: command name
+      var td = document.createElement('TD');
+      td.innerHTML = command;
+      tr.appendChild(td);
+
+      // Column 2: styleWithCSS
+      var td = document.createElement('TD');
+      td.innerHTML = styleWithCSS ? 'true' : 'false';
+      tr.appendChild(td);
+
+      // Column 3: pass/fail
+      td = document.createElement('TD');
+      td.innerHTML = success ? 'PASS' : 'FAIL';
+      tr.appendChild(td);
+
+      // Column 4: generated HTML (for passing commands)
+      td = document.createElement('TD');
+      // Escape the HTML in the result for printing.
+      result = result.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
+      td.innerHTML = result;
+      tr.appendChild(td);
+      var table = document.getElementById('apply_output');
+      table.appendChild(tr);
+      return success;
+    }
+
+    /**
+     * Does the test for an unapply execCommand.
+     */
+    function doUnapplyTest(command, index) {
+      try {
+        var wordStart = TEST_CONTENT.indexOf(TEST_WORD);
+        resetIframe(
+            TEST_CONTENT.substring(0, wordStart) +
+            UNAPPLY_TESTS[command].tags[index][0] +
+            TEST_WORD +
+            UNAPPLY_TESTS[command].tags[index][1] +
+            TEST_CONTENT.substring(wordStart + TEST_WORD.length));
+        selectWord(TEST_WORD);
+        try {
+          editorDoc.execCommand(command, false, UNAPPLY_TESTS[command].opt_arg || null);
+        } catch (ex) {
+          return UNSUPPORTED;
+        }
+        return getSurroundingTags(TEST_WORD);
+      } catch (ex) {
+        return UNSUPPORTED;
+      }
+    }
+
+    /**
+     * Check if the given unapply execCommand succeeded. It succeeded if
+     * the following conditions are true:
+     *   - The execCommand did not throw an exception
+     *   - One of the following:
+     *     - The html was removed after the execCommand
+     *     - The html was block and the html was replaced with <p> or <br>
+     */
+    function unapplyCommandSucceeded(command, result) {
+      if (result != UNSUPPORTED) {
+        if (!result) {
+          return true;
+        } else if (UNAPPLY_TESTS[command].block) {
+          for (var i = 0; i < BLOCK_REMOVE_TAGS.length; i++) {
+            if (result.match(BLOCK_REMOVE_TAGS[i])) {
+              return true;
+            }
+          }
+        }
+      }
+      return false;
+    }
+
+    /**
+     * Outputs the result of the unapply command to a table.
+     * @return {boolean} success
+     */
+    function outputUnapplyResult(command, result, index) {
+      // The apply command "succeeded" if HTML was removed.
+      var success = unapplyCommandSucceeded(command, result);
+      results.push('u-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+      // Each command is displayed as a table row with 5 columns
+      var tr = document.createElement('TR');
+      tr.className = success ? 'success' : 'fail';
+
+      // Column 1: command name
+      var td = document.createElement('TD');
+      td.innerHTML = command;
+      tr.appendChild(td);
+
+      // Column 2: command name being unapplied
+      var td = document.createElement('TD');
+      td.innerHTML = UNAPPLY_TESTS[command].unapply || command;
+      tr.appendChild(td);
+
+      // Column 3: pass/fail
+      td = document.createElement('TD');
+      td.innerHTML = success ? 'PASS' : 'FAIL';
+      tr.appendChild(td);
+
+      // Column 4: html being removed
+      td = document.createElement('TD');
+      // Escape the html for printing.
+      var htmlToRemove = UNAPPLY_TESTS[command].tags[index][0].replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
+      td.innerHTML = htmlToRemove;
+      tr.appendChild(td);
+
+      // Column 5: resulting html
+      td = document.createElement('TD');
+      // Escape the HTML in the result for printing.
+      result = result.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
+      td.innerHTML = success ? '&nbsp;' : result;
+      tr.appendChild(td);
+      var table = document.getElementById('unapply_output');
+      table.appendChild(tr);
+      return success;
+    }
+
+    /**
+     * Does a queryCommandState or queryCommandValue test for an execCommand.
+     */
+    function doQueryTest(command, index) {
+      try {
+        resetIframe(QUERY_TESTS[command].tests[index].html);
+        selectWord(TEST_WORD);
+        // Dummy val that won't match any expected vals, including false.
+        var result = UNSUPPORTED;
+        if (QUERY_TESTS[command].type == 'state') {
+          try {
+            result = editorDoc.queryCommandState(command);
+          } catch (ex) {
+            result = UNSUPPORTED;
+          }
+        } else {
+          try {
+            // A return value of false indicates the command is not supported.
+            result = editorDoc.queryCommandValue(command) || UNSUPPORTED;
+          } catch (ex) {
+            result = UNSUPPORTED;
+          }
+        }
+        return result;
+      } catch (ex) {
+        return UNSUPPORTED;
+      }
+    }
+
+    /**
+     * Check if the given queryCommandState or queryCommandValue succeeded.
+     */
+    function queryCommandSucceeded(command, index, result) {
+      var expected = QUERY_TESTS[command].tests[index].expected;
+      if (expected instanceof Color) {
+        return expected.compare(new Color(result));
+      } else if (expected instanceof Size) {
+        return expected.compare(new Size(result));
+      } else {
+        return (result == expected);
+      }
+    }
+
+    /**
+     * @return {boolean} success
+     */
+    function outputQueryResult(command, index, result) {
+      // Create table row for results.
+      var tr = document.createElement('TR');
+      var success = queryCommandSucceeded(command, index, result);
+      tr.className = success ? 'success' : 'fail';
+      results.push('q-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+      // Column 1: command name
+      var td = document.createElement('TD');
+      td.innerHTML = command;
+      tr.appendChild(td);
+
+      // Column 2: pass/fail
+      td = document.createElement('TD');
+      td.innerHTML = success ? 'PASS' : 'FAIL';
+      tr.appendChild(td);
+
+      // Column 3: test HTML
+      td = document.createElement('TD');
+      var testHtml = QUERY_TESTS[command].tests[index].html.replace(/</g, '&lt;').replace(/>/g, '&gt;');
+      td.innerHTML = testHtml.substring(0, testHtml.indexOf(TEST_CONTENT));
+      tr.appendChild(td);
+
+      // Column 4: Expected result
+      td = document.createElement('TD');
+      td.innerHTML = QUERY_TESTS[command].tests[index].expected;
+      tr.appendChild(td);
+
+      // Column 5: Actual result
+      td = document.createElement('TD');
+      td.innerHTML = result;
+      tr.appendChild(td);
+
+      // Append result to the state or value table, depending on what
+      // type of command this is.
+      var table = document.getElementById(
+      QUERY_TESTS[command].type == 'state' ? 'querystate_output' : 'queryvalue_output');
+      table.appendChild(tr);
+      return success;
+    }
+
+    function doChangeTest(command, index) {
+      try {
+        resetIframe(CHANGE_TESTS[command].tests[index].html);
+        selectWord(TEST_CONTENT);
+        try {
+          editorDoc.execCommand(command, false, CHANGE_TESTS[command].tests[index].opt_arg);
+        } catch (ex) {
+          return UNSUPPORTED;
+        }
+      } catch (ex) {
+        return UNSUPPORTED;
+      }
+    }
+
+    function checkChangeSuccess(command, index) {
+      var textNode = findTextNode(TEST_CONTENT, editorDoc.body);
+      if (!textNode) {
+        // The text has been removed from the document, or split up for no reason.
+        return false;
+      }
+      var expected = null, attributeName = null, styleName = null;
+      switch (command) {
+        case 'backcolor':
+        case 'hilitecolor':
+          expected = new Color(CHANGE_TESTS[command].tests[index].opt_arg);
+          styleName = 'backgroundColor';
+          break;
+        case 'fontname':
+          expected = CHANGE_TESTS[command].tests[index].opt_arg;
+          attributeName = 'face';
+          styleName = 'fontFamily';
+          break;
+        case 'fontsize':
+          expected = new Size(CHANGE_TESTS[command].tests[index].opt_arg);
+          attributeName = 'size';
+          styleName = 'fontSize';
+          break;
+        case 'forecolor':
+          expected = new Color(CHANGE_TESTS[command].tests[index].opt_arg);
+          attributeName = 'color';
+          styleName = 'color';
+      }
+      var foundExpected = false;
+
+      // Loop through all the parent nodes that format the text node,
+      // checking that there is exactly one font attribute or
+      // style, and that it's set correctly.
+      var currentNode = textNode.parentNode;
+      while(currentNode && currentNode.nodeName != 'BODY') {
+        // Check font attribute.
+        if (attributeName && currentNode.nodeName == 'FONT' && currentNode.getAttribute(attributeName)) {
+          var foundAttribute = false;
+          switch(command) {
+            case 'backcolor':
+            case 'forecolor':
+            case 'hilitecolor':
+              foundAttribute = new Color(currentNode.getAttribute(attributeName)).compare(expected);
+              break;
+            case 'fontsize':
+              foundAttribute = new Size(currentNode.getAttribute(attributeName)).compare(expected);
+              break;
+            case 'fontname':
+              foundAttribute = (currentNode.getAttribute(attributeName).toLowerCase() == expected.toLowerCase());
+          }
+          if (foundAttribute && foundExpected) {
+            // This is the correct attribute, but the style has been applied
+            // twice. This makes it hard for other browsers to remove the
+            // style.
+            return false;
+          } else if (!foundAttribute) {
+            // This node has an incorrect font attribute.
+            return false;
+          }
+          // The expected font attribute was found.
+          foundExpected = true;
+        }
+        // Check node style.
+        if (currentNode.style[styleName]) {
+          var foundStyle = false;
+          switch(command) {
+            case 'backcolor':
+            case 'forecolor':
+            case 'hilitecolor':
+              foundStyle = new Color(currentNode.style[styleName]).compare(expected);
+              break;
+            case 'fontsize':
+              foundStyle = new Size(currentNode.style[styleName]).compare(expected);
+              break;
+            case 'fontname':
+              foundStyle = (currentNode.style[styleName].toLowerCase() == expected.toLowerCase());
+          }
+          if (foundStyle && foundExpected) {
+            // This is the correct style, but the style has been
+            // applied twice. This makes it hard for other browsers to
+            // remove the style.
+            return false;
+          } else if (!foundStyle) {
+            // This node has an incorrect font style.
+            return false;
+          }
+          foundExpected = true;
+        }
+        currentNode = currentNode.parentNode;
+      }
+      return foundExpected;
+    }
+
+    /**
+     * @return {boolean} success
+     */
+    function outputChangeResult(command, index) {
+      // Each command is displayed as a table row with 4 columns
+      var tr = document.createElement('TR');
+      var success = checkChangeSuccess(command, index);
+      tr.className = success ? 'success' : 'fail';
+      results.push('c-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+      // Column 1: command name
+      var td = document.createElement('TD');
+      td.innerHTML = command;
+      tr.appendChild(td);
+
+      // Column 2: status
+      td = document.createElement('TD');
+      td.innerHTML = (success == null) ? '?' : (success == true ? 'PASS' : 'FAIL');
+      tr.appendChild(td);
+
+      // Column 3: opt_arg
+      td = document.createElement('TD');
+      td.innerHTML = CHANGE_TESTS[command].tests[index].opt_arg;
+      tr.appendChild(td);
+
+      // Column 4: original html
+      td = document.createElement('TD');
+      td.innerHTML = CHANGE_TESTS[command].tests[index].html.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');;
+      tr.appendChild(td);
+
+      // Column 5: resulting html
+      td = document.createElement('TD');
+      td.innerHTML = editorDoc.body.innerHTML.replace(/\</g, '&lt;').replace(/\>/g, '&gt;');;
+      tr.appendChild(td);
+
+      var table = document.getElementById('change_output');
+      table.appendChild(tr);
+      return success;
+    }
+
+    function runTests() {
+      // Wrap initialization code in a try/catch so we can fail gracefully
+      // on older browsers.
+      try {
+        editorDoc = document.getElementById('editor').contentWindow.document;
+        // Default styleWithCSS to false, since it's not supported by IE.
+        try {
+          editorDoc.execCommand('styleWithCSS', false, false);
+        } catch (ex) {
+          // Not supported by IE.
+        }
+      } catch (ex) {}
+
+      // Apply tests
+      var apply_score = 0;
+      var apply_count = 0;
+      var unapply_score= 0;
+      var unapply_count = 0;
+      var change_score = 0;
+      var change_count = 0;
+      var query_score = 0;
+      var query_count = 0;
+      for (var command in APPLY_TESTS) {
+        try {
+          var result = doApplyTest(command, false);
+          var success = outputApplyResult(command, result, false);
+          apply_score += success ? 1 : 0;
+        } catch (ex) {
+          // An exception is counted as a failed test, don't increment success.
+        }
+        apply_count++;
+        if (APPLY_TESTS[command].styleWithCSS) {
+          try {
+            var result = doApplyTest(command, true);
+            var success = outputApplyResult(command, result, true);
+            apply_score += success ? 1 : 0;
+          } catch (ex) {
+            // An exception is counted as a failed test, don't increment success.
+          }
+          apply_count++;
+        }
+      }
+
+      // Unapply tests
+      for (var command in UNAPPLY_TESTS) {
+        for (var i = 0; i < UNAPPLY_TESTS[command].tags.length; i++) {
+          try {
+            var result = doUnapplyTest(command, i);
+            var success = outputUnapplyResult(command, result, i);
+            unapply_score += success ? 1 : 0;
+          } catch (ex) {
+            // An exception is counted as a failed test, don't increment success.
+          }
+          unapply_count++;
+        }
+      }
+
+      // Query tests
+      for (var command in QUERY_TESTS) {
+        for (var i = 0; i < QUERY_TESTS[command].tests.length; i++) {
+          try {
+            var result = doQueryTest(command, i);
+            var success = outputQueryResult(command, i, result);
+            query_score += success ? 1 : 0;
+          } catch (ex) {
+            // An exception is counted as a failed test, don't increment success.
+          }
+          query_count++;
+        }
+      }
+
+      // Change tests
+      for (var command in CHANGE_TESTS) {
+        for (var i = 0; i < CHANGE_TESTS[command].tests.length; i++) {
+          try {
+            doChangeTest(command, i);
+            var success = outputChangeResult(command, i);
+            change_score += success ? 1 : 0;
+          } catch (ex) {
+            // An exception is counted as a failed test, don't increment success.
+          }
+          change_count++;
+        }
+      }
+
+      // Beacon all test results.
+      // and construct a shorter version for the results page.
+      try {
+        document.getElementById('apply-score').innerHTML =
+            apply_score + '/' + apply_count;
+        document.getElementById('unapply-score').innerHTML =
+            unapply_score + '/' + unapply_count;
+        document.getElementById('query-score').innerHTML =
+            query_score + '/' + query_count;
+        document.getElementById('change-score').innerHTML =
+            change_score + '/' + change_count;
+      } catch (ex) {}
+      var continueParams = [
+        'apply=' + apply_score,
+        'unapply=' + unapply_score,
+        'query=' + query_score,
+        'change=' + change_score
+      ];
+      parent.sendScore(results, continueParams);
+    }
+  </script>
+  <style>
+    .success {
+      background-color: #93c47d;
+    }
+    .fail {
+      background-color: #ea9999;
+    }
+    .score {
+      color: #666;
+    }
+  </style>
+</head>
+<body onload="runTests()">
+  <h1>Apply Formatting <span id="apply-score" class="score"></span></h1>
+  <table id="apply"><tbody id="apply_output"><tr><th>Command</th><th>styleWithCSS</th><th>Status</th><th>Output</th></tr></tbody></table>
+  <h1>Unapply Formatting <span id="unapply-score" class="score"></span></h1>
+  <table id="unapply">
+    <thead><tr><th>Command</th><th>Command unapplied</th><th>Status</th><th>HTML Attempted to Unapply</th><th>Resulting HTML</th></tr></thead>
+    <tbody id="unapply_output"></tbody></table>
+  <h1>Query Formatting State <span id="query-score" class="score"></span></h1>
+  <table id="querystate">
+    <thead><tr><th>Command</th><th>Status</th><th>HTML</th><th>Expected</th><th>Actual</th></tr></thead>
+    <tbody id="querystate_output"></tbody></table>
+  <h1>Query Formatting Value </h1>
+  <table id="queryvalue">
+    <thead><tr><th>Command</th><th>Status</th><th>HTML</th><th>Expected</th><th>Actual</th></tr></thead>
+    <tbody id="queryvalue_output"></tbody></table>
+  <h1>Change Formatting <span id="change-score" class="score"></span></h1>
+  <table id="change">
+    <thead><tr><th>Command</th><th>Status</th><th>Argument</th><th>Original HTML</th><th>Resulting HTML</th></tr></thead>
+    <tbody id="change_output"></tbody></table>
+  <iframe name="editor" id="editor" src="editable.html"></iframe>
+</body>
+</html>
new file mode 100755
--- /dev/null
+++ b/editor/libeditor/html/tests/browserscope/lib/browserscope/update_from_upstream
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+set -x
+
+if test -d richtext; then
+  rm -drf richtext;
+fi
+
+svn checkout http://browserscope.googlecode.com/svn/trunk/categories/richtext/static richtext | tail -1 | sed 's/[^0-9]//g' > current_revision
+
+find richtext -type d -name .svn -exec rm -drf \{\} \; 2> /dev/null
+
+hg add current_revision richtext
+
+hg stat .
+
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/html/tests/browserscope/test_browserscope.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+BrowserScope richtext category tests
+-->
+<head>
+  <title>BrowserScope Richtext Tests</title>
+  <script type="application/javascript" src="/MochiKit/packed.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>  
+  <script type="text/javascript" src="lib/browserscope/currentStatus.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=550569">Mozilla Bug 550569</a>
+<p id="display"></p>
+<div id="content">
+  <iframe src="lib/browserscope/richtext/richtext.html"></iframe>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+// Running all of the tests can take a long time, try to account for it
+SimpleTest.requestLongerTimeout(5);
+
+function sendScore(results, continueParams) {
+  ok(results.length > 1, "At least one test should have been run");
+  for (var i = 1; i < results.length; ++i) {
+    var result = results[i];
+    [type, command, param, success] = result.split(/[\-=]/);
+    var comp = is;
+    if (isKnownFailure(type, command, param)) {
+      comp = todo_is;
+    }
+    comp(success, "1", "Browserscope category=" + type +
+         " test=" + command +
+         " param=" + param);
+  }
+}
+
+document.getElementsByTagName("iframe")[0].addEventListener("load", function() {
+  SimpleTest.finish();
+}, false);
+
+</script>
+</pre>
+</body>
+</html>