[Pkg-javascript-commits] [node-text-encoding] 01/03: Imported Upstream version 0.5.3

Julien Puydt julien.puydt at laposte.net
Fri Sep 11 14:09:25 UTC 2015


This is an automated email from the git hooks/post-receive script.

jpuydt-guest pushed a commit to branch master
in repository node-text-encoding.

commit b41465978987b41b5b7b828f70f87d36c104afce
Author: Julien Puydt <julien.puydt at laposte.net>
Date:   Fri Sep 11 15:50:57 2015 +0200

    Imported Upstream version 0.5.3
---
 LICENSE.md                  |  202 +++
 README.md                   |  103 ++
 bower.json                  |   22 +
 examples-no-indexes.html    |   58 +
 examples.html               |   59 +
 index.js                    |   20 +
 lib/encoding-indexes.js     |   54 +
 lib/encoding.js             | 3106 +++++++++++++++++++++++++++++++++++++++++++
 package.json                |   28 +
 test/test-big5.js           |   22 +
 test/test-euc-jp.js         |   22 +
 test/test-euc-kr.js         |   22 +
 test/test-gbk.js            |   22 +
 test/test-iso-2022-jp.js    |   22 +
 test/test-misc.js           |  321 +++++
 test/test-shift_jis.js      |   22 +
 test/test-utf.js            |  163 +++
 test/test-x-user-defined.js |   29 +
 test/testharness.css        |   92 ++
 test/testharness.js         | 2121 +++++++++++++++++++++++++++++
 test/testharnessreport.js   |  380 ++++++
 test/tests.html             |   31 +
 util/externs.js             |   28 +
 23 files changed, 6949 insertions(+)

diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..6d00d2e
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2011
+                        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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f3e1cc2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,103 @@
+text-encoding
+==============
+
+This is a polyfill for the [Encoding Living Standard](https://encoding.spec.whatwg.org/)
+API for the Web, allowing encoding and decoding of textual data to and from Typed Array
+buffers for binary data in JavaScript.
+
+By default it adheres to the spec and does not support *encoding* to non-UTF encodings,
+only *decoding*. It is also implemented to match the specification's algorithms, rather
+than for performance. The intended use is within Web pages, so it has no dependency
+on server frameworks or particular module schemes.
+
+Basic examples and tests are included.
+
+### Install ###
+
+There are a few ways you can get the `text-encoding` library.
+
+#### Node ####
+
+`text-encoding` is on `npm`. Simply run:
+
+```js
+npm install text-encoding
+```
+
+Or add it to your `package.json` dependencies.
+
+#### Bower ####
+
+`text-encoding` is on `bower` as well. Install with bower like so:
+
+```js
+bower install text-encoding
+```
+
+Or add it to your `bower.json` dependencies.
+
+### HTML Page Usage ###
+
+```html
+  <!-- Required for non-UTF encodings -->
+  <script src="encoding-indexes.js"></script>
+  <script src="encoding.js"></script>
+```
+
+### API Overview ###
+
+Basic Usage
+
+```js
+  var uint8array = TextEncoder(encoding).encode(string);
+  var string = TextDecoder(encoding).decode(uint8array);
+```
+
+Streaming Decode
+
+```js
+  var string = "", decoder = TextDecoder(encoding), buffer;
+  while (buffer = next_chunk()) {
+    string += decoder.decode(buffer, {stream:true});
+  }
+  string += decoder.decode(); // finish the stream
+```
+
+### Encodings ###
+
+All encodings from the Encoding specification are supported:
+
+utf-8 ibm866 iso-8859-2 iso-8859-3 iso-8859-4 iso-8859-5 iso-8859-6 iso-8859-7 iso-8859-8 iso-8859-8-i iso-8859-10 iso-8859-13 iso-8859-14 iso-8859-15 iso-8859-16 koi8-r koi8-u macintosh windows-874 windows-1250 windows-1251 windows-1252 windows-1253 windows-1254 windows-1255 windows-1256 windows-1257 windows-1258 x-mac-cyrillic gb18030 hz-gb-2312 big5 euc-jp iso-2022-jp shift_jis euc-kr replacement utf-16be utf-16le x-user-defined
+
+(Some encodings may be supported under other names, e.g. ascii, iso-8859-1, etc.
+See [Encoding](https://encoding.spec.whatwg.org/) for additional labels for each encoding.)
+
+Encodings other than **utf-8**, **utf-16le** and **utf-16be** require an additional
+`encoding-indexes.js` file to be included. It is rather large
+(596kB uncompressed, 188kB gzipped); portions may be deleted if
+support for some encodings is not required.
+
+### Non-Standard Behavior ###
+
+As required by the specification, only encoding to **utf-8**,
+**utf-16le** and **utf-16be** is supported. If you want to try it out, you can
+force a non-standard behavior by passing the `NONSTANDARD_allowLegacyEncoding`
+option to TextEncoder. For example:
+
+```js
+var uint8array = new TextEncoder(
+  'windows-1252', { NONSTANDARD_allowLegacyEncoding: true }).encode(text);
+```
+
+But note that the above won't work if you're using the polyfill in a browser that
+natively supports the TextEncoder API natively, since the polyfill won't be used!
+You'd need to fork and modify the polyfill to... not be a polyfill.
+
+To support the legacy encodings (which may be stateful), the TextEncoder `encode()`
+method accepts an optional dictionary and `stream` option,
+e.g. `encoder.encode(string, {stream: true});` This is not needed for the
+stateless UTF encodings since the input is always in complete code points.
+
+The polyfill also allows construction of encoder/decoder objects without the
+`new` keyword. (This non-standard behavior is supported for constructing DOM
+objects in Firefox but not other browsers.)
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..63dd654
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,22 @@
+{
+  "name": "text-encoding",
+  "version": "0.5.3",
+  "homepage": "https://github.com/inexorabletash/text-encoding",
+  "authors": [
+    "Joshua Bell <inexorabletash at gmail.com>"
+  ],
+  "description": "Polyfill for the Encoding Living Standard's API",
+  "main": [ "lib/encoding.js", "lib/encoding-indexes.js" ],
+  "keywords": [
+    "decoding",
+    "encoding",
+    "living",
+    "standards"
+  ],
+  "license": "Apache-2.0",
+  "ignore": [
+    "**/.*",
+    "test",
+    "*examples*.html"
+  ]
+}
diff --git a/examples-no-indexes.html b/examples-no-indexes.html
new file mode 100644
index 0000000..58d70d5
--- /dev/null
+++ b/examples-no-indexes.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<script src="lib/encoding.js"></script>
+<script>
+function encodeArrayOfStrings(strings, encoding) {
+  var encoder, encoded, len, i, bytes, view, offset;
+
+  encoder = new TextEncoder(encoding);
+  encoded = [];
+
+  len = Uint32Array.BYTES_PER_ELEMENT;
+  for (i = 0; i < strings.length; i += 1) {
+    len += Uint32Array.BYTES_PER_ELEMENT;
+    encoded[i] = new TextEncoder(encoding).encode(strings[i]);
+    len += encoded[i].byteLength;
+  }
+
+  bytes = new Uint8Array(len);
+  view = new DataView(bytes.buffer);
+  offset = 0;
+
+  view.setUint32(offset, strings.length);
+  offset += Uint32Array.BYTES_PER_ELEMENT;
+  for (i = 0; i < encoded.length; i += 1) {
+    len = encoded[i].byteLength;
+    view.setUint32(offset, len);
+    offset += Uint32Array.BYTES_PER_ELEMENT;
+    bytes.set(encoded[i], offset);
+    offset += len;
+  }
+  return bytes.buffer;
+}
+
+function decodeArrayOfStrings(buffer, encoding) {
+  var decoder, view, offset, num_strings, strings, i, len;
+
+  decoder = new TextDecoder(encoding);
+  view = new DataView(buffer);
+  offset = 0;
+  strings = [];
+
+  num_strings = view.getUint32(offset);
+  offset += Uint32Array.BYTES_PER_ELEMENT;
+  for (i = 0; i < num_strings; i += 1) {
+    len = view.getUint32(offset);
+    offset += Uint32Array.BYTES_PER_ELEMENT;
+    strings[i] = decoder.decode(
+      new DataView(view.buffer, offset, len));
+    offset += len;
+  }
+  return strings;
+}
+
+var strings = ["Hello", "string", "encoding!"];
+var buffer = encodeArrayOfStrings(strings, "utf-8");
+var results = decodeArrayOfStrings(buffer, "utf-8");
+document.write("Encoded " + JSON.stringify(strings) + "<br>");
+document.write("Decoded " + JSON.stringify(results) + "<br>");
+</script>
diff --git a/examples.html b/examples.html
new file mode 100644
index 0000000..b60dd2b
--- /dev/null
+++ b/examples.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<script src="lib/encoding-indexes.js"></script>
+<script src="lib/encoding.js"></script>
+<script>
+function encodeArrayOfStrings(strings, encoding) {
+  var encoder, encoded, len, i, bytes, view, offset;
+
+  encoder = new TextEncoder(encoding);
+  encoded = [];
+
+  len = Uint32Array.BYTES_PER_ELEMENT;
+  for (i = 0; i < strings.length; i += 1) {
+    len += Uint32Array.BYTES_PER_ELEMENT;
+    encoded[i] = new TextEncoder(encoding).encode(strings[i]);
+    len += encoded[i].byteLength;
+  }
+
+  bytes = new Uint8Array(len);
+  view = new DataView(bytes.buffer);
+  offset = 0;
+
+  view.setUint32(offset, strings.length);
+  offset += Uint32Array.BYTES_PER_ELEMENT;
+  for (i = 0; i < encoded.length; i += 1) {
+    len = encoded[i].byteLength;
+    view.setUint32(offset, len);
+    offset += Uint32Array.BYTES_PER_ELEMENT;
+    bytes.set(encoded[i], offset);
+    offset += len;
+  }
+  return bytes.buffer;
+}
+
+function decodeArrayOfStrings(buffer, encoding) {
+  var decoder, view, offset, num_strings, strings, i, len;
+
+  decoder = new TextDecoder(encoding);
+  view = new DataView(buffer);
+  offset = 0;
+  strings = [];
+
+  num_strings = view.getUint32(offset);
+  offset += Uint32Array.BYTES_PER_ELEMENT;
+  for (i = 0; i < num_strings; i += 1) {
+    len = view.getUint32(offset);
+    offset += Uint32Array.BYTES_PER_ELEMENT;
+    strings[i] = decoder.decode(
+      new DataView(view.buffer, offset, len));
+    offset += len;
+  }
+  return strings;
+}
+
+var strings = ["Hello", "string", "encoding!"];
+var buffer = encodeArrayOfStrings(strings, "utf-8");
+var results = decodeArrayOfStrings(buffer, "utf-8");
+document.write("Encoded " + JSON.stringify(strings) + "<br>");
+document.write("Decoded " + JSON.stringify(results) + "<br>");
+</script>
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..819b19d
--- /dev/null
+++ b/index.js
@@ -0,0 +1,20 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+var encoding = require("./lib/encoding.js");
+
+module.exports = {
+  TextEncoder: encoding.TextEncoder,
+  TextDecoder: encoding.TextDecoder,
+};
diff --git a/lib/encoding-indexes.js b/lib/encoding-indexes.js
new file mode 100644
index 0000000..122de54
--- /dev/null
+++ b/lib/encoding-indexes.js
@@ -0,0 +1,54 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+// Indexes from: https://encoding.spec.whatwg.org/indexes.json
+
+(function(global) {
+  'use strict';
+  global["encoding-indexes"] = {
+  "big5":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,nu [...]
+  "euc-kr":[44034,44035,44037,44038,44043,44044,44045,44046,44047,44056,44062,44063,44065,44066,44067,44069,44070,44071,44072,44073,44074,44075,44078,44082,44083,44084,null,null,null,null,null,null,44085,44086,44087,44090,44091,44093,44094,44095,44097,44098,44099,44100,44101,44102,44103,44104,44105,44106,44108,44110,44111,44112,44113,44114,44115,44117,null,null,null,null,null,null,44118,44119,44121,44122,44123,44125,44126,44127,44128,44129,44130,44131,44132,44133,44134,44135,44136,44137, [...]
+  "gb18030":[19970,19972,19973,19974,19983,19986,19991,19999,20000,20001,20003,20006,20009,20014,20015,20017,20019,20021,20023,20028,20032,20033,20034,20036,20038,20042,20049,20053,20055,20058,20059,20066,20067,20068,20069,20071,20072,20074,20075,20076,20077,20078,20079,20082,20084,20085,20086,20087,20088,20089,20090,20091,20092,20093,20095,20096,20097,20098,20099,20100,20101,20103,20106,20112,20118,20119,20121,20124,20125,20126,20131,20138,20143,20144,20145,20148,20150,20151,20152,20153 [...]
+  "gb18030-ranges":[[0,128],[36,165],[38,169],[45,178],[50,184],[81,216],[89,226],[95,235],[96,238],[100,244],[103,248],[104,251],[105,253],[109,258],[126,276],[133,284],[148,300],[172,325],[175,329],[179,334],[208,364],[306,463],[307,465],[308,467],[309,469],[310,471],[311,473],[312,475],[313,477],[341,506],[428,594],[443,610],[544,712],[545,716],[558,730],[741,930],[742,938],[749,962],[750,970],[805,1026],[819,1104],[820,1106],[7922,8209],[7924,8215],[7925,8218],[7927,8222],[7934,8231] [...]
+  "jis0208":[12288,12289,12290,65292,65294,12539,65306,65307,65311,65281,12443,12444,180,65344,168,65342,65507,65343,12541,12542,12445,12446,12291,20189,12293,12294,12295,12540,8213,8208,65295,65340,65374,8741,65372,8230,8229,8216,8217,8220,8221,65288,65289,12308,12309,65339,65341,65371,65373,12296,12297,12298,12299,12300,12301,12302,12303,12304,12305,65291,65293,177,215,247,65309,8800,65308,65310,8806,8807,8734,8756,9794,9792,176,8242,8243,8451,65509,65284,65504,65505,65285,65283,65286, [...]
+  "jis0212":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null [...]
+  "ibm866":[1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,9617,9618,9619,9474,9508,9569,9570,9558,9557,9571,9553,9559,9565,9564,9563,9488,9492,9524,9516,9500,9472,9532,9566,9567,9562,9556,9577,9574,9568,9552,9580,9575,9576,9572,9573,9561,9560,9554,9555,9579,9578,9496,9484,9608,9604,9612,9616,9600, [...]
+  "iso-8859-2":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,728,321,164,317,346,167,168,352,350,356,377,173,381,379,176,261,731,322,180,318,347,711,184,353,351,357,378,733,382,380,340,193,194,258,196,313,262,199,268,201,280,203,282,205,206,270,272,323,327,211,212,336,214,215,344,366,218,368,220,221,354,223,341,225,226,259,228,314,263,231,269,233,281,235,283,237,238,271,273,324,328,243,244,337,246, [...]
+  "iso-8859-3":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,294,728,163,164,null,292,167,168,304,350,286,308,173,null,379,176,295,178,179,180,181,293,183,184,305,351,287,309,189,null,380,192,193,194,null,196,266,264,199,200,201,202,203,204,205,206,207,null,209,210,211,212,288,214,215,284,217,218,219,220,364,348,223,224,225,226,null,228,267,265,231,232,233,234,235,236,237,238,239,null,241,242,243,244,2 [...]
+  "iso-8859-4":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,312,342,164,296,315,167,168,352,274,290,358,173,381,175,176,261,731,343,180,297,316,711,184,353,275,291,359,330,382,331,256,193,194,195,196,197,198,302,268,201,280,203,278,205,206,298,272,325,332,310,212,213,214,215,216,370,218,219,220,360,362,223,257,225,226,227,228,229,230,303,269,233,281,235,279,237,238,299,273,326,333,311,244,245,246, [...]
+  "iso-8859-5":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,173,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093, [...]
+  "iso-8859-6":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,null,null,null,164,null,null,null,null,null,null,null,1548,173,null,null,null,null,null,null,null,null,null,null,null,null,null,1563,null,null,null,1567,null,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,null,null,null,null,null,1600,1601,1602,1603,1604,1605,1 [...]
+  "iso-8859-7":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,8216,8217,163,8364,8367,166,167,168,169,890,171,172,173,null,8213,176,177,178,179,900,901,902,183,904,905,906,187,908,189,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,null,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,9 [...]
+  "iso-8859-8":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,null,162,163,164,165,166,167,168,169,215,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,247,187,188,189,190,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,8215,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,149 [...]
+  "iso-8859-10":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,274,290,298,296,310,167,315,272,352,358,381,173,362,330,176,261,275,291,299,297,311,183,316,273,353,359,382,8213,363,331,256,193,194,195,196,197,198,302,268,201,280,203,278,205,206,207,208,325,332,211,212,213,214,360,216,370,218,219,220,221,222,223,257,225,226,227,228,229,230,303,269,233,281,235,279,237,238,239,240,326,333,243,244,245,24 [...]
+  "iso-8859-13":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,8221,162,163,164,8222,166,167,216,169,342,171,172,173,174,198,176,177,178,179,8220,181,182,183,248,185,343,187,188,189,190,230,260,302,256,262,196,197,280,274,268,201,377,278,290,310,298,315,352,323,325,211,332,213,214,215,370,321,346,362,220,379,381,223,261,303,257,263,228,229,281,275,269,233,378,279,291,311,299,316,353,324,326,243,333,245, [...]
+  "iso-8859-14":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,7682,7683,163,266,267,7690,167,7808,169,7810,7691,7922,173,174,376,7710,7711,288,289,7744,7745,182,7766,7809,7767,7811,7776,7923,7812,7813,7777,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,372,209,210,211,212,213,214,7786,216,217,218,219,220,221,374,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,373,24 [...]
+  "iso-8859-15":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,8364,165,352,167,353,169,170,171,172,173,174,175,176,177,178,179,381,181,182,183,382,185,186,187,338,339,376,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,24 [...]
+  "iso-8859-16":[128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,260,261,321,8364,8222,352,167,353,169,536,171,377,173,378,379,176,177,268,322,381,8221,182,183,382,269,537,187,338,339,376,380,192,193,194,258,196,262,198,199,200,201,202,203,204,205,206,207,272,323,210,211,212,336,214,346,368,217,218,219,220,280,538,223,224,225,226,259,228,263,230,231,232,233,234,235,236,237,238,239,273,324,242,243,244,337, [...]
+  "koi8-r":[9472,9474,9484,9488,9492,9496,9500,9508,9516,9524,9532,9600,9604,9608,9612,9616,9617,9618,9619,8992,9632,8729,8730,8776,8804,8805,160,8993,176,178,183,247,9552,9553,9554,1105,9555,9556,9557,9558,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568,9569,1025,9570,9571,9572,9573,9574,9575,9576,9577,9578,9579,9580,169,1102,1072,1073,1094,1076,1077,1092,1075,1093,1080,1081,1082,1083,1084,1085,1086,1087,1103,1088,1089,1090,1091,1078,1074,1100,1099,1079,1096,1101,1097,1095,1098,1070,1 [...]
+  "koi8-u":[9472,9474,9484,9488,9492,9496,9500,9508,9516,9524,9532,9600,9604,9608,9612,9616,9617,9618,9619,8992,9632,8729,8730,8776,8804,8805,160,8993,176,178,183,247,9552,9553,9554,1105,1108,9556,1110,1111,9559,9560,9561,9562,9563,1169,1118,9566,9567,9568,9569,1025,1028,9571,1030,1031,9574,9575,9576,9577,9578,1168,1038,169,1102,1072,1073,1094,1076,1077,1092,1075,1093,1080,1081,1082,1083,1084,1085,1086,1087,1103,1088,1089,1090,1091,1078,1074,1100,1099,1079,1096,1101,1097,1095,1098,1070,1 [...]
+  "macintosh":[196,197,199,201,209,214,220,225,224,226,228,227,229,231,233,232,234,235,237,236,238,239,241,243,242,244,246,245,250,249,251,252,8224,176,162,163,167,8226,182,223,174,169,8482,180,168,8800,198,216,8734,177,8804,8805,165,181,8706,8721,8719,960,8747,170,186,937,230,248,191,161,172,8730,402,8776,8710,171,187,8230,160,192,195,213,338,339,8211,8212,8220,8221,8216,8217,247,9674,255,376,8260,8364,8249,8250,64257,64258,8225,183,8218,8222,8240,194,202,193,203,200,205,206,207,204,211 [...]
+  "windows-874":[8364,129,130,131,132,8230,134,135,136,137,138,139,140,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,152,153,154,155,156,157,158,159,160,3585,3586,3587,3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603,3604,3605,3606,3607,3608,3609,3610,3611,3612,3613,3614,3615,3616,3617,3618,3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,3630,3631,3632,3633,3634,3635,3636,3637,3638,3639,3640,3641,3642,null,null,null,null,3647,3648,3649,3650,3651 [...]
+  "windows-1250":[8364,129,8218,131,8222,8230,8224,8225,136,8240,352,8249,346,356,381,377,144,8216,8217,8220,8221,8226,8211,8212,152,8482,353,8250,347,357,382,378,160,711,728,321,164,260,166,167,168,169,350,171,172,173,174,379,176,177,731,322,180,181,182,183,184,261,351,187,317,733,318,380,340,193,194,258,196,313,262,199,268,201,280,203,282,205,206,270,272,323,327,211,212,336,214,215,344,366,218,368,220,221,354,223,341,225,226,259,228,314,263,231,269,233,281,235,283,237,238,271,273,324,3 [...]
+  "windows-1251":[1026,1027,8218,1107,8222,8230,8224,8225,8364,8240,1033,8249,1034,1036,1035,1039,1106,8216,8217,8220,8221,8226,8211,8212,152,8482,1113,8250,1114,1116,1115,1119,160,1038,1118,1032,164,1168,166,167,1025,169,1028,171,172,173,174,1031,176,177,1030,1110,1169,181,182,183,1105,8470,1108,187,1112,1029,1109,1111,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073, [...]
+  "windows-1252":[8364,129,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,141,381,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,157,382,376,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,2 [...]
+  "windows-1253":[8364,129,8218,402,8222,8230,8224,8225,136,8240,138,8249,140,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,152,8482,154,8250,156,157,158,159,160,901,902,163,164,165,166,167,168,169,null,171,172,173,174,8213,176,177,178,179,900,181,182,183,904,905,906,187,908,189,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,null,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,96 [...]
+  "windows-1254":[8364,129,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,157,158,376,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,286,209,210,211,212,213,214,215,216,217,218,219,220,304,350,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,287,241,2 [...]
+  "windows-1255":[8364,129,8218,402,8222,8230,8224,8225,710,8240,138,8249,140,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,154,8250,156,157,158,159,160,161,162,163,8362,165,166,167,168,169,215,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,247,187,188,189,190,191,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,null,1467,1468,1469,1470,1471,1472,1473,1474,1475,1520,1521,1522,1523,1524,null,null,null,null,null,null,null,1488,1489,1490,1491,1492,1493,1494,1495, [...]
+  "windows-1256":[8364,1662,8218,402,8222,8230,8224,8225,710,8240,1657,8249,338,1670,1688,1672,1711,8216,8217,8220,8221,8226,8211,8212,1705,8482,1681,8250,339,8204,8205,1722,160,1548,162,163,164,165,166,167,168,169,1726,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,1563,187,188,189,190,1567,1729,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,215,1591,1592,1593,1594,1600,1601,1602,1603,224,1604,226,1605,1606,1607 [...]
+  "windows-1257":[8364,129,8218,131,8222,8230,8224,8225,136,8240,138,8249,140,168,711,184,144,8216,8217,8220,8221,8226,8211,8212,152,8482,154,8250,156,175,731,159,160,null,162,163,164,null,166,167,216,169,342,171,172,173,174,198,176,177,178,179,180,181,182,183,248,185,343,187,188,189,190,230,260,302,256,262,196,197,280,274,268,201,377,278,290,310,298,315,352,323,325,211,332,213,214,215,370,321,346,362,220,379,381,223,261,303,257,263,228,229,281,275,269,233,378,279,291,311,299,316,353,324 [...]
+  "windows-1258":[8364,129,8218,402,8222,8230,8224,8225,710,8240,138,8249,338,141,142,143,144,8216,8217,8220,8221,8226,8211,8212,732,8482,154,8250,339,157,158,376,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,258,196,197,198,199,200,201,202,203,768,205,206,207,272,209,777,211,212,416,214,215,216,217,218,219,220,431,771,223,224,225,226,259,228,229,230,231,232,233,234,235,769,237,238,239,273,241,8 [...]
+  "x-mac-cyrillic":[1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,8224,176,1168,163,167,8226,182,1030,174,169,8482,1026,1106,8800,1027,1107,8734,177,8804,8805,1110,181,1169,1032,1028,1108,1031,1111,1033,1113,1034,1114,1112,1029,172,8730,402,8776,8710,171,187,8230,160,1035,1115,1036,1116,1109,8211,8212,8220,8221,8216,8217,247,8222,1038,1118,1039,1119,8470,1025,1105,1103,1072,1 [...]
+};
+}(this));
diff --git a/lib/encoding.js b/lib/encoding.js
new file mode 100644
index 0000000..bbdd071
--- /dev/null
+++ b/lib/encoding.js
@@ -0,0 +1,3106 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+// If we're in node require encoding-indexes and attach it to the global.
+/**
+ * @fileoverview Global |this| required for resolving indexes in node.
+ * @suppress {globalThis}
+ */
+if (typeof module !== "undefined" && module.exports) {
+  this["encoding-indexes"] =
+    require("./encoding-indexes.js")["encoding-indexes"];
+}
+
+(function(global) {
+  'use strict';
+
+  //
+  // Utilities
+  //
+
+  /**
+   * @param {number} a The number to test.
+   * @param {number} min The minimum value in the range, inclusive.
+   * @param {number} max The maximum value in the range, inclusive.
+   * @return {boolean} True if a >= min and a <= max.
+   */
+  function inRange(a, min, max) {
+    return min <= a && a <= max;
+  }
+
+  /**
+   * @param {number} n The numerator.
+   * @param {number} d The denominator.
+   * @return {number} The result of the integer division of n by d.
+   */
+  function div(n, d) {
+    return Math.floor(n / d);
+  }
+
+  /**
+   * @param {*} o
+   * @return {Object}
+   */
+  function ToDictionary(o) {
+    if (o === undefined) return {};
+    if (o === Object(o)) return o;
+    throw TypeError('Could not convert argument to dictionary');
+  }
+
+  /**
+   * @param {string} string Input string of UTF-16 code units.
+   * @return {!Array.<number>} Code points.
+   */
+  function stringToCodePoints(string) {
+    // https://heycam.github.io/webidl/#dfn-obtain-unicode
+
+    // 1. Let S be the DOMString value.
+    var s = String(string);
+
+    // 2. Let n be the length of S.
+    var n = s.length;
+
+    // 3. Initialize i to 0.
+    var i = 0;
+
+    // 4. Initialize U to be an empty sequence of Unicode characters.
+    var u = [];
+
+    // 5. While i < n:
+    while (i < n) {
+
+      // 1. Let c be the code unit in S at index i.
+      var c = s.charCodeAt(i);
+
+      // 2. Depending on the value of c:
+
+      // c < 0xD800 or c > 0xDFFF
+      if (c < 0xD800 || c > 0xDFFF) {
+        // Append to U the Unicode character with code point c.
+        u.push(c);
+      }
+
+      // 0xDC00 ≤ c ≤ 0xDFFF
+      else if (0xDC00 <= c && c <= 0xDFFF) {
+        // Append to U a U+FFFD REPLACEMENT CHARACTER.
+        u.push(0xFFFD);
+      }
+
+      // 0xD800 ≤ c ≤ 0xDBFF
+      else if (0xD800 <= c && c <= 0xDBFF) {
+        // 1. If i = n−1, then append to U a U+FFFD REPLACEMENT
+        // CHARACTER.
+        if (i === n - 1) {
+          u.push(0xFFFD);
+        }
+        // 2. Otherwise, i < n−1:
+        else {
+          // 1. Let d be the code unit in S at index i+1.
+          var d = string.charCodeAt(i + 1);
+
+          // 2. If 0xDC00 ≤ d ≤ 0xDFFF, then:
+          if (0xDC00 <= d && d <= 0xDFFF) {
+            // 1. Let a be c & 0x3FF.
+            var a = c & 0x3FF;
+
+            // 2. Let b be d & 0x3FF.
+            var b = d & 0x3FF;
+
+            // 3. Append to U the Unicode character with code point
+            // 2^16+2^10*a+b.
+            u.push(0x10000 + (a << 10) + b);
+
+            // 4. Set i to i+1.
+            i += 1;
+          }
+
+          // 3. Otherwise, d < 0xDC00 or d > 0xDFFF. Append to U a
+          // U+FFFD REPLACEMENT CHARACTER.
+          else  {
+            u.push(0xFFFD);
+          }
+        }
+      }
+
+      // 3. Set i to i+1.
+      i += 1;
+    }
+
+    // 6. Return U.
+    return u;
+  }
+
+  /**
+   * @param {!Array.<number>} code_points Array of code points.
+   * @return {string} string String of UTF-16 code units.
+   */
+  function codePointsToString(code_points) {
+    var s = '';
+    for (var i = 0; i < code_points.length; ++i) {
+      var cp = code_points[i];
+      if (cp <= 0xFFFF) {
+        s += String.fromCharCode(cp);
+      } else {
+        cp -= 0x10000;
+        s += String.fromCharCode((cp >> 10) + 0xD800,
+                                 (cp & 0x3FF) + 0xDC00);
+      }
+    }
+    return s;
+  }
+
+
+  //
+  // Implementation of Encoding specification
+  // https://encoding.spec.whatwg.org/
+  //
+
+  //
+  // 3. Terminology
+  //
+
+  /**
+   * End-of-stream is a special token that signifies no more tokens
+   * are in the stream.
+   * @const
+   */ var end_of_stream = -1;
+
+  /**
+   * A stream represents an ordered sequence of tokens.
+   *
+   * @constructor
+   * @param {!(Array.<number>|Uint8Array)} tokens Array of tokens that provide the
+   * stream.
+   */
+  function Stream(tokens) {
+    /** @type {!Array.<number>} */
+    this.tokens = [].slice.call(tokens);
+  }
+
+  Stream.prototype = {
+    /**
+     * @return {boolean} True if end-of-stream has been hit.
+     */
+    endOfStream: function() {
+      return !this.tokens.length;
+    },
+
+    /**
+     * When a token is read from a stream, the first token in the
+     * stream must be returned and subsequently removed, and
+     * end-of-stream must be returned otherwise.
+     *
+     * @return {number} Get the next token from the stream, or
+     * end_of_stream.
+     */
+     read: function() {
+      if (!this.tokens.length)
+        return end_of_stream;
+       return this.tokens.shift();
+     },
+
+    /**
+     * When one or more tokens are prepended to a stream, those tokens
+     * must be inserted, in given order, before the first token in the
+     * stream.
+     *
+     * @param {(number|!Array.<number>)} token The token(s) to prepend to the stream.
+     */
+    prepend: function(token) {
+      if (Array.isArray(token)) {
+        var tokens = /**@type {!Array.<number>}*/(token);
+        while (tokens.length)
+          this.tokens.unshift(tokens.pop());
+      } else {
+        this.tokens.unshift(token);
+      }
+    },
+
+    /**
+     * When one or more tokens are pushed to a stream, those tokens
+     * must be inserted, in given order, after the last token in the
+     * stream.
+     *
+     * @param {(number|!Array.<number>)} token The tokens(s) to prepend to the stream.
+     */
+    push: function(token) {
+      if (Array.isArray(token)) {
+        var tokens = /**@type {!Array.<number>}*/(token);
+        while (tokens.length)
+          this.tokens.push(tokens.shift());
+      } else {
+        this.tokens.push(token);
+      }
+    }
+  };
+
+  //
+  // 4. Encodings
+  //
+
+  // 4.1 Encoders and decoders
+
+  /** @const */
+  var finished = -1;
+
+  /**
+   * @param {boolean} fatal If true, decoding errors raise an exception.
+   * @param {number=} opt_code_point Override the standard fallback code point.
+   * @return {number} The code point to insert on a decoding error.
+   */
+  function decoderError(fatal, opt_code_point) {
+    if (fatal)
+      throw TypeError('Decoder error');
+    return opt_code_point || 0xFFFD;
+  }
+
+  /**
+   * @param {number} code_point The code point that could not be encoded.
+   * @return {number} Always throws, no value is actually returned.
+   */
+  function encoderError(code_point) {
+    throw TypeError('The code point ' + code_point + ' could not be encoded.');
+  }
+
+  /** @interface */
+  function Decoder() {}
+  Decoder.prototype = {
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point, or |finished|.
+     */
+    handler: function(stream, bite) {}
+  };
+
+  /** @interface */
+  function Encoder() {}
+  Encoder.prototype = {
+    /**
+     * @param {Stream} stream The stream of code points being encoded.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit, or |finished|.
+     */
+    handler: function(stream, code_point) {}
+  };
+
+  // 4.2 Names and labels
+
+  // TODO: Define @typedef for Encoding: {name:string,labels:Array.<string>}
+  // https://github.com/google/closure-compiler/issues/247
+
+  /**
+   * @param {string} label The encoding label.
+   * @return {?{name:string,labels:Array.<string>}}
+   */
+  function getEncoding(label) {
+    // 1. Remove any leading and trailing ASCII whitespace from label.
+    label = String(label).trim().toLowerCase();
+
+    // 2. If label is an ASCII case-insensitive match for any of the
+    // labels listed in the table below, return the corresponding
+    // encoding, and failure otherwise.
+    if (Object.prototype.hasOwnProperty.call(label_to_encoding, label)) {
+      return label_to_encoding[label];
+    }
+    return null;
+  }
+
+  /**
+   * Encodings table: https://encoding.spec.whatwg.org/encodings.json
+   * @const
+   * @type {!Array.<{
+   *          heading: string,
+   *          encodings: Array.<{name:string,labels:Array.<string>}>
+   *        }>}
+   */
+  var encodings = [
+  {
+    "encodings": [
+      {
+        "labels": [
+          "unicode-1-1-utf-8",
+          "utf-8",
+          "utf8"
+        ],
+        "name": "utf-8"
+      }
+    ],
+    "heading": "The Encoding"
+  },
+  {
+    "encodings": [
+      {
+        "labels": [
+          "866",
+          "cp866",
+          "csibm866",
+          "ibm866"
+        ],
+        "name": "ibm866"
+      },
+      {
+        "labels": [
+          "csisolatin2",
+          "iso-8859-2",
+          "iso-ir-101",
+          "iso8859-2",
+          "iso88592",
+          "iso_8859-2",
+          "iso_8859-2:1987",
+          "l2",
+          "latin2"
+        ],
+        "name": "iso-8859-2"
+      },
+      {
+        "labels": [
+          "csisolatin3",
+          "iso-8859-3",
+          "iso-ir-109",
+          "iso8859-3",
+          "iso88593",
+          "iso_8859-3",
+          "iso_8859-3:1988",
+          "l3",
+          "latin3"
+        ],
+        "name": "iso-8859-3"
+      },
+      {
+        "labels": [
+          "csisolatin4",
+          "iso-8859-4",
+          "iso-ir-110",
+          "iso8859-4",
+          "iso88594",
+          "iso_8859-4",
+          "iso_8859-4:1988",
+          "l4",
+          "latin4"
+        ],
+        "name": "iso-8859-4"
+      },
+      {
+        "labels": [
+          "csisolatincyrillic",
+          "cyrillic",
+          "iso-8859-5",
+          "iso-ir-144",
+          "iso8859-5",
+          "iso88595",
+          "iso_8859-5",
+          "iso_8859-5:1988"
+        ],
+        "name": "iso-8859-5"
+      },
+      {
+        "labels": [
+          "arabic",
+          "asmo-708",
+          "csiso88596e",
+          "csiso88596i",
+          "csisolatinarabic",
+          "ecma-114",
+          "iso-8859-6",
+          "iso-8859-6-e",
+          "iso-8859-6-i",
+          "iso-ir-127",
+          "iso8859-6",
+          "iso88596",
+          "iso_8859-6",
+          "iso_8859-6:1987"
+        ],
+        "name": "iso-8859-6"
+      },
+      {
+        "labels": [
+          "csisolatingreek",
+          "ecma-118",
+          "elot_928",
+          "greek",
+          "greek8",
+          "iso-8859-7",
+          "iso-ir-126",
+          "iso8859-7",
+          "iso88597",
+          "iso_8859-7",
+          "iso_8859-7:1987",
+          "sun_eu_greek"
+        ],
+        "name": "iso-8859-7"
+      },
+      {
+        "labels": [
+          "csiso88598e",
+          "csisolatinhebrew",
+          "hebrew",
+          "iso-8859-8",
+          "iso-8859-8-e",
+          "iso-ir-138",
+          "iso8859-8",
+          "iso88598",
+          "iso_8859-8",
+          "iso_8859-8:1988",
+          "visual"
+        ],
+        "name": "iso-8859-8"
+      },
+      {
+        "labels": [
+          "csiso88598i",
+          "iso-8859-8-i",
+          "logical"
+        ],
+        "name": "iso-8859-8-i"
+      },
+      {
+        "labels": [
+          "csisolatin6",
+          "iso-8859-10",
+          "iso-ir-157",
+          "iso8859-10",
+          "iso885910",
+          "l6",
+          "latin6"
+        ],
+        "name": "iso-8859-10"
+      },
+      {
+        "labels": [
+          "iso-8859-13",
+          "iso8859-13",
+          "iso885913"
+        ],
+        "name": "iso-8859-13"
+      },
+      {
+        "labels": [
+          "iso-8859-14",
+          "iso8859-14",
+          "iso885914"
+        ],
+        "name": "iso-8859-14"
+      },
+      {
+        "labels": [
+          "csisolatin9",
+          "iso-8859-15",
+          "iso8859-15",
+          "iso885915",
+          "iso_8859-15",
+          "l9"
+        ],
+        "name": "iso-8859-15"
+      },
+      {
+        "labels": [
+          "iso-8859-16"
+        ],
+        "name": "iso-8859-16"
+      },
+      {
+        "labels": [
+          "cskoi8r",
+          "koi",
+          "koi8",
+          "koi8-r",
+          "koi8_r"
+        ],
+        "name": "koi8-r"
+      },
+      {
+        "labels": [
+          "koi8-ru",
+          "koi8-u"
+        ],
+        "name": "koi8-u"
+      },
+      {
+        "labels": [
+          "csmacintosh",
+          "mac",
+          "macintosh",
+          "x-mac-roman"
+        ],
+        "name": "macintosh"
+      },
+      {
+        "labels": [
+          "dos-874",
+          "iso-8859-11",
+          "iso8859-11",
+          "iso885911",
+          "tis-620",
+          "windows-874"
+        ],
+        "name": "windows-874"
+      },
+      {
+        "labels": [
+          "cp1250",
+          "windows-1250",
+          "x-cp1250"
+        ],
+        "name": "windows-1250"
+      },
+      {
+        "labels": [
+          "cp1251",
+          "windows-1251",
+          "x-cp1251"
+        ],
+        "name": "windows-1251"
+      },
+      {
+        "labels": [
+          "ansi_x3.4-1968",
+          "ascii",
+          "cp1252",
+          "cp819",
+          "csisolatin1",
+          "ibm819",
+          "iso-8859-1",
+          "iso-ir-100",
+          "iso8859-1",
+          "iso88591",
+          "iso_8859-1",
+          "iso_8859-1:1987",
+          "l1",
+          "latin1",
+          "us-ascii",
+          "windows-1252",
+          "x-cp1252"
+        ],
+        "name": "windows-1252"
+      },
+      {
+        "labels": [
+          "cp1253",
+          "windows-1253",
+          "x-cp1253"
+        ],
+        "name": "windows-1253"
+      },
+      {
+        "labels": [
+          "cp1254",
+          "csisolatin5",
+          "iso-8859-9",
+          "iso-ir-148",
+          "iso8859-9",
+          "iso88599",
+          "iso_8859-9",
+          "iso_8859-9:1989",
+          "l5",
+          "latin5",
+          "windows-1254",
+          "x-cp1254"
+        ],
+        "name": "windows-1254"
+      },
+      {
+        "labels": [
+          "cp1255",
+          "windows-1255",
+          "x-cp1255"
+        ],
+        "name": "windows-1255"
+      },
+      {
+        "labels": [
+          "cp1256",
+          "windows-1256",
+          "x-cp1256"
+        ],
+        "name": "windows-1256"
+      },
+      {
+        "labels": [
+          "cp1257",
+          "windows-1257",
+          "x-cp1257"
+        ],
+        "name": "windows-1257"
+      },
+      {
+        "labels": [
+          "cp1258",
+          "windows-1258",
+          "x-cp1258"
+        ],
+        "name": "windows-1258"
+      },
+      {
+        "labels": [
+          "x-mac-cyrillic",
+          "x-mac-ukrainian"
+        ],
+        "name": "x-mac-cyrillic"
+      }
+    ],
+    "heading": "Legacy single-byte encodings"
+  },
+  {
+    "encodings": [
+      {
+        "labels": [
+          "chinese",
+          "csgb2312",
+          "csiso58gb231280",
+          "gb2312",
+          "gb_2312",
+          "gb_2312-80",
+          "gbk",
+          "iso-ir-58",
+          "x-gbk"
+        ],
+        "name": "gbk"
+      },
+      {
+        "labels": [
+          "gb18030"
+        ],
+        "name": "gb18030"
+      }
+    ],
+    "heading": "Legacy multi-byte Chinese (simplified) encodings"
+  },
+  {
+    "encodings": [
+      {
+        "labels": [
+          "big5",
+          "big5-hkscs",
+          "cn-big5",
+          "csbig5",
+          "x-x-big5"
+        ],
+        "name": "big5"
+      }
+    ],
+    "heading": "Legacy multi-byte Chinese (traditional) encodings"
+  },
+  {
+    "encodings": [
+      {
+        "labels": [
+          "cseucpkdfmtjapanese",
+          "euc-jp",
+          "x-euc-jp"
+        ],
+        "name": "euc-jp"
+      },
+      {
+        "labels": [
+          "csiso2022jp",
+          "iso-2022-jp"
+        ],
+        "name": "iso-2022-jp"
+      },
+      {
+        "labels": [
+          "csshiftjis",
+          "ms932",
+          "ms_kanji",
+          "shift-jis",
+          "shift_jis",
+          "sjis",
+          "windows-31j",
+          "x-sjis"
+        ],
+        "name": "shift_jis"
+      }
+    ],
+    "heading": "Legacy multi-byte Japanese encodings"
+  },
+  {
+    "encodings": [
+      {
+        "labels": [
+          "cseuckr",
+          "csksc56011987",
+          "euc-kr",
+          "iso-ir-149",
+          "korean",
+          "ks_c_5601-1987",
+          "ks_c_5601-1989",
+          "ksc5601",
+          "ksc_5601",
+          "windows-949"
+        ],
+        "name": "euc-kr"
+      }
+    ],
+    "heading": "Legacy multi-byte Korean encodings"
+  },
+  {
+    "encodings": [
+      {
+        "labels": [
+          "csiso2022kr",
+          "hz-gb-2312",
+          "iso-2022-cn",
+          "iso-2022-cn-ext",
+          "iso-2022-kr"
+        ],
+        "name": "replacement"
+      },
+      {
+        "labels": [
+          "utf-16be"
+        ],
+        "name": "utf-16be"
+      },
+      {
+        "labels": [
+          "utf-16",
+          "utf-16le"
+        ],
+        "name": "utf-16le"
+      },
+      {
+        "labels": [
+          "x-user-defined"
+        ],
+        "name": "x-user-defined"
+      }
+    ],
+    "heading": "Legacy miscellaneous encodings"
+  }
+  ];
+
+  // Label to encoding registry.
+  /** @type {Object.<string,{name:string,labels:Array.<string>}>} */
+  var label_to_encoding = {};
+  encodings.forEach(function(category) {
+    category.encodings.forEach(function(encoding) {
+      encoding.labels.forEach(function(label) {
+        label_to_encoding[label] = encoding;
+      });
+    });
+  });
+
+  // Registry of of encoder/decoder factories, by encoding name.
+  /** @type {Object.<string, function({fatal:boolean}): Encoder>} */
+  var encoders = {};
+  /** @type {Object.<string, function({fatal:boolean}): Decoder>} */
+  var decoders = {};
+
+  //
+  // 5. Indexes
+  //
+
+  /**
+   * @param {number} pointer The |pointer| to search for.
+   * @param {(!Array.<?number>|undefined)} index The |index| to search within.
+   * @return {?number} The code point corresponding to |pointer| in |index|,
+   *     or null if |code point| is not in |index|.
+   */
+  function indexCodePointFor(pointer, index) {
+    if (!index) return null;
+    return index[pointer] || null;
+  }
+
+  /**
+   * @param {number} code_point The |code point| to search for.
+   * @param {!Array.<?number>} index The |index| to search within.
+   * @return {?number} The first pointer corresponding to |code point| in
+   *     |index|, or null if |code point| is not in |index|.
+   */
+  function indexPointerFor(code_point, index) {
+    var pointer = index.indexOf(code_point);
+    return pointer === -1 ? null : pointer;
+  }
+
+  /**
+   * @param {string} name Name of the index.
+   * @return {(!Array.<number>|!Array.<Array.<number>>)}
+   *  */
+  function index(name) {
+    if (!('encoding-indexes' in global)) {
+      throw Error("Indexes missing." +
+                  " Did you forget to include encoding-indexes.js?");
+    }
+    return global['encoding-indexes'][name];
+  }
+
+  /**
+   * @param {number} pointer The |pointer| to search for in the gb18030 index.
+   * @return {?number} The code point corresponding to |pointer| in |index|,
+   *     or null if |code point| is not in the gb18030 index.
+   */
+  function indexGB18030RangesCodePointFor(pointer) {
+    // 1. If pointer is greater than 39419 and less than 189000, or
+    // pointer is greater than 1237575, return null.
+    if ((pointer > 39419 && pointer < 189000) || (pointer > 1237575))
+      return null;
+
+    // 2. Let offset be the last pointer in index gb18030 ranges that
+    // is equal to or less than pointer and let code point offset be
+    // its corresponding code point.
+    var offset = 0;
+    var code_point_offset = 0;
+    var idx = index('gb18030');
+    var i;
+    for (i = 0; i < idx.length; ++i) {
+      /** @type {!Array.<number>} */
+      var entry = idx[i];
+      if (entry[0] <= pointer) {
+        offset = entry[0];
+        code_point_offset = entry[1];
+      } else {
+        break;
+      }
+    }
+
+    // 3. Return a code point whose value is code point offset +
+    // pointer − offset.
+    return code_point_offset + pointer - offset;
+  }
+
+  /**
+   * @param {number} code_point The |code point| to locate in the gb18030 index.
+   * @return {number} The first pointer corresponding to |code point| in the
+   *     gb18030 index.
+   */
+  function indexGB18030RangesPointerFor(code_point) {
+    // 1. Let offset be the last code point in index gb18030 ranges
+    // that is equal to or less than code point and let pointer offset
+    // be its corresponding pointer.
+    var offset = 0;
+    var pointer_offset = 0;
+    var idx = index('gb18030');
+    var i;
+    for (i = 0; i < idx.length; ++i) {
+      /** @type {!Array.<number>} */
+      var entry = idx[i];
+      if (entry[1] <= code_point) {
+        offset = entry[1];
+        pointer_offset = entry[0];
+      } else {
+        break;
+      }
+    }
+
+    // 2. Return a pointer whose value is pointer offset + code point
+    // − offset.
+    return pointer_offset + code_point - offset;
+  }
+
+  /**
+   * @param {number} code_point The |code_point| to search for in the shift_jis index.
+   * @return {?number} The code point corresponding to |pointer| in |index|,
+   *     or null if |code point| is not in the shift_jis index.
+   */
+  function indexShiftJISPointerFor(code_point) {
+    // 1. Let index be index jis0208 excluding all pointers in the
+    // range 8272 to 8835.
+    var pointer = indexPointerFor(code_point, index('jis0208'));
+    if (pointer === null || inRange(pointer, 8272, 8835))
+      return null;
+
+    // 2. Return the index pointer for code point in index.
+    return pointer;
+  }
+
+  /**
+   * @param {number} code_point The |code_point| to search for in the big5 index.
+   * @return {?number} The code point corresponding to |pointer| in |index|,
+   *     or null if |code point| is not in the big5 index.
+   */
+  function indexBig5PointerFor(code_point) {
+
+    // 1. Let index be index big5.
+    var index_ = index('big5');
+
+    // 2. If code point is U+2550, U+255E, U+2561, U+256A, U+5341, or
+    // U+5345, return the last pointer corresponding to code point in
+    // index.
+    if (code_point === 0x2550 || code_point === 0x255E ||
+        code_point === 0x2561 || code_point === 0x256A ||
+        code_point === 0x5341 || code_point === 0x5345) {
+      return index.lastIndexOf(code_point);
+    }
+
+    // 3. Return the index pointer for code point in index.
+    return indexPointerFor(code_point, index_);
+  }
+
+  //
+  // 7. API
+  //
+
+  /** @const */ var DEFAULT_ENCODING = 'utf-8';
+
+  // 7.1 Interface TextDecoder
+
+  /**
+   * @constructor
+   * @param {string=} encoding The label of the encoding;
+   *     defaults to 'utf-8'.
+   * @param {Object=} options
+   */
+  function TextDecoder(encoding, options) {
+    if (!(this instanceof TextDecoder)) {
+      return new TextDecoder(encoding, options);
+    }
+    encoding = encoding !== undefined ? String(encoding) : DEFAULT_ENCODING;
+    options = ToDictionary(options);
+    /** @private */
+    this._encoding = getEncoding(encoding);
+    if (this._encoding === null || this._encoding.name === 'replacement')
+      throw RangeError('Unknown encoding: ' + encoding);
+
+    if (!decoders[this._encoding.name]) {
+      throw Error('Decoder not present.' +
+                  ' Did you forget to include encoding-indexes.js?');
+    }
+
+    /** @private @type {boolean} */
+    this._streaming = false;
+    /** @private @type {boolean} */
+    this._BOMseen = false;
+    /** @private @type {?Decoder} */
+    this._decoder = null;
+    /** @private @type {boolean} */
+    this._fatal = Boolean(options['fatal']);
+    /** @private @type {boolean} */
+    this._ignoreBOM = Boolean(options['ignoreBOM']);
+
+    if (Object.defineProperty) {
+      Object.defineProperty(this, 'encoding', {value: this._encoding.name});
+      Object.defineProperty(this, 'fatal', {value: this._fatal});
+      Object.defineProperty(this, 'ignoreBOM', {value: this._ignoreBOM});
+    } else {
+      this.encoding = this._encoding.name;
+      this.fatal = this._fatal;
+      this.ignoreBOM = this._ignoreBOM;
+    }
+
+    return this;
+  }
+
+  TextDecoder.prototype = {
+    /**
+     * @param {ArrayBufferView=} input The buffer of bytes to decode.
+     * @param {Object=} options
+     * @return {string} The decoded string.
+     */
+    decode: function decode(input, options) {
+      var bytes;
+      if (typeof input === 'object' && input instanceof ArrayBuffer) {
+        bytes = new Uint8Array(input);
+      } else if (typeof input === 'object' && 'buffer' in input &&
+                 input.buffer instanceof ArrayBuffer) {
+        bytes = new Uint8Array(input.buffer,
+                               input.byteOffset,
+                               input.byteLength);
+      } else {
+        bytes = new Uint8Array(0);
+      }
+
+      options = ToDictionary(options);
+
+      if (!this._streaming) {
+        this._decoder = decoders[this._encoding.name]({fatal: this._fatal});
+        this._BOMseen = false;
+      }
+      this._streaming = Boolean(options['stream']);
+
+      var input_stream = new Stream(bytes);
+
+      var code_points = [];
+
+      /** @type {?(number|!Array.<number>)} */
+      var result;
+
+      while (!input_stream.endOfStream()) {
+        result = this._decoder.handler(input_stream, input_stream.read());
+        if (result === finished)
+          break;
+        if (result === null)
+          continue;
+        if (Array.isArray(result))
+          code_points.push.apply(code_points, /**@type {!Array.<number>}*/(result));
+        else
+          code_points.push(result);
+      }
+      if (!this._streaming) {
+        do {
+          result = this._decoder.handler(input_stream, input_stream.read());
+          if (result === finished)
+            break;
+          if (result === null)
+            continue;
+          if (Array.isArray(result))
+            code_points.push.apply(code_points, /**@type {!Array.<number>}*/(result));
+          else
+            code_points.push(result);
+        } while (!input_stream.endOfStream());
+        this._decoder = null;
+      }
+
+      if (code_points.length) {
+        // If encoding is one of utf-8, utf-16be, and utf-16le, and
+        // ignore BOM flag and BOM seen flag are unset, run these
+        // subsubsteps:
+        if (['utf-8', 'utf-16le', 'utf-16be'].indexOf(this.encoding) !== -1 &&
+            !this._ignoreBOM && !this._BOMseen) {
+          // If token is U+FEFF, set BOM seen flag.
+          if (code_points[0] === 0xFEFF) {
+            this._BOMseen = true;
+            code_points.shift();
+          } else {
+            // Otherwise, if token is not end-of-stream, set BOM seen
+            // flag and append token to output.
+            this._BOMseen = true;
+          }
+        }
+      }
+
+      return codePointsToString(code_points);
+    }
+  };
+
+  // 7.2 Interface TextEncoder
+
+  /**
+   * @constructor
+   * @param {string=} encoding The label of the encoding;
+   *     defaults to 'utf-8'.
+   * @param {Object=} options
+   */
+  function TextEncoder(encoding, options) {
+    if (!(this instanceof TextEncoder))
+      return new TextEncoder(encoding, options);
+    encoding = encoding !== undefined ? String(encoding) : DEFAULT_ENCODING;
+    options = ToDictionary(options);
+    /** @private */
+    this._encoding = getEncoding(encoding);
+    if (this._encoding === null || this._encoding.name === 'replacement')
+      throw RangeError('Unknown encoding: ' + encoding);
+
+    var allowLegacyEncoding =
+          Boolean(options['NONSTANDARD_allowLegacyEncoding']);
+    var isLegacyEncoding = (this._encoding.name !== 'utf-8' &&
+                            this._encoding.name !== 'utf-16le' &&
+                            this._encoding.name !== 'utf-16be');
+    if (this._encoding === null || (isLegacyEncoding && !allowLegacyEncoding))
+      throw RangeError('Unknown encoding: ' + encoding);
+
+    if (!encoders[this._encoding.name]) {
+      throw Error('Encoder not present.' +
+                  ' Did you forget to include encoding-indexes.js?');
+    }
+
+    /** @private @type {boolean} */
+    this._streaming = false;
+    /** @private @type {?Encoder} */
+    this._encoder = null;
+    /** @private @type {{fatal: boolean}} */
+    this._options = {fatal: Boolean(options['fatal'])};
+
+    if (Object.defineProperty)
+      Object.defineProperty(this, 'encoding', {value: this._encoding.name});
+    else
+      this.encoding = this._encoding.name;
+
+    return this;
+  }
+
+  TextEncoder.prototype = {
+    /**
+     * @param {string=} opt_string The string to encode.
+     * @param {Object=} options
+     * @return {Uint8Array} Encoded bytes, as a Uint8Array.
+     */
+    encode: function encode(opt_string, options) {
+      opt_string = opt_string ? String(opt_string) : '';
+      options = ToDictionary(options);
+
+      // NOTE: This option is nonstandard. None of the encodings
+      // permitted for encoding (i.e. UTF-8, UTF-16) are stateful,
+      // so streaming is not necessary.
+      if (!this._streaming)
+        this._encoder = encoders[this._encoding.name](this._options);
+      this._streaming = Boolean(options['stream']);
+
+      var bytes = [];
+      var input_stream = new Stream(stringToCodePoints(opt_string));
+      /** @type {?(number|!Array.<number>)} */
+      var result;
+      while (!input_stream.endOfStream()) {
+        result = this._encoder.handler(input_stream, input_stream.read());
+        if (result === finished)
+          break;
+        if (Array.isArray(result))
+          bytes.push.apply(bytes, /**@type {!Array.<number>}*/(result));
+        else
+          bytes.push(result);
+      }
+      if (!this._streaming) {
+        while (true) {
+          result = this._encoder.handler(input_stream, input_stream.read());
+          if (result === finished)
+            break;
+          if (Array.isArray(result))
+            bytes.push.apply(bytes, /**@type {!Array.<number>}*/(result));
+          else
+            bytes.push(result);
+        }
+        this._encoder = null;
+      }
+      return new Uint8Array(bytes);
+    }
+  };
+
+
+  //
+  // 8. The encoding
+  //
+
+  // 8.1 utf-8
+
+  /**
+   * @constructor
+   * @implements {Decoder}
+   * @param {{fatal: boolean}} options
+   */
+  function UTF8Decoder(options) {
+    var fatal = options.fatal;
+
+    // utf-8's decoder's has an associated utf-8 code point, utf-8
+    // bytes seen, and utf-8 bytes needed (all initially 0), a utf-8
+    // lower boundary (initially 0x80), and a utf-8 upper boundary
+    // (initially 0xBF).
+    var /** @type {number} */ utf8_code_point = 0,
+        /** @type {number} */ utf8_bytes_seen = 0,
+        /** @type {number} */ utf8_bytes_needed = 0,
+        /** @type {number} */ utf8_lower_boundary = 0x80,
+        /** @type {number} */ utf8_upper_boundary = 0xBF;
+
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point.
+     */
+    this.handler = function(stream, bite) {
+      // 1. If byte is end-of-stream and utf-8 bytes needed is not 0,
+      // set utf-8 bytes needed to 0 and return error.
+      if (bite === end_of_stream && utf8_bytes_needed !== 0) {
+        utf8_bytes_needed = 0;
+        return decoderError(fatal);
+      }
+
+      // 2. If byte is end-of-stream, return finished.
+      if (bite === end_of_stream)
+        return finished;
+
+      // 3. If utf-8 bytes needed is 0, based on byte:
+      if (utf8_bytes_needed === 0) {
+
+        // 0x00 to 0x7F
+        if (inRange(bite, 0x00, 0x7F)) {
+          // Return a code point whose value is byte.
+          return bite;
+        }
+
+        // 0xC2 to 0xDF
+        if (inRange(bite, 0xC2, 0xDF)) {
+          // Set utf-8 bytes needed to 1 and utf-8 code point to byte
+          // − 0xC0.
+          utf8_bytes_needed = 1;
+          utf8_code_point = bite - 0xC0;
+        }
+
+        // 0xE0 to 0xEF
+        else if (inRange(bite, 0xE0, 0xEF)) {
+          // 1. If byte is 0xE0, set utf-8 lower boundary to 0xA0.
+          if (bite === 0xE0)
+            utf8_lower_boundary = 0xA0;
+          // 2. If byte is 0xED, set utf-8 upper boundary to 0x9F.
+          if (bite === 0xED)
+            utf8_upper_boundary = 0x9F;
+          // 3. Set utf-8 bytes needed to 2 and utf-8 code point to
+          // byte − 0xE0.
+          utf8_bytes_needed = 2;
+          utf8_code_point = bite - 0xE0;
+        }
+
+        // 0xF0 to 0xF4
+        else if (inRange(bite, 0xF0, 0xF4)) {
+          // 1. If byte is 0xF0, set utf-8 lower boundary to 0x90.
+          if (bite === 0xF0)
+            utf8_lower_boundary = 0x90;
+          // 2. If byte is 0xF4, set utf-8 upper boundary to 0x8F.
+          if (bite === 0xF4)
+            utf8_upper_boundary = 0x8F;
+          // 3. Set utf-8 bytes needed to 3 and utf-8 code point to
+          // byte − 0xF0.
+          utf8_bytes_needed = 3;
+          utf8_code_point = bite - 0xF0;
+        }
+
+        // Otherwise
+        else {
+          // Return error.
+          return decoderError(fatal);
+        }
+
+        // Then (byte is in the range 0xC2 to 0xF4) set utf-8 code
+        // point to utf-8 code point << (6 × utf-8 bytes needed) and
+        // return continue.
+        utf8_code_point = utf8_code_point << (6 * utf8_bytes_needed);
+        return null;
+      }
+
+      // 4. If byte is not in the range utf-8 lower boundary to utf-8
+      // upper boundary, run these substeps:
+      if (!inRange(bite, utf8_lower_boundary, utf8_upper_boundary)) {
+
+        // 1. Set utf-8 code point, utf-8 bytes needed, and utf-8
+        // bytes seen to 0, set utf-8 lower boundary to 0x80, and set
+        // utf-8 upper boundary to 0xBF.
+        utf8_code_point = utf8_bytes_needed = utf8_bytes_seen = 0;
+        utf8_lower_boundary = 0x80;
+        utf8_upper_boundary = 0xBF;
+
+        // 2. Prepend byte to stream.
+        stream.prepend(bite);
+
+        // 3. Return error.
+        return decoderError(fatal);
+      }
+
+      // 5. Set utf-8 lower boundary to 0x80 and utf-8 upper boundary
+      // to 0xBF.
+      utf8_lower_boundary = 0x80;
+      utf8_upper_boundary = 0xBF;
+
+      // 6. Increase utf-8 bytes seen by one and set utf-8 code point
+      // to utf-8 code point + (byte − 0x80) << (6 × (utf-8 bytes
+      // needed − utf-8 bytes seen)).
+      utf8_bytes_seen += 1;
+      utf8_code_point += (bite - 0x80) << (6 * (utf8_bytes_needed - utf8_bytes_seen));
+
+      // 7. If utf-8 bytes seen is not equal to utf-8 bytes needed,
+      // continue.
+      if (utf8_bytes_seen !== utf8_bytes_needed)
+        return null;
+
+      // 8. Let code point be utf-8 code point.
+      var code_point = utf8_code_point;
+
+      // 9. Set utf-8 code point, utf-8 bytes needed, and utf-8 bytes
+      // seen to 0.
+      utf8_code_point = utf8_bytes_needed = utf8_bytes_seen = 0;
+
+      // 10. Return a code point whose value is code point.
+      return code_point;
+    };
+  }
+
+  /**
+   * @constructor
+   * @implements {Encoder}
+   * @param {{fatal: boolean}} options
+   */
+  function UTF8Encoder(options) {
+    var fatal = options.fatal;
+    /**
+     * @param {Stream} stream Input stream.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit.
+     */
+    this.handler = function(stream, code_point) {
+      // 1. If code point is end-of-stream, return finished.
+      if (code_point === end_of_stream)
+        return finished;
+
+      // 2. If code point is in the range U+0000 to U+007F, return a
+      // byte whose value is code point.
+      if (inRange(code_point, 0x0000, 0x007f))
+        return code_point;
+
+      // 3. Set count and offset based on the range code point is in:
+      var count, offset;
+      // U+0080 to U+07FF:    1 and 0xC0
+      if (inRange(code_point, 0x0080, 0x07FF)) {
+        count = 1;
+        offset = 0xC0;
+      }
+      // U+0800 to U+FFFF:    2 and 0xE0
+      else if (inRange(code_point, 0x0800, 0xFFFF)) {
+        count = 2;
+        offset = 0xE0;
+      }
+      // U+10000 to U+10FFFF: 3 and 0xF0
+      else if (inRange(code_point, 0x10000, 0x10FFFF)) {
+        count = 3;
+        offset = 0xF0;
+      }
+
+      // 4.Let bytes be a byte sequence whose first byte is (code
+      // point >> (6 × count)) + offset.
+      var bytes = [(code_point >> (6 * count)) + offset];
+
+      // 5. Run these substeps while count is greater than 0:
+      while (count > 0) {
+
+        // 1. Set temp to code point >> (6 × (count − 1)).
+        var temp = code_point >> (6 * (count - 1));
+
+        // 2. Append to bytes 0x80 | (temp & 0x3F).
+        bytes.push(0x80 | (temp & 0x3F));
+
+        // 3. Decrease count by one.
+        count -= 1;
+      }
+
+      // 6. Return bytes bytes, in order.
+      return bytes;
+    };
+  }
+
+  /** @param {{fatal: boolean}} options */
+  encoders['utf-8'] = function(options) {
+    return new UTF8Encoder(options);
+  };
+  /** @param {{fatal: boolean}} options */
+  decoders['utf-8'] = function(options) {
+    return new UTF8Decoder(options);
+  };
+
+  //
+  // 9. Legacy single-byte encodings
+  //
+
+  // 9.1 single-byte decoder
+  /**
+   * @constructor
+   * @implements {Decoder}
+   * @param {!Array.<number>} index The encoding index.
+   * @param {{fatal: boolean}} options
+   */
+  function SingleByteDecoder(index, options) {
+    var fatal = options.fatal;
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point.
+     */
+    this.handler = function(stream, bite) {
+      // 1. If byte is end-of-stream, return finished.
+      if (bite === end_of_stream)
+        return finished;
+
+      // 2. If byte is in the range 0x00 to 0x7F, return a code point
+      // whose value is byte.
+      if (inRange(bite, 0x00, 0x7F))
+        return bite;
+
+      // 3. Let code point be the index code point for byte − 0x80 in
+      // index single-byte.
+      var code_point = index[bite - 0x80];
+
+      // 4. If code point is null, return error.
+      if (code_point === null)
+        return decoderError(fatal);
+
+      // 5. Return a code point whose value is code point.
+      return code_point;
+    };
+  }
+
+  // 9.2 single-byte encoder
+  /**
+   * @constructor
+   * @implements {Encoder}
+   * @param {!Array.<?number>} index The encoding index.
+   * @param {{fatal: boolean}} options
+   */
+  function SingleByteEncoder(index, options) {
+    var fatal = options.fatal;
+    /**
+     * @param {Stream} stream Input stream.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit.
+     */
+    this.handler = function(stream, code_point) {
+      // 1. If code point is end-of-stream, return finished.
+      if (code_point === end_of_stream)
+        return finished;
+
+      // 2. If code point is in the range U+0000 to U+007F, return a
+      // byte whose value is code point.
+      if (inRange(code_point, 0x0000, 0x007F))
+        return code_point;
+
+      // 3. Let pointer be the index pointer for code point in index
+      // single-byte.
+      var pointer = indexPointerFor(code_point, index);
+
+      // 4. If pointer is null, return error with code point.
+      if (pointer === null)
+        encoderError(code_point);
+
+      // 5. Return a byte whose value is pointer + 0x80.
+      return pointer + 0x80;
+    };
+  }
+
+  (function() {
+    if (!('encoding-indexes' in global))
+      return;
+    encodings.forEach(function(category) {
+      if (category.heading !== 'Legacy single-byte encodings')
+        return;
+      category.encodings.forEach(function(encoding) {
+        var name = encoding.name;
+        var idx = index(name);
+        /** @param {{fatal: boolean}} options */
+        decoders[name] = function(options) {
+          return new SingleByteDecoder(idx, options);
+        };
+        /** @param {{fatal: boolean}} options */
+        encoders[name] = function(options) {
+          return new SingleByteEncoder(idx, options);
+        };
+      });
+    });
+  }());
+
+  //
+  // 10. Legacy multi-byte Chinese (simplified) encodings
+  //
+
+  // 10.1 gbk
+
+  // 10.1.1 gbk decoder
+  // gbk's decoder is gb18030's decoder.
+  /** @param {{fatal: boolean}} options */
+  decoders['gbk'] = function(options) {
+    return new GB18030Decoder(options);
+  };
+
+  // 10.1.2 gbk encoder
+  // gbk's encoder is gb18030's encoder with its gbk flag set.
+  /** @param {{fatal: boolean}} options */
+  encoders['gbk'] = function(options) {
+    return new GB18030Encoder(options, true);
+  };
+
+  // 10.2 gb18030
+
+  // 10.2.1 gb18030 decoder
+  /**
+   * @constructor
+   * @implements {Decoder}
+   * @param {{fatal: boolean}} options
+   */
+  function GB18030Decoder(options) {
+    var fatal = options.fatal;
+    // gb18030's decoder has an associated gb18030 first, gb18030
+    // second, and gb18030 third (all initially 0x00).
+    var /** @type {number} */ gb18030_first = 0x00,
+        /** @type {number} */ gb18030_second = 0x00,
+        /** @type {number} */ gb18030_third = 0x00;
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point.
+     */
+    this.handler = function(stream, bite) {
+      // 1. If byte is end-of-stream and gb18030 first, gb18030
+      // second, and gb18030 third are 0x00, return finished.
+      if (bite === end_of_stream && gb18030_first === 0x00 &&
+          gb18030_second === 0x00 && gb18030_third === 0x00) {
+        return finished;
+      }
+      // 2. If byte is end-of-stream, and gb18030 first, gb18030
+      // second, or gb18030 third is not 0x00, set gb18030 first,
+      // gb18030 second, and gb18030 third to 0x00, and return error.
+      if (bite === end_of_stream &&
+          (gb18030_first !== 0x00 || gb18030_second !== 0x00 || gb18030_third !== 0x00)) {
+        gb18030_first = 0x00;
+        gb18030_second = 0x00;
+        gb18030_third = 0x00;
+        decoderError(fatal);
+      }
+      var code_point;
+      // 3. If gb18030 third is not 0x00, run these substeps:
+      if (gb18030_third !== 0x00) {
+        // 1. Let code point be null.
+        code_point = null;
+        // 2. If byte is in the range 0x30 to 0x39, set code point to
+        // the index gb18030 ranges code point for (((gb18030 first −
+        // 0x81) × 10 + gb18030 second − 0x30) × 126 + gb18030 third −
+        // 0x81) × 10 + byte − 0x30.
+        if (inRange(bite, 0x30, 0x39)) {
+          code_point = indexGB18030RangesCodePointFor(
+              (((gb18030_first - 0x81) * 10 + (gb18030_second - 0x30)) * 126 +
+               (gb18030_third - 0x81)) * 10 + bite - 0x30);
+        }
+
+        // 3. Let buffer be a byte sequence consisting of gb18030
+        // second, gb18030 third, and byte, in order.
+        var buffer = [gb18030_second, gb18030_third, bite];
+
+        // 4. Set gb18030 first, gb18030 second, and gb18030 third to
+        // 0x00.
+        gb18030_first = 0x00;
+        gb18030_second = 0x00;
+        gb18030_third = 0x00;
+
+        // 5. If code point is null, prepend buffer to stream and
+        // return error.
+        if (code_point === null) {
+          stream.prepend(buffer);
+          return decoderError(fatal);
+        }
+
+        // 6. Return a code point whose value is code point.
+        return code_point;
+      }
+
+      // 4. If gb18030 second is not 0x00, run these substeps:
+      if (gb18030_second !== 0x00) {
+
+        // 1. If byte is in the range 0x81 to 0xFE, set gb18030 third
+        // to byte and return continue.
+        if (inRange(bite, 0x81, 0xFE)) {
+          gb18030_third = bite;
+          return null;
+        }
+
+        // 2. Prepend gb18030 second followed by byte to stream, set
+        // gb18030 first and gb18030 second to 0x00, and return error.
+        stream.prepend([gb18030_second, bite]);
+        gb18030_first = 0x00;
+        gb18030_second = 0x00;
+        return decoderError(fatal);
+      }
+
+      // 5. If gb18030 first is not 0x00, run these substeps:
+      if (gb18030_first !== 0x00) {
+
+        // 1. If byte is in the range 0x30 to 0x39, set gb18030 second
+        // to byte and return continue.
+        if (inRange(bite, 0x30, 0x39)) {
+          gb18030_second = bite;
+          return null;
+        }
+
+        // 2. Let lead be gb18030 first, let pointer be null, and set
+        // gb18030 first to 0x00.
+        var lead = gb18030_first;
+        var pointer = null;
+        gb18030_first = 0x00;
+
+        // 3. Let offset be 0x40 if byte is less than 0x7F and 0x41
+        // otherwise.
+        var offset = bite < 0x7F ? 0x40 : 0x41;
+
+        // 4. If byte is in the range 0x40 to 0x7E or 0x80 to 0xFE,
+        // set pointer to (lead − 0x81) × 190 + (byte − offset).
+        if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFE))
+          pointer = (lead - 0x81) * 190 + (bite - offset);
+
+        // 5. Let code point be null if pointer is null and the index
+        // code point for pointer in index gb18030 otherwise.
+        code_point = pointer === null ? null :
+            indexCodePointFor(pointer, index('gb18030'));
+
+        // 6. If code point is null and byte is in the range 0x00 to
+        // 0x7F, prepend byte to stream.
+        if (code_point === null && inRange(bite, 0x00, 0x7F))
+          stream.prepend(bite);
+
+        // 7. If code point is null, return error.
+        if (code_point === null)
+          return decoderError(fatal);
+
+        // 8. Return a code point whose value is code point.
+        return code_point;
+      }
+
+      // 6. If byte is in the range 0x00 to 0x7F, return a code point
+      // whose value is byte.
+      if (inRange(bite, 0x00, 0x7F))
+        return bite;
+
+      // 7. If byte is 0x80, return code point U+20AC.
+      if (bite === 0x80)
+        return 0x20AC;
+
+      // 8. If byte is in the range 0x81 to 0xFE, set gb18030 first to
+      // byte and return continue.
+      if (inRange(bite, 0x81, 0xFE)) {
+        gb18030_first = bite;
+        return null;
+      }
+
+      // 9. Return error.
+      return decoderError(fatal);
+    };
+  }
+
+  // 10.2.2 gb18030 encoder
+  /**
+   * @constructor
+   * @implements {Encoder}
+   * @param {{fatal: boolean}} options
+   * @param {boolean=} gbk_flag
+   */
+  function GB18030Encoder(options, gbk_flag) {
+    var fatal = options.fatal;
+    // gb18030's decoder has an associated gbk flag (initially unset).
+    /**
+     * @param {Stream} stream Input stream.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit.
+     */
+    this.handler = function(stream, code_point) {
+      // 1. If code point is end-of-stream, return finished.
+      if (code_point === end_of_stream)
+        return finished;
+
+      // 2. If code point is in the range U+0000 to U+007F, return a
+      // byte whose value is code point.
+      if (inRange(code_point, 0x0000, 0x007F)) {
+        return code_point;
+      }
+
+      // 3. If the gbk flag is set and code point is U+20AC, return
+      // byte 0x80.
+      if (gbk_flag && code_point === 0x20AC)
+        return 0x80;
+
+      // 4. Let pointer be the index pointer for code point in index
+      // gb18030.
+      var pointer = indexPointerFor(code_point, index('gb18030'));
+
+      // 5. If pointer is not null, run these substeps:
+      if (pointer !== null) {
+
+        // 1. Let lead be pointer / 190 + 0x81.
+        var lead = div(pointer, 190) + 0x81;
+
+        // 2. Let trail be pointer % 190.
+        var trail = pointer % 190;
+
+        // 3. Let offset be 0x40 if trail is less than 0x3F and 0x41 otherwise.
+        var offset = trail < 0x3F ? 0x40 : 0x41;
+
+        // 4. Return two bytes whose values are lead and trail + offset.
+        return [lead, trail + offset];
+      }
+
+      // 6. If gbk flag is set, return error with code point.
+      if (gbk_flag)
+        return encoderError(code_point);
+
+      // 7. Set pointer to the index gb18030 ranges pointer for code
+      // point.
+      pointer = indexGB18030RangesPointerFor(code_point);
+
+      // 8. Let byte1 be pointer / 10 / 126 / 10.
+      var byte1 = div(div(div(pointer, 10), 126), 10);
+
+      // 9. Set pointer to pointer − byte1 × 10 × 126 × 10.
+      pointer = pointer - byte1 * 10 * 126 * 10;
+
+      // 10. Let byte2 be pointer / 10 / 126.
+      var byte2 = div(div(pointer, 10), 126);
+
+      // 11. Set pointer to pointer − byte2 × 10 × 126.
+      pointer = pointer - byte2 * 10 * 126;
+
+      // 12. Let byte3 be pointer / 10.
+      var byte3 = div(pointer, 10);
+
+      // 13. Let byte4 be pointer − byte3 × 10.
+      var byte4 = pointer - byte3 * 10;
+
+      // 14. Return four bytes whose values are byte1 + 0x81, byte2 +
+      // 0x30, byte3 + 0x81, byte4 + 0x30.
+      return [byte1 + 0x81,
+              byte2 + 0x30,
+              byte3 + 0x81,
+              byte4 + 0x30];
+    };
+  }
+
+  /** @param {{fatal: boolean}} options */
+  encoders['gb18030'] = function(options) {
+    return new GB18030Encoder(options);
+  };
+  /** @param {{fatal: boolean}} options */
+  decoders['gb18030'] = function(options) {
+    return new GB18030Decoder(options);
+  };
+
+
+  //
+  // 11. Legacy multi-byte Chinese (traditional) encodings
+  //
+
+  // 11.1 big5
+
+  /**
+   * @constructor
+   * @implements {Decoder}
+   * @param {{fatal: boolean}} options
+   */
+  function Big5Decoder(options) {
+    var fatal = options.fatal;
+    // big5's decoder has an associated big5 lead (initially 0x00).
+    var /** @type {number} */ big5_lead = 0x00;
+
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point.
+     */
+    this.handler = function(stream, bite) {
+      // 1. If byte is end-of-stream and big5 lead is not 0x00, set
+      // big5 lead to 0x00 and return error.
+      if (bite === end_of_stream && big5_lead !== 0x00) {
+        big5_lead = 0x00;
+        return decoderError(fatal);
+      }
+
+      // 2. If byte is end-of-stream and big5 lead is 0x00, return
+      // finished.
+      if (bite === end_of_stream && big5_lead === 0x00)
+        return finished;
+
+      // 3. If big5 lead is not 0x00, let lead be big5 lead, let
+      // pointer be null, set big5 lead to 0x00, and then run these
+      // substeps:
+      if (big5_lead !== 0x00) {
+        var lead = big5_lead;
+        var pointer = null;
+        big5_lead = 0x00;
+
+        // 1. Let offset be 0x40 if byte is less than 0x7F and 0x62
+        // otherwise.
+        var offset = bite < 0x7F ? 0x40 : 0x62;
+
+        // 2. If byte is in the range 0x40 to 0x7E or 0xA1 to 0xFE,
+        // set pointer to (lead − 0x81) × 157 + (byte − offset).
+        if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0xA1, 0xFE))
+          pointer = (lead - 0x81) * 157 + (bite - offset);
+
+        // 3. If there is a row in the table below whose first column
+        // is pointer, return the two code points listed in its second
+        // column
+        // Pointer | Code points
+        // --------+--------------
+        // 1133    | U+00CA U+0304
+        // 1135    | U+00CA U+030C
+        // 1164    | U+00EA U+0304
+        // 1166    | U+00EA U+030C
+        switch (pointer) {
+          case 1133: return [0x00CA, 0x0304];
+          case 1135: return [0x00CA, 0x030C];
+          case 1164: return [0x00EA, 0x0304];
+          case 1166: return [0x00EA, 0x030C];
+        }
+
+        // 4. Let code point be null if pointer is null and the index
+        // code point for pointer in index big5 otherwise.
+        var code_point = (pointer === null) ? null :
+            indexCodePointFor(pointer, index('big5'));
+
+        // 5. If code point is null and byte is in the range 0x00 to
+        // 0x7F, prepend byte to stream.
+        if (code_point === null && inRange(bite, 0x00, 0x7F))
+          stream.prepend(bite);
+
+        // 6. If code point is null, return error.
+        if (code_point === null)
+          return decoderError(fatal);
+
+        // 7. Return a code point whose value is code point.
+        return code_point;
+      }
+
+      // 4. If byte is in the range 0x00 to 0x7F, return a code point
+      // whose value is byte.
+      if (inRange(bite, 0x00, 0x7F))
+        return bite;
+
+      // 5. If byte is in the range 0x81 to 0xFE, set big5 lead to
+      // byte and return continue.
+      if (inRange(bite, 0x81, 0xFE)) {
+        big5_lead = bite;
+        return null;
+      }
+
+      // 6. Return error.
+      return decoderError(fatal);
+    };
+  }
+
+  /**
+   * @constructor
+   * @implements {Encoder}
+   * @param {{fatal: boolean}} options
+   */
+  function Big5Encoder(options) {
+    var fatal = options.fatal;
+    /**
+     * @param {Stream} stream Input stream.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit.
+     */
+    this.handler = function(stream, code_point) {
+      // 1. If code point is end-of-stream, return finished.
+      if (code_point === end_of_stream)
+        return finished;
+
+      // 2. If code point is in the range U+0000 to U+007F, return a
+      // byte whose value is code point.
+      if (inRange(code_point, 0x0000, 0x007F))
+        return code_point;
+
+      // 3. Let pointer be the index big5 pointer for code point.
+      var pointer = indexBig5PointerFor(code_point, index('big5'));
+
+      // 4. If pointer is null, return error with code point.
+      if (pointer === null)
+        return encoderError(code_point);
+
+      // 5. Let lead be pointer / 157 + 0x81.
+      var lead = div(pointer, 157) + 0x81;
+
+      // 6. If lead is less than 0xA1, return error with code point.
+      if (lead < 0xA1)
+        return encoderError(code_point);
+
+      // 7. Let trail be pointer % 157.
+      var trail = pointer % 157;
+
+      // 8. Let offset be 0x40 if trail is less than 0x3F and 0x62
+      // otherwise.
+      var offset = trail < 0x3F ? 0x40 : 0x62;
+
+      // Return two bytes whose values are lead and trail + offset.
+      return [lead, trail + offset];
+    };
+  }
+
+  /** @param {{fatal: boolean}} options */
+  encoders['big5'] = function(options) {
+    return new Big5Encoder(options);
+  };
+  /** @param {{fatal: boolean}} options */
+  decoders['big5'] = function(options) {
+    return new Big5Decoder(options);
+  };
+
+
+  //
+  // 12. Legacy multi-byte Japanese encodings
+  //
+
+  // 12.1 euc-jp
+
+  /**
+   * @constructor
+   * @implements {Decoder}
+   * @param {{fatal: boolean}} options
+   */
+  function EUCJPDecoder(options) {
+    var fatal = options.fatal;
+
+    // euc-jp's decoder has an associated euc-jp jis0212 flag
+    // (initially unset) and euc-jp lead (initially 0x00).
+    var /** @type {boolean} */ eucjp_jis0212_flag = false,
+        /** @type {number} */ eucjp_lead = 0x00;
+
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point.
+     */
+    this.handler = function(stream, bite) {
+      // 1. If byte is end-of-stream and euc-jp lead is not 0x00, set
+      // euc-jp lead to 0x00, and return error.
+      if (bite === end_of_stream && eucjp_lead !== 0x00) {
+        eucjp_lead = 0x00;
+        return decoderError(fatal);
+      }
+
+      // 2. If byte is end-of-stream and euc-jp lead is 0x00, return
+      // finished.
+      if (bite === end_of_stream && eucjp_lead === 0x00)
+        return finished;
+
+      // 3. If euc-jp lead is 0x8E and byte is in the range 0xA1 to
+      // 0xDF, set euc-jp lead to 0x00 and return a code point whose
+      // value is 0xFF61 + byte − 0xA1.
+      if (eucjp_lead === 0x8E && inRange(bite, 0xA1, 0xDF)) {
+        eucjp_lead = 0x00;
+        return 0xFF61 + bite - 0xA1;
+      }
+
+      // 4. If euc-jp lead is 0x8F and byte is in the range 0xA1 to
+      // 0xFE, set the euc-jp jis0212 flag, set euc-jp lead to byte,
+      // and return continue.
+      if (eucjp_lead === 0x8F && inRange(bite, 0xA1, 0xFE)) {
+        eucjp_jis0212_flag = true;
+        eucjp_lead = bite;
+        return null;
+      }
+
+      // 5. If euc-jp lead is not 0x00, let lead be euc-jp lead, set
+      // euc-jp lead to 0x00, and run these substeps:
+      if (eucjp_lead !== 0x00) {
+        var lead = eucjp_lead;
+        eucjp_lead = 0x00;
+
+        // 1. Let code point be null.
+        var code_point = null;
+
+        // 2. If lead and byte are both in the range 0xA1 to 0xFE, set
+        // code point to the index code point for (lead − 0xA1) × 94 +
+        // byte − 0xA1 in index jis0208 if the euc-jp jis0212 flag is
+        // unset and in index jis0212 otherwise.
+        if (inRange(lead, 0xA1, 0xFE) && inRange(bite, 0xA1, 0xFE)) {
+          code_point = indexCodePointFor(
+            (lead - 0xA1) * 94 + (bite - 0xA1),
+            index(!eucjp_jis0212_flag ? 'jis0208' : 'jis0212'));
+        }
+
+        // 3. Unset the euc-jp jis0212 flag.
+        eucjp_jis0212_flag = false;
+
+        // 4. If byte is not in the range 0xA1 to 0xFE, prepend byte
+        // to stream.
+        if (!inRange(bite, 0xA1, 0xFE))
+          stream.prepend(bite);
+
+        // 5. If code point is null, return error.
+        if (code_point === null)
+          return decoderError(fatal);
+
+        // 6. Return a code point whose value is code point.
+        return code_point;
+      }
+
+      // 6. If byte is in the range 0x00 to 0x7F, return a code point
+      // whose value is byte.
+      if (inRange(bite, 0x00, 0x7F))
+        return bite;
+
+      // 7. If byte is 0x8E, 0x8F, or in the range 0xA1 to 0xFE, set
+      // euc-jp lead to byte and return continue.
+      if (bite === 0x8E || bite === 0x8F || inRange(bite, 0xA1, 0xFE)) {
+        eucjp_lead = bite;
+        return null;
+      }
+
+      // 8. Return error.
+      return decoderError(fatal);
+    };
+  }
+
+  /**
+   * @constructor
+   * @implements {Encoder}
+   * @param {{fatal: boolean}} options
+   */
+  function EUCJPEncoder(options) {
+    var fatal = options.fatal;
+    /**
+     * @param {Stream} stream Input stream.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit.
+     */
+    this.handler = function(stream, code_point) {
+      // 1. If code point is end-of-stream, return finished.
+      if (code_point === end_of_stream)
+        return finished;
+
+      // 2. If code point is in the range U+0000 to U+007F, return a
+      // byte whose value is code point.
+      if (inRange(code_point, 0x0000, 0x007F))
+        return code_point;
+
+      // 3. If code point is U+00A5, return byte 0x5C.
+      if (code_point === 0x00A5)
+        return 0x5C;
+
+      // 4. If code point is U+203E, return byte 0x7E.
+      if (code_point === 0x203E)
+        return 0x7E;
+
+      // 5. If code point is in the range U+FF61 to U+FF9F, return two
+      // bytes whose values are 0x8E and code point − 0xFF61 + 0xA1.
+      if (inRange(code_point, 0xFF61, 0xFF9F))
+        return [0x8E, code_point - 0xFF61 + 0xA1];
+
+      // 6. If code point is U+2022, set it to U+FF0D.
+      if (code_point === 0x2022)
+        code_point = 0xFF0D;
+
+      // 7. Let pointer be the index pointer for code point in index
+      // jis0208.
+      var pointer = indexPointerFor(code_point, index('jis0208'));
+
+      // 8. If pointer is null, return error with code point.
+      if (pointer === null)
+        return encoderError(code_point);
+
+      // 9. Let lead be pointer / 94 + 0xA1.
+      var lead = div(pointer, 94) + 0xA1;
+
+      // 10. Let trail be pointer % 94 + 0xA1.
+      var trail = pointer % 94 + 0xA1;
+
+      // 11. Return two bytes whose values are lead and trail.
+      return [lead, trail];
+    };
+  }
+
+  /** @param {{fatal: boolean}} options */
+  encoders['euc-jp'] = function(options) {
+    return new EUCJPEncoder(options);
+  };
+  /** @param {{fatal: boolean}} options */
+  decoders['euc-jp'] = function(options) {
+    return new EUCJPDecoder(options);
+  };
+
+  // 12.2 iso-2022-jp
+
+  /**
+   * @constructor
+   * @implements {Decoder}
+   * @param {{fatal: boolean}} options
+   */
+  function ISO2022JPDecoder(options) {
+    var fatal = options.fatal;
+    /** @enum */
+    var states = {
+      ASCII: 0,
+      Roman: 1,
+      Katakana: 2,
+      LeadByte: 3,
+      TrailByte: 4,
+      EscapeStart: 5,
+      Escape: 6
+    };
+    // iso-2022-jp's decoder has an associated iso-2022-jp decoder
+    // state (initially ASCII), iso-2022-jp decoder output state
+    // (initially ASCII), iso-2022-jp lead (initially 0x00), and
+    // iso-2022-jp output flag (initially unset).
+    var /** @type {number} */ iso2022jp_decoder_state = states.ASCII,
+        /** @type {number} */ iso2022jp_decoder_output_state = states.ASCII,
+        /** @type {number} */ iso2022jp_lead = 0x00,
+        /** @type {boolean} */ iso2022jp_output_flag = false;
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point.
+     */
+    this.handler = function(stream, bite) {
+      // switching on iso-2022-jp decoder state:
+      switch (iso2022jp_decoder_state) {
+      default:
+      case states.ASCII:
+        // ASCII
+        // Based on byte:
+
+        // 0x1B
+        if (bite === 0x1B) {
+          // Set iso-2022-jp decoder state to escape start and return
+          // continue.
+          iso2022jp_decoder_state = states.EscapeStart;
+          return null;
+        }
+
+        // 0x00 to 0x7F, excluding 0x0E, 0x0F, and 0x1B
+        if (inRange(bite, 0x00, 0x7F) && bite !== 0x0E
+            && bite !== 0x0F && bite !== 0x1B) {
+          // Unset the iso-2022-jp output flag and return a code point
+          // whose value is byte.
+          iso2022jp_output_flag = false;
+          return bite;
+        }
+
+        // end-of-stream
+        if (bite === end_of_stream) {
+          // Return finished.
+          return finished;
+        }
+
+        // Otherwise
+        // Unset the iso-2022-jp output flag and return error.
+        iso2022jp_output_flag = false;
+        return decoderError(fatal);
+
+      case states.Roman:
+        // Roman
+        // Based on byte:
+
+        // 0x1B
+        if (bite === 0x1B) {
+          // Set iso-2022-jp decoder state to escape start and return
+          // continue.
+          iso2022jp_decoder_state = states.EscapeStart;
+          return null;
+        }
+
+        // 0x5C
+        if (bite === 0x5C) {
+          // Unset the iso-2022-jp output flag and return code point
+          // U+00A5.
+          iso2022jp_output_flag = false;
+          return 0x00A5;
+        }
+
+        // 0x7E
+        if (bite === 0x7E) {
+          // Unset the iso-2022-jp output flag and return code point
+          // U+203E.
+          iso2022jp_output_flag = false;
+          return 0x203E;
+        }
+
+        // 0x00 to 0x7F, excluding 0x0E, 0x0F, 0x1B, 0x5C, and 0x7E
+        if (inRange(bite, 0x00, 0x7F) && bite !== 0x0E && bite !== 0x0F
+            && bite !== 0x1B && bite !== 0x5C && bite !== 0x7E) {
+          // Unset the iso-2022-jp output flag and return a code point
+          // whose value is byte.
+          iso2022jp_output_flag = false;
+          return bite;
+        }
+
+        // end-of-stream
+        if (bite === end_of_stream) {
+          // Return finished.
+          return finished;
+        }
+
+        // Otherwise
+        // Unset the iso-2022-jp output flag and return error.
+        iso2022jp_output_flag = false;
+        return decoderError(fatal);
+
+      case states.Katakana:
+        // Katakana
+        // Based on byte:
+
+        // 0x1B
+        if (bite === 0x1B) {
+          // Set iso-2022-jp decoder state to escape start and return
+          // continue.
+          iso2022jp_decoder_state = states.EscapeStart;
+          return null;
+        }
+
+        // 0x21 to 0x5F
+        if (inRange(bite, 0x21, 0x5F)) {
+          // Unset the iso-2022-jp output flag and return a code point
+          // whose value is 0xFF61 + byte − 0x21.
+          iso2022jp_output_flag = false;
+          return 0xFF61 + bite - 0x21;
+        }
+
+        // end-of-stream
+        if (bite === end_of_stream) {
+          // Return finished.
+          return finished;
+        }
+
+        // Otherwise
+        // Unset the iso-2022-jp output flag and return error.
+        iso2022jp_output_flag = false;
+        return decoderError(fatal);
+
+      case states.LeadByte:
+        // Lead byte
+        // Based on byte:
+
+        // 0x1B
+        if (bite === 0x1B) {
+          // Set iso-2022-jp decoder state to escape start and return
+          // continue.
+          iso2022jp_decoder_state = states.EscapeStart;
+          return null;
+        }
+
+        // 0x21 to 0x7E
+        if (inRange(bite, 0x21, 0x7E)) {
+          // Unset the iso-2022-jp output flag, set iso-2022-jp lead
+          // to byte, iso-2022-jp decoder state to trail byte, and
+          // return continue.
+          iso2022jp_output_flag = false;
+          iso2022jp_lead = bite;
+          iso2022jp_decoder_state = states.TrailByte;
+          return null;
+        }
+
+        // end-of-stream
+        if (bite === end_of_stream) {
+          // Return finished.
+          return finished;
+        }
+
+        // Otherwise
+        // Unset the iso-2022-jp output flag and return error.
+        iso2022jp_output_flag = false;
+        return decoderError(fatal);
+
+      case states.TrailByte:
+        // Trail byte
+        // Based on byte:
+
+        // 0x1B
+        if (bite === 0x1B) {
+          // Set iso-2022-jp decoder state to escape start and return
+          // continue.
+          iso2022jp_decoder_state = states.EscapeStart;
+          return decoderError(fatal);
+        }
+
+        // 0x21 to 0x7E
+        if (inRange(bite, 0x21, 0x7E)) {
+          // 1. Set the iso-2022-jp decoder state to lead byte.
+          iso2022jp_decoder_state = states.LeadByte;
+
+          // 2. Let pointer be (iso-2022-jp lead − 0x21) × 94 + byte − 0x21.
+          var pointer = (iso2022jp_lead - 0x21) * 94 + bite - 0x21;
+
+          // 3. Let code point be the index code point for pointer in index jis0208.
+          var code_point = indexCodePointFor(pointer, index('jis0208'));
+
+          // 4. If code point is null, return error.
+          if (code_point === null)
+            return decoderError(fatal);
+
+          // 5. Return a code point whose value is code point.
+          return code_point;
+        }
+
+        // end-of-stream
+        if (bite === end_of_stream) {
+          // Set the iso-2022-jp decoder state to lead byte, prepend
+          // byte to stream, and return error.
+          iso2022jp_decoder_state = states.LeadByte;
+          stream.prepend(bite);
+          return decoderError(fatal);
+        }
+
+        // Otherwise
+        // Set iso-2022-jp decoder state to lead byte and return
+        // error.
+        iso2022jp_decoder_state = states.LeadByte;
+        return decoderError(fatal);
+
+      case states.EscapeStart:
+        // Escape start
+
+        // 1. If byte is either 0x24 or 0x28, set iso-2022-jp lead to
+        // byte, iso-2022-jp decoder state to escape, and return
+        // continue.
+        if (bite === 0x24 || bite === 0x28) {
+          iso2022jp_lead = bite;
+          iso2022jp_decoder_state = states.Escape;
+          return null;
+        }
+
+        // 2. Prepend byte to stream.
+        stream.prepend(bite);
+
+        // 3. Unset the iso-2022-jp output flag, set iso-2022-jp
+        // decoder state to iso-2022-jp decoder output state, and
+        // return error.
+        iso2022jp_output_flag = false;
+        iso2022jp_decoder_state = iso2022jp_decoder_output_state;
+        return decoderError(fatal);
+
+      case states.Escape:
+        // Escape
+
+        // 1. Let lead be iso-2022-jp lead and set iso-2022-jp lead to
+        // 0x00.
+        var lead = iso2022jp_lead;
+        iso2022jp_lead = 0x00;
+
+        // 2. Let state be null.
+        var state = null;
+
+        // 3. If lead is 0x28 and byte is 0x42, set state to ASCII.
+        if (lead === 0x28 && bite === 0x42)
+          state = states.ASCII;
+
+        // 4. If lead is 0x28 and byte is 0x4A, set state to Roman.
+        if (lead === 0x28 && bite === 0x4A)
+          state = states.Roman;
+
+        // 5. If lead is 0x28 and byte is 0x49, set state to Katakana.
+        if (lead === 0x28 && bite === 0x49)
+          state = states.Katakana;
+
+        // 6. If lead is 0x24 and byte is either 0x40 or 0x42, set
+        // state to lead byte.
+        if (lead === 0x24 && (bite === 0x40 || bite === 0x42))
+          state = states.LeadByte;
+
+        // 7. If state is non-null, run these substeps:
+        if (state !== null) {
+          // 1. Set iso-2022-jp decoder state and iso-2022-jp decoder
+          // output state to states.
+          iso2022jp_decoder_state = iso2022jp_decoder_state = state;
+
+          // 2. Let output flag be the iso-2022-jp output flag.
+          var output_flag = iso2022jp_output_flag;
+
+          // 3. Set the iso-2022-jp output flag.
+          iso2022jp_output_flag = true;
+
+          // 4. Return continue, if output flag is unset, and error
+          // otherwise.
+          return !output_flag ? null : decoderError(fatal);
+        }
+
+        // 8. Prepend lead and byte to stream.
+        stream.prepend([lead, bite]);
+
+        // 9. Unset the iso-2022-jp output flag, set iso-2022-jp
+        // decoder state to iso-2022-jp decoder output state and
+        // return error.
+        iso2022jp_output_flag = false;
+        iso2022jp_decoder_state = iso2022jp_decoder_output_state;
+        return decoderError(fatal);
+      }
+    };
+  }
+
+  /**
+   * @constructor
+   * @implements {Encoder}
+   * @param {{fatal: boolean}} options
+   */
+  function ISO2022JPEncoder(options) {
+    var fatal = options.fatal;
+    // iso-2022-jp's encoder has an associated iso-2022-jp encoder
+    // state which is one of ASCII, Roman, and jis0208 (initially
+    // ASCII).
+    /** @enum */
+    var states = {
+      ASCII: 0,
+      Roman: 1,
+      jis0208: 2
+    };
+    var /** @type {number} */ iso2022jp_state = states.ASCII;
+    /**
+     * @param {Stream} stream Input stream.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit.
+     */
+    this.handler = function(stream, code_point) {
+      // 1. If code point is end-of-stream and iso-2022-jp encoder
+      // state is not ASCII, prepend code point to stream, set
+      // iso-2022-jp encoder state to ASCII, and return three bytes
+      // 0x1B 0x28 0x42.
+      if (code_point === end_of_stream &&
+          iso2022jp_state !== states.ASCII) {
+        stream.prepend(code_point);
+        return [0x1B, 0x28, 0x42];
+      }
+
+      // 2. If code point is end-of-stream and iso-2022-jp encoder
+      // state is ASCII, return finished.
+      if (code_point === end_of_stream && iso2022jp_state === states.ASCII)
+        return finished;
+
+      // 3. If iso-2022-jp encoder state is ASCII and code point is in
+      // the range U+0000 to U+007F, return a byte whose value is code
+      // point.
+      if (iso2022jp_state === states.ASCII &&
+          inRange(code_point, 0x0000, 0x007F))
+        return code_point;
+
+      // 4. If iso-2022-jp encoder state is Roman and code point is in
+      // the range U+0000 to U+007F, excluding U+005C and U+007E, or
+      // is U+00A5 or U+203E, run these substeps:
+      if (iso2022jp_state === states.Roman &&
+          inRange(code_point, 0x0000, 0x007F) &&
+          code_point !== 0x005C && code_point !== 0x007E) {
+
+        // 1. If code point is in the range U+0000 to U+007F, return a
+        // byte whose value is code point.
+        if (inRange(code_point, 0x0000, 0x007F))
+          return code_point;
+
+        // 2. If code point is U+00A5, return byte 0x5C.
+        if (code_point === 0x00A5)
+          return 0x5C;
+
+        // 3. If code point is U+203E, return byte 0x7E.
+        if (code_point === 0x203E)
+          return 0x7E;
+      }
+
+      // 5. If code point is in the range U+0000 to U+007F, and
+      // iso-2022-jp encoder state is not ASCII, prepend code point to
+      // stream, set iso-2022-jp encoder state to ASCII, and return
+      // three bytes 0x1B 0x28 0x42.
+      if (inRange(code_point, 0x0000, 0x007F) &&
+          iso2022jp_state !== states.ASCII) {
+        stream.prepend(code_point);
+        iso2022jp_state = states.ASCII;
+        return [0x1B, 0x28, 0x42];
+      }
+
+      // 6. If code point is either U+00A5 or U+203E, and iso-2022-jp
+      // encoder state is not Roman, prepend code point to stream, set
+      // iso-2022-jp encoder state to Roman, and return three bytes
+      // 0x1B 0x28 0x4A.
+      if ((code_point === 0x00A5 || code_point === 0x203E) &&
+          iso2022jp_state !== states.Roman) {
+        stream.prepend(code_point);
+        iso2022jp_state = states.Roman;
+        return [0x1B, 0x28, 0x4A];
+      }
+
+      // 7. If code point is U+2022, set it to U+FF0D.
+      if (code_point === 0x2022)
+        code_point = 0xFF0D;
+
+      // 8. Let pointer be the index pointer for code point in index
+      // jis0208.
+      var pointer = indexPointerFor(code_point, index('jis0208'));
+
+      // 9. If pointer is null, return error with code point.
+      if (pointer === null)
+        return encoderError(code_point);
+
+      // 10. If iso-2022-jp encoder state is not jis0208, prepend code
+      // point to stream, set iso-2022-jp encoder state to jis0208,
+      // and return three bytes 0x1B 0x24 0x42.
+      if (iso2022jp_state !== states.jis0208) {
+        stream.prepend(code_point);
+        iso2022jp_state = states.jis0208;
+        return [0x1B, 0x24, 0x42];
+      }
+
+      // 11. Let lead be pointer / 94 + 0x21.
+      var lead = div(pointer, 94) + 0x21;
+
+      // 12. Let trail be pointer % 94 + 0x21.
+      var trail = pointer % 94 + 0x21;
+
+      // 13. Return two bytes whose values are lead and trail.
+      return [lead, trail];
+    };
+  }
+
+  /** @param {{fatal: boolean}} options */
+  encoders['iso-2022-jp'] = function(options) {
+    return new ISO2022JPEncoder(options);
+  };
+  /** @param {{fatal: boolean}} options */
+  decoders['iso-2022-jp'] = function(options) {
+    return new ISO2022JPDecoder(options);
+  };
+
+  // 12.3 shift_jis
+
+  /**
+   * @constructor
+   * @implements {Decoder}
+   * @param {{fatal: boolean}} options
+   */
+  function ShiftJISDecoder(options) {
+    var fatal = options.fatal;
+    // shift_jis's decoder has an associated shift_jis lead (initially
+    // 0x00).
+    var /** @type {number} */ shiftjis_lead = 0x00;
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point.
+     */
+    this.handler = function(stream, bite) {
+      // 1. If byte is end-of-stream and shift_jis lead is not 0x00,
+      // set shift_jis lead to 0x00 and return error.
+      if (bite === end_of_stream && shiftjis_lead !== 0x00) {
+        shiftjis_lead = 0x00;
+        return decoderError(fatal);
+      }
+
+      // 2. If byte is end-of-stream and shift_jis lead is 0x00,
+      // return finished.
+      if (bite === end_of_stream && shiftjis_lead === 0x00)
+        return finished;
+
+      // 3. If shift_jis lead is not 0x00, let lead be shift_jis lead,
+      // let pointer be null, set shift_jis lead to 0x00, and then run
+      // these substeps:
+      if (shiftjis_lead !== 0x00) {
+        var lead = shiftjis_lead;
+        var pointer = null;
+        shiftjis_lead = 0x00;
+
+        // 1. Let offset be 0x40, if byte is less than 0x7F, and 0x41
+        // otherwise.
+        var offset = (bite < 0x7F) ? 0x40 : 0x41;
+
+        // 2. Let lead offset be 0x81, if lead is less than 0xA0, and
+        // 0xC1 otherwise.
+        var lead_offset = (lead < 0xA0) ? 0x81 : 0xC1;
+
+        // 3. If byte is in the range 0x40 to 0x7E or 0x80 to 0xFC,
+        // set pointer to (lead − lead offset) × 188 + byte − offset.
+        if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFC))
+          pointer = (lead - lead_offset) * 188 + bite - offset;
+
+        // 4. Let code point be null, if pointer is null, and the
+        // index code point for pointer in index jis0208 otherwise.
+        var code_point = (pointer === null) ? null :
+              indexCodePointFor(pointer, index('jis0208'));
+
+        // 5. If code point is null and pointer is in the range 8836
+        // to 10528, return a code point whose value is 0xE000 +
+        // pointer − 8836.
+        if (code_point === null && pointer !== null &&
+            inRange(pointer, 8836, 10528))
+          return 0xE000 + pointer - 8836;
+
+        // 6. If code point is null and byte is in the range 0x00 to
+        // 0x7F, prepend byte to stream.
+        if (code_point === null && inRange(bite, 0x00, 0x7F))
+          stream.prepend(bite);
+
+        // 7. If code point is null, return error.
+        if (code_point === null)
+          return decoderError(fatal);
+
+        // 8. Return a code point whose value is code point.
+        return code_point;
+      }
+
+      // 4. If byte is in the range 0x00 to 0x80, return a code point
+      // whose value is byte.
+      if (inRange(bite, 0x00, 0x80))
+        return bite;
+
+      // 5. If byte is in the range 0xA1 to 0xDF, return a code point
+      // whose value is 0xFF61 + byte − 0xA1.
+      if (inRange(bite, 0xA1, 0xDF))
+        return 0xFF61 + bite - 0xA1;
+
+      // 6. If byte is in the range 0x81 to 0x9F or 0xE0 to 0xFC, set
+      // shift_jis lead to byte and return continue.
+      if (inRange(bite, 0x81, 0x9F) || inRange(bite, 0xE0, 0xFC)) {
+        shiftjis_lead = bite;
+        return null;
+      }
+
+      // 7. Return error.
+      return decoderError(fatal);
+    };
+  }
+
+  /**
+   * @constructor
+   * @implements {Encoder}
+   * @param {{fatal: boolean}} options
+   */
+  function ShiftJISEncoder(options) {
+    var fatal = options.fatal;
+    /**
+     * @param {Stream} stream Input stream.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit.
+     */
+    this.handler = function(stream, code_point) {
+      // 1. If code point is end-of-stream, return finished.
+      if (code_point === end_of_stream)
+        return finished;
+
+      // 2. If code point is in the range U+0000 to U+0080, return a
+      // byte whose value is code point.
+      if (inRange(code_point, 0x0000, 0x0080))
+        return code_point;
+
+      // 3. If code point is U+00A5, return byte 0x5C.
+      if (code_point === 0x00A5)
+        return 0x5C;
+
+      // 4. If code point is U+203E, return byte 0x7E.
+      if (code_point === 0x203E)
+        return 0x7E;
+
+      // 5. If code point is in the range U+FF61 to U+FF9F, return a
+      // byte whose value is code point − 0xFF61 + 0xA1.
+      if (inRange(code_point, 0xFF61, 0xFF9F))
+        return code_point - 0xFF61 + 0xA1;
+
+      // 6. If code point is U+2022, set it to U+FF0D.
+      if (code_point === 0x2022)
+        code_point = 0xFF0D;
+
+      // 7. Let pointer be the index shift_jis pointer for code point.
+      var pointer = indexShiftJISPointerFor(code_point);
+
+      // 8. If pointer is null, return error with code point.
+      if (pointer === null)
+        return encoderError(code_point);
+
+      // 9. Let lead be pointer / 188.
+      var lead = div(pointer, 188);
+
+      // 10. Let lead offset be 0x81, if lead is less than 0x1F, and
+      // 0xC1 otherwise.
+      var lead_offset = (lead < 0x1F) ? 0x81 : 0xC1;
+
+      // 11. Let trail be pointer % 188.
+      var trail = pointer % 188;
+
+      // 12. Let offset be 0x40, if trail is less than 0x3F, and 0x41
+      // otherwise.
+      var offset = (trail < 0x3F) ? 0x40 : 0x41;
+
+      // 13. Return two bytes whose values are lead + lead offset and
+      // trail + offset.
+      return [lead + lead_offset, trail + offset];
+    };
+  }
+
+  /** @param {{fatal: boolean}} options */
+  encoders['shift_jis'] = function(options) {
+    return new ShiftJISEncoder(options);
+  };
+  /** @param {{fatal: boolean}} options */
+  decoders['shift_jis'] = function(options) {
+    return new ShiftJISDecoder(options);
+  };
+
+  //
+  // 13. Legacy multi-byte Korean encodings
+  //
+
+  // 13.1 euc-kr
+
+  /**
+   * @constructor
+   * @implements {Decoder}
+   * @param {{fatal: boolean}} options
+   */
+  function EUCKRDecoder(options) {
+    var fatal = options.fatal;
+
+    // euc-kr's decoder has an associated euc-kr lead (initially 0x00).
+    var /** @type {number} */ euckr_lead = 0x00;
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point.
+     */
+    this.handler = function(stream, bite) {
+      // 1. If byte is end-of-stream and euc-kr lead is not 0x00, set
+      // euc-kr lead to 0x00 and return error.
+      if (bite === end_of_stream && euckr_lead !== 0) {
+        euckr_lead = 0x00;
+        return decoderError(fatal);
+      }
+
+      // 2. If byte is end-of-stream and euc-kr lead is 0x00, return
+      // finished.
+      if (bite === end_of_stream && euckr_lead === 0)
+        return finished;
+
+      // 3. If euc-kr lead is not 0x00, let lead be euc-kr lead, let
+      // pointer be null, set euc-kr lead to 0x00, and then run these
+      // substeps:
+      if (euckr_lead !== 0x00) {
+        var lead = euckr_lead;
+        var pointer = null;
+        euckr_lead = 0x00;
+
+        // 1. If byte is in the range 0x41 to 0xFE, set pointer to
+        // (lead − 0x81) × 190 + (byte − 0x41).
+        if (inRange(bite, 0x41, 0xFE))
+          pointer = (lead - 0x81) * 190 + (bite - 0x41);
+
+        // 2. Let code point be null, if pointer is null, and the
+        // index code point for pointer in index euc-kr otherwise.
+        var code_point = (pointer === null) ? null : indexCodePointFor(pointer, index('euc-kr'));
+
+        // 3. If code point is null and byte is in the range 0x00 to
+        // 0x7F, prepend byte to stream.
+        if (pointer === null && inRange(bite, 0x00, 0x7F))
+          stream.prepend(bite);
+
+        // 4. If code point is null, return error.
+        if (code_point === null)
+          return decoderError(fatal);
+
+        // 5. Return a code point whose value is code point.
+        return code_point;
+      }
+
+      // 4. If byte is in the range 0x00 to 0x7F, return a code point
+      // whose value is byte.
+      if (inRange(bite, 0x00, 0x7F))
+        return bite;
+
+      // 5. If byte is in the range 0x81 to 0xFE, set euc-kr lead to
+      // byte and return continue.
+      if (inRange(bite, 0x81, 0xFE)) {
+        euckr_lead = bite;
+        return null;
+      }
+
+      // 6. Return error.
+      return decoderError(fatal);
+    };
+  }
+
+  /**
+   * @constructor
+   * @implements {Encoder}
+   * @param {{fatal: boolean}} options
+   */
+  function EUCKREncoder(options) {
+    var fatal = options.fatal;
+    /**
+     * @param {Stream} stream Input stream.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit.
+     */
+    this.handler = function(stream, code_point) {
+      // 1. If code point is end-of-stream, return finished.
+      if (code_point === end_of_stream)
+        return finished;
+
+      // 2. If code point is in the range U+0000 to U+007F, return a
+      // byte whose value is code point.
+      if (inRange(code_point, 0x0000, 0x007F))
+        return code_point;
+
+      // 3. Let pointer be the index pointer for code point in index
+      // euc-kr.
+      var pointer = indexPointerFor(code_point, index('euc-kr'));
+
+      // 4. If pointer is null, return error with code point.
+      if (pointer === null)
+        return encoderError(code_point);
+
+      // 5. Let lead be pointer / 190 + 0x81.
+      var lead = div(pointer, 190) + 0x81;
+
+      // 6. Let trail be pointer % 190 + 0x41.
+      var trail = (pointer % 190) + 0x41;
+
+      // 7. Return two bytes whose values are lead and trail.
+      return [lead, trail];
+    };
+  }
+
+  /** @param {{fatal: boolean}} options */
+  encoders['euc-kr'] = function(options) {
+    return new EUCKREncoder(options);
+  };
+  /** @param {{fatal: boolean}} options */
+  decoders['euc-kr'] = function(options) {
+    return new EUCKRDecoder(options);
+  };
+
+
+  //
+  // 14. Legacy miscellaneous encodings
+  //
+
+  // 14.1 replacement
+
+  // Not needed - API throws RangeError
+
+  // 14.2 utf-16
+
+  /**
+   * @param {number} code_unit
+   * @param {boolean} utf16be
+   * @return {!Array.<number>} bytes
+   */
+  function convertCodeUnitToBytes(code_unit, utf16be) {
+    // 1. Let byte1 be code unit >> 8.
+    var byte1 = code_unit >> 8;
+
+    // 2. Let byte2 be code unit & 0x00FF.
+    var byte2 = code_unit & 0x00FF;
+
+    // 3. Then return the bytes in order:
+        // utf-16be flag is set: byte1, then byte2.
+    if (utf16be)
+      return [byte1, byte2];
+    // utf-16be flag is unset: byte2, then byte1.
+    return [byte2, byte1];
+  }
+
+  /**
+   * @constructor
+   * @implements {Decoder}
+   * @param {boolean} utf16_be True if big-endian, false if little-endian.
+   * @param {{fatal: boolean}} options
+   */
+  function UTF16Decoder(utf16_be, options) {
+    var fatal = options.fatal;
+    var /** @type {?number} */ utf16_lead_byte = null,
+        /** @type {?number} */ utf16_lead_surrogate = null;
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point.
+     */
+    this.handler = function(stream, bite) {
+      // 1. If byte is end-of-stream and either utf-16 lead byte or
+      // utf-16 lead surrogate is not null, set utf-16 lead byte and
+      // utf-16 lead surrogate to null, and return error.
+      if (bite === end_of_stream && (utf16_lead_byte !== null ||
+                                utf16_lead_surrogate !== null)) {
+        return decoderError(fatal);
+      }
+
+      // 2. If byte is end-of-stream and utf-16 lead byte and utf-16
+      // lead surrogate are null, return finished.
+      if (bite === end_of_stream && utf16_lead_byte === null &&
+          utf16_lead_surrogate === null) {
+        return finished;
+      }
+
+      // 3. If utf-16 lead byte is null, set utf-16 lead byte to byte
+      // and return continue.
+      if (utf16_lead_byte === null) {
+        utf16_lead_byte = bite;
+        return null;
+      }
+
+      // 4. Let code unit be the result of:
+      var code_unit;
+      if (utf16_be) {
+        // utf-16be decoder flag is set
+        //   (utf-16 lead byte << 8) + byte.
+        code_unit = (utf16_lead_byte << 8) + bite;
+      } else {
+        // utf-16be decoder flag is unset
+        //   (byte << 8) + utf-16 lead byte.
+        code_unit = (bite << 8) + utf16_lead_byte;
+      }
+      // Then set utf-16 lead byte to null.
+      utf16_lead_byte = null;
+
+      // 5. If utf-16 lead surrogate is not null, let lead surrogate
+      // be utf-16 lead surrogate, set utf-16 lead surrogate to null,
+      // and then run these substeps:
+      if (utf16_lead_surrogate !== null) {
+        var lead_surrogate = utf16_lead_surrogate;
+        utf16_lead_surrogate = null;
+
+        // 1. If code unit is in the range U+DC00 to U+DFFF, return a
+        // code point whose value is 0x10000 + ((lead surrogate −
+        // 0xD800) << 10) + (code unit − 0xDC00).
+        if (inRange(code_unit, 0xDC00, 0xDFFF)) {
+          return 0x10000 + (lead_surrogate - 0xD800) * 0x400 +
+              (code_unit - 0xDC00);
+        }
+
+        // 2. Prepend the sequence resulting of converting code unit
+        // to bytes using utf-16be decoder flag to stream and return
+        // error.
+        stream.prepend(convertCodeUnitToBytes(code_unit, utf16_be));
+        return decoderError(fatal);
+      }
+
+      // 6. If code unit is in the range U+D800 to U+DBFF, set utf-16
+      // lead surrogate to code unit and return continue.
+      if (inRange(code_unit, 0xD800, 0xDBFF)) {
+        utf16_lead_surrogate = code_unit;
+        return null;
+      }
+
+      // 7. If code unit is in the range U+DC00 to U+DFFF, return
+      // error.
+      if (inRange(code_unit, 0xDC00, 0xDFFF))
+        return decoderError(fatal);
+
+      // 8. Return code point code unit.
+      return code_unit;
+    };
+  }
+
+  /**
+   * @constructor
+   * @implements {Encoder}
+   * @param {boolean} utf16_be True if big-endian, false if little-endian.
+   * @param {{fatal: boolean}} options
+   */
+  function UTF16Encoder(utf16_be, options) {
+    var fatal = options.fatal;
+    /**
+     * @param {Stream} stream Input stream.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit.
+     */
+    this.handler = function(stream, code_point) {
+      // 1. If code point is end-of-stream, return finished.
+      if (code_point === end_of_stream)
+        return finished;
+
+      // 2. If code point is in the range U+0000 to U+FFFF, return the
+      // sequence resulting of converting code point to bytes using
+      // utf-16be encoder flag.
+      if (inRange(code_point, 0x0000, 0xFFFF))
+        return convertCodeUnitToBytes(code_point, utf16_be);
+
+      // 3. Let lead be ((code point − 0x10000) >> 10) + 0xD800,
+      // converted to bytes using utf-16be encoder flag.
+      var lead = convertCodeUnitToBytes(
+        ((code_point - 0x10000) >> 10) + 0xD800, utf16_be);
+
+      // 4. Let trail be ((code point − 0x10000) & 0x3FF) + 0xDC00,
+      // converted to bytes using utf-16be encoder flag.
+      var trail = convertCodeUnitToBytes(
+        ((code_point - 0x10000) & 0x3FF) + 0xDC00, utf16_be);
+
+      // 5. Return a byte sequence of lead followed by trail.
+      return lead.concat(trail);
+    };
+  }
+
+  // 14.3 utf-16be
+  /** @param {{fatal: boolean}} options */
+  encoders['utf-16be'] = function(options) {
+    return new UTF16Encoder(true, options);
+  };
+  /** @param {{fatal: boolean}} options */
+  decoders['utf-16be'] = function(options) {
+    return new UTF16Decoder(true, options);
+  };
+
+  // 14.4 utf-16le
+  /** @param {{fatal: boolean}} options */
+  encoders['utf-16le'] = function(options) {
+    return new UTF16Encoder(false, options);
+  };
+  /** @param {{fatal: boolean}} options */
+  decoders['utf-16le'] = function(options) {
+    return new UTF16Decoder(false, options);
+  };
+
+  // 14.5 x-user-defined
+
+  /**
+   * @constructor
+   * @implements {Decoder}
+   * @param {{fatal: boolean}} options
+   */
+  function XUserDefinedDecoder(options) {
+    var fatal = options.fatal;
+    /**
+     * @param {Stream} stream The stream of bytes being decoded.
+     * @param {number} bite The next byte read from the stream.
+     * @return {?(number|!Array.<number>)} The next code point(s)
+     *     decoded, or null if not enough data exists in the input
+     *     stream to decode a complete code point.
+     */
+    this.handler = function(stream, bite) {
+      // 1. If byte is end-of-stream, return finished.
+      if (bite === end_of_stream)
+        return finished;
+
+      // 2. If byte is in the range 0x00 to 0x7F, return a code point
+      // whose value is byte.
+      if (inRange(bite, 0x00, 0x7F))
+        return bite;
+
+      // 3. Return a code point whose value is 0xF780 + byte − 0x80.
+      return 0xF780 + bite - 0x80;
+    };
+  }
+
+  /**
+   * @constructor
+   * @implements {Encoder}
+   * @param {{fatal: boolean}} options
+   */
+  function XUserDefinedEncoder(options) {
+    var fatal = options.fatal;
+    /**
+     * @param {Stream} stream Input stream.
+     * @param {number} code_point Next code point read from the stream.
+     * @return {(number|!Array.<number>)} Byte(s) to emit.
+     */
+    this.handler = function(stream, code_point) {
+      // 1.If code point is end-of-stream, return finished.
+      if (code_point === end_of_stream)
+        return finished;
+
+      // 2. If code point is in the range U+0000 to U+007F, return a
+      // byte whose value is code point.
+      if (inRange(code_point, 0x0000, 0x007F))
+        return code_point;
+
+      // 3. If code point is in the range U+F780 to U+F7FF, return a
+      // byte whose value is code point − 0xF780 + 0x80.
+      if (inRange(code_point, 0xF780, 0xF7FF))
+        return code_point - 0xF780 + 0x80;
+
+      // 4. Return error with code point.
+      return encoderError(code_point);
+    };
+  }
+
+  /** @param {{fatal: boolean}} options */
+  encoders['x-user-defined'] = function(options) {
+    return new XUserDefinedEncoder(options);
+  };
+  /** @param {{fatal: boolean}} options */
+  decoders['x-user-defined'] = function(options) {
+    return new XUserDefinedDecoder(options);
+  };
+
+  if (!('TextEncoder' in global))
+    global['TextEncoder'] = TextEncoder;
+  if (!('TextDecoder' in global))
+    global['TextDecoder'] = TextDecoder;
+}(this));
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..add3a6f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+  "name": "text-encoding",
+  "author": "Joshua Bell <inexorabletash at gmail.com>",
+  "contributors": [
+    "Rick Eyre <rick.eyre at outlook.com>"
+  ],
+  "version": "0.5.3",
+  "description": "Polyfill for the Encoding Living Standard's API.",
+  "main": "index.js",
+  "files": [
+    "index.js",
+    "lib/encoding.js",
+    "lib/encoding-indexes.js"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/inexorabletash/text-encoding.git"
+  },
+  "keywords": [
+    "encoding",
+    "decoding",
+    "living standard"
+  ],
+  "bugs": {
+    "url": "https://github.com/inexorabletash/text-encoding/issues"
+  },
+  "homepage": "https://github.com/inexorabletash/text-encoding"
+}
diff --git a/test/test-big5.js b/test/test-big5.js
new file mode 100644
index 0000000..8197c05
--- /dev/null
+++ b/test/test-big5.js
@@ -0,0 +1,22 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+test(
+  function () {
+    var bytes = [161,64,161,65,161,66,161,67,161,68,161,70,161,71,161,72,161,73,161,74,161,75,161,76,161,77,161,79,161,80,161,81,161,82,161,83,161,84,161,85,161,86,161,87,161,88,161,89,161,90,161,91,161,92,161,93,161,94,161,95,161,96,161,97,161,98,161,99,161,100,161,101,161,102,161,103,161,104,161,105,161,106,161,107,161,108,161,109,161,110,161,111,161,112,161,113,161,114,161,115,161,116,161,117,161,118,161,119,161,120,161,121,161,122,161,123,161,124,161,125,161,126,161,161,161,162,161,1 [...]
+    var string = "\u3000\uFF0C\u3001\u3002\uFF0E\uFF1B\uFF1A\uFF1F\uFF01\uFE30\u2026\u2025\uFE50\uFE52\u00B7\uFE54\uFE55\uFE56\uFE57\uFF5C\u2013\uFE31\u2014\uFE33\u2574\uFE34\uFE4F\uFF08\uFF09\uFE35\uFE36\uFF5B\uFF5D\uFE37\uFE38\u3014\u3015\uFE39\uFE3A\u3010\u3011\uFE3B\uFE3C\u300A\u300B\uFE3D\uFE3E\u3008\u3009\uFE3F\uFE40\u300C\u300D\uFE41\uFE42\u300E\u300F\uFE43\uFE44\uFE59\uFE5A\uFE5B\uFE5C\uFE5D\uFE5E\u2018\u2019\u201C\u201D\u301D\u301E\u2035\u2032\uFF03\uFF06\uFF0A\u203B\u00A7\u3003 [...]
+    assert_equals(new TextDecoder("big5").decode(new Uint8Array(bytes)), string, "decoded");
+  },
+  "big5"
+);
diff --git a/test/test-euc-jp.js b/test/test-euc-jp.js
new file mode 100644
index 0000000..3822a28
--- /dev/null
+++ b/test/test-euc-jp.js
@@ -0,0 +1,22 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+test(
+  function () {
+    var bytes = [161,161,161,162,161,163,161,164,161,165,161,166,161,167,161,168,161,169,161,170,161,171,161,172,161,173,161,174,161,175,161,176,161,177,161,178,161,179,161,180,161,181,161,182,161,183,161,184,161,185,161,186,161,187,161,188,161,189,161,190,161,191,161,192,161,195,161,196,161,197,161,198,161,199,161,200,161,201,161,202,161,203,161,204,161,205,161,206,161,207,161,208,161,209,161,210,161,211,161,212,161,213,161,214,161,215,161,216,161,217,161,218,161,219,161,220,161,222,161 [...]
+    var string = "\u3000\u3001\u3002\uFF0C\uFF0E\u30FB\uFF1A\uFF1B\uFF1F\uFF01\u309B\u309C\u00B4\uFF40\u00A8\uFF3E\uFFE3\uFF3F\u30FD\u30FE\u309D\u309E\u3003\u4EDD\u3005\u3006\u3007\u30FC\u2015\u2010\uFF0F\uFF3C\uFF5C\u2026\u2025\u2018\u2019\u201C\u201D\uFF08\uFF09\u3014\u3015\uFF3B\uFF3D\uFF5B\uFF5D\u3008\u3009\u300A\u300B\u300C\u300D\u300E\u300F\u3010\u3011\uFF0B\u00B1\u00D7\u00F7\uFF1D\u2260\uFF1C\uFF1E\u2266\u2267\u221E\u2234\u2642\u2640\u00B0\u2032\u2033\u2103\uFFE5\uFF04\uFF05\uFF03 [...]
+    assert_equals(new TextDecoder("euc-jp").decode(new Uint8Array(bytes)), string, "decoded");
+  },
+  "euc-jp"
+);
diff --git a/test/test-euc-kr.js b/test/test-euc-kr.js
new file mode 100644
index 0000000..7751946
--- /dev/null
+++ b/test/test-euc-kr.js
@@ -0,0 +1,22 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+test(
+  function () {
+    var bytes = [161,161,161,162,161,163,161,164,161,165,161,166,161,167,161,168,161,169,161,170,161,171,161,172,161,173,161,174,161,175,161,176,161,177,161,178,161,179,161,180,161,181,161,182,161,183,161,184,161,185,161,186,161,187,161,188,161,189,161,190,161,191,161,192,161,193,161,194,161,195,161,196,161,197,161,198,161,199,161,200,161,201,161,202,161,203,161,204,161,205,161,206,161,207,161,208,161,209,161,210,161,211,161,212,161,213,161,214,161,215,161,216,161,217,161,218,161,219,161 [...]
+    var string = "\u3000\u3001\u3002\u00B7\u2025\u2026\u00A8\u3003\u00AD\u2015\u2225\uFF3C\u223C\u2018\u2019\u201C\u201D\u3014\u3015\u3008\u3009\u300A\u300B\u300C\u300D\u300E\u300F\u3010\u3011\u00B1\u00D7\u00F7\u2260\u2264\u2265\u221E\u2234\u00B0\u2032\u2033\u2103\u212B\uFFE0\uFFE1\uFFE5\u2642\u2640\u2220\u22A5\u2312\u2202\u2207\u2261\u2252\u00A7\u203B\u2606\u2605\u25CB\u25CF\u25CE\u25C7\u25C6\u25A1\u25A0\u25B3\u25B2\u25BD\u25BC\u2192\u2190\u2191\u2193\u2194\u3013\u226A\u226B\u221A\u223D [...]
+    assert_equals(new TextDecoder("euc-kr").decode(new Uint8Array(bytes)), string, "decoded");
+  },
+  "euc-kr"
+);
diff --git a/test/test-gbk.js b/test/test-gbk.js
new file mode 100644
index 0000000..0b09214
--- /dev/null
+++ b/test/test-gbk.js
@@ -0,0 +1,22 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+test(
+  function () {
+    var bytes = [129,64,129,65,129,66,129,67,129,68,129,69,129,70,129,71,129,72,129,73,129,74,129,75,129,76,129,77,129,78,129,79,129,80,129,81,129,82,129,83,129,84,129,85,129,86,129,87,129,88,129,89,129,90,129,91,129,92,129,93,129,94,129,95,129,96,129,97,129,98,129,99,129,100,129,101,129,102,129,103,129,104,129,105,129,106,129,107,129,108,129,109,129,110,129,111,129,112,129,113,129,114,129,115,129,116,129,117,129,118,129,119,129,120,129,121,129,122,129,123,129,124,129,125,129,126,129,128 [...]
+    var string = "\u4E02\u4E04\u4E05\u4E06\u4E0F\u4E12\u4E17\u4E1F\u4E20\u4E21\u4E23\u4E26\u4E29\u4E2E\u4E2F\u4E31\u4E33\u4E35\u4E37\u4E3C\u4E40\u4E41\u4E42\u4E44\u4E46\u4E4A\u4E51\u4E55\u4E57\u4E5A\u4E5B\u4E62\u4E63\u4E64\u4E65\u4E67\u4E68\u4E6A\u4E6B\u4E6C\u4E6D\u4E6E\u4E6F\u4E72\u4E74\u4E75\u4E76\u4E77\u4E78\u4E79\u4E7A\u4E7B\u4E7C\u4E7D\u4E7F\u4E80\u4E81\u4E82\u4E83\u4E84\u4E85\u4E87\u4E8A\u4E90\u4E96\u4E97\u4E99\u4E9C\u4E9D\u4E9E\u4EA3\u4EAA\u4EAF\u4EB0\u4EB1\u4EB4\u4EB6\u4EB7\u4EB8 [...]
+    assert_equals(new TextDecoder("gbk").decode(new Uint8Array(bytes)), string, "decoded");
+  },
+  "gbk"
+);
diff --git a/test/test-iso-2022-jp.js b/test/test-iso-2022-jp.js
new file mode 100644
index 0000000..a84221c
--- /dev/null
+++ b/test/test-iso-2022-jp.js
@@ -0,0 +1,22 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+test(
+  function () {
+    var bytes = [27,36,66,33,33,33,34,33,35,33,36,33,37,33,38,33,39,33,40,33,41,33,42,33,43,33,44,33,45,33,46,33,47,33,48,33,49,33,50,33,51,33,52,33,53,33,54,33,55,33,56,33,57,33,58,33,59,33,60,33,61,33,62,33,63,33,64,33,67,33,68,33,69,33,70,33,71,33,72,33,73,33,74,33,75,33,76,33,77,33,78,33,79,33,80,33,81,33,82,33,83,33,84,33,85,33,86,33,87,33,88,33,89,33,90,33,91,33,92,33,94,33,95,33,96,33,97,33,98,33,99,33,100,33,101,33,102,33,103,33,104,33,105,33,106,33,107,33,108,33,109,33,110,33,11 [...]
+    var string = "\u3000\u3001\u3002\uFF0C\uFF0E\u30FB\uFF1A\uFF1B\uFF1F\uFF01\u309B\u309C\u00B4\uFF40\u00A8\uFF3E\uFFE3\uFF3F\u30FD\u30FE\u309D\u309E\u3003\u4EDD\u3005\u3006\u3007\u30FC\u2015\u2010\uFF0F\uFF3C\uFF5C\u2026\u2025\u2018\u2019\u201C\u201D\uFF08\uFF09\u3014\u3015\uFF3B\uFF3D\uFF5B\uFF5D\u3008\u3009\u300A\u300B\u300C\u300D\u300E\u300F\u3010\u3011\uFF0B\u00B1\u00D7\u00F7\uFF1D\u2260\uFF1C\uFF1E\u2266\u2267\u221E\u2234\u2642\u2640\u00B0\u2032\u2033\u2103\uFFE5\uFF04\uFF05\uFF03 [...]
+    assert_equals(new TextDecoder("iso-2022-jp").decode(new Uint8Array(bytes)), string, "decoded");
+  },
+  "iso-2022-jp"
+);
diff --git a/test/test-misc.js b/test/test-misc.js
new file mode 100644
index 0000000..d161be2
--- /dev/null
+++ b/test/test-misc.js
@@ -0,0 +1,321 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+// NOTE: Requires testharness.js
+// http://www.w3.org/2008/webapps/wiki/Harness
+
+var UTF_ENCODINGS = ['utf-8', 'utf-16le', 'utf-16be'];
+
+var LEGACY_ENCODINGS = [
+  'ibm866', 'iso-8859-2', 'iso-8859-3', 'iso-8859-4', 'iso-8859-5',
+  'iso-8859-6', 'iso-8859-7', 'iso-8859-8', 'iso-8859-10',
+  'iso-8859-13', 'iso-8859-14', 'iso-8859-15', 'iso-8859-16', 'koi8-r',
+  'koi8-u', 'macintosh', 'windows-874', 'windows-1250', 'windows-1251',
+  'windows-1252', 'windows-1253', 'windows-1254', 'windows-1255',
+  'windows-1256', 'windows-1257', 'windows-1258', 'x-mac-cyrillic',
+  'gbk', 'gb18030', 'big5', 'euc-jp', 'iso-2022-jp', 'shift_jis',
+  'euc-kr'
+];
+
+// Miscellaneous tests
+
+test(function() {
+  assert_false(/\[native code\]/.test(String(TextDecoder)),
+               'Native implementation present - polyfill not tested.');
+}, 'TextDecoder Polyfill (will fail if natively supported)');
+
+test(function() {
+  assert_false(/\[native code\]/.test(String(TextEncoder)),
+               'Native implementation present - polyfill not tested.');
+}, 'TextEncoder Polyfill (will fail if natively supported)');
+
+test(function() {
+  assert_true('encoding' in new TextEncoder());
+  assert_equals(new TextEncoder().encoding, 'utf-8');
+  assert_equals(new TextEncoder('utf-16le').encoding, 'utf-16le');
+
+  assert_true('encoding' in new TextDecoder());
+  assert_equals(new TextDecoder().encoding, 'utf-8');
+  assert_equals(new TextDecoder('utf-16le').encoding, 'utf-16le');
+  assert_true('fatal' in new TextDecoder());
+  assert_false(new TextDecoder('utf-8').fatal);
+  assert_true(new TextDecoder('utf-8', {fatal: true}).fatal);
+  assert_true('ignoreBOM' in new TextDecoder());
+  assert_false(new TextDecoder('utf-8').ignoreBOM);
+  assert_true(new TextDecoder('utf-8', {ignoreBOM: true}).ignoreBOM);
+}, 'Attributes');
+
+test(function() {
+  var badStrings = [
+    { input: '\ud800', expected: '\ufffd' }, // Surrogate half
+    { input: '\udc00', expected: '\ufffd' }, // Surrogate half
+    { input: 'abc\ud800def', expected: 'abc\ufffddef' }, // Surrogate half
+    { input: 'abc\udc00def', expected: 'abc\ufffddef' }, // Surrogate half
+    { input: '\udc00\ud800', expected: '\ufffd\ufffd' } // Wrong order
+  ];
+
+  badStrings.forEach(
+    function(t) {
+      var encoded = new TextEncoder('utf-8').encode(t.input);
+      var decoded = new TextDecoder('utf-8').decode(encoded);
+      assert_equals(t.expected, decoded);
+    });
+}, 'bad data');
+
+test(function() {
+  var bad = [
+    { encoding: 'utf-8', input: [0xC0] }, // ends early
+    { encoding: 'utf-8', input: [0xC0, 0x00] }, // invalid trail
+    { encoding: 'utf-8', input: [0xC0, 0xC0] }, // invalid trail
+    { encoding: 'utf-8', input: [0xE0] }, // ends early
+    { encoding: 'utf-8', input: [0xE0, 0x00] }, // invalid trail
+    { encoding: 'utf-8', input: [0xE0, 0xC0] }, // invalid trail
+    { encoding: 'utf-8', input: [0xE0, 0x80, 0x00] }, // invalid trail
+    { encoding: 'utf-8', input: [0xE0, 0x80, 0xC0] }, // invalid trail
+    { encoding: 'utf-8', input: [0xFC, 0x80, 0x80, 0x80, 0x80, 0x80] }, // > 0x10FFFF
+    { encoding: 'utf-16le', input: [0x00] }, // truncated code unit
+    { encoding: 'utf-16le', input: [0x00, 0xd8] }, // surrogate half
+    { encoding: 'utf-16le', input: [0x00, 0xd8, 0x00, 0x00] }, // surrogate half
+    { encoding: 'utf-16le', input: [0x00, 0xdc, 0x00, 0x00] }, // trail surrogate
+    { encoding: 'utf-16le', input: [0x00, 0xdc, 0x00, 0xd8] }  // swapped surrogates
+    // TODO: Single byte encoding cases
+  ];
+
+  bad.forEach(
+    function(t) {
+      assert_throws({name: 'TypeError'}, function() {
+        new TextDecoder(t.encoding, {fatal: true}).decode(new Uint8Array(t.input));
+      });
+    });
+}, 'fatal flag');
+
+test(function() {
+  var encodings = [
+    { label: 'utf-8', encoding: 'utf-8' },
+    { label: 'utf-16', encoding: 'utf-16le' },
+    { label: 'utf-16le', encoding: 'utf-16le' },
+    { label: 'utf-16be', encoding: 'utf-16be' },
+    { label: 'ascii', encoding: 'windows-1252' },
+    { label: 'iso-8859-1', encoding: 'windows-1252' }
+  ];
+
+  encodings.forEach(
+    function(test) {
+      assert_equals(new TextDecoder(test.label.toLowerCase()).encoding, test.encoding);
+      assert_equals(new TextDecoder(test.label.toUpperCase()).encoding, test.encoding);
+    });
+}, 'Encoding names are case insensitive');
+
+test(function() {
+  var utf8_bom = [0xEF, 0xBB, 0xBF];
+  var utf8 = [0x7A, 0xC2, 0xA2, 0xE6, 0xB0, 0xB4, 0xF0, 0x9D, 0x84, 0x9E, 0xF4, 0x8F, 0xBF, 0xBD];
+
+  var utf16le_bom = [0xff, 0xfe];
+  var utf16le = [0x7A, 0x00, 0xA2, 0x00, 0x34, 0x6C, 0x34, 0xD8, 0x1E, 0xDD, 0xFF, 0xDB, 0xFD, 0xDF];
+
+  var utf16be_bom = [0xfe, 0xff];
+  var utf16be = [0x00, 0x7A, 0x00, 0xA2, 0x6C, 0x34, 0xD8, 0x34, 0xDD, 0x1E, 0xDB, 0xFF, 0xDF, 0xFD];
+
+  var string = 'z\xA2\u6C34\uD834\uDD1E\uDBFF\uDFFD'; // z, cent, CJK water, G-Clef, Private-use character
+
+  // missing BOMs
+  assert_equals(new TextDecoder('utf-8').decode(new Uint8Array(utf8)), string);
+  assert_equals(new TextDecoder('utf-16le').decode(new Uint8Array(utf16le)), string);
+  assert_equals(new TextDecoder('utf-16be').decode(new Uint8Array(utf16be)), string);
+
+  // matching BOMs
+  assert_equals(new TextDecoder('utf-8').decode(new Uint8Array(utf8_bom.concat(utf8))), string);
+  assert_equals(new TextDecoder('utf-16le').decode(new Uint8Array(utf16le_bom.concat(utf16le))), string);
+  assert_equals(new TextDecoder('utf-16be').decode(new Uint8Array(utf16be_bom.concat(utf16be))), string);
+
+  // matching BOMs split
+  var decoder8 = new TextDecoder('utf-8');
+  assert_equals(decoder8.decode(new Uint8Array(utf8_bom.slice(0, 1)), {stream: true}), '');
+  assert_equals(decoder8.decode(new Uint8Array(utf8_bom.slice(1).concat(utf8))), string);
+  assert_equals(decoder8.decode(new Uint8Array(utf8_bom.slice(0, 2)), {stream: true}), '');
+  assert_equals(decoder8.decode(new Uint8Array(utf8_bom.slice(2).concat(utf8))), string);
+  var decoder16le = new TextDecoder('utf-16le');
+  assert_equals(decoder16le.decode(new Uint8Array(utf16le_bom.slice(0, 1)), {stream: true}), '');
+  assert_equals(decoder16le.decode(new Uint8Array(utf16le_bom.slice(1).concat(utf16le))), string);
+  var decoder16be = new TextDecoder('utf-16be');
+  assert_equals(decoder16be.decode(new Uint8Array(utf16be_bom.slice(0, 1)), {stream: true}), '');
+  assert_equals(decoder16be.decode(new Uint8Array(utf16be_bom.slice(1).concat(utf16be))), string);
+
+  // mismatching BOMs
+  assert_not_equals(new TextDecoder('utf-8').decode(new Uint8Array(utf16le_bom.concat(utf8))), string);
+  assert_not_equals(new TextDecoder('utf-8').decode(new Uint8Array(utf16be_bom.concat(utf8))), string);
+  assert_not_equals(new TextDecoder('utf-16le').decode(new Uint8Array(utf8_bom.concat(utf16le))), string);
+  assert_not_equals(new TextDecoder('utf-16le').decode(new Uint8Array(utf16be_bom.concat(utf16le))), string);
+  assert_not_equals(new TextDecoder('utf-16be').decode(new Uint8Array(utf8_bom.concat(utf16be))), string);
+  assert_not_equals(new TextDecoder('utf-16be').decode(new Uint8Array(utf16le_bom.concat(utf16be))), string);
+
+  // ignore BOMs
+  assert_equals(new TextDecoder('utf-8', {ignoreBOM: true})
+                .decode(new Uint8Array(utf8_bom.concat(utf8))),
+                '\uFEFF' + string);
+  assert_equals(new TextDecoder('utf-16le', {ignoreBOM: true})
+                .decode(new Uint8Array(utf16le_bom.concat(utf16le))),
+                '\uFEFF' + string);
+  assert_equals(new TextDecoder('utf-16be', {ignoreBOM: true})
+                .decode(new Uint8Array(utf16be_bom.concat(utf16be))),
+                '\uFEFF' + string);
+}, 'Byte-order marks');
+
+test(function() {
+  assert_equals(new TextDecoder('utf-8').encoding, 'utf-8'); // canonical case
+  assert_equals(new TextDecoder('UTF-16').encoding, 'utf-16le'); // canonical case and name
+  assert_equals(new TextDecoder('UTF-16BE').encoding, 'utf-16be'); // canonical case and name
+  assert_equals(new TextDecoder('iso8859-1').encoding, 'windows-1252'); // canonical case and name
+  assert_equals(new TextDecoder('iso-8859-1').encoding, 'windows-1252'); // canonical case and name
+}, 'Encoding names');
+
+test(function() {
+  ['utf-8', 'utf-16le', 'utf-16be'].forEach(function(encoding) {
+    var string = '\x00123ABCabc\x80\xFF\u0100\u1000\uFFFD\uD800\uDC00\uDBFF\uDFFF';
+    var encoded = new TextEncoder(encoding).encode(string);
+
+    for (var len = 1; len <= 5; ++len) {
+      var out = '', decoder = new TextDecoder(encoding);
+      for (var i = 0; i < encoded.length; i += len) {
+        var sub = [];
+        for (var j = i; j < encoded.length && j < i + len; ++j) {
+          sub.push(encoded[j]);
+        }
+        out += decoder.decode(new Uint8Array(sub), {stream: true});
+      }
+      out += decoder.decode();
+      assert_equals(out, string, 'streaming decode ' + encoding);
+    }
+  });
+}, 'Streaming Decode');
+
+test(function() {
+  var jis = [0x82, 0xC9, 0x82, 0xD9, 0x82, 0xF1];
+  var expected = '\u306B\u307B\u3093'; // Nihon
+  assert_equals(new TextDecoder('shift_jis').decode(new Uint8Array(jis)), expected);
+}, 'Shift_JIS Decode');
+
+test(function() {
+  var encodings = ['utf-8'].concat(LEGACY_ENCODINGS);
+
+  encodings.forEach(function(encoding) {
+    var string = '', bytes = [];
+    for (var i = 0; i < 128; ++i) {
+
+      // Encodings that have escape codes in 0x00-0x7F
+      if (encoding === 'iso-2022-jp' &&
+          (i === 0x0E || i === 0x0F || i === 0x1B))
+        continue;
+
+      string += String.fromCharCode(i);
+      bytes.push(i);
+    }
+    var ascii_encoded = new TextEncoder('utf-8').encode(string);
+    assert_equals(new TextDecoder(encoding).decode(ascii_encoded), string, encoding);
+      //assert_array_equals(new TextEncoder(encoding).encode(string), bytes, encoding);
+  });
+}, 'Supersets of ASCII decode ASCII correctly');
+
+test(function() {
+  assert_throws({name: 'TypeError'}, function() { new TextDecoder('utf-8', {fatal: true}).decode(new Uint8Array([0xff])); });
+  // This should not hang:
+  new TextDecoder('utf-8').decode(new Uint8Array([0xff]));
+
+  assert_throws({name: 'TypeError'}, function() { new TextDecoder('utf-16le', {fatal: true}).decode(new Uint8Array([0x00])); });
+  // This should not hang:
+  new TextDecoder('utf-16le').decode(new Uint8Array([0x00]));
+
+  assert_throws({name: 'TypeError'}, function() { new TextDecoder('utf-16be', {fatal: true}).decode(new Uint8Array([0x00])); });
+  // This should not hang:
+  new TextDecoder('utf-16be').decode(new Uint8Array([0x00]));
+}, 'Non-fatal errors at EOF');
+
+test(function() {
+  UTF_ENCODINGS.forEach(function(encoding) {
+    assert_equals(new TextDecoder(encoding).encoding, encoding);
+    assert_equals(new TextEncoder(encoding).encoding, encoding);
+  });
+
+  LEGACY_ENCODINGS.forEach(function(encoding) {
+    assert_equals(new TextDecoder(encoding).encoding, encoding);
+    assert_throws({name: 'RangeError'}, function() { new TextEncoder(encoding); });
+  });
+}, 'Non-UTF encodings supported only for decode, not encode');
+
+test(function() {
+  [
+    'csiso2022kr',
+    'hz-gb-2312',
+    'iso-2022-cn',
+    'iso-2022-cn-ext',
+    'iso-2022-kr'
+  ].forEach(function(encoding) {
+
+    assert_throws({name: 'RangeError'},
+                  function() { new TextEncoder(encoding); });
+
+    assert_throws({name: 'RangeError'},
+                  function() {
+                    var decoder = new TextDecoder(encoding, {fatal: true});
+                  });
+
+    assert_throws({name: 'RangeError'},
+                  function() {
+                    var decoder = new TextDecoder(encoding, {fatal: false});
+                    });
+  });
+}, 'Replacement encoding labels');
+
+test(function() {
+  var decoder = new TextDecoder();
+  var bytes = [65, 66, 97, 98, 99, 100, 101, 102, 103, 104, 67, 68, 69, 70, 71, 72];
+  var chars = 'ABabcdefghCDEFGH';
+  var buffer = new Uint8Array(bytes).buffer;
+  assert_equals(decoder.decode(buffer), chars,
+               'Decoding from ArrayBuffer should match expected text.');
+
+  ['Uint8Array', 'Int8Array', 'Uint8ClampedArray',
+   'Uint16Array', 'Int16Array',
+   'Uint32Array', 'Int32Array',
+   'Float32Array', 'Float64Array'].forEach(function(typeName) {
+     var type = self[typeName];
+
+     var array = new type(buffer);
+     assert_equals(decoder.decode(array), chars,
+                   'Decoding from ' + typeName + ' should match expected text.');
+
+     var subset = new type(buffer, type.BYTES_PER_ELEMENT, 8 / type.BYTES_PER_ELEMENT);
+     assert_equals(decoder.decode(subset),
+                   chars.substring(type.BYTES_PER_ELEMENT, type.BYTES_PER_ELEMENT + 8),
+                   'Decoding from ' + typeName + ' should match expected text.');
+   });
+}, 'ArrayBuffer, ArrayBufferView and buffer offsets');
+
+test(function() {
+  assert_throws({name: 'RangeError'},
+                function() { new TextDecoder(null); },
+                'Null should coerce to "null" and be invalid encoding name.');
+
+  assert_throws({name: 'TypeError'},
+                function() { new TextDecoder('utf-8', ''); },
+                'String should not coerce to dictionary.');
+
+  assert_throws({name: 'TypeError'},
+                function() { new TextDecoder('utf-8').decode(null, ''); },
+                'String should not coerce to dictionary.');
+
+  assert_throws({name: 'RangeError'},
+                function() { new TextEncoder(null); },
+                'Null should coerce to "null" and be invalid encoding name.');
+}, 'Invalid parameters');
diff --git a/test/test-shift_jis.js b/test/test-shift_jis.js
new file mode 100644
index 0000000..a01e49e
--- /dev/null
+++ b/test/test-shift_jis.js
@@ -0,0 +1,22 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+test(
+  function () {
+    var bytes = [129,64,129,65,129,66,129,67,129,68,129,69,129,70,129,71,129,72,129,73,129,74,129,75,129,76,129,77,129,78,129,79,129,80,129,81,129,82,129,83,129,84,129,85,129,86,129,87,129,88,129,89,129,90,129,91,129,92,129,93,129,94,129,95,129,98,129,99,129,100,129,101,129,102,129,103,129,104,129,105,129,106,129,107,129,108,129,109,129,110,129,111,129,112,129,113,129,114,129,115,129,116,129,117,129,118,129,119,129,120,129,121,129,122,129,123,129,125,129,126,129,128,129,129,129,130,129,1 [...]
+    var string = "\u3000\u3001\u3002\uFF0C\uFF0E\u30FB\uFF1A\uFF1B\uFF1F\uFF01\u309B\u309C\u00B4\uFF40\u00A8\uFF3E\uFFE3\uFF3F\u30FD\u30FE\u309D\u309E\u3003\u4EDD\u3005\u3006\u3007\u30FC\u2015\u2010\uFF0F\uFF3C\uFF5C\u2026\u2025\u2018\u2019\u201C\u201D\uFF08\uFF09\u3014\u3015\uFF3B\uFF3D\uFF5B\uFF5D\u3008\u3009\u300A\u300B\u300C\u300D\u300E\u300F\u3010\u3011\uFF0B\u00B1\u00D7\u00F7\uFF1D\u2260\uFF1C\uFF1E\u2266\u2267\u221E\u2234\u2642\u2640\u00B0\u2032\u2033\u2103\uFFE5\uFF04\uFF05\uFF03 [...]
+    assert_equals(new TextDecoder("shift_jis").decode(new Uint8Array(bytes)), string, "decoded");
+  },
+  "shift_jis"
+);
diff --git a/test/test-utf.js b/test/test-utf.js
new file mode 100644
index 0000000..57b5c3f
--- /dev/null
+++ b/test/test-utf.js
@@ -0,0 +1,163 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+// NOTE: Requires testharness.js
+// http://www.w3.org/2008/webapps/wiki/Harness
+
+// Extension to testharness.js API which avoids logging enormous strings
+// on a coding failure.
+function assert_string_equals(actual, expected, description) {
+  // short circuit success case
+  if (actual === expected) {
+    assert_true(true, description + ": <actual> === <expected>");
+    return;
+  }
+
+  // length check
+  assert_equals(actual.length, expected.length,
+                description + ": string lengths");
+
+  for (var i = 0; i < actual.length; i++) {
+    var a = actual.charCodeAt(i);
+    var b = expected.charCodeAt(i);
+    if (a !== b)
+      assert_true(false,
+                  description +
+                  ": code unit " + i.toString() + " unequal: " +
+                  cpname(a) + " != " + cpname(b)); // doesn't return
+  }
+
+  // It should be impossible to get here, because the initial
+  // comparison failed, so either the length comparison or the
+  // codeunit-by-codeunit comparison should also fail.
+  assert_true(false, description + ": failed to detect string difference");
+}
+
+// Inspired by:
+// http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
+function encode_utf8(string) {
+  var utf8 = unescape(encodeURIComponent(string));
+  var octets = new Uint8Array(utf8.length), i;
+  for (i = 0; i < utf8.length; i += 1) {
+    octets[i] = utf8.charCodeAt(i);
+  }
+  return octets;
+}
+
+function decode_utf8(octets) {
+  var utf8 = String.fromCharCode.apply(null, octets);
+  return decodeURIComponent(escape(utf8));
+}
+
+// Helpers for test_utf_roundtrip.
+function cpname(n) {
+  if (n+0 !== n)
+    return n.toString();
+  var w = (n <= 0xFFFF) ? 4 : 6;
+  return 'U+' + ('000000' + n.toString(16).toUpperCase()).slice(-w);
+}
+
+function genblock(from, len, skip) {
+  var block = [];
+  for (var i = 0; i < len; i += skip) {
+    var cp = from + i;
+    if (0xD800 <= cp && cp <= 0xDFFF)
+      continue;
+    if (cp < 0x10000) {
+      block.push(String.fromCharCode(cp));
+      continue;
+    }
+    cp = cp - 0x10000;
+    block.push(String.fromCharCode(0xD800 + (cp >> 10)));
+    block.push(String.fromCharCode(0xDC00 + (cp & 0x3FF)));
+  }
+  return block.join('');
+}
+
+function test_utf_roundtrip () {
+  var MIN_CODEPOINT = 0;
+  var MAX_CODEPOINT = 0x10FFFF;
+  var BLOCK_SIZE = 0x1000;
+  var SKIP_SIZE = 31;
+
+  var TE_U16LE = new TextEncoder("UTF-16LE");
+  var TD_U16LE = new TextDecoder("UTF-16LE");
+
+  var TE_U16BE = new TextEncoder("UTF-16BE");
+  var TD_U16BE = new TextDecoder("UTF-16BE");
+
+  var TE_U8    = new TextEncoder("UTF-8");
+  var TD_U8    = new TextDecoder("UTF-8");
+
+  for (var i = MIN_CODEPOINT; i < MAX_CODEPOINT; i += BLOCK_SIZE) {
+    var block_tag = cpname(i) + " - " + cpname(i + BLOCK_SIZE - 1);
+    var block = genblock(i, BLOCK_SIZE, SKIP_SIZE);
+
+    // test UTF-16LE, UTF-16BE, and UTF-8 encodings against themselves
+    var encoded = TE_U16LE.encode(block);
+    var decoded = TD_U16LE.decode(encoded);
+    assert_string_equals(block, decoded, "UTF-16LE round trip " + block_tag);
+
+    encoded = TE_U16BE.encode(block);
+    decoded = TD_U16BE.decode(encoded);
+    assert_string_equals(block, decoded, "UTF-16BE round trip " + block_tag);
+
+    encoded = TE_U8.encode(block);
+    decoded = TD_U8.decode(encoded);
+    assert_string_equals(block, decoded, "UTF-8 round trip " + block_tag);
+
+    // test TextEncoder(UTF-8) against the older idiom
+    var exp_encoded = encode_utf8(block);
+    assert_array_equals(encoded, exp_encoded,
+                        "UTF-8 reference encoding " + block_tag);
+
+    var exp_decoded = decode_utf8(exp_encoded);
+    assert_string_equals(decoded, exp_decoded,
+                         "UTF-8 reference decoding " + block_tag);
+  }
+}
+
+function test_utf_samples () {
+  // z, cent, CJK water, G-Clef, Private-use character
+  var sample = "z\xA2\u6C34\uD834\uDD1E\uDBFF\uDFFD";
+  var cases = [
+    { encoding: "utf-8",
+      expected: [0x7A, 0xC2, 0xA2, 0xE6, 0xB0, 0xB4, 0xF0, 0x9D, 0x84, 0x9E, 0xF4, 0x8F, 0xBF, 0xBD] },
+    { encoding: "utf-16le",
+      expected: [0x7A, 0x00, 0xA2, 0x00, 0x34, 0x6C, 0x34, 0xD8, 0x1E, 0xDD, 0xFF, 0xDB, 0xFD, 0xDF] },
+    { encoding: "utf-16",
+      expected: [0x7A, 0x00, 0xA2, 0x00, 0x34, 0x6C, 0x34, 0xD8, 0x1E, 0xDD, 0xFF, 0xDB, 0xFD, 0xDF] },
+    { encoding: "utf-16be",
+      expected: [0x00, 0x7A, 0x00, 0xA2, 0x6C, 0x34, 0xD8, 0x34, 0xDD, 0x1E, 0xDB, 0xFF, 0xDF, 0xFD] }
+  ];
+
+  cases.forEach(
+    function(t) {
+      var encoded = new TextEncoder(t.encoding).encode(sample);
+      assert_array_equals(encoded, t.expected,
+                          "expected equal encodings - " + t.encoding);
+
+      var decoded = new TextDecoder(t.encoding)
+                        .decode(new Uint8Array(t.expected));
+      assert_equals(decoded, sample,
+                    "expected equal decodings - " + t.encoding);
+    });
+}
+
+test(test_utf_samples,
+     "UTF-8, UTF-16LE, UTF-16BE - Encode/Decode - reference sample");
+
+test(test_utf_roundtrip,
+     "UTF-8, UTF-16LE, UTF-16BE - Encode/Decode - full roundtrip and "+
+     "agreement with encode/decodeURIComponent");
diff --git a/test/test-x-user-defined.js b/test/test-x-user-defined.js
new file mode 100644
index 0000000..108005c
--- /dev/null
+++ b/test/test-x-user-defined.js
@@ -0,0 +1,29 @@
+// Copyright 2014 Joshua Bell. All rights reserved.
+//
+// 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
+//
+//     https://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.
+
+// NOTE: Requires testharness.js
+// http://www.w3.org/2008/webapps/wiki/Harness
+
+test(
+  function() {
+    assert_throws({name: 'RangeError'}, function() { new TextEncoder('x-user-defined'); });
+
+    var decoder = new TextDecoder('x-user-defined');
+    for (var i = 0; i < 0x80; ++i) {
+      assert_equals(decoder.decode(new Uint8Array([i])), String.fromCharCode(i));
+      assert_equals(decoder.decode(new Uint8Array([i + 0x80])), String.fromCharCode(i + 0xF780));
+    }
+  },
+  "x-user-defined encoding"
+);
diff --git a/test/testharness.css b/test/testharness.css
new file mode 100644
index 0000000..365084f
--- /dev/null
+++ b/test/testharness.css
@@ -0,0 +1,92 @@
+html {
+    font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans;
+}
+
+#log .warning,
+#log .warning a {
+  color: black;
+  background: yellow;
+}
+
+#log .error,
+#log .error a {
+  color: white;
+  background: red;
+}
+
+#log pre {
+  border: 1px solid black;
+  padding: 1em;
+}
+
+section#summary {
+    margin-bottom:1em;
+}
+
+table#results {
+    border-collapse:collapse;
+    table-layout:fixed;
+    width:100%;
+}
+
+table#results th:first-child,
+table#results td:first-child {
+    width:4em;
+}
+
+table#results th:last-child,
+table#results td:last-child {
+    width:50%;
+}
+
+table#results.assertions th:last-child,
+table#results.assertions td:last-child {
+    width:35%;
+}
+
+table#results th {
+    padding:0;
+    padding-bottom:0.5em;
+    border-bottom:medium solid black;
+}
+
+table#results td {
+    padding:1em;
+    padding-bottom:0.5em;
+    border-bottom:thin solid black;
+}
+
+tr.pass > td:first-child {
+    color:green;
+}
+
+tr.fail > td:first-child {
+    color:red;
+}
+
+tr.timeout > td:first-child {
+    color:red;
+}
+
+tr.notrun > td:first-child {
+    color:blue;
+}
+
+.pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child {
+    font-variant:small-caps;
+}
+
+table#results span {
+    display:block;
+}
+
+table#results span.expected {
+    font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;
+    white-space:pre;
+}
+
+table#results span.actual {
+    font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;
+    white-space:pre;
+}
+
diff --git a/test/testharness.js b/test/testharness.js
new file mode 100644
index 0000000..4b2a147
--- /dev/null
+++ b/test/testharness.js
@@ -0,0 +1,2121 @@
+/*
+Distributed under both the W3C Test Suite License [1] and the W3C
+3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
+policies and contribution forms [3].
+
+[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
+[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
+[3] http://www.w3.org/2004/10/27-testcases
+*/
+
+/*
+ * == Introduction ==
+ *
+ * This file provides a framework for writing testcases. It is intended to
+ * provide a convenient API for making common assertions, and to work both
+ * for testing synchronous and asynchronous DOM features in a way that
+ * promotes clear, robust, tests.
+ *
+ * == Basic Usage ==
+ *
+ * To use this file, import the script and the testharnessreport script into
+ * the test document:
+ * <script src="/resources/testharness.js"></script>
+ * <script src="/resources/testharnessreport.js"></script>
+ *
+ * Within each file one may define one or more tests. Each test is atomic
+ * in the sense that a single test has a single result (pass/fail/timeout).
+ * Within each test one may have a number of asserts. The test fails at the
+ * first failing assert, and the remainder of the test is (typically) not run.
+ *
+ * If the file containing the tests is a HTML file with an element of id "log"
+ * this will be populated with a table containing the test results after all
+ * the tests have run.
+ *
+ * NOTE: By default tests must be created before the load event fires. For ways
+ *       to create tests after the load event, see "Determining when all tests
+ *       are complete", below
+ *
+ * == Synchronous Tests ==
+ *
+ * To create a synchronous test use the test() function:
+ *
+ * test(test_function, name, properties)
+ *
+ * test_function is a function that contains the code to test. For example a
+ * trivial passing test would be:
+ *
+ * test(function() {assert_true(true)}, "assert_true with true")
+ *
+ * The function passed in is run in the test() call.
+ *
+ * properties is an object that overrides default test properties. The
+ * recognised properties are:
+ *    timeout - the test timeout in ms
+ *
+ * e.g.
+ * test(test_function, "Sample test", {timeout:1000})
+ *
+ * would run test_function with a timeout of 1s.
+ *
+ * Additionally, test-specific metadata can be passed in the properties. These
+ * are used when the individual test has different metadata from that stored
+ * in the <head>.
+ * The recognized metadata properties are:
+ *
+ *    help - The url of the part of the specification being tested
+ *
+ *    assert - A human readable description of what the test is attempting
+ *             to prove
+ *
+ *    author - Name and contact information for the author of the test in the
+ *             format: "Name <email_addr>" or "Name https://contact/url"
+ *
+ * == Asynchronous Tests ==
+ *
+ * Testing asynchronous features is somewhat more complex since the result of
+ * a test may depend on one or more events or other callbacks. The API provided
+ * for testing these features is indended to be rather low-level but hopefully
+ * applicable to many situations.
+ *
+ * To create a test, one starts by getting a Test object using async_test:
+ *
+ * async_test(name, properties)
+ *
+ * e.g.
+ * var t = async_test("Simple async test")
+ *
+ * Assertions can be added to the test by calling the step method of the test
+ * object with a function containing the test assertions:
+ *
+ * t.step(function() {assert_true(true)});
+ *
+ * When all the steps are complete, the done() method must be called:
+ *
+ * t.done();
+ *
+ * The properties argument is identical to that for test().
+ *
+ * In many cases it is convenient to run a step in response to an event or a
+ * callback. A convenient method of doing this is through the step_func method
+ * which returns a function that, when called runs a test step. For example
+ *
+ * object.some_event = t.step_func(function(e) {assert_true(e.a)});
+ *
+ * == Making assertions ==
+ *
+ * Functions for making assertions start assert_
+ * The best way to get a list is to look in this file for functions names
+ * matching that pattern. The general signature is
+ *
+ * assert_something(actual, expected, description)
+ *
+ * although not all assertions precisely match this pattern e.g. assert_true
+ * only takes actual and description as arguments.
+ *
+ * The description parameter is used to present more useful error messages when
+ * a test fails
+ *
+ * NOTE: All asserts must be located in a test() or a step of an async_test().
+ *       asserts outside these places won't be detected correctly by the harness
+ *       and may cause a file to stop testing.
+ *
+ * == Setup ==
+ *
+ * Sometimes tests require non-trivial setup that may fail. For this purpose
+ * there is a setup() function, that may be called with one or two arguments.
+ * The two argument version is:
+ *
+ * setup(func, properties)
+ *
+ * The one argument versions may omit either argument.
+ * func is a function to be run synchronously. setup() becomes a no-op once
+ * any tests have returned results. Properties are global properties of the test
+ * harness. Currently recognised properties are:
+ *
+ * timeout - The time in ms after which the harness should stop waiting for
+ *           tests to complete (this is different to the per-test timeout
+ *           because async tests do not start their timer until .step is called)
+ *
+ * explicit_done - Wait for an explicit call to done() before declaring all
+ *                 tests complete (see below)
+ *
+ * output_document - The document to which results should be logged. By default
+ *                   this is the current document but could be an ancestor
+ *                   document in some cases e.g. a SVG test loaded in an HTML
+ *                   wrapper
+ *
+ * explicit_timeout - disable file timeout; only stop waiting for results
+ *                    when the timeout() function is called (typically for
+ *                    use when integrating with some existing test framework
+ *                    that has its own timeout mechanism).
+ *
+ * == Determining when all tests are complete ==
+ *
+ * By default the test harness will assume there are no more results to come
+ * when:
+ * 1) There are no Test objects that have been created but not completed
+ * 2) The load event on the document has fired
+ *
+ * This behaviour can be overridden by setting the explicit_done property to
+ * true in a call to setup(). If explicit_done is true, the test harness will
+ * not assume it is done until the global done() function is called. Once done()
+ * is called, the two conditions above apply like normal.
+ *
+ * == Generating tests ==
+ *
+ * NOTE: this functionality may be removed
+ *
+ * There are scenarios in which is is desirable to create a large number of
+ * (synchronous) tests that are internally similar but vary in the parameters
+ * used. To make this easier, the generate_tests function allows a single
+ * function to be called with each set of parameters in a list:
+ *
+ * generate_tests(test_function, parameter_lists, properties)
+ *
+ * For example:
+ *
+ * generate_tests(assert_equals, [
+ *     ["Sum one and one", 1+1, 2],
+ *     ["Sum one and zero", 1+0, 1]
+ *     ])
+ *
+ * Is equivalent to:
+ *
+ * test(function() {assert_equals(1+1, 2)}, "Sum one and one")
+ * test(function() {assert_equals(1+0, 1)}, "Sum one and zero")
+ *
+ * Note that the first item in each parameter list corresponds to the name of
+ * the test.
+ *
+ * The properties argument is identical to that for test(). This may be a
+ * single object (used for all generated tests) or an array.
+ *
+ * == Callback API ==
+ *
+ * The framework provides callbacks corresponding to 3 events:
+ *
+ * start - happens when the first Test is created
+ * result - happens when a test result is recieved
+ * complete - happens when all results are recieved
+ *
+ * The page defining the tests may add callbacks for these events by calling
+ * the following methods:
+ *
+ *   add_start_callback(callback) - callback called with no arguments
+ *   add_result_callback(callback) - callback called with a test argument
+ *   add_completion_callback(callback) - callback called with an array of tests
+ *                                       and an status object
+ *
+ * tests have the following properties:
+ *   status: A status code. This can be compared to the PASS, FAIL, TIMEOUT and
+ *           NOTRUN properties on the test object
+ *   message: A message indicating the reason for failure. In the future this
+ *            will always be a string
+ *
+ *  The status object gives the overall status of the harness. It has the
+ *  following properties:
+ *    status: Can be compared to the OK, ERROR and TIMEOUT properties
+ *    message: An error message set when the status is ERROR
+ *
+ * == External API ==
+ *
+ * In order to collect the results of multiple pages containing tests, the test
+ * harness will, when loaded in a nested browsing context, attempt to call
+ * certain functions in each ancestor and opener browsing context:
+ *
+ * start - start_callback
+ * result - result_callback
+ * complete - completion_callback
+ *
+ * These are given the same arguments as the corresponding internal callbacks
+ * described above.
+ *
+ * == External API through cross-document messaging ==
+ *
+ * Where supported, the test harness will also send messages using
+ * cross-document messaging to each ancestor and opener browsing context. Since
+ * it uses the wildcard keyword (*), cross-origin communication is enabled and
+ * script on different origins can collect the results.
+ *
+ * This API follows similar conventions as those described above only slightly
+ * modified to accommodate message event API. Each message is sent by the harness
+ * is passed a single vanilla object, available as the `data` property of the
+ * event object. These objects are structures as follows:
+ *
+ * start - { type: "start" }
+ * result - { type: "result", test: Test }
+ * complete - { type: "complete", tests: [Test, ...], status: TestsStatus }
+ *
+ * == List of assertions ==
+ *
+ * assert_true(actual, description)
+ *   asserts that /actual/ is strictly true
+ *
+ * assert_false(actual, description)
+ *   asserts that /actual/ is strictly false
+ *
+ * assert_equals(actual, expected, description)
+ *   asserts that /actual/ is the same value as /expected/
+ *
+ * assert_not_equals(actual, expected, description)
+ *   asserts that /actual/ is a different value to /expected/. Yes, this means
+ *   that "expected" is a misnomer
+ *
+ * assert_in_array(actual, expected, description)
+ *   asserts that /expected/ is an Array, and /actual/ is equal to one of the
+ *   members -- expected.indexOf(actual) != -1
+ *
+ * assert_array_equals(actual, expected, description)
+ *   asserts that /actual/ and /expected/ have the same length and the value of
+ *   each indexed property in /actual/ is the strictly equal to the corresponding
+ *   property value in /expected/
+ *
+ * assert_approx_equals(actual, expected, epsilon, description)
+ *   asserts that /actual/ is a number within +/- /epsilon/ of /expected/
+ *
+ * assert_regexp_match(actual, expected, description)
+ *   asserts that /actual/ matches the regexp /expected/
+ *
+ * assert_class_string(object, class_name, description)
+ *   asserts that the class string of /object/ as returned in
+ *   Object.prototype.toString is equal to /class_name/.
+ *
+ * assert_own_property(object, property_name, description)
+ *   assert that object has own property property_name
+ *
+ * assert_inherits(object, property_name, description)
+ *   assert that object does not have an own property named property_name
+ *   but that property_name is present in the prototype chain for object
+ *
+ * assert_idl_attribute(object, attribute_name, description)
+ *   assert that an object that is an instance of some interface has the
+ *   attribute attribute_name following the conditions specified by WebIDL
+ *
+ * assert_readonly(object, property_name, description)
+ *   assert that property property_name on object is readonly
+ *
+ * assert_throws(code, func, description)
+ *   code - the expected exception:
+ *     o string: the thrown exception must be a DOMException with the given
+ *               name, e.g., "TimeoutError" (for compatibility with existing
+ *               tests, a constant is also supported, e.g., "TIMEOUT_ERR")
+ *     o object: the thrown exception must have a property called "name" that
+ *               matches code.name
+ *     o null:   allow any exception (in general, one of the options above
+ *               should be used)
+ *   func - a function that should throw
+ *
+ * assert_unreached(description)
+ *   asserts if called. Used to ensure that some codepath is *not* taken e.g.
+ *   an event does not fire.
+ *
+ * assert_any(assert_func, actual, expected_array, extra_arg_1, ... extra_arg_N)
+ *   asserts that one assert_func(actual, expected_array_N, extra_arg1, ..., extra_arg_N)
+ *   is true for some expected_array_N in expected_array. This only works for assert_func
+ *   with signature assert_func(actual, expected, args_1, ..., args_N). Note that tests
+ *   with multiple allowed pass conditions are bad practice unless the spec specifically
+ *   allows multiple behaviours. Test authors should not use this method simply to hide
+ *   UA bugs.
+ *
+ * assert_exists(object, property_name, description)
+ *   *** deprecated ***
+ *   asserts that object has an own property property_name
+ *
+ * assert_not_exists(object, property_name, description)
+ *   *** deprecated ***
+ *   assert that object does not have own property property_name
+ */
+
+(function ()
+{
+    var debug = false;
+    // default timeout is 5 seconds, test can override if needed
+    var settings = {
+      output:true,
+      timeout:5000,
+      test_timeout:2000
+    };
+
+    var xhtml_ns = "http://www.w3.org/1999/xhtml";
+
+    // script_prefix is used by Output.prototype.show_results() to figure out
+    // where to get testharness.css from.  It's enclosed in an extra closure to
+    // not pollute the library's namespace with variables like "src".
+    var script_prefix = null;
+    (function ()
+    {
+        var scripts = document.getElementsByTagName("script");
+        for (var i = 0; i < scripts.length; i++)
+        {
+            if (scripts[i].src)
+            {
+                var src = scripts[i].src;
+            }
+            else if (scripts[i].href)
+            {
+                //SVG case
+                var src = scripts[i].href.baseVal;
+            }
+            if (src && src.slice(src.length - "testharness.js".length) === "testharness.js")
+            {
+                script_prefix = src.slice(0, src.length - "testharness.js".length);
+                break;
+            }
+        }
+    })();
+
+    /*
+     * API functions
+     */
+
+    var name_counter = 0;
+    function next_default_name()
+    {
+        //Don't use document.title to work around an Opera bug in XHTML documents
+        var prefix = document.getElementsByTagName("title").length > 0 ?
+                         document.getElementsByTagName("title")[0].firstChild.data :
+                         "Untitled";
+        var suffix = name_counter > 0 ? " " + name_counter : "";
+        name_counter++;
+        return prefix + suffix;
+    }
+
+    function test(func, name, properties)
+    {
+        var test_name = name ? name : next_default_name();
+        properties = properties ? properties : {};
+        var test_obj = new Test(test_name, properties);
+        test_obj.step(func);
+        if (test_obj.status === test_obj.NOTRUN) {
+            test_obj.done();
+        }
+    }
+
+    function async_test(name, properties)
+    {
+        var test_name = name ? name : next_default_name();
+        properties = properties ? properties : {};
+        var test_obj = new Test(test_name, properties);
+        return test_obj;
+    }
+
+    function setup(func_or_properties, maybe_properties)
+    {
+        var func = null;
+        var properties = {};
+        if (arguments.length === 2) {
+            func = func_or_properties;
+            properties = maybe_properties;
+        } else if (func_or_properties instanceof Function){
+            func = func_or_properties;
+        } else {
+            properties = func_or_properties;
+        }
+        tests.setup(func, properties);
+        output.setup(properties);
+    }
+
+    function done() {
+        tests.end_wait();
+    }
+
+    function generate_tests(func, args, properties) {
+        forEach(args, function(x, i)
+                {
+                    var name = x[0];
+                    test(function()
+                         {
+                             func.apply(this, x.slice(1));
+                         },
+                         name,
+                         Array.isArray(properties) ? properties[i] : properties);
+                });
+    }
+
+    function on_event(object, event, callback)
+    {
+      object.addEventListener(event, callback, false);
+    }
+
+    expose(test, 'test');
+    expose(async_test, 'async_test');
+    expose(generate_tests, 'generate_tests');
+    expose(setup, 'setup');
+    expose(done, 'done');
+    expose(on_event, 'on_event');
+
+    /*
+     * Return a string truncated to the given length, with ... added at the end
+     * if it was longer.
+     */
+    function truncate(s, len)
+    {
+        if (s.length > len) {
+            return s.substring(0, len - 3) + "...";
+        }
+        return s;
+    }
+
+    /*
+     * Convert a value to a nice, human-readable string
+     */
+    function format_value(val)
+    {
+        if (Array.isArray(val))
+        {
+            return "[" + val.map(format_value).join(", ") + "]";
+        }
+
+        switch (typeof val)
+        {
+        case "string":
+            val = val.replace("\\", "\\\\");
+            for (var i = 0; i < 32; i++)
+            {
+                var replace = "\\";
+                switch (i) {
+                case 0: replace += "0"; break;
+                case 1: replace += "x01"; break;
+                case 2: replace += "x02"; break;
+                case 3: replace += "x03"; break;
+                case 4: replace += "x04"; break;
+                case 5: replace += "x05"; break;
+                case 6: replace += "x06"; break;
+                case 7: replace += "x07"; break;
+                case 8: replace += "b"; break;
+                case 9: replace += "t"; break;
+                case 10: replace += "n"; break;
+                case 11: replace += "v"; break;
+                case 12: replace += "f"; break;
+                case 13: replace += "r"; break;
+                case 14: replace += "x0e"; break;
+                case 15: replace += "x0f"; break;
+                case 16: replace += "x10"; break;
+                case 17: replace += "x11"; break;
+                case 18: replace += "x12"; break;
+                case 19: replace += "x13"; break;
+                case 20: replace += "x14"; break;
+                case 21: replace += "x15"; break;
+                case 22: replace += "x16"; break;
+                case 23: replace += "x17"; break;
+                case 24: replace += "x18"; break;
+                case 25: replace += "x19"; break;
+                case 26: replace += "x1a"; break;
+                case 27: replace += "x1b"; break;
+                case 28: replace += "x1c"; break;
+                case 29: replace += "x1d"; break;
+                case 30: replace += "x1e"; break;
+                case 31: replace += "x1f"; break;
+                }
+                val = val.replace(RegExp(String.fromCharCode(i), "g"), replace);
+            }
+            return '"' + val.replace(/"/g, '\\"') + '"';
+        case "boolean":
+        case "undefined":
+            return String(val);
+        case "number":
+            // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
+            // special-case.
+            if (val === -0 && 1/val === -Infinity)
+            {
+                return "-0";
+            }
+            return String(val);
+        case "object":
+            if (val === null)
+            {
+                return "null";
+            }
+
+            // Special-case Node objects, since those come up a lot in my tests.  I
+            // ignore namespaces.  I use duck-typing instead of instanceof, because
+            // instanceof doesn't work if the node is from another window (like an
+            // iframe's contentWindow):
+            // https://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
+            if ("nodeType" in val
+            && "nodeName" in val
+            && "nodeValue" in val
+            && "childNodes" in val)
+            {
+                switch (val.nodeType)
+                {
+                case Node.ELEMENT_NODE:
+                    var ret = "<" + val.tagName.toLowerCase();
+                    for (var i = 0; i < val.attributes.length; i++)
+                    {
+                        ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"';
+                    }
+                    ret += ">" + val.innerHTML + "</" + val.tagName.toLowerCase() + ">";
+                    return "Element node " + truncate(ret, 60);
+                case Node.TEXT_NODE:
+                    return 'Text node "' + truncate(val.data, 60) + '"';
+                case Node.PROCESSING_INSTRUCTION_NODE:
+                    return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60));
+                case Node.COMMENT_NODE:
+                    return "Comment node <!--" + truncate(val.data, 60) + "-->";
+                case Node.DOCUMENT_NODE:
+                    return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+                case Node.DOCUMENT_TYPE_NODE:
+                    return "DocumentType node";
+                case Node.DOCUMENT_FRAGMENT_NODE:
+                    return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+                default:
+                    return "Node object of unknown type";
+                }
+            }
+
+            // Fall through to default
+        default:
+            return typeof val + ' "' + truncate(String(val), 60) + '"';
+        }
+    }
+    expose(format_value, "format_value");
+
+    /*
+     * Assertions
+     */
+
+    function assert_true(actual, description)
+    {
+        assert(actual === true, "assert_true", description,
+                                "expected true got ${actual}", {actual:actual});
+    };
+    expose(assert_true, "assert_true");
+
+    function assert_false(actual, description)
+    {
+        assert(actual === false, "assert_false", description,
+                                 "expected false got ${actual}", {actual:actual});
+    };
+    expose(assert_false, "assert_false");
+
+    function same_value(x, y) {
+        if (y !== y)
+        {
+            //NaN case
+            return x !== x;
+        }
+        else if (x === 0 && y === 0) {
+            //Distinguish +0 and -0
+            return 1/x === 1/y;
+        }
+        else
+        {
+            //typical case
+            return x === y;
+        }
+    }
+
+    function assert_equals(actual, expected, description)
+    {
+         /*
+          * Test if two primitives are equal or two objects
+          * are the same object
+          */
+        if (typeof actual != typeof expected)
+        {
+            assert(false, "assert_equals", description,
+                          "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}",
+                          {expected:expected, actual:actual});
+            return;
+        }
+        assert(same_value(actual, expected), "assert_equals", description,
+                                             "expected ${expected} but got ${actual}",
+                                             {expected:expected, actual:actual});
+    };
+    expose(assert_equals, "assert_equals");
+
+    function assert_not_equals(actual, expected, description)
+    {
+         /*
+          * Test if two primitives are unequal or two objects
+          * are different objects
+          */
+        assert(!same_value(actual, expected), "assert_not_equals", description,
+                                              "got disallowed value ${actual}",
+                                              {actual:actual});
+    };
+    expose(assert_not_equals, "assert_not_equals");
+
+    function assert_in_array(actual, expected, description)
+    {
+        assert(expected.indexOf(actual) != -1, "assert_in_array", description,
+                                               "value ${actual} not in array ${expected}",
+                                               {actual:actual, expected:expected});
+    }
+    expose(assert_in_array, "assert_in_array");
+
+    function assert_object_equals(actual, expected, description)
+    {
+         //This needs to be improved a great deal
+         function check_equal(actual, expected, stack)
+         {
+             stack.push(actual);
+
+             var p;
+             for (p in actual)
+             {
+                 assert(expected.hasOwnProperty(p), "assert_object_equals", description,
+                                                    "unexpected property ${p}", {p:p});
+
+                 if (typeof actual[p] === "object" && actual[p] !== null)
+                 {
+                     if (stack.indexOf(actual[p]) === -1)
+                     {
+                         check_equal(actual[p], expected[p], stack);
+                     }
+                 }
+                 else
+                 {
+                     assert(actual[p] === expected[p], "assert_object_equals", description,
+                                                       "property ${p} expected ${expected} got ${actual}",
+                                                       {p:p, expected:expected, actual:actual});
+                 }
+             }
+             for (p in expected)
+             {
+                 assert(actual.hasOwnProperty(p),
+                        "assert_object_equals", description,
+                        "expected property ${p} missing", {p:p});
+             }
+             stack.pop();
+         }
+         check_equal(actual, expected, []);
+    };
+    expose(assert_object_equals, "assert_object_equals");
+
+    function assert_array_equals(actual, expected, description)
+    {
+        assert(actual.length === expected.length,
+               "assert_array_equals", description,
+               "lengths differ, expected ${expected} got ${actual}",
+               {expected:expected.length, actual:actual.length});
+
+        for (var i=0; i < actual.length; i++)
+        {
+            assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+                   "assert_array_equals", description,
+                   "property ${i}, property expected to be $expected but was $actual",
+                   {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+                   actual:actual.hasOwnProperty(i) ? "present" : "missing"});
+            assert(expected[i] === actual[i],
+                   "assert_array_equals", description,
+                   "property ${i}, expected ${expected} but got ${actual}",
+                   {i:i, expected:expected[i], actual:actual[i]});
+        }
+    }
+    expose(assert_array_equals, "assert_array_equals");
+
+    function assert_approx_equals(actual, expected, epsilon, description)
+    {
+        /*
+         * Test if two primitive numbers are equal withing +/- epsilon
+         */
+        assert(typeof actual === "number",
+               "assert_approx_equals", description,
+               "expected a number but got a ${type_actual}",
+               {type_actual:typeof actual});
+
+        assert(Math.abs(actual - expected) <= epsilon,
+               "assert_approx_equals", description,
+               "expected ${expected} +/- ${epsilon} but got ${actual}",
+               {expected:expected, actual:actual, epsilon:epsilon});
+    };
+    expose(assert_approx_equals, "assert_approx_equals");
+
+    function assert_regexp_match(actual, expected, description) {
+        /*
+         * Test if a string (actual) matches a regexp (expected)
+         */
+        assert(expected.test(actual),
+               "assert_regexp_match", description,
+               "expected ${expected} but got ${actual}",
+               {expected:expected, actual:actual});
+    }
+    expose(assert_regexp_match, "assert_regexp_match");
+
+    function assert_class_string(object, class_string, description) {
+        assert_equals({}.toString.call(object), "[object " + class_string + "]",
+                      description);
+    }
+    expose(assert_class_string, "assert_class_string");
+
+
+    function _assert_own_property(name) {
+        return function(object, property_name, description)
+        {
+            assert(object.hasOwnProperty(property_name),
+                   name, description,
+                   "expected property ${p} missing", {p:property_name});
+        };
+    }
+    expose(_assert_own_property("assert_exists"), "assert_exists");
+    expose(_assert_own_property("assert_own_property"), "assert_own_property");
+
+    function assert_not_exists(object, property_name, description)
+    {
+        assert(!object.hasOwnProperty(property_name),
+               "assert_not_exists", description,
+               "unexpected property ${p} found", {p:property_name});
+    };
+    expose(assert_not_exists, "assert_not_exists");
+
+    function _assert_inherits(name) {
+        return function (object, property_name, description)
+        {
+            assert(typeof object === "object",
+                   name, description,
+                   "provided value is not an object");
+
+            assert("hasOwnProperty" in object,
+                   name, description,
+                   "provided value is an object but has no hasOwnProperty method");
+
+            assert(!object.hasOwnProperty(property_name),
+                   name, description,
+                   "property ${p} found on object expected in prototype chain",
+                   {p:property_name});
+
+            assert(property_name in object,
+                   name, description,
+                   "property ${p} not found in prototype chain",
+                   {p:property_name});
+        };
+    }
+    expose(_assert_inherits("assert_inherits"), "assert_inherits");
+    expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
+
+    function assert_readonly(object, property_name, description)
+    {
+         var initial_value = object[property_name];
+         try {
+             //Note that this can have side effects in the case where
+             //the property has PutForwards
+             object[property_name] = initial_value + "a"; //XXX use some other value here?
+             assert(object[property_name] === initial_value,
+                    "assert_readonly", description,
+                    "changing property ${p} succeeded",
+                    {p:property_name});
+         }
+         finally
+         {
+             object[property_name] = initial_value;
+         }
+    };
+    expose(assert_readonly, "assert_readonly");
+
+    function assert_throws(code, func, description)
+    {
+        try
+        {
+            func.call(this);
+            assert(false, "assert_throws", description,
+                   "${func} did not throw", {func:func});
+        }
+        catch(e)
+        {
+            if (e instanceof AssertionError) {
+                throw(e);
+            }
+            if (code === null)
+            {
+                return;
+            }
+            if (typeof code === "object")
+            {
+                assert(typeof e == "object" && "name" in e && e.name == code.name,
+                       "assert_throws", description,
+                       "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})",
+                                    {func:func, actual:e, actual_name:e.name,
+                                     expected:code,
+                                     expected_name:code.name});
+                return;
+            }
+
+            var code_name_map = {
+                INDEX_SIZE_ERR: 'IndexSizeError',
+                HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
+                WRONG_DOCUMENT_ERR: 'WrongDocumentError',
+                INVALID_CHARACTER_ERR: 'InvalidCharacterError',
+                NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
+                NOT_FOUND_ERR: 'NotFoundError',
+                NOT_SUPPORTED_ERR: 'NotSupportedError',
+                INVALID_STATE_ERR: 'InvalidStateError',
+                SYNTAX_ERR: 'SyntaxError',
+                INVALID_MODIFICATION_ERR: 'InvalidModificationError',
+                NAMESPACE_ERR: 'NamespaceError',
+                INVALID_ACCESS_ERR: 'InvalidAccessError',
+                TYPE_MISMATCH_ERR: 'TypeMismatchError',
+                SECURITY_ERR: 'SecurityError',
+                NETWORK_ERR: 'NetworkError',
+                ABORT_ERR: 'AbortError',
+                URL_MISMATCH_ERR: 'URLMismatchError',
+                QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
+                TIMEOUT_ERR: 'TimeoutError',
+                INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
+                DATA_CLONE_ERR: 'DataCloneError',
+            };
+
+            var name = code in code_name_map ? code_name_map[code] : code;
+
+            var name_code_map = {
+                IndexSizeError: 1,
+                HierarchyRequestError: 3,
+                WrongDocumentError: 4,
+                InvalidCharacterError: 5,
+                NoModificationAllowedError: 7,
+                NotFoundError: 8,
+                NotSupportedError: 9,
+                InvalidStateError: 11,
+                SyntaxError: 12,
+                InvalidModificationError: 13,
+                NamespaceError: 14,
+                InvalidAccessError: 15,
+                TypeMismatchError: 17,
+                SecurityError: 18,
+                NetworkError: 19,
+                AbortError: 20,
+                URLMismatchError: 21,
+                QuotaExceededError: 22,
+                TimeoutError: 23,
+                InvalidNodeTypeError: 24,
+                DataCloneError: 25,
+
+                UnknownError: 0,
+                ConstraintError: 0,
+                DataError: 0,
+                TransactionInactiveError: 0,
+                ReadOnlyError: 0,
+                VersionError: 0,
+            };
+
+            if (!(name in name_code_map))
+            {
+                throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()');
+            }
+
+            var required_props = { code: name_code_map[name] };
+
+            if (required_props.code === 0
+            || ("name" in e && e.name !== e.name.toUpperCase() && e.name !== "DOMException"))
+            {
+                // New style exception: also test the name property.
+                required_props.name = name;
+            }
+
+            //We'd like to test that e instanceof the appropriate interface,
+            //but we can't, because we don't know what window it was created
+            //in.  It might be an instanceof the appropriate interface on some
+            //unknown other window.  TODO: Work around this somehow?
+
+            assert(typeof e == "object",
+                   "assert_throws", description,
+                   "${func} threw ${e} with type ${type}, not an object",
+                   {func:func, e:e, type:typeof e});
+
+            for (var prop in required_props)
+            {
+                assert(typeof e == "object" && prop in e && e[prop] == required_props[prop],
+                       "assert_throws", description,
+                       "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}",
+                       {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
+            }
+        }
+    }
+    expose(assert_throws, "assert_throws");
+
+    function assert_unreached(description) {
+         assert(false, "assert_unreached", description,
+                "Reached unreachable code");
+    }
+    expose(assert_unreached, "assert_unreached");
+
+    function assert_any(assert_func, actual, expected_array)
+    {
+        var args = [].slice.call(arguments, 3)
+        var errors = []
+        var passed = false;
+        forEach(expected_array,
+                function(expected)
+                {
+                    try {
+                        assert_func.apply(this, [actual, expected].concat(args))
+                        passed = true;
+                    } catch(e) {
+                        errors.push(e.message);
+                    }
+                });
+        if (!passed) {
+            throw new AssertionError(errors.join("\n\n"));
+        }
+    }
+    expose(assert_any, "assert_any");
+
+    function Test(name, properties)
+    {
+        this.name = name;
+        this.status = this.NOTRUN;
+        this.timeout_id = null;
+        this.is_done = false;
+
+        this.properties = properties;
+        this.timeout_length = properties.timeout ? properties.timeout : settings.test_timeout;
+
+        this.message = null;
+
+        var this_obj = this;
+        this.steps = [];
+
+        tests.push(this);
+    }
+
+    Test.statuses = {
+        PASS:0,
+        FAIL:1,
+        TIMEOUT:2,
+        NOTRUN:3
+    };
+
+    Test.prototype = merge({}, Test.statuses);
+
+    Test.prototype.structured_clone = function()
+    {
+        if(!this._structured_clone)
+        {
+            var msg = this.message;
+            msg = msg ? String(msg) : msg;
+            this._structured_clone = merge({
+                name:String(this.name),
+                status:this.status,
+                message:msg
+            }, Test.statuses);
+        }
+        return this._structured_clone;
+    };
+
+    Test.prototype.step = function(func, this_obj)
+    {
+        //In case the test has already failed
+        if (this.status !== this.NOTRUN)
+        {
+          return;
+        }
+
+        tests.started = true;
+
+        if (this.timeout_id === null) {
+            this.set_timeout();
+        }
+
+        this.steps.push(func);
+
+        if (arguments.length === 1)
+        {
+            this_obj = this;
+        }
+
+        try
+        {
+            func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
+        }
+        catch(e)
+        {
+            //This can happen if something called synchronously invoked another
+            //step
+            if (this.status !== this.NOTRUN)
+            {
+                return;
+            }
+            this.status = this.FAIL;
+            this.message = (typeof e === "object" && e !== null) ? e.message : e;
+            if (typeof e.stack != "undefined" && typeof e.message == "string") {
+                //Try to make it more informative for some exceptions, at least
+                //in Gecko and WebKit.  This results in a stack dump instead of
+                //just errors like "Cannot read property 'parentNode' of null"
+                //or "root is null".  Makes it a lot longer, of course.
+                this.message += "(stack: " + e.stack + ")";
+            }
+            this.done();
+            if (debug && e.constructor !== AssertionError) {
+                throw e;
+            }
+        }
+    };
+
+    Test.prototype.step_func = function(func, this_obj)
+    {
+        var test_this = this;
+
+        if (arguments.length === 1)
+        {
+            this_obj = test_this;
+        }
+
+        return function()
+        {
+            test_this.step.apply(test_this, [func, this_obj].concat(
+                Array.prototype.slice.call(arguments)));
+        };
+    };
+
+    Test.prototype.step_func_done = function(func, this_obj)
+    {
+        var test_this = this;
+
+        if (arguments.length === 1)
+        {
+            this_obj = test_this;
+        }
+
+        return function()
+        {
+            test_this.step.apply(test_this, [func, this_obj].concat(
+                Array.prototype.slice.call(arguments)));
+            test_this.done();
+        };
+    };
+
+    Test.prototype.set_timeout = function()
+    {
+        var this_obj = this;
+        this.timeout_id = setTimeout(function()
+                                     {
+                                         this_obj.timeout();
+                                     }, this.timeout_length);
+    };
+
+    Test.prototype.timeout = function()
+    {
+        this.status = this.TIMEOUT;
+        this.timeout_id = null;
+        this.message = "Test timed out";
+        this.done();
+    };
+
+    Test.prototype.done = function()
+    {
+        if (this.is_done) {
+            return;
+        }
+        clearTimeout(this.timeout_id);
+        if (this.status === this.NOTRUN)
+        {
+            this.status = this.PASS;
+        }
+        this.is_done = true;
+        tests.result(this);
+    };
+
+
+    /*
+     * Harness
+     */
+
+    function TestsStatus()
+    {
+        this.status = null;
+        this.message = null;
+    }
+
+    TestsStatus.statuses = {
+        OK:0,
+        ERROR:1,
+        TIMEOUT:2
+    };
+
+    TestsStatus.prototype = merge({}, TestsStatus.statuses);
+
+    TestsStatus.prototype.structured_clone = function()
+    {
+        if(!this._structured_clone)
+        {
+            var msg = this.message;
+            msg = msg ? String(msg) : msg;
+            this._structured_clone = merge({
+                status:this.status,
+                message:msg
+            }, TestsStatus.statuses);
+        }
+        return this._structured_clone;
+    };
+
+    function Tests()
+    {
+        this.tests = [];
+        this.num_pending = 0;
+
+        this.phases = {
+            INITIAL:0,
+            SETUP:1,
+            HAVE_TESTS:2,
+            HAVE_RESULTS:3,
+            COMPLETE:4
+        };
+        this.phase = this.phases.INITIAL;
+
+        this.properties = {};
+
+        //All tests can't be done until the load event fires
+        this.all_loaded = false;
+        this.wait_for_finish = false;
+        this.processing_callbacks = false;
+
+        this.timeout_length = settings.timeout;
+        this.timeout_id = null;
+
+        this.start_callbacks = [];
+        this.test_done_callbacks = [];
+        this.all_done_callbacks = [];
+
+        this.status = new TestsStatus();
+
+        var this_obj = this;
+
+        on_event(window, "load",
+                 function()
+                 {
+                     this_obj.all_loaded = true;
+                     if (this_obj.all_done())
+                     {
+                         this_obj.complete();
+                     }
+                 });
+
+        this.set_timeout();
+    }
+
+    Tests.prototype.setup = function(func, properties)
+    {
+        if (this.phase >= this.phases.HAVE_RESULTS)
+        {
+            return;
+        }
+        if (this.phase < this.phases.SETUP)
+        {
+            this.phase = this.phases.SETUP;
+        }
+
+        for (var p in properties)
+        {
+            if (properties.hasOwnProperty(p))
+            {
+                this.properties[p] = properties[p];
+            }
+        }
+
+        if (properties.timeout)
+        {
+            this.timeout_length = properties.timeout;
+        }
+        if (properties.explicit_done)
+        {
+            this.wait_for_finish = true;
+        }
+        if (properties.explicit_timeout) {
+            this.timeout_length = null;
+        }
+
+        if (func)
+        {
+            try
+            {
+                func();
+            } catch(e)
+            {
+                this.status.status = this.status.ERROR;
+                this.status.message = e;
+            };
+        }
+        this.set_timeout();
+    };
+
+    Tests.prototype.set_timeout = function()
+    {
+        var this_obj = this;
+        clearTimeout(this.timeout_id);
+        if (this.timeout_length !== null)
+        {
+            this.timeout_id = setTimeout(function() {
+                                             this_obj.timeout();
+                                         }, this.timeout_length);
+        }
+    };
+
+    Tests.prototype.timeout = function() {
+        this.status.status = this.status.TIMEOUT;
+        this.complete();
+    };
+
+    Tests.prototype.end_wait = function()
+    {
+        this.wait_for_finish = false;
+        if (this.all_done()) {
+            this.complete();
+        }
+    };
+
+    Tests.prototype.push = function(test)
+    {
+        if (this.phase < this.phases.HAVE_TESTS) {
+            this.start();
+        }
+        this.num_pending++;
+        this.tests.push(test);
+    };
+
+    Tests.prototype.all_done = function() {
+        return (this.all_loaded && this.num_pending === 0 &&
+                !this.wait_for_finish && !this.processing_callbacks);
+    };
+
+    Tests.prototype.start = function() {
+        this.phase = this.phases.HAVE_TESTS;
+        this.notify_start();
+    };
+
+    Tests.prototype.notify_start = function() {
+        var this_obj = this;
+        forEach (this.start_callbacks,
+                 function(callback)
+                 {
+                     callback(this_obj.properties);
+                 });
+        forEach_windows(
+                function(w, is_same_origin)
+                {
+                    if(is_same_origin && w.start_callback)
+                    {
+                        try
+                        {
+                            w.start_callback(this_obj.properties);
+                        }
+                        catch(e)
+                        {
+                            if (debug)
+                            {
+                                throw(e);
+                            }
+                        }
+                    }
+                    if (supports_post_message(w))
+                    {
+                        w.postMessage({
+                            type: "start",
+                            properties: this_obj.properties
+                        }, "*");
+                    }
+                });
+    };
+
+    Tests.prototype.result = function(test)
+    {
+        if (this.phase > this.phases.HAVE_RESULTS)
+        {
+            return;
+        }
+        this.phase = this.phases.HAVE_RESULTS;
+        this.num_pending--;
+        this.notify_result(test);
+    };
+
+    Tests.prototype.notify_result = function(test) {
+        var this_obj = this;
+        this.processing_callbacks = true;
+        forEach(this.test_done_callbacks,
+                function(callback)
+                {
+                    callback(test, this_obj);
+                });
+
+        forEach_windows(
+                function(w, is_same_origin)
+                {
+                    if(is_same_origin && w.result_callback)
+                    {
+                        try
+                        {
+                            w.result_callback(test);
+                        }
+                        catch(e)
+                        {
+                            if(debug) {
+                                throw e;
+                            }
+                        }
+                    }
+                    if (supports_post_message(w))
+                    {
+                        w.postMessage({
+                            type: "result",
+                            test: test.structured_clone()
+                        }, "*");
+                    }
+                });
+        this.processing_callbacks = false;
+        if (this_obj.all_done())
+        {
+            this_obj.complete();
+        }
+    };
+
+    Tests.prototype.complete = function() {
+        if (this.phase === this.phases.COMPLETE) {
+            return;
+        }
+        this.phase = this.phases.COMPLETE;
+        var this_obj = this;
+        this.tests.forEach(
+            function(x)
+            {
+                if(x.status === x.NOTRUN)
+                {
+                    this_obj.notify_result(x);
+                }
+            }
+        );
+        this.notify_complete();
+    };
+
+    Tests.prototype.notify_complete = function()
+    {
+        clearTimeout(this.timeout_id);
+        var this_obj = this;
+        var tests = map(this_obj.tests,
+                        function(test)
+                        {
+                            return test.structured_clone();
+                        });
+        if (this.status.status === null)
+        {
+            this.status.status = this.status.OK;
+        }
+
+        forEach (this.all_done_callbacks,
+                 function(callback)
+                 {
+                     callback(this_obj.tests, this_obj.status);
+                 });
+
+        forEach_windows(
+                function(w, is_same_origin)
+                {
+                    if(is_same_origin && w.completion_callback)
+                    {
+                        try
+                        {
+                            w.completion_callback(this_obj.tests, this_obj.status);
+                        }
+                        catch(e)
+                        {
+                            if (debug)
+                            {
+                                throw e;
+                            }
+                        }
+                    }
+                    if (supports_post_message(w))
+                    {
+                        w.postMessage({
+                            type: "complete",
+                            tests: tests,
+                            status: this_obj.status.structured_clone()
+                        }, "*");
+                    }
+                });
+    };
+
+    var tests = new Tests();
+
+    function timeout() {
+        if (tests.timeout_length === null)
+        {
+            tests.timeout();
+        }
+    }
+    expose(timeout, 'timeout');
+
+    function add_start_callback(callback) {
+        tests.start_callbacks.push(callback);
+    }
+
+    function add_result_callback(callback)
+    {
+        tests.test_done_callbacks.push(callback);
+    }
+
+    function add_completion_callback(callback)
+    {
+       tests.all_done_callbacks.push(callback);
+    }
+
+    expose(add_start_callback, 'add_start_callback');
+    expose(add_result_callback, 'add_result_callback');
+    expose(add_completion_callback, 'add_completion_callback');
+
+    /*
+     * Output listener
+    */
+
+    function Output() {
+      this.output_document = null;
+      this.output_node = null;
+      this.done_count = 0;
+      this.enabled = settings.output;
+      this.phase = this.INITIAL;
+    }
+
+    Output.prototype.INITIAL = 0;
+    Output.prototype.STARTED = 1;
+    Output.prototype.HAVE_RESULTS = 2;
+    Output.prototype.COMPLETE = 3;
+
+    Output.prototype.setup = function(properties) {
+        if (this.phase > this.INITIAL) {
+            return;
+        }
+
+        //If output is disabled in testharnessreport.js the test shouldn't be
+        //able to override that
+        this.enabled = this.enabled && (properties.hasOwnProperty("output") ?
+                                        properties.output : settings.output);
+    };
+
+    Output.prototype.init = function(properties)
+    {
+        if (this.phase >= this.STARTED) {
+            return;
+        }
+        if (properties.output_document) {
+            this.output_document = properties.output_document;
+        } else {
+            this.output_document = document;
+        }
+        this.phase = this.STARTED;
+    };
+
+    Output.prototype.resolve_log = function()
+    {
+        var output_document;
+        if (typeof this.output_document === "function")
+        {
+            output_document = this.output_document.apply(undefined);
+        } else
+        {
+            output_document = this.output_document;
+        }
+        if (!output_document)
+        {
+            return;
+        }
+        var node = output_document.getElementById("log");
+        if (node)
+        {
+            this.output_document = output_document;
+            this.output_node = node;
+        }
+    };
+
+    Output.prototype.show_status = function(test)
+    {
+        if (this.phase < this.STARTED)
+        {
+            this.init();
+        }
+        if (!this.enabled)
+        {
+            return;
+        }
+        if (this.phase < this.HAVE_RESULTS)
+        {
+            this.resolve_log();
+            this.phase = this.HAVE_RESULTS;
+        }
+        this.done_count++;
+        if (this.output_node)
+        {
+            if (this.done_count < 100
+            || (this.done_count < 1000 && this.done_count % 100 == 0)
+            || this.done_count % 1000 == 0) {
+                this.output_node.textContent = "Running, "
+                    + this.done_count + " complete, "
+                    + tests.num_pending + " remain";
+            }
+        }
+    };
+
+    Output.prototype.show_results = function (tests, harness_status)
+    {
+        if (this.phase >= this.COMPLETE) {
+            return;
+        }
+        if (!this.enabled)
+        {
+            return;
+        }
+        if (!this.output_node) {
+            this.resolve_log();
+        }
+        this.phase = this.COMPLETE;
+
+        var log = this.output_node;
+        if (!log)
+        {
+            return;
+        }
+        var output_document = this.output_document;
+
+        while (log.lastChild)
+        {
+            log.removeChild(log.lastChild);
+        }
+
+        if (script_prefix != null) {
+            var stylesheet = output_document.createElementNS(xhtml_ns, "link");
+            stylesheet.setAttribute("rel", "stylesheet");
+            stylesheet.setAttribute("href", script_prefix + "testharness.css");
+            var heads = output_document.getElementsByTagName("head");
+            if (heads.length) {
+                heads[0].appendChild(stylesheet);
+            }
+        }
+
+        var status_text = {};
+        status_text[Test.prototype.PASS] = "Pass";
+        status_text[Test.prototype.FAIL] = "Fail";
+        status_text[Test.prototype.TIMEOUT] = "Timeout";
+        status_text[Test.prototype.NOTRUN] = "Not Run";
+
+        var status_number = {};
+        forEach(tests, function(test) {
+                    var status = status_text[test.status];
+                    if (status_number.hasOwnProperty(status))
+                    {
+                        status_number[status] += 1;
+                    } else {
+                        status_number[status] = 1;
+                    }
+                });
+
+        function status_class(status)
+        {
+            return status.replace(/\s/g, '').toLowerCase();
+        }
+
+        var summary_template = ["section", {"id":"summary"},
+                                ["h2", {}, "Summary"],
+                                ["p", {}, "Found ${num_tests} tests"],
+                                function(vars) {
+                                    var rv = [["div", {}]];
+                                    var i=0;
+                                    while (status_text.hasOwnProperty(i)) {
+                                        if (status_number.hasOwnProperty(status_text[i])) {
+                                            var status = status_text[i];
+                                            rv[0].push(["div", {"class":status_class(status)},
+                                                        ["label", {},
+                                                         ["input", {type:"checkbox", checked:"checked"}],
+                                                         status_number[status] + " " + status]]);
+                                        }
+                                        i++;
+                                    }
+                                    return rv;
+                                }];
+
+        log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
+
+        forEach(output_document.querySelectorAll("section#summary label"),
+                function(element)
+                {
+                    on_event(element, "click",
+                             function(e)
+                             {
+                                 if (output_document.getElementById("results") === null)
+                                 {
+                                     e.preventDefault();
+                                     return;
+                                 }
+                                 var result_class = element.parentNode.getAttribute("class");
+                                 var style_element = output_document.querySelector("style#hide-" + result_class);
+                                 var input_element = element.querySelector("input");
+                                 if (!style_element && !input_element.checked) {
+                                     style_element = output_document.createElementNS(xhtml_ns, "style");
+                                     style_element.id = "hide-" + result_class;
+                                     style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}";
+                                     output_document.body.appendChild(style_element);
+                                 } else if (style_element && input_element.checked) {
+                                     style_element.parentNode.removeChild(style_element);
+                                 }
+                             });
+                });
+
+        // This use of innerHTML plus manual escaping is not recommended in
+        // general, but is necessary here for performance.  Using textContent
+        // on each individual <td> adds tens of seconds of execution time for
+        // large test suites (tens of thousands of tests).
+        function escape_html(s)
+        {
+            return s.replace(/\&/g, "&")
+                .replace(/</g, "<")
+                .replace(/"/g, """)
+                .replace(/'/g, "'");
+        }
+
+        function has_assertions()
+        {
+            for (var i = 0; i < tests.length; i++) {
+                if (tests[i].properties.hasOwnProperty("assert")) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        function get_assertion(test)
+        {
+            if (test.properties.hasOwnProperty("assert")) {
+                if (Array.isArray(test.properties.assert)) {
+                    return test.properties.assert.join(' ');
+                }
+                return test.properties.assert;
+            }
+            return '';
+        }
+
+        log.appendChild(document.createElementNS(xhtml_ns, "section"));
+        var assertions = has_assertions();
+        var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">"
+            + "<thead><tr><th>Result</th><th>Test Name</th>"
+            + (assertions ? "<th>Assertion</th>" : "")
+            + "<th>Message</th></tr></thead>"
+            + "<tbody>";
+        for (var i = 0; i < tests.length; i++) {
+            html += '<tr class="'
+                + escape_html(status_class(status_text[tests[i].status]))
+                + '"><td>'
+                + escape_html(status_text[tests[i].status])
+                + "</td><td>"
+                + escape_html(tests[i].name)
+                + "</td><td>"
+                + (assertions ? escape_html(get_assertion(tests[i])) + "</td><td>" : "")
+                + escape_html(tests[i].message ? tests[i].message : " ")
+                + "</td></tr>";
+        }
+        html += "</tbody></table>";
+        try {
+            log.lastChild.innerHTML = html;
+        } catch (e) {
+            log.appendChild(document.createElementNS(xhtml_ns, "p"))
+               .textContent = "Setting innerHTML for the log threw an exception.";
+            log.appendChild(document.createElementNS(xhtml_ns, "pre"))
+               .textContent = html;
+        }
+    };
+
+    var output = new Output();
+    add_start_callback(function (properties) {output.init(properties);});
+    add_result_callback(function (test) {output.show_status(tests);});
+    add_completion_callback(function (tests, harness_status) {output.show_results(tests, harness_status);});
+
+    /*
+     * Template code
+     *
+     * A template is just a javascript structure. An element is represented as:
+     *
+     * [tag_name, {attr_name:attr_value}, child1, child2]
+     *
+     * the children can either be strings (which act like text nodes), other templates or
+     * functions (see below)
+     *
+     * A text node is represented as
+     *
+     * ["{text}", value]
+     *
+     * String values have a simple substitution syntax; ${foo} represents a variable foo.
+     *
+     * It is possible to embed logic in templates by using a function in a place where a
+     * node would usually go. The function must either return part of a template or null.
+     *
+     * In cases where a set of nodes are required as output rather than a single node
+     * with children it is possible to just use a list
+     * [node1, node2, node3]
+     *
+     * Usage:
+     *
+     * render(template, substitutions) - take a template and an object mapping
+     * variable names to parameters and return either a DOM node or a list of DOM nodes
+     *
+     * substitute(template, substitutions) - take a template and variable mapping object,
+     * make the variable substitutions and return the substituted template
+     *
+     */
+
+    function is_single_node(template)
+    {
+        return typeof template[0] === "string";
+    }
+
+    function substitute(template, substitutions)
+    {
+        if (typeof template === "function") {
+            var replacement = template(substitutions);
+            if (replacement)
+            {
+                var rv = substitute(replacement, substitutions);
+                return rv;
+            }
+            else
+            {
+                return null;
+            }
+        }
+        else if (is_single_node(template))
+        {
+            return substitute_single(template, substitutions);
+        }
+        else
+        {
+            return filter(map(template, function(x) {
+                                  return substitute(x, substitutions);
+                              }), function(x) {return x !== null;});
+        }
+    }
+
+    function substitute_single(template, substitutions)
+    {
+        var substitution_re = /\${([^ }]*)}/g;
+
+        function do_substitution(input) {
+            var components = input.split(substitution_re);
+            var rv = [];
+            for (var i=0; i<components.length; i+=2)
+            {
+                rv.push(components[i]);
+                if (components[i+1])
+                {
+                    rv.push(String(substitutions[components[i+1]]));
+                }
+            }
+            return rv;
+        }
+
+        var rv = [];
+        rv.push(do_substitution(String(template[0])).join(""));
+
+        if (template[0] === "{text}") {
+            substitute_children(template.slice(1), rv);
+        } else {
+            substitute_attrs(template[1], rv);
+            substitute_children(template.slice(2), rv);
+        }
+
+        function substitute_attrs(attrs, rv)
+        {
+            rv[1] = {};
+            for (var name in template[1])
+            {
+                if (attrs.hasOwnProperty(name))
+                {
+                    var new_name = do_substitution(name).join("");
+                    var new_value = do_substitution(attrs[name]).join("");
+                    rv[1][new_name] = new_value;
+                };
+            }
+        }
+
+        function substitute_children(children, rv)
+        {
+            for (var i=0; i<children.length; i++)
+            {
+                if (children[i] instanceof Object) {
+                    var replacement = substitute(children[i], substitutions);
+                    if (replacement !== null)
+                    {
+                        if (is_single_node(replacement))
+                        {
+                            rv.push(replacement);
+                        }
+                        else
+                        {
+                            extend(rv, replacement);
+                        }
+                    }
+                }
+                else
+                {
+                    extend(rv, do_substitution(String(children[i])));
+                }
+            }
+            return rv;
+        }
+
+        return rv;
+    }
+
+ function make_dom_single(template, doc)
+ {
+     var output_document = doc || document;
+     if (template[0] === "{text}")
+     {
+         var element = output_document.createTextNode("");
+         for (var i=1; i<template.length; i++)
+         {
+             element.data += template[i];
+         }
+     }
+     else
+     {
+         var element = output_document.createElementNS(xhtml_ns, template[0]);
+         for (var name in template[1]) {
+             if (template[1].hasOwnProperty(name))
+             {
+                 element.setAttribute(name, template[1][name]);
+             }
+         }
+         for (var i=2; i<template.length; i++)
+         {
+             if (template[i] instanceof Object)
+             {
+                 var sub_element = make_dom(template[i]);
+                 element.appendChild(sub_element);
+             }
+             else
+             {
+                 var text_node = output_document.createTextNode(template[i]);
+                 element.appendChild(text_node);
+             }
+         }
+     }
+
+     return element;
+ }
+
+
+
+ function make_dom(template, substitutions, output_document)
+    {
+        if (is_single_node(template))
+        {
+            return make_dom_single(template, output_document);
+        }
+        else
+        {
+            return map(template, function(x) {
+                           return make_dom_single(x, output_document);
+                       });
+        }
+    }
+
+ function render(template, substitutions, output_document)
+    {
+        return make_dom(substitute(template, substitutions), output_document);
+    }
+
+    /*
+     * Utility funcions
+     */
+    function assert(expected_true, function_name, description, error, substitutions)
+    {
+        if (expected_true !== true)
+        {
+            throw new AssertionError(make_message(function_name, description,
+                                                  error, substitutions));
+        }
+    }
+
+    function AssertionError(message)
+    {
+        this.message = message;
+    }
+
+    function make_message(function_name, description, error, substitutions)
+    {
+        for (var p in substitutions) {
+            if (substitutions.hasOwnProperty(p)) {
+                substitutions[p] = format_value(substitutions[p]);
+            }
+        }
+        var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
+                                   merge({function_name:function_name,
+                                          description:(description?description + " ":"")},
+                                          substitutions));
+        return node_form.slice(1).join("");
+    }
+
+    function filter(array, callable, thisObj) {
+        var rv = [];
+        for (var i=0; i<array.length; i++)
+        {
+            if (array.hasOwnProperty(i))
+            {
+                var pass = callable.call(thisObj, array[i], i, array);
+                if (pass) {
+                    rv.push(array[i]);
+                }
+            }
+        }
+        return rv;
+    }
+
+    function map(array, callable, thisObj)
+    {
+        var rv = [];
+        rv.length = array.length;
+        for (var i=0; i<array.length; i++)
+        {
+            if (array.hasOwnProperty(i))
+            {
+                rv[i] = callable.call(thisObj, array[i], i, array);
+            }
+        }
+        return rv;
+    }
+
+    function extend(array, items)
+    {
+        Array.prototype.push.apply(array, items);
+    }
+
+    function forEach (array, callback, thisObj)
+    {
+        for (var i=0; i<array.length; i++)
+        {
+            if (array.hasOwnProperty(i))
+            {
+                callback.call(thisObj, array[i], i, array);
+            }
+        }
+    }
+
+    function merge(a,b)
+    {
+        var rv = {};
+        var p;
+        for (p in a)
+        {
+            rv[p] = a[p];
+        }
+        for (p in b) {
+            rv[p] = b[p];
+        }
+        return rv;
+    }
+
+    function expose(object, name)
+    {
+        var components = name.split(".");
+        var target = window;
+        for (var i=0; i<components.length - 1; i++)
+        {
+            if (!(components[i] in target))
+            {
+                target[components[i]] = {};
+            }
+            target = target[components[i]];
+        }
+        target[components[components.length - 1]] = object;
+    }
+
+    function forEach_windows(callback) {
+        // Iterate of the the windows [self ... top, opener]. The callback is passed
+        // two objects, the first one is the windows object itself, the second one
+        // is a boolean indicating whether or not its on the same origin as the
+        // current window.
+        var cache = forEach_windows.result_cache;
+        if (!cache) {
+            cache = [[self, true]];
+            var w = self;
+            var i = 0;
+            var so;
+            var origins = location.ancestorOrigins;
+            while (w != w.parent)
+            {
+                w = w.parent;
+                // In WebKit, calls to parent windows' properties that aren't on the same
+                // origin cause an error message to be displayed in the error console but
+                // don't throw an exception. This is a deviation from the current HTML5
+                // spec. See: https://bugs.webkit.org/show_bug.cgi?id=43504
+                // The problem with WebKit's behavior is that it pollutes the error console
+                // with error messages that can't be caught.
+                //
+                // This issue can be mitigated by relying on the (for now) proprietary
+                // `location.ancestorOrigins` property which returns an ordered list of
+                // the origins of enclosing windows. See:
+                // https://trac.webkit.org/changeset/113945.
+                if(origins) {
+                    so = (location.origin == origins[i]);
+                }
+                else
+                {
+                    so = is_same_origin(w);
+                }
+                cache.push([w, so]);
+                i++;
+            }
+            w = window.opener;
+            if(w)
+            {
+                // window.opener isn't included in the `location.ancestorOrigins` prop.
+                // We'll just have to deal with a simple check and an error msg on WebKit
+                // browsers in this case.
+                cache.push([w, is_same_origin(w)]);
+            }
+            forEach_windows.result_cache = cache;
+        }
+
+        forEach(cache,
+                function(a)
+                {
+                    callback.apply(null, a);
+                });
+    }
+
+    function is_same_origin(w) {
+        try {
+            'random_prop' in w;
+            return true;
+        } catch(e) {
+            return false;
+        }
+    }
+
+    function supports_post_message(w)
+    {
+        var supports;
+        var type;
+        // Given IE  implements postMessage across nested iframes but not across
+        // windows or tabs, you can't infer cross-origin communication from the presence
+        // of postMessage on the current window object only.
+        //
+        // Touching the postMessage prop on a window can throw if the window is
+        // not from the same origin AND post message is not supported in that
+        // browser. So just doing an existence test here won't do, you also need
+        // to wrap it in a try..cacth block.
+        try
+        {
+            type = typeof w.postMessage;
+            if (type === "function")
+            {
+                supports = true;
+            }
+            // IE8 supports postMessage, but implements it as a host object which
+            // returns "object" as its `typeof`.
+            else if (type === "object")
+            {
+                supports = true;
+            }
+            // This is the case where postMessage isn't supported AND accessing a
+            // window property across origins does NOT throw (e.g. old Safari browser).
+            else
+            {
+                supports = false;
+            }
+        }
+        catch(e) {
+            // This is the case where postMessage isn't supported AND accessing a
+            // window property across origins throws (e.g. old Firefox browser).
+            supports = false;
+        }
+        return supports;
+    }
+})();
+// vim: set expandtab shiftwidth=4 tabstop=4:
diff --git a/test/testharnessreport.js b/test/testharnessreport.js
new file mode 100644
index 0000000..fa73347
--- /dev/null
+++ b/test/testharnessreport.js
@@ -0,0 +1,380 @@
+/*
+ * This file is intended for vendors to implement
+ * code needed to integrate testharness.js tests with their own test systems.
+ *
+ * The default implementation extracts metadata from the tests and validates 
+ * it against the cached version that should be present in the test source 
+ * file. If the cache is not found or is out of sync, source code suitable for
+ * caching the metadata is optionally generated.
+ *
+ * The cached metadata is present for extraction by test processing tools that
+ * are unable to execute javascript.
+ *
+ * Metadata is attached to tests via the properties parameter in the test
+ * constructor. See testharness.js for details.
+ *
+ * Typically test system integration will attach callbacks when each test has
+ * run, using add_result_callback(callback(test)), or when the whole test file
+ * has completed, using 
+ * add_completion_callback(callback(tests, harness_status)).
+ *
+ * For more documentation about the callback functions and the
+ * parameters they are called with see testharness.js
+ */
+
+
+
+var metadata_generator = {
+
+    currentMetadata: {},
+    cachedMetadata: false,
+    metadataProperties: ['help', 'assert', 'author'],
+    
+    error: function(message) {
+        var messageElement = document.createElement('p');
+        messageElement.setAttribute('class', 'error');
+        this.appendText(messageElement, message);
+        
+        var summary = document.getElementById('summary');
+        if (summary) {
+            summary.parentNode.insertBefore(messageElement, summary);
+        }
+        else {
+            document.body.appendChild(messageElement);
+        }
+    },
+
+    /**
+     * Ensure property value has contact information
+     */
+    validateContact: function(test, propertyName) {
+        var result = true;
+        var value = test.properties[propertyName];
+        var values = Array.isArray(value) ? value : [value];
+        for (var index = 0; index < values.length; index++) {
+            value = values[index];
+            var re = /(\S+)(\s*)<(.*)>(.*)/;
+            if (! re.test(value)) {
+                re = /(\S+)(\s+)(http[s]?:\/\/)(.*)/
+                if (! re.test(value)) {
+                    this.error('Metadata property "' + propertyName + 
+                        '" for test: "' + test.name +
+                        '" must have name and contact information ' +
+                        '("name <email>" or "name http(s)://")');
+                    result = false;
+                }
+            }
+        }
+        return result;
+    },
+    
+    /**
+     * Extract metadata from test object
+     */
+    extractFromTest: function(test) {
+        var testMetadata = {};
+        // filter out metadata from other properties in test
+        for (var metaIndex = 0; metaIndex < this.metadataProperties.length;
+             metaIndex++) {
+            var meta = this.metadataProperties[metaIndex];
+            if (test.properties.hasOwnProperty(meta)) {
+                if ('author' == meta) {
+                    this.validateContact(test, meta);
+                }
+                testMetadata[meta] = test.properties[meta];
+            }
+        }
+        return testMetadata;
+    },
+    
+    /**
+     * Compare cached metadata to extracted metadata
+     */
+    validateCache: function() {
+        for (var testName in this.currentMetadata) {
+            if (! this.cachedMetadata.hasOwnProperty(testName)) {
+                return false;
+            }
+            var testMetadata = this.currentMetadata[testName];
+            var cachedTestMetadata = this.cachedMetadata[testName];
+            delete this.cachedMetadata[testName];
+            
+            for (var metaIndex = 0; metaIndex < this.metadataProperties.length;
+                 metaIndex++) {
+                var meta = this.metadataProperties[metaIndex];
+                if (cachedTestMetadata.hasOwnProperty(meta) && 
+                    testMetadata.hasOwnProperty(meta)) {
+                    if (Array.isArray(cachedTestMetadata[meta])) {
+                      if (! Array.isArray(testMetadata[meta])) {
+                          return false;
+                      }
+                      if (cachedTestMetadata[meta].length == 
+                          testMetadata[meta].length) {
+                          for (var index = 0; 
+                               index < cachedTestMetadata[meta].length; 
+                               index++) {
+                              if (cachedTestMetadata[meta][index] != 
+                                  testMetadata[meta][index]) {
+                                  return false;
+                              }
+                          }
+                      }
+                      else {
+                          return false;
+                      }
+                    }
+                    else {
+                      if (Array.isArray(testMetadata[meta])) {
+                        return false;
+                      }
+                      if (cachedTestMetadata[meta] != testMetadata[meta]) {
+                        return false;
+                      }
+                    }
+                }
+                else if (cachedTestMetadata.hasOwnProperty(meta) || 
+                         testMetadata.hasOwnProperty(meta)) {
+                    return false;
+                }
+            }
+        }
+        for (var testName in this.cachedMetadata) {
+            return false;
+        }
+        return true;
+    },
+  
+    appendText: function(elemement, text) {
+        elemement.appendChild(document.createTextNode(text));
+    },
+  
+    jsonifyArray: function(arrayValue, indent) {
+        var output = '[';
+
+        if (1 == arrayValue.length) {
+            output += JSON.stringify(arrayValue[0]);
+        }
+        else {
+            for (var index = 0; index < arrayValue.length; index++) {
+                if (0 < index) {
+                    output += ',\n  ' + indent;
+                }
+                output += JSON.stringify(arrayValue[index]);
+            }
+        }
+        output += ']';
+        return output;
+    },
+    
+    jsonifyObject: function(objectValue, indent) {
+        var output = '{';
+        
+        var count = 0;
+        for (var property in objectValue) {
+            ++count;
+            if (Array.isArray(objectValue[property]) || 
+                ('object' == typeof(value))) {
+                ++count;
+            }
+        }
+        if (1 == count) {
+            for (var property in objectValue) {
+                output += ' "' + property + '": '
+                          + JSON.stringify(objectValue[property])
+                          + ' ';
+            }
+        }
+        else {
+            var first = true;
+            for (var property in objectValue) {
+                if (! first) {
+                    output += ',';
+                }
+                first = false;
+                output += '\n  ' + indent + '"' + property + '": ';
+                var value = objectValue[property];
+                if (Array.isArray(value)) {
+                    output += this.jsonifyArray(value, indent + 
+                        '                '.substr(0, 5 + property.length));
+                }
+                else if ('object' == typeof(value)) {
+                    output += this.jsonifyObject(value, indent + '  ');
+                }
+                else {
+                    output += JSON.stringify(value);
+                }
+            }
+            if (1 < output.length) {
+                output += '\n' + indent;
+            }
+        }
+        output += '}';
+        return output;
+    },
+  
+    /**
+     * Generate javascript source code for captured metadata
+     * Metadata is in pretty-printed JSON format
+     */
+    generateSource: function() {
+        var source = 
+            '<script id="metadata_cache">/*\n' + 
+            this.jsonifyObject(this.currentMetadata, '') + '\n' + 
+            '*/</script>\n';
+        return source;
+    },
+    
+    /**
+     * Add element containing metadata source code
+     */
+    addSourceElement: function(event) {
+        var sourceWrapper = document.createElement('div');
+        sourceWrapper.setAttribute('id', 'metadata_source');
+
+        var instructions = document.createElement('p');
+        if (this.cachedMetadata) {
+            this.appendText(instructions, 
+                'Replace the existing <script id="metadata_cache"> element ' + 
+                'in the test\'s <head> with the following:');
+        }
+        else {
+            this.appendText(instructions, 
+                'Copy the following into the <head> element of the test ' +
+                'or the test\'s metadata sidecar file:');
+        }
+        sourceWrapper.appendChild(instructions);
+        
+        var sourceElement = document.createElement('pre');
+        this.appendText(sourceElement, this.generateSource());
+
+        sourceWrapper.appendChild(sourceElement);
+        
+        var messageElement = document.getElementById('metadata_issue');
+        messageElement.parentNode.insertBefore(sourceWrapper, 
+                                               messageElement.nextSibling);
+        messageElement.parentNode.removeChild(messageElement);
+
+        (event.preventDefault) ? event.preventDefault() : 
+                                 event.returnValue = false;
+    },
+    
+    /**
+     * Extract the metadata cache from the cache element if present
+     */
+    getCachedMetadata: function() {
+        var cacheElement = document.getElementById('metadata_cache');
+        
+        if (cacheElement) {
+            var cacheText = cacheElement.firstChild.nodeValue;
+            var openBrace = cacheText.indexOf('{');
+            var closeBrace = cacheText.lastIndexOf('}');
+            if ((-1 < openBrace) && (-1 < closeBrace)) {
+                cacheText = cacheText.slice(openBrace, closeBrace + 1);
+                try {
+                    this.cachedMetadata = JSON.parse(cacheText);
+                }
+                catch (exc) {
+                    this.cachedMetadata = 'Invalid JSON in Cached metadata. ';
+                }
+            }
+            else {
+                this.cachedMetadata = 'Metadata not found in cache element. ';
+            }
+        }
+    },
+    
+    /**
+     * Main entry point, extract metadata from tests, compare to cached version
+     * if present.
+     * If cache not present or differs from extrated metadata, generate an error
+     */
+    process: function(tests, harness_status) {
+        for (var index = 0; index < tests.length; index++) {
+            var test = tests[index];
+            if (this.currentMetadata.hasOwnProperty(test.name)) {
+                this.error('Duplicate test name: ' + test.name);
+            }
+            else {
+                this.currentMetadata[test.name] = this.extractFromTest(test);
+            }
+        }
+
+        this.getCachedMetadata();
+        
+        var message = null;
+        var messageClass = 'warning';
+        var showSource = false;
+        
+        if (0 == tests.length) {
+            if (this.cachedMetadata) {
+                message = 'Cached metadata present but no tests. ';
+            }
+        }
+        else if (1 == tests.length) {
+            if (this.cachedMetadata) {
+                message = 'Single test files should not have cached metadata. ';
+            }
+            else {
+                var testMetadata = this.currentMetadata[tests[0].name];
+                var hasMetadata = false;
+                for (var meta in testMetadata) {
+                    hasMetadata |= testMetadata.hasOwnProperty(meta);
+                }
+                if (hasMetadata) {
+                    message = 'Single tests should not have metadata. ' +
+                              'Move metadata to <head>. ';
+                }
+            }
+        }
+        else {
+            if (this.cachedMetadata) {
+                messageClass = 'error';
+                if ('string' == typeof(this.cachedMetadata)) {
+                    message = this.cachedMetadata;
+                    showSource = true;
+                }
+                else if (! this.validateCache()) {
+                    message = 'Cached metadata out of sync. ';
+                    showSource = true;
+                }
+            }
+        }
+        
+        if (message) {
+            var messageElement = document.createElement('p');
+            messageElement.setAttribute('id', 'metadata_issue');
+            messageElement.setAttribute('class', messageClass);
+            this.appendText(messageElement, message);
+            
+            if (showSource) {
+                var link = document.createElement('a');
+                this.appendText(link, 'Click for source code.');
+                link.setAttribute('href', '#');
+                link.setAttribute('onclick', 
+                                  'metadata_generator.addSourceElement(event)');
+                messageElement.appendChild(link);
+            }
+            
+            var summary = document.getElementById('summary');
+            if (summary) {
+                summary.parentNode.insertBefore(messageElement, summary);
+            }
+            else {
+                var log = document.getElementById('log');
+                if (log) {
+                    log.appendChild(messageElement);
+                }
+            }
+        }
+    },
+
+    setup: function() {
+        add_completion_callback(
+            function (tests, harness_status) { 
+                metadata_generator.process(tests, harness_status)
+            });
+    }
+}
+
+metadata_generator.setup();
+// vim: set expandtab shiftwidth=4 tabstop=4:
diff --git a/test/tests.html b/test/tests.html
new file mode 100644
index 0000000..526351c
--- /dev/null
+++ b/test/tests.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Encoding API Tests</title>
+<link rel="stylesheet" href="testharness.css">
+</head>
+<body>
+<h1>Encoding API Tests</h1>
+<div id="log"></div>
+<script src="testharness.js"></script>
+<script src="testharnessreport.js"></script>
+<script>
+setup({explicit_timeout: true});
+</script>
+
+<script src="../lib/encoding-indexes.js"></script>
+<script src="../lib/encoding.js"></script>
+<script src="test-misc.js"></script>
+<script src="test-utf.js"></script>
+<!-- TODO: test for all single-byte encoding indexes -->
+
+<!-- TODO: gb18030 -->
+
+<script src="test-big5.js"></script>
+<script src="test-euc-jp.js"></script>
+<script src="test-iso-2022-jp.js"></script>
+<script src="test-shift_jis.js"></script>
+<script src="test-euc-kr.js"></script>
+<script src="test-x-user-defined.js"></script>
+</body>
+</html>
diff --git a/util/externs.js b/util/externs.js
new file mode 100644
index 0000000..aea77d4
--- /dev/null
+++ b/util/externs.js
@@ -0,0 +1,28 @@
+//
+// Externs for Closure Compiler
+// https://developers.google.com/closure/compiler/
+//
+// Usage:
+//   java -jar compiler.jar \
+//     --jscomp_warning reportUnknownTypes \
+//     --warning_level VERBOSE \
+//     --summary_detail_level 3 \
+//     --externs util/externs.js \
+//     lib/encoding.js
+//
+
+/**
+ * @param {string} name
+ * @return {*}
+ */
+function require(name) {}
+
+/**
+ * @type {Object}
+ */
+var module;
+
+/**
+ * @type {Object.<string,*>}
+ */
+module.exports;

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-text-encoding.git



More information about the Pkg-javascript-commits mailing list