view js/ext/domita.js @ 0:633c9cb05555

Origination.
author Atul Varma <varmaa@toolness.com>
date Sun, 07 Jun 2009 19:29:10 -0700
parents
children
line wrap: on
line source

// Copyright (C) 2008 Google Inc.
//
// 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.

/**
 * @fileoverview
 * A partially tamed browser object model based on
 * <a href="http://www.w3.org/TR/DOM-Level-2-HTML/Overview.html"
 * >DOM-Level-2-HTML</a> and specifically, the
 * <a href="http://www.w3.org/TR/DOM-Level-2-HTML/ecma-script-binding.html"
 * >ECMAScript Language Bindings</a>.
 *
 * Caveats:<ul>
 * <li>This is not a full implementation.
 * <li>Security Review is pending.
 * <li><code>===</code> and <code>!==</code> on node lists will not
 *   behave the same as with untamed node lists.  Specifically, it is
 *   not always true that {@code nodeA.childNodes === nodeA.childNodes}.
 * <li>Properties backed by setters/getters like {@code HTMLElement.innerHTML}
 *   will not appear to uncajoled code as DOM nodes do, since they are
 *   implemented using cajita property handlers.
 * </ul>
 *
 * <p>
 * TODO(ihab.awad): Our implementation of getAttribute (and friends)
 * is such that standard DOM attributes which we disallow for security
 * reasons (like 'form:enctype') are placed in the "virtual"
 * attributes map (this.node___.attributes___). They appear to be
 * settable and gettable, but their values are ignored and do not have
 * the expected semantics per the DOM API. This is because we do not
 * have a column in html4-defs.js stating that an attribute is valid
 * but explicitly blacklisted. Alternatives would be to always throw
 * upon access to these attributes; to make them always appear to be
 * null; etc. Revisit this decision if needed.
 *
 * @author mikesamuel@gmail.com
 * @requires console, document, window
 * @requires clearInterval, clearTimeout, setInterval, setTimeout
 * @requires ___, bridal, cajita, css, html, html4, unicode
 * @provides attachDocumentStub, plugin_dispatchEvent___
 * @overrides domitaModules
 */

var domitaModules;
if (!domitaModules) { domitaModules = {}; }

domitaModules.classUtils = function() {

  /**
   * Add setter and getter hooks so that the caja {@code node.innerHTML = '...'}
   * works as expected.
   */
  function exportFields(object, fields) {
    for (var i = fields.length; --i >= 0;) {
      var field = fields[i];
      var fieldUCamel = field.charAt(0).toUpperCase() + field.substring(1);
      var getterName = 'get' + fieldUCamel;
      var setterName = 'set' + fieldUCamel;
      var count = 0;
      if (object[getterName]) {
        ++count;
        ___.useGetHandler(
           object, field, object[getterName]);
      }
      if (object[setterName]) {
        ++count;
        ___.useSetHandler(
           object, field, object[setterName]);
      }
      if (!count) {
        throw new Error('Failed to export field ' + field + ' on ' + object);
      }
    }
  }

  /**
   * Makes the first a subclass of the second.
   */
  function extend(subClass, baseClass) {
    var noop = function () {};
    noop.prototype = baseClass.prototype;
    subClass.prototype = new noop();
    subClass.prototype.constructor = subClass;
  }

  return {
    exportFields: exportFields,
    extend: extend
  };
};

/** XMLHttpRequest or an equivalent on IE 6. */
domitaModules.XMLHttpRequestCtor = function (XMLHttpRequest, ActiveXObject) {
  if (XMLHttpRequest) {
    return XMLHttpRequest;
  } else if (ActiveXObject) {
    // The first time the ctor is called, find an ActiveX class supported by
    // this version of IE.
    var activeXClassId;
    return function ActiveXObjectForIE() {
      if (activeXClassId === void 0) {
        activeXClassId = null;
        /** Candidate Active X types. */
        var activeXClassIds = [
            'MSXML2.XMLHTTP.5.0', 'MSXML2.XMLHTTP.4.0',
            'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP',
            'MICROSOFT.XMLHTTP.1.0', 'MICROSOFT.XMLHTTP.1',
            'MICROSOFT.XMLHTTP'];
        for (var i = 0, n = activeXClassIds.length; i < n; i++) {
          var candidate = activeXClassIds[i];
          try {
            void new ActiveXObject(candidate);
            activeXClassId = candidate;
            break;
          } catch (e) {
            // do nothing; try next choice
          }
        }
        activeXClassIds = null;
      }
      return new ActiveXObject(activeXClassId);
    };
  } else {
    throw new Error('ActiveXObject not available');
  }
};

domitaModules.TameXMLHttpRequest = function(
    xmlHttpRequestMaker,
    uriCallback) {
  var classUtils = domitaModules.classUtils();

  // See http://www.w3.org/TR/XMLHttpRequest/

  // TODO(ihab.awad): Improve implementation (interleaving, memory leaks)
  // per http://www.ilinsky.com/articles/XMLHttpRequest/

  function TameXMLHttpRequest() {
    this.xhr___ = new xmlHttpRequestMaker();
    classUtils.exportFields(
        this,
        ['onreadystatechange', 'readyState', 'responseText', 'responseXML',
         'status', 'statusText']);
  }
  TameXMLHttpRequest.prototype.setOnreadystatechange = function (handler) {
    // TODO(ihab.awad): Do we need more attributes of the event than 'target'?
    // May need to implement full "tame event" wrapper similar to DOM events.
    var self = this;
    this.xhr___.onreadystatechange = function(event) {
      var evt = { target: self };
      return ___.callPub(handler, 'call', [void 0, evt]);
    };
    // Store for later direct invocation if need be
    this.handler___ = handler;
  };
  TameXMLHttpRequest.prototype.getReadyState = function () {
    // The ready state should be a number
    return Number(this.xhr___.readyState);
  };
  TameXMLHttpRequest.prototype.open = function (
      method, URL, opt_async, opt_userName, opt_password) {
    method = String(method);
    // The XHR interface does not tell us the MIME type in advance, so we
    // must assume the broadest possible.
    var safeUri = uriCallback.rewrite(String(URL), "*/*");
    // If the uriCallback rejects the URL, we throw an exception, but we do not
    // put the URI in the exception so as not to put the caller at risk of some
    // code in its stack sniffing the URI.
    if (safeUri === void 0) { throw 'URI violates security policy'; }
    switch (arguments.length) {
    case 2:
      this.async___ = true;
      this.xhr___.open(method, safeUri);
      break;
    case 3:
      this.async___ = opt_async;
      this.xhr___.open(method, safeUri, Boolean(opt_async));
      break;
    case 4:
      this.async___ = opt_async;
      this.xhr___.open(
          method, safeUri, Boolean(opt_async), String(opt_userName));
      break;
    case 5:
      this.async___ = opt_async;
      this.xhr___.open(
          method, safeUri, Boolean(opt_async), String(opt_userName),
          String(opt_password));
      break;
    default:
      throw 'XMLHttpRequest cannot accept ' + arguments.length + ' arguments';
      break;
    }
  };
  TameXMLHttpRequest.prototype.setRequestHeader = function (label, value) {
    this.xhr___.setRequestHeader(String(label), String(value));
  };
  TameXMLHttpRequest.prototype.send = function(opt_data) {
    if (arguments.length === 0) {
      // TODO(ihab.awad): send()-ing an empty string because send() with no
      // args does not work on FF3, others?
      this.xhr___.send('');
    } else if (typeof opt_data === 'string') {
      this.xhr___.send(opt_data);
    } else /* if XML document */ {
      // TODO(ihab.awad): Expect tamed XML document; unwrap and send
      this.xhr___.send('');
    }

    // Firefox does not call the 'onreadystatechange' handler in
    // the case of a synchronous XHR. We simulate this behavior by
    // calling the handler explicitly.
    if (this.xhr___.overrideMimeType) {
      // This is Firefox
      if (!this.async___ && this.handler___) {
        var evt = { target: this };
        ___.callPub(this.handler___, 'call', [void 0, evt]);
      }
    }
  };
  TameXMLHttpRequest.prototype.abort = function () {
    this.xhr___.abort();
  };
  TameXMLHttpRequest.prototype.getAllResponseHeaders = function () {
    var result = this.xhr___.getAllResponseHeaders();
    return (result === undefined || result === null) ?
      result : String(result);
  };
  TameXMLHttpRequest.prototype.getResponseHeader = function (headerName) {
    var result = this.xhr___.getResponseHeader(String(headerName));
    return (result === undefined || result === null) ?
      result : String(result);
  };
  TameXMLHttpRequest.prototype.getResponseText = function () {
    var result = this.xhr___.responseText;
    return (result === undefined || result === null) ?
      result : String(result);
  };
  TameXMLHttpRequest.prototype.getResponseXML = function () {
    // TODO(ihab.awad): Implement a taming layer for XML. Requires generalizing
    // the HTML node hierarchy as well so we have a unified implementation.
    return {};
  };
  TameXMLHttpRequest.prototype.getStatus = function () {
    var result = this.xhr___.status;
    return (result === undefined || result === null) ?
      result : Number(result);
  };
  TameXMLHttpRequest.prototype.getStatusText = function () {
    var result = this.xhr___.statusText;
    return (result === undefined || result === null) ?
      result : String(result);
  };
  TameXMLHttpRequest.prototype.toString = function () {
    return 'Not a real XMLHttpRequest';
  };
  ___.ctor(TameXMLHttpRequest, void 0, 'TameXMLHttpRequest');
  ___.all2(___.grantTypedGeneric, TameXMLHttpRequest.prototype,
           ['open', 'setRequestHeader', 'send', 'abort',
            'getAllResponseHeaders', 'getResponseHeader']);

  return TameXMLHttpRequest;
};

/**
 * Add a tamed document implementation to a Gadget's global scope.
 *
 * @param {string} idSuffix a string suffix appended to all node IDs.
 * @param {Object} uriCallback an object like <pre>{
 *       rewrite: function (uri, mimeType) { return safeUri }
 *     }</pre>.
 *     The rewrite function should be idempotent to allow rewritten HTML
 *     to be reinjected.
 * @param {Object} imports the gadget's global scope.
 * @param {Node} pseudoBodyNode an HTML node to act as the "body" of the
 *     virtual document provided to Cajoled code.
 * @param {Object} optPseudoWindowLocation a record containing the
 *     properties of the browser "window.location" object, which will
 *     be provided to the Cajoled code.
 */
var attachDocumentStub = (function () {
  // Array Remove - By John Resig (MIT Licensed)
  function arrayRemove(array, from, to) {
    var rest = array.slice((to || from) + 1 || array.length);
    array.length = from < 0 ? array.length + from : from;
    return array.push.apply(array, rest);
  }

  var tameNodeTrademark = cajita.Trademark('tameNode');
  var tameEventTrademark = cajita.Trademark('tameEvent');

  // Define a wrapper type for known safe HTML, and a trademarker.
  // This does not actually use the trademarking functions since trademarks
  // cannot be applied to strings.
  function Html(htmlFragment) { this.html___ = String(htmlFragment || ''); }
  Html.prototype.valueOf = Html.prototype.toString
      = function () { return this.html___; };
  function safeHtml(htmlFragment) {
    return (htmlFragment instanceof Html)
        ? htmlFragment.html___
        : html.escapeAttrib(String(htmlFragment || ''));
  }
  function blessHtml(htmlFragment) {
    return (htmlFragment instanceof Html)
        ? htmlFragment
        : new Html(htmlFragment);
  }

  var XML_SPACE = '\t\n\r ';

  var XML_NAME_PATTERN = new RegExp(
      '^[' + unicode.LETTER + '_:][' + unicode.LETTER + unicode.DIGIT + '.\\-_:'
      + unicode.COMBINING_CHAR + unicode.EXTENDER + ']*$');

  var XML_NMTOKEN_PATTERN = new RegExp(
      '^[' + unicode.LETTER + unicode.DIGIT + '.\\-_:'
      + unicode.COMBINING_CHAR + unicode.EXTENDER + ']+$');

  var XML_NMTOKENS_PATTERN = new RegExp(
      '^(?:[' + XML_SPACE + ']*[' + unicode.LETTER + unicode.DIGIT + '.\\-_:'
      + unicode.COMBINING_CHAR + unicode.EXTENDER + ']+)+[' + XML_SPACE + ']*$'
      );

  var JS_SPACE = '\t\n\r ';
  // An identifier that does not end with __.
  var JS_IDENT = '(?:[a-zA-Z_][a-zA-Z0-9$_]*[a-zA-Z0-9$]|[a-zA-Z])_?';
  var SIMPLE_HANDLER_PATTERN = new RegExp(
      '^[' + JS_SPACE + ']*'
      + '(return[' + JS_SPACE + ']+)?'  // Group 1 is present if it returns.
      + '(' + JS_IDENT + ')[' + JS_SPACE + ']*'  // Group 2 is a function name.
      // Which can be passed optionally this node, and optionally the event.
      + '\\((?:this'
        + '(?:[' + JS_SPACE + ']*,[' + JS_SPACE + ']*event)?'
        + '[' + JS_SPACE + ']*)?\\)'
      // And it can end with a semicolon.
      + '[' + JS_SPACE + ']*(?:;?[' + JS_SPACE + ']*)$');

  /**
   * Coerces the string to a valid XML Name.
   * @see http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name
   */
  function isXmlName(s) {
    return XML_NAME_PATTERN.test(s);
  }

  /**
   * Coerces the string to valid XML Nmtokens
   * @see http://www.w3.org/TR/2000/REC-xml-20001006#NT-Nmtokens
   */
  function isXmlNmTokens(s) {
    return XML_NMTOKENS_PATTERN.test(s);
  }

  // Trim whitespace from the beginning and end of a CSS string.

  function trimCssSpaces(input) {
    return input.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, '');
  }

  /**
   * The plain text equivalent of a CSS string body.
   * @param {string} s the body of a CSS string literal w/o quotes
   *     or CSS identifier.
   * @return {string} plain text.
   * {@updoc
   * $ decodeCssString('')
   * # ''
   * $ decodeCssString('foo')
   * # 'foo'
   * $ decodeCssString('foo\\\nbar\\\r\nbaz\\\rboo\\\ffar')
   * # 'foobarbazboofar'
   * $ decodeCssString('foo\\000a bar\\000Abaz')
   * # 'foo' + '\n' + 'bar' + '\u0ABA' + 'z'
   * $ decodeCssString('foo\\\\bar\\\'baz')
   * # "foo\\bar'baz"
   * }
   */
  function decodeCssString(s) {
    // Decode a CSS String literal.
    // From http://www.w3.org/TR/CSS21/grammar.html
    //     string1    \"([^\n\r\f\\"]|\\{nl}|{escape})*\"
    //     unicode    \\{h}{1,6}(\r\n|[ \t\r\n\f])?
    //     escape     {unicode}|\\[^\r\n\f0-9a-f]
    //     s          [ \t\r\n\f]+
    //     nl         \n|\r\n|\r|\f
    return s.replace(
        /\\(?:(\r\n?|\n|\f)|([0-9a-f]{1,6})(?:\r\n?|[ \t\n\f])?|(.))/gi,
        function (_, nl, hex, esc) {
          return esc || (nl ? '' : String.fromCharCode(parseInt(hex, 16)));
        });
  }

  /**
   * Sanitize the 'style' attribute value of an HTML element.
   *
   * @param styleAttrValue the value of a 'style' attribute, which we
   * assume has already been checked by the caller to be a plain String.
   *
   * @return a sanitized version of the attribute value.
   */
  function sanitizeStyleAttrValue(styleAttrValue) {
    var sanitizedDeclarations = [];
    var declarations = styleAttrValue.split(/;/g);

    for (var i = 0; declarations && i < declarations.length; i++) {
      var parts = declarations[i].split(':');
      var property = trimCssSpaces(parts[0]).toLowerCase();
      var value = trimCssSpaces(parts.slice(1).join(":"));
      if (css.properties.hasOwnProperty(property)
          && css.properties[property].test(value + ' ')) {
        sanitizedDeclarations.push(property + ': ' + value);
      }
    }

    return sanitizedDeclarations.join(' ; ');
  }

  function mimeTypeForAttr(tagName, attribName) {
    if (attribName === 'src') {
      if (tagName === 'img') { return 'image/*'; }
      if (tagName === 'script') { return 'text/javascript'; }
    }
    return '*/*';
  }

  // TODO(ihab.awad): Does this work on IE, where console output
  // goes to a DOM node?
  function assert(cond) {
    if (!cond) {
      if (typeof console !== 'undefined') {
        console.error('domita assertion failed');
        console.trace();
      }
      throw new Error();
    }
  }

  var classUtils = domitaModules.classUtils();

  var cssSealerUnsealerPair = cajita.makeSealerUnsealerPair();

  // Implementations of setTimeout, setInterval, clearTimeout, and
  // clearInterval that only allow simple functions as timeouts and
  // that treat timeout ids as capabilities.
  // This is safe even if accessed across frame since the same
  // trademark value is never used with more than one version of
  // setTimeout.
  var timeoutIdTrademark = cajita.Trademark('timeoutId');
  function tameSetTimeout(timeout, delayMillis) {
    // Existing browsers treat a timeout of null or undefined as a noop.
    var timeoutId;
    if (timeout) {
      if (typeof timeout === 'string') {
        throw new Error(
            'setTimeout called with a string.'
            + '  Please pass a function instead of a string of javascript');
      }
      timeoutId = setTimeout(
          function () { ___.callPub(timeout, 'call', [___.USELESS]); },
          delayMillis | 0);
    } else {
      // tameClearTimeout checks for NaN and handles it specially.
      timeoutId = NaN;
    }
    return ___.freeze(___.stamp(timeoutIdTrademark,
                                { timeoutId___: timeoutId }));
  }
  ___.frozenFunc(tameSetTimeout);
  function tameClearTimeout(timeoutId) {
    ___.guard(timeoutIdTrademark, timeoutId);
    var rawTimeoutId = timeoutId.timeoutId___;
    // Skip NaN values created for null timeouts above.
    if (rawTimeoutId === rawTimeoutId) { clearTimeout(rawTimeoutId); }
  }
  ___.frozenFunc(tameClearTimeout);
  var intervalIdTrademark = cajita.Trademark('intervalId');
  function tameSetInterval(interval, delayMillis) {
    // Existing browsers treat an interval of null or undefined as a noop.
    var intervalId;
    if (interval) {
      if (typeof interval === 'string') {
        throw new Error(
            'setInterval called with a string.'
            + '  Please pass a function instead of a string of javascript');
      }
      intervalId = setInterval(
          function () { ___.callPub(interval, 'call', [___.USELESS]); },
          delayMillis | 0);
    } else {
      intervalId = NaN;
    }
    return ___.freeze(___.stamp(intervalIdTrademark,
                                { intervalId___: intervalId }));
  }
  ___.frozenFunc(tameSetInterval);
  function tameClearInterval(intervalId) {
    ___.guard(intervalIdTrademark, intervalId);
    var rawIntervalId = intervalId.intervalId___;
    if (rawIntervalId === rawIntervalId) { clearInterval(rawIntervalId); }
  }
  ___.frozenFunc(tameClearInterval);

  function makeScrollable(element) {
    var overflow;
    if (element.currentStyle) {
      overflow = element.currentStyle.overflow;
    } else if (window.getComputedStyle) {
      overflow = window.getComputedStyle(element, void 0).overflow;
    } else {
      overflow = null;
    }
    switch (overflow && overflow.toLowerCase()) {
      case 'visible':
      case 'hidden':
        element.style.overflow = 'auto';
        break;
    }
  }

  /**
   * Moves the given pixel within the element's frame of reference as close to
   * the top-left-most pixel of the element's viewport as possible without
   * moving the viewport beyond the bounds of the content.
   * @param {number} x x-coord of a pixel in the element's frame of reference.
   * @param {number} y y-coord of a pixel in the element's frame of reference.
   */
  function tameScrollTo(element, x, y) {
    if (x !== +x || y !== +y || x < 0 || y < 0) {
      throw new Error('Cannot scroll to ' + x + ':' + typeof x + ','
                      + y + ' : ' + typeof y);
    }
    element.scrollLeft = x;
    element.scrollTop = y;
  }

  /**
   * Moves the origin of the given element's view-port by the given offset.
   * @param {number} dx a delta in pixels.
   * @param {number} dy a delta in pixels.
   */
  function tameScrollBy(element, dx, dy) {
    if (dx !== +dx || dy !== +dy) {
      throw new Error('Cannot scroll by ' + dx + ':' + typeof dx + ', '
                      + dy + ':' + typeof dy);
    }
    element.scrollLeft += dx;
    element.scrollTop += dy;
  }

  function guessPixelsFromCss(cssStr) {
    if (!cssStr) { return 0; }
    var m = cssStr.match(/^([0-9]+)/);
    return m ? +m[1] : 0;
  }

  function tameResizeTo(element, w, h) {
    if (w !== +w || h !== +h) {
      throw new Error('Cannot resize to ' + w + ':' + typeof w + ', '
                      + h + ':' + typeof h);
    }
    element.style.width = w + 'px';
    element.style.height = h + 'px';
  }

  function tameResizeBy(element, dw, dh) {
    if (dw !== +dw || dh !== +dh) {
      throw new Error('Cannot resize by ' + dw + ':' + typeof dw + ', '
                      + dh + ':' + typeof dh);
    }
    if (!dw && !dh) { return; }

    // scrollWidth is width + padding + border.
    // offsetWidth is width + padding + border, but excluding the non-visible
    // area.
    // clientWidth iw width + padding, and like offsetWidth, clips to the
    // viewport.
    // margin does not count in any of these calculations.
    //
    // scrollWidth/offsetWidth
    //   +------------+
    //   |            |
    //
    // +----------------+
    // |                | Margin-top
    // | +------------+ |
    // | |############| | Border-top
    // | |#+--------+#| |
    // | |#|        |#| | Padding-top
    // | |#| +----+ |#| |
    // | |#| |    | |#| | Height
    // | |#| |    | |#| |
    // | |#| +----+ |#| |
    // | |#|        |#| |
    // | |#+--------+#| |
    // | |############| |
    // | +------------+ |
    // |                |
    // +----------------+
    //
    //     |        |
    //     +--------+
    //     clientWidth (but excludes content outside viewport)

    var style = element.currentStyle;
    if (!style) {
      style = window.getComputedStyle(element, void 0);
    }

    // We guess the padding since it's not always expressed in px on IE
    var extraHeight = guessPixelsFromCss(style.paddingBottom)
        + guessPixelsFromCss(style.paddingTop);
    var extraWidth = guessPixelsFromCss(style.paddingLeft)
        + guessPixelsFromCss(style.paddingRight);

    var goalHeight = element.clientHeight + dh;
    var goalWidth = element.clientWidth + dw;

    var h = goalHeight - extraHeight;
    var w = goalWidth - extraWidth;

    if (dh) { element.style.height = Math.max(0, h) + 'px'; }
    if (dw) { element.style.width = Math.max(0, w) + 'px'; }

    // Correct if our guesses re padding and borders were wrong.
    // We may still not be able to resize if e.g. the deltas would take
    // a dimension negative.
    if (dh && element.clientHeight !== goalHeight) {
      var hError = element.clientHeight - goalHeight;
      element.style.height = Math.max(0, h - hError) + 'px';
    }
    if (dw && element.clientWidth !== goalWidth) {
      var wError = element.clientWidth - goalWidth;
      element.style.width = Math.max(0, w - wError) + 'px';
    }
  }

  // See above for a description of this function.
  function attachDocumentStub(
      idSuffix, uriCallback, imports, pseudoBodyNode, optPseudoWindowLocation) {
    if (arguments.length < 4) {
      throw new Error('arity mismatch: ' + arguments.length);
    }
    if (!optPseudoWindowLocation) {
        optPseudoWindowLocation = {};
    }
    var elementPolicies = {};
    elementPolicies.form = function (attribs) {
      // Forms must have a gated onsubmit handler or they must have an
      // external target.
      var sawHandler = false;
      for (var i = 0, n = attribs.length; i < n; i += 2) {
        if (attribs[i] === 'onsubmit') {
          sawHandler = true;
        }
      }
      if (!sawHandler) {
        attribs.push('onsubmit', 'return false');
      }
      return attribs;
    };
    elementPolicies.a = elementPolicies.area = function (attribs) {
      // Anchor tags must have a target.
      attribs.push('target', '_blank');
      return attribs;
    };


    /** Sanitize HTML applying the appropriate transformations. */
    function sanitizeHtml(htmlText) {
      var out = [];
      htmlSanitizer(htmlText, out);
      return out.join('');
    }
    var htmlSanitizer = html.makeHtmlSanitizer(
        function sanitizeAttributes(tagName, attribs) {
          for (var i = 0; i < attribs.length; i += 2) {
            var attribName = attribs[i];
            var value = attribs[i + 1];
            var atype = null, attribKey;
            if ((attribKey = tagName + ':' + attribName,
                 html4.ATTRIBS.hasOwnProperty(attribKey))
                || (attribKey = '*:' + attribName,
                    html4.ATTRIBS.hasOwnProperty(attribKey))) {
              atype = html4.ATTRIBS[attribKey];
              value = rewriteAttribute(tagName, attribName, atype, value);
            } else {
              value = null;
            }
            if (value !== null && value !== void 0) {
              attribs[i + 1] = value;
            } else {
              attribs.splice(i, 2);
              i -= 2;
            }
          }
          var policy = elementPolicies[tagName];
          if (policy && elementPolicies.hasOwnProperty(tagName)) {
            return policy(attribs);
          }
          return attribs;
        });

    /**
     * Undoes some of the changes made by sanitizeHtml, e.g. stripping ID
     * prefixes.
     */
    function tameInnerHtml(htmlText) {
      var out = [];
      innerHtmlTamer(htmlText, out);
      return out.join('');
    }
    var innerHtmlTamer = html.makeSaxParser({
        startTag: function (tagName, attribs, out) {
          out.push('<', tagName);
          for (var i = 0; i < attribs.length; i += 2) {
            var attribName = attribs[i];
            if (attribName === 'target') { continue; }
            var attribKey;
            var atype;
            if ((attribKey = tagName + ':' + attribName,
                html4.ATTRIBS.hasOwnProperty(attribKey))
                || (attribKey = '*:' + attribName,
                    html4.ATTRIBS.hasOwnProperty(attribKey))) {
              atype = html4.ATTRIBS[attribKey];
            } else {
              return;
            }
            var value = attribs[i + 1];
            switch (atype) {
              case html4.atype.ID:
              case html4.atype.IDREF:
              case html4.atype.IDREFS:
                if (value.length <= idSuffix.length
                    || (idSuffix
                        !== value.substring(value.length - idSuffix.length))) {
                  continue;
                }
                value = value.substring(0, value.length - idSuffix.length);
                break;
            }
            if (value !== null) {
              out.push(' ', attribName, '="', html.escapeAttrib(value), '"');
            }
          }
          out.push('>');
        },
        endTag: function (name, out) { out.push('</', name, '>'); },
        pcdata: function (text, out) { out.push(text); },
        rcdata: function (text, out) { out.push(text); },
        cdata: function (text, out) { out.push(text); }
      });

    var illegalSuffix = /__(?:\s|$)/;
    /**
     * Returns a normalized attribute value, or null if the attribute should
     * be omitted.
     * <p>This function satisfies the attribute rewriter interface defined in
     * {@link html-sanitizer.js}.  As such, the parameters are keys into
     * data structures defined in {@link html4-defs.js}.
     *
     * @param {string} tagName a canonical tag name.
     * @param {string} attribName a canonical tag name.
     * @param type as defined in html4-defs.js.
     *
     * @return {string|null} null to indicate that the attribute should not
     *   be set.
     */
    function rewriteAttribute(tagName, attribName, type, value) {
      switch (type) {
        case html4.atype.ID:
        case html4.atype.IDREF:
        case html4.atype.IDREFS:
          value = String(value);
          if (value && !illegalSuffix.test(value) && isXmlName(value)) {
            return value + idSuffix;
          }
          return null;
        case html4.atype.CLASSES:
        case html4.atype.GLOBAL_NAME:
        case html4.atype.LOCAL_NAME:
          value = String(value);
          if (value && !illegalSuffix.test(value) && isXmlNmTokens(value)) {
            return value;
          }
          return null;
        case html4.atype.SCRIPT:
          value = String(value);
          // Translate a handler that calls a simple function like
          //   return foo(this, event)

          // TODO(mikesamuel): integrate cajita compiler to allow arbitrary
          // cajita in event handlers.
          var match = value.match(SIMPLE_HANDLER_PATTERN);
          if (!match) { return null; }
          var doesReturn = match[1];
          var fnName = match[2];
          var pluginId = ___.getId(imports);
          value = (doesReturn ? 'return ' : '') + 'plugin_dispatchEvent___('
              + 'this, event, ' + pluginId + ', "'
              + fnName + '");';
          if (attribName === 'onsubmit') {
            value = 'try { ' + value + ' } finally { return false; }';
          }
          return value;
        case html4.atype.URI:
          value = String(value);
          if (!uriCallback) { return null; }
          // TODO(mikesamuel): determine mime type properly.
          return uriCallback.rewrite(
              value, mimeTypeForAttr(tagName, attribName)) || null;
        case html4.atype.STYLE:
          if ('function' !== typeof value) {
            return sanitizeStyleAttrValue(String(value));
          }
          var cssPropertiesAndValues = cssSealerUnsealerPair.unseal(value);
          if (!cssPropertiesAndValues) { return null; }

          var css = [];
          for (var i = 0; i < cssPropertiesAndValues.length; i += 2) {
            var propName = cssPropertiesAndValues[i];
            var propValue = cssPropertiesAndValues[i + 1];
            // If the propertyName differs between DOM and CSS, there will
            // be a semicolon between the two.
            // E.g., 'background-color;backgroundColor'
            // See CssTemplate.toPropertyValueList.
            var semi = propName.indexOf(';');
            if (semi >= 0) { propName = propName.substring(0, semi); }
            css.push(propName + ' : ' + propValue);
          }
          return css.join(' ; ');
        case html4.atype.FRAME_TARGET:
          // Frames are ambient, so disallow reference.
          return null;
        default:
          return String(value);
      }
    }

    function makeCache() {
      var cache = cajita.newTable(false);
      cache.set(null, null);
      cache.set(void 0, null);
      return cache;
    }

    var editableTameNodeCache = makeCache();
    var readOnlyTameNodeCache = makeCache();

    /**
     * returns a tame DOM node.
     * @param {Node} node
     * @param {boolean} editable
     * @see <a href="http://www.w3.org/TR/DOM-Level-2-HTML/html.html"
     *       >DOM Level 2</a>
     */
    function tameNode(node, editable) {
      if (node === null || node === void 0) { return null; }
      // TODO(mikesamuel): make sure it really is a DOM node

      var cache = editable ? editableTameNodeCache : readOnlyTameNodeCache;
      var tamed = cache.get(node);
      if (tamed !== void 0) {
        return tamed;
      }

      switch (node.nodeType) {
        case 1:  // Element
          var tagName = node.tagName.toLowerCase();
          switch (tagName) {
            case 'a':
              tamed = new TameAElement(node, editable);
              break;
            case 'form':
              tamed = new TameFormElement(node, editable);
              break;
            case 'select':
            case 'button':
            case 'option':
            case 'textarea':
            case 'input':
              tamed = new TameInputElement(node, editable);
              break;
            case 'img':
              tamed = new TameImageElement(node, editable);
              break;
            case 'script':
              tamed = new TameScriptElement(node, editable);
              break;
            case 'td':
            case 'tr':
            case 'thead':
            case 'tfoot':
            case 'tbody':
            case 'th':
              tamed = new TameTableCompElement(node, editable);
              break;
            case 'table':
              tamed = new TameTableElement(node, editable);
              break;
            default:
              if (!html4.ELEMENTS.hasOwnProperty(tagName)
                  || (html4.ELEMENTS[tagName] & html4.eflags.UNSAFE)) {
                // If an unrecognized or unsafe node, return a
                // placeholder that doesn't prevent tree navigation,
                // but that doesn't allow mutation or leak attribute
                // information.
                tamed = new TameOpaqueNode(node, editable);
              } else {
                tamed = new TameElement(node, editable, editable);
              }
              break;
          }
          break;
        case 2:  // Attr
          tamed = new TameAttrNode(node, editable);
          break;
        case 3:  // Text
          tamed = new TameTextNode(node, editable);
          break;
        case 8:  // Comment
          tamed = new TameCommentNode(node, editable);
          break;
        default:
          tamed = new TameOpaqueNode(node, editable);
          break;
      }

      if (node.nodeType === 1) {
        cache.set(node, tamed);
      }
      return tamed;
    }

    function tameRelatedNode(node, editable) {
      if (node === null || node === void 0) { return null; }
      // catch errors because node might be from a different domain
      try {
        for (var ancestor = node; ancestor; ancestor = ancestor.parentNode) {
          // TODO(mikesamuel): replace with cursors so that subtrees are
          // delegable.
          // TODO: handle multiple classes.
          if (idClass === ancestor.className) {
            return tameNode(node, editable);
          }
        }
      } catch (e) {}
      return null;
    }

    /**
     * Returns a NodeList like object.
     */
    function tameNodeList(nodeList, editable, opt_keyAttrib) {
      var tamed = [];
      var node;

      // Work around NamedNodeMap bugs in IE, Opera, and Safari as discussed
      // at http://code.google.com/p/google-caja/issues/detail?id=935
      var limit = nodeList.length;
      if (limit !== +limit) { limit = 1/0; }
      for (var i = 0; i < limit && (node = nodeList[i]); ++i) {
        node = tameNode(nodeList.item(i), editable);
        tamed[i] = node;
        // Make the node available via its name if doing so would not mask
        // any properties of tamed.
        var key = opt_keyAttrib && node.getAttribute(opt_keyAttrib);
        // TODO(mikesamuel): if key in tamed, we have an ambiguous match.
        // Include neither?  This may happen with radio buttons in a form's
        // elements list.
        if (key && !(key.charAt(key.length - 1) === '_' || (key in tamed)
                     || key === String(key & 0x7fffffff))) {
          tamed[key] = node;
        }
      }
      node = nodeList = null;

      tamed.item = ___.frozenFunc(function (k) {
        k &= 0x7fffffff;
        if (k !== k) { throw new Error(); }
        return tamed[k] || null;
      });
      // TODO(mikesamuel): if opt_keyAttrib, could implement getNamedItem
      return cajita.freeze(tamed);
    }

    function tameGetElementsByTagName(rootNode, tagName, editable) {
      tagName = String(tagName);
      if (tagName !== '*') {
        tagName = tagName.toLowerCase();
        if (!___.hasOwnProp(html4.ELEMENTS, tagName)
            || html4.ELEMENTS[tagName] & html4.ELEMENTS.UNSAFE) {
          // Allowing getElementsByTagName to work for opaque element types
          // would leak information about those elements.
          return new fakeNodeList([]);
        }
      }
      return tameNodeList(rootNode.getElementsByTagName(tagName), editable);
    }

    /**
     * Implements http://www.whatwg.org/specs/web-apps/current-work/#dom-document-getelementsbyclassname
     * using an existing implementation on browsers that have one.
     */
    function tameGetElementsByClassName(rootNode, className, editable) {
      className = String(className);

      // The quotes below are taken from the HTML5 draft referenced above.

      // "having obtained the classes by splitting a string on spaces"
      // Instead of using split, we use match with the global modifier so that
      // we don't have to remove leading and trailing spaces.
      var classes = className.match(/[^\t\n\f\r ]+/g);

      // Filter out classnames in the restricted namespace.
      for (var i = classes ? classes.length : 0; --i >= 0;) {
        var classi = classes[i];
        if (illegalSuffix.test(classi) || !isXmlNmTokens(classi)) {
          classes[i] = classes[classes.length - 1];
          --classes.length;
        }
      }

      if (!classes || classes.length === 0) {
        // "If there are no tokens specified in the argument, then the method
        //  must return an empty NodeList" [instead of all elements]
        // This means that
        //     htmlEl.ownerDocument.getElementsByClassName(htmlEl.className)
        // will return an HtmlCollection containing htmlElement iff
        // htmlEl.className contains a non-space character.
        return fakeNodeList([]);
      }

      // "unordered set of unique space-separated tokens representing classes"
      if (typeof rootNode.getElementsByClassName === 'function') {
        return tameNodeList(
            rootNode.getElementsByClassName(classes.join(' ')), editable);
      } else {
        // Add spaces around each class so that we can use indexOf later to find
        // a match.
        // This use of indexOf is strictly incorrect since
        // http://www.whatwg.org/specs/web-apps/current-work/#reflecting-content-attributes-in-dom-attributes
        // does not normalize spaces in unordered sets of unique space-separated
        // tokens.  This is not a problem since HTML5 compliant implementations
        // already have a getElementsByClassName implementation, and legacy
        // implementations do normalize according to comments on issue 935.

        // We assume standards mode, so the HTML5 requirement that
        //   "If the document is in quirks mode, then the comparisons for the
        //    classes must be done in an ASCII case-insensitive  manner,"
        // is not operative.
        var nClasses = classes.length;
        for (var i = nClasses; --i >= 0;) {
          classes[i] = ' ' + classes[i] + ' ';
        }

        // We comply with the requirement that the result is a list
        //   "containing all the elements in the document, in tree order,"
        // since the spec for getElementsByTagName has the same language.
        var candidates = rootNode.getElementsByTagName('*');
        var matches = [];
        var limit = candidates.length;
        if (limit !== +limit) { limit = 1/0; }  // See issue 935
        candidate_loop:
        for (var j = 0, candidate, k = -1;
             j < limit && (candidate = candidates[j]);
             ++j) {
          var candidateClass = ' ' + candidate.className + ' ';
          for (var i = nClasses; --i >= 0;) {
            if (-1 === candidateClass.indexOf(classes[i])) {
              continue candidate_loop;
            }
          }
          var tamed = tameNode(candidate, editable);
          if (tamed) {
            matches[++k] = tamed;
          }
        }
        // "the method must return a live NodeList object"
        return fakeNodeList(matches);
      }
    }

    function makeEventHandlerWrapper(thisNode, listener) {
      if ('function' !== typeof listener
          // Allow disfunctions
          && !('object' === (typeof listener) && listener !== null
               && ___.canCallPub(listener, 'call'))) {
        throw new Error('Expected function not ' + typeof listener);
      }
      function wrapper(event) {
        return plugin_dispatchEvent___(
            thisNode, event, ___.getId(imports), listener);
      }
      return wrapper;
    }

    var NOT_EDITABLE = "Node not editable.";
    var INVALID_SUFFIX = "Property names may not end in '__'.";
    var UNSAFE_TAGNAME = "Unsafe tag name.";
    var UNKNOWN_TAGNAME = "Unknown tag name.";

    // Implementation of EventTarget::addEventListener
    function tameAddEventListener(name, listener, useCapture) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      if (!this.wrappedListeners___) { this.wrappedListeners___ = []; }
      useCapture = Boolean(useCapture);
      var wrappedListener = makeEventHandlerWrapper(this.node___, listener);
      wrappedListener = bridal.addEventListener(
          this.node___, name, wrappedListener, useCapture);
      wrappedListener.originalListener___ = listener;
      this.wrappedListeners___.push(wrappedListener);
    }

    // Implementation of EventTarget::removeEventListener
    function tameRemoveEventListener(name, listener, useCapture) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      if (!this.wrappedListeners___) { return; }
      var wrappedListener = null;
      for (var i = this.wrappedListeners___.length; --i >= 0;) {
        if (this.wrappedListeners___[i].originalListener___ === listener) {
          wrappedListener = this.wrappedListeners___[i];
          arrayRemove(this.wrappedListeners___, i, i);
          break;
        }
      }
      if (!wrappedListener) { return; }
      bridal.removeEventListener(
          this.node___, name, wrappedListener, useCapture);
    }

    // A map of tamed node classes, keyed by DOM Level 2 standard name, which
    // will be exposed to the client.
    var nodeClasses = {};

    var tameNodeFields = [
        'nodeType', 'nodeValue', 'nodeName', 'firstChild',
        'lastChild', 'nextSibling', 'previousSibling', 'parentNode',
        'ownerDocument', 'childNodes', 'attributes'];

    /**
     * Base class for a Node wrapper.  Do not create directly -- use the
     * tameNode factory instead.
     * @param {boolean} editable true if the node's value, attributes, children,
     *     or custom properties are mutable.
     * @constructor
     */
    function TameNode(editable) {
      this.editable___ = editable;
      ___.stamp(tameNodeTrademark, this, true);
      classUtils.exportFields(this, tameNodeFields);
    }
    TameNode.prototype.getOwnerDocument = function () {
      // TODO(mikesamuel): upward navigation breaks capability discipline.
      if (!this.editable___ && tameDocument.editable___) {
        throw new Error(NOT_EDITABLE);
      }
      return tameDocument;
    };
    nodeClasses.Node = TameNode;
    ___.ctor(TameNode, void 0, 'TameNode');
    // abstract TameNode.prototype.getNodeType
    // abstract TameNode.prototype.getNodeName
    // abstract TameNode.prototype.getNodeValue
    // abstract TameNode.prototype.cloneNode
    // abstract TameNode.prototype.appendChild
    // abstract TameNode.prototype.insertBefore
    // abstract TameNode.prototype.removeChild
    // abstract TameNode.prototype.replaceChild
    // abstract TameNode.prototype.getFirstChild
    // abstract TameNode.prototype.getLastChild
    // abstract TameNode.prototype.getNextSibling
    // abstract TameNode.prototype.getPreviousSibling
    // abstract TameNode.prototype.getParentNode
    // abstract TameNode.prototype.getElementsByTagName
    // abstract TameNode.prototype.getElementsByClassName
    // abstract TameNode.prototype.getChildNodes
    // abstract TameNode.prototype.getAttributes
    var tameNodeMembers = [
        'getNodeType', 'getNodeValue', 'getNodeName', 'cloneNode',
        'appendChild', 'insertBefore', 'removeChild', 'replaceChild',
        'getFirstChild', 'getLastChild', 'getNextSibling', 'getPreviousSibling',
        'getElementsByClassName', 'getElementsByTagName',
        'getOwnerDocument',
        'dispatchEvent',
        'hasChildNodes'
        ];


    /**
     * A tame node that is backed by a real node.
     * @param {boolean} childrenEditable true iff the child list is mutable.
     * @constructor
     */
    function TameBackedNode(node, editable, childrenEditable) {
      if (!node) {
        throw new Error('Creating tame node with undefined native delegate');
      }
      this.node___ = node;
      this.childrenEditable___ = editable && childrenEditable;
      TameNode.call(this, editable);
    }
    classUtils.extend(TameBackedNode, TameNode);
    TameBackedNode.prototype.getNodeType = function () {
      return this.node___.nodeType;
    };
    TameBackedNode.prototype.getNodeName = function () {
      return this.node___.nodeName;
    };
    TameBackedNode.prototype.getNodeValue = function () {
      return this.node___.nodeValue;
    };
    TameBackedNode.prototype.cloneNode = function (deep) {
      var clone = bridal.cloneNode(this.node___, Boolean(deep));
      // From http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-3A0ED0A4
      //     "Note that cloning an immutable subtree results in a mutable copy"
      return tameNode(clone, true);
    };
    TameBackedNode.prototype.appendChild = function (child) {
      // Child must be editable since appendChild can remove it from its parent.
      cajita.guard(tameNodeTrademark, child);
      if (!this.childrenEditable___ || !child.editable___) {
        throw new Error(NOT_EDITABLE);
      }
      this.node___.appendChild(child.node___);
    };
    TameBackedNode.prototype.insertBefore = function (toInsert, child) {
      cajita.guard(tameNodeTrademark, toInsert);
      if (child === void 0) { child = null; }
      if (child !== null) { cajita.guard(tameNodeTrademark, child); }
      if (!this.childrenEditable___ || !toInsert.editable___) {
        throw new Error(NOT_EDITABLE);
      }
      this.node___.insertBefore(
          toInsert.node___, child !== null ? child.node___ : null);
    };
    TameBackedNode.prototype.removeChild = function (child) {
      cajita.guard(tameNodeTrademark, child);
      if (!this.childrenEditable___ || !child.editable___) {
        throw new Error(NOT_EDITABLE);
      }
      this.node___.removeChild(child.node___);
    };
    TameBackedNode.prototype.replaceChild = function (child, replacement) {
      cajita.guard(tameNodeTrademark, child);
      cajita.guard(tameNodeTrademark, replacement);
      if (!this.childrenEditable___ || !replacement.editable___) {
        throw new Error(NOT_EDITABLE);
      }
      this.node___.replaceChild(child.node___, replacement.node___);
    };
    TameBackedNode.prototype.getFirstChild = function () {
      return tameNode(this.node___.firstChild, this.childrenEditable___);
    };
    TameBackedNode.prototype.getLastChild = function () {
      return tameNode(this.node___.lastChild, this.childrenEditable___);
    };
    TameBackedNode.prototype.getNextSibling = function () {
      // TODO(mikesamuel): replace with cursors so that subtrees are delegable
      return tameNode(this.node___.nextSibling, this.editable___);
    };
    TameBackedNode.prototype.getPreviousSibling = function () {
      // TODO(mikesamuel): replace with cursors so that subtrees are delegable
      return tameNode(this.node___.previousSibling, this.editable___);
    };
    TameBackedNode.prototype.getParentNode = function () {
      var parent = this.node___.parentNode;
      if (parent === tameDocument.body___) {
        if (tameDocument.editable___ && !this.editable___) {
          // FIXME: return a non-editable version of body.
          throw new Error(NOT_EDITABLE);
        }
        return tameDocument.getBody();
      }
      return tameRelatedNode(this.node___.parentNode, this.editable___);
    };
    TameBackedNode.prototype.getElementsByTagName = function (tagName) {
      return tameGetElementsByTagName(
          this.node___, tagName, this.childrenEditable___);
    };
    TameBackedNode.prototype.getElementsByClassName = function (className) {
      return tameGetElementsByClassName(
          this.node___, className, this.childrenEditable___);
    };
    TameBackedNode.prototype.getChildNodes = function () {
      return tameNodeList(this.node___.childNodes, this.childrenEditable___);
    };
    TameBackedNode.prototype.getAttributes = function () {
      return tameNodeList(this.node___.attributes, this.editable___);
    };
    var endsWith__ = /__$/;
    // TODO(erights): Come up with some notion of a keeper chain so we can
    // say, "let every other keeper try to handle this first".
    TameBackedNode.prototype.handleRead___ = function (name) {
      name = String(name);
      if (endsWith__.test(name)) { return void 0; }
      var handlerName = name + '_getter___';
      if (this[handlerName]) {
        return this[handlerName]();
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName]();
      }
      if (___.hasOwnProp(this.node___.properties___, name)) {
        return this.node___.properties___[name];
      } else {
        return void 0;
      }
    };
    TameBackedNode.prototype.handleCall___ = function (name, args) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      var handlerName = name + '_handler___';
      if (this[handlerName]) {
        return this[handlerName].call(this, args);
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName].call(this, args);
      }
      if (___.hasOwnProp(this.node___.properties___, name)) {
        return this.node___.properties___[name].call(this, args);
      } else {
        throw new TypeError(name + ' is not a function.');
      }
    };
    TameBackedNode.prototype.handleSet___ = function (name, val) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      var handlerName = name + '_setter___';
      if (this[handlerName]) {
        return this[handlerName](val);
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName](val);
      }
      if (!this.node___.properties___) {
        this.node___.properties___ = {};
      }
      this[name + '_canEnum___'] = true;
      return this.node___.properties___[name] = val;
    };
    TameBackedNode.prototype.handleDelete___ = function (name) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      var handlerName = name + '_deleter___';
      if (this[handlerName]) {
        return this[handlerName]();
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName]();
      }
      if (this.node___.properties___) {
        return (
            delete this.node___.properties___[name]
            && delete this[name + '_canEnum___']);
      } else {
        return true;
      }
    };
    /**
     * @param {boolean} ownFlag ignored
     */
    TameBackedNode.prototype.handleEnum___ = function (ownFlag) {
      // TODO(metaweta): Add code to list all the other handled stuff we know
      // about.
      if (this.node___.properties___) {
        return cajita.allKeys(this.node___.properties___);
      }
      return [];
    };
    TameBackedNode.prototype.hasChildNodes = function () {
      return !!this.node___.hasChildNodes();
    };
    // http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget :
    // "The EventTarget interface is implemented by all Nodes"
    TameBackedNode.prototype.dispatchEvent = function dispatchEvent(evt) {
      cajita.guard(tameEventTrademark, evt);
      bridal.dispatchEvent(this.node___, evt.event___);
    };
    ___.ctor(TameBackedNode, TameNode, 'TameBackedNode');
    ___.all2(___.grantTypedGeneric, TameBackedNode.prototype, tameNodeMembers);
    if (document.documentElement.contains) {  // typeof is 'object' on IE
      TameBackedNode.prototype.contains = function (other) {
        cajita.guard(tameNodeTrademark, other);
        var otherNode = other.node___;
        return this.node___.contains(otherNode);
      };
    }
    if ('function' ===
        typeof document.documentElement.compareDocumentPosition) {
      /**
       * Speced in <a href="http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition">DOM-Level-3</a>.
       */
      TameBackedNode.prototype.compareDocumentPosition = function (other) {
        cajita.guard(tameNodeTrademark, other);
        var otherNode = other.node___;
        if (!otherNode) { return 0; }
        var bitmask = +this.node___.compareDocumentPosition(otherNode);
        // To avoid leaking information about the relative positioning of
        // different roots, if neither contains the other, then we mask out
        // the preceding/following bits.
        // 0x18 is (CONTAINS | CONTAINED)
        // 0x1f is all the bits documented at
        //     http://www.w3.org/TR/DOM-Level-3-Core/core.html#DocumentPosition
        //     except IMPLEMENTATION_SPECIFIC
        // 0x01 is DISCONNECTED
        /*
        if (!(bitmask & 0x18)) {
          // TODO: If they are not under the same virtual doc root, return
          // DOCUMENT_POSITION_DISCONNECTED instead of leaking information
          // about PRECEDING | FOLLOWING.
        }
        */
        // Firefox3 returns spurious PRECEDING and FOLLOWING bits for
        // disconnected trees.
        // https://bugzilla.mozilla.org/show_bug.cgi?id=486002
        if (bitmask & 1) {
          bitmask &= ~6;
        }
        return bitmask & 0x1f;
      };
      if (!___.hasOwnProp(TameBackedNode.prototype, 'contains')) {
        // http://www.quirksmode.org/blog/archives/2006/01/contains_for_mo.html
        TameBackedNode.prototype.contains = function (other) {
          var docPos = this.compareDocumentPosition(other);
          return !(!(docPos & 0x10) && docPos);
        };
      }
    }
    ___.all2(function (o, k) {
               if (___.hasOwnProp(o, k)) { ___.grantTypedGeneric(o, k);  }
             }, TameBackedNode.prototype,
             ['contains', 'compareDocumentPosition']);

    /**
     * A fake node that is not backed by a real DOM node.
     * @constructor
     */
    function TamePseudoNode(editable) {
      TameNode.call(this, editable);
      this.properties___ = {};
    }
    classUtils.extend(TamePseudoNode, TameNode);
    TamePseudoNode.prototype.appendChild =
    TamePseudoNode.prototype.insertBefore =
    TamePseudoNode.prototype.removeChild =
    TamePseudoNode.prototype.replaceChild = function (child) {
      cajita.log("Node not editable; no action performed.");
    };
    TamePseudoNode.prototype.getFirstChild = function () {
      var children = this.getChildNodes();
      return children.length ? children[0] : null;
    };
    TamePseudoNode.prototype.getLastChild = function () {
      var children = this.getChildNodes();
      return children.length ? children[children.length - 1] : null;
    };
    TamePseudoNode.prototype.getNextSibling = function () {
      var parentNode = this.getParentNode();
      if (!parentNode) { return null; }
      var siblings = parentNode.getChildNodes();
      for (var i = siblings.length - 1; --i >= 0;) {
        if (siblings[i] === this) { return siblings[i + 1]; }
      }
      return null;
    };
    TamePseudoNode.prototype.getPreviousSibling = function () {
      var parentNode = this.getParentNode();
      if (!parentNode) { return null; }
      var siblings = parentNode.getChildNodes();
      for (var i = siblings.length; --i >= 1;) {
        if (siblings[i] === this) { return siblings[i - 1]; }
      }
      return null;
    };
    TamePseudoNode.prototype.handleRead___ = function (name) {
      name = String(name);
      if (endsWith__.test(name)) { return void 0; }
      var handlerName = name + '_getter___';
      if (this[handlerName]) {
        return this[handlerName]();
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName]();
      }
      if (___.hasOwnProp(this.properties___, name)) {
        return this.properties___[name];
      } else {
        return void 0;
      }
    };
    TamePseudoNode.prototype.handleCall___ = function (name, args) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      var handlerName = name + '_handler___';
      if (this[handlerName]) {
        return this[handlerName].call(this, args);
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName].call(this, args);
      }
      if (___.hasOwnProp(this.properties___, name)) {
        return this.properties___[name].call(this, args);
      } else {
        throw new TypeError(name + ' is not a function.');
      }
    };
    TamePseudoNode.prototype.handleSet___ = function (name, val) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      var handlerName = name + '_setter___';
      if (this[handlerName]) {
        return this[handlerName](val);
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName](val);
      }
      if (!this.properties___) {
        this.properties___ = {};
      }
      this[name + '_canEnum___'] = true;
      return this.properties___[name] = val;
    };
    TamePseudoNode.prototype.handleDelete___ = function (name) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      var handlerName = name + '_deleter___';
      if (this[handlerName]) {
        return this[handlerName]();
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName]();
      }
      if (this.properties___) {
        return (
            delete this.properties___[name]
            && delete this[name + '_canEnum___']);
      } else {
        return true;
      }
    };
    TamePseudoNode.prototype.handleEnum___ = function (ownFlag) {
      // TODO(metaweta): Add code to list all the other handled stuff we know
      // about.
      if (this.properties___) {
        return cajita.allKeys(this.properties___);
      }
      return [];
    };
    TamePseudoNode.prototype.hasChildNodes = function () {
      return this.getFirstChild() != null;
    };
    ___.ctor(TamePseudoNode, TameNode, 'TamePseudoNode');
    ___.all2(___.grantTypedGeneric, TamePseudoNode.prototype, tameNodeMembers);


    function TamePseudoElement(
        tagName, tameDoc, childNodesGetter, parentNodeGetter, innerHTMLGetter,
        geometryDelegate, editable) {
      TamePseudoNode.call(this, editable);
      this.tagName___ = tagName;
      this.tameDoc___ = tameDoc;
      this.childNodesGetter___ = childNodesGetter;
      this.parentNodeGetter___ = parentNodeGetter;
      this.innerHTMLGetter___ = innerHTMLGetter;
      this.geometryDelegate___ = geometryDelegate;
      classUtils.exportFields(this, ['tagName', 'innerHTML']);
    }
    classUtils.extend(TamePseudoElement, TamePseudoNode);
    // TODO(mikesamuel): make nodeClasses work.
    TamePseudoElement.prototype.getNodeType = function () { return 1; };
    TamePseudoElement.prototype.getNodeName
        = function () { return this.tagName___; };
    TamePseudoElement.prototype.getTagName
        = function () { return this.tagName___; };
    TamePseudoElement.prototype.getNodeValue = function () { return null; };
    TamePseudoElement.prototype.getAttribute
        = function (attribName) { return null; };
    TamePseudoElement.prototype.setAttribute
        = function (attribName, value) { };
    TamePseudoElement.prototype.hasAttribute
        = function (attribName) { return false; };
    TamePseudoElement.prototype.removeAttribute
        = function (attribName) { };
    TamePseudoElement.prototype.getOwnerDocument
        = function () { return this.tameDoc___; };
    TamePseudoElement.prototype.getChildNodes
        = function () { return this.childNodesGetter___(); };
    TamePseudoElement.prototype.getAttributes
        = function () { return tameNodeList([], false); };
    TamePseudoElement.prototype.getParentNode
        = function () { return this.parentNodeGetter___(); };
    TamePseudoElement.prototype.getInnerHTML
        = function () { return this.innerHTMLGetter___(); };
    TamePseudoElement.prototype.getElementsByTagName = function (tagName) {
      tagName = String(tagName).toLowerCase();
      if (tagName === this.tagName___) {
        // Works since html, head, body, and title can't contain themselves.
        return fakeNodeList([]);
      }
      return this.getOwnerDocument().getElementsByTagName(tagName);
    };
    TamePseudoElement.prototype.getElementsByClassName = function (className) {
      return this.getOwnerDocument().getElementsByClassName(className);
    };
    TamePseudoElement.prototype.getBoundingClientRect = function () {
      return this.geometryDelegate___.getBoundingClientRect();
    };
    TamePseudoElement.prototype.getGeometryDelegate___ = function () {
      return this.geometryDelegate___;
    };
    TamePseudoElement.prototype.toString = function () {
      return '<' + this.tagName___ + '>';
    };
    ___.ctor(TamePseudoElement, TamePseudoNode, 'TamePseudoElement');
    ___.all2(___.grantTypedGeneric, TamePseudoElement.prototype,
             ['getTagName', 'getAttribute', 'setAttribute',
              'hasAttribute', 'removeAttribute',
              'getBoundingClientRect', 'getElementsByTagName']);

    function TameOpaqueNode(node, editable) {
      TameBackedNode.call(this, node, editable, editable);
    }
    classUtils.extend(TameOpaqueNode, TameBackedNode);
    TameOpaqueNode.prototype.getNodeValue
        = TameBackedNode.prototype.getNodeValue;
    TameOpaqueNode.prototype.getNodeType
        = TameBackedNode.prototype.getNodeType;
    TameOpaqueNode.prototype.getNodeName
        = TameBackedNode.prototype.getNodeName;
    TameOpaqueNode.prototype.getNextSibling
        = TameBackedNode.prototype.getNextSibling;
    TameOpaqueNode.prototype.getPreviousSibling
        = TameBackedNode.prototype.getPreviousSibling;
    TameOpaqueNode.prototype.getFirstChild
        = TameBackedNode.prototype.getFirstChild;
    TameOpaqueNode.prototype.getLastChild
        = TameBackedNode.prototype.getLastChild;
    TameOpaqueNode.prototype.getParentNode
        = TameBackedNode.prototype.getParentNode;
    TameOpaqueNode.prototype.getChildNodes
        = TameBackedNode.prototype.getChildNodes;
    TameOpaqueNode.prototype.getAttributes
        = function () { return tameNodeList([], false); };
    for (var i = tameNodeMembers.length; --i >= 0;) {
      var k = tameNodeMembers[i];
      if (!TameOpaqueNode.prototype.hasOwnProperty(k)) {
        TameOpaqueNode.prototype[k] = ___.frozenFunc(function () {
          throw new Error('Node is opaque');
        });
      }
    }
    ___.all2(___.grantTypedGeneric, TameOpaqueNode.prototype, tameNodeMembers);

    function TameAttrNode(node, editable) {
      assert(node.nodeType === 2);
      TameBackedNode.call(this, node, editable, editable);
      classUtils.exportFields(
          this, ['name', 'nodeValue', 'value', 'specified']);
    }
    classUtils.extend(TameAttrNode, TameBackedNode);
    nodeClasses.Attr = TameAttrNode;
    TameAttrNode.prototype.setNodeValue = function (value) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.nodeValue = String(value || '');
      return value;
    };
    TameAttrNode.prototype.getName = TameAttrNode.prototype.getNodeName;
    TameAttrNode.prototype.getValue = TameAttrNode.prototype.getNodeValue;
    TameAttrNode.prototype.setValue = TameAttrNode.prototype.setNodeValue;
    TameAttrNode.prototype.getSpecified = function () {
      return this.node___.specified;
    };
    TameAttrNode.prototype.toString = function () {
      return '#attr';
    };
    ___.ctor(TameAttrNode, TameBackedNode, 'TameAttrNode');

    function TameTextNode(node, editable) {
      assert(node.nodeType === 3);

      // The below should not be strictly necessary since childrenEditable for
      // TameScriptElements is always false, but it protects against tameNode
      // being called naively on a text node from container code.
      var pn = node.parentNode;
      if (editable && pn) {
        if (1 === pn.nodeType
            && (html4.ELEMENTS[pn.tagName.toLowerCase()]
                & html4.eflags.UNSAFE)) {
          // Do not allow mutation of text inside script elements.
          // See the testScriptLoading testcase for examples of exploits.
          editable = false;
        }
      }

      TameBackedNode.call(this, node, editable, editable);
      classUtils.exportFields(this, ['nodeValue', 'data']);
    }
    classUtils.extend(TameTextNode, TameBackedNode);
    nodeClasses.Text = TameTextNode;
    TameTextNode.prototype.setNodeValue = function (value) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.nodeValue = String(value || '');
      return value;
    };
    TameTextNode.prototype.getData = TameTextNode.prototype.getNodeValue;
    TameTextNode.prototype.setData = TameTextNode.prototype.setNodeValue;
    TameTextNode.prototype.toString = function () {
      return '#text';
    };
    ___.ctor(TameTextNode, TameBackedNode, 'TameTextNode');
    ___.all2(___.grantTypedGeneric, TameTextNode.prototype,
             ['setNodeValue', 'getData', 'setData']);

    function TameCommentNode(node, editable) {
      assert(node.nodeType === 8);
      TameBackedNode.call(this, node, editable, editable);
    }
    classUtils.extend(TameCommentNode, TameBackedNode);
    nodeClasses.CommentNode = TameCommentNode;
    TameCommentNode.prototype.toString = function () {
      return '#comment';
    };
    ___.ctor(TameCommentNode, TameBackedNode, 'TameCommentNode');

    function getAttributeType(tagName, attribName) {
      var attribKey;
      attribKey = tagName + ':' + attribName;
      if (html4.ATTRIBS.hasOwnProperty(attribKey)) {
        return html4.ATTRIBS[attribKey];
      }
      attribKey = '*:' + attribName;
      if (html4.ATTRIBS.hasOwnProperty(attribKey)) {
        return html4.ATTRIBS[attribKey];
      }
      return void 0;
    }

    /**
     * Plays the role of an Attr node for TameElement objects.
     */
    function TameBackedAttributeNode(elem, name){
      TameNode.call(this, false);
      classUtils.exportFields(this,
          ['name', 'specified', 'value', 'ownerElement']);
      this.name___ = name;
      this.ownerElement___ = elem;
    }
    classUtils.extend(TameBackedAttributeNode, TameNode);
    ___.ctor(TameBackedAttributeNode, TameNode, 'TameBackedAttributeNode');
    TameBackedAttributeNode.prototype.getNodeName =
    TameBackedAttributeNode.prototype.getName =
        function () { return this.name___; };
    TameBackedAttributeNode.prototype.getSpecified =
        function () { return this.ownerElement___.hasAttribute(this.name___); };
    TameBackedAttributeNode.prototype.getNodeValue =
    TameBackedAttributeNode.prototype.getValue =
        function () { return this.ownerElement___.getAttribute(this.name___); };
    TameBackedAttributeNode.prototype.getOwnerElement =
        function () { return this.ownerElement___; };
    TameBackedAttributeNode.prototype.getNodeType = function () { return 2; };
    TameBackedAttributeNode.prototype.cloneNode = function () {
      return new TameBackedAttributeNode(this.ownerElement___, this.name___);
    };
    TameBackedAttributeNode.prototype.appendChild =
    TameBackedAttributeNode.prototype.insertBefore =
    TameBackedAttributeNode.prototype.removeChild =
    TameBackedAttributeNode.prototype.replaceChild =
    TameBackedAttributeNode.prototype.getFirstChild =
    TameBackedAttributeNode.prototype.getLastChild =
    TameBackedAttributeNode.prototype.getNextSibling =
    TameBackedAttributeNode.prototype.getPreviousSibling =
    TameBackedAttributeNode.prototype.getParentNode =
    TameBackedAttributeNode.prototype.getElementsByTagName =
    TameBackedAttributeNode.prototype.getElementsByClassName =
    TameBackedAttributeNode.prototype.getChildNodes =
    TameBackedAttributeNode.prototype.getAttributes = function () {
      throw new Error ("Not implemented.");
    };

    function TameElement(node, editable, childrenEditable) {
      assert(node.nodeType === 1);
      TameBackedNode.call(this, node, editable, childrenEditable);
      classUtils.exportFields(
          this,
          ['className', 'id', 'innerHTML', 'tagName', 'style',
           'offsetParent', 'title', 'dir']);
    }
    classUtils.extend(TameElement, TameBackedNode);
    nodeClasses.Element = nodeClasses.HTMLElement = TameElement;
    TameElement.prototype.getId = function () {
      return this.getAttribute('id') || '';
    };
    TameElement.prototype.setId = function (newId) {
      return this.setAttribute('id', newId);
    };
    TameElement.prototype.getAttribute = function (attribName) {
      attribName = String(attribName).toLowerCase();
      var tagName = this.node___.tagName.toLowerCase();
      var atype = getAttributeType(tagName, attribName);
      if (atype === void 0) {
        // Unrecognized attribute; use virtual map
        if (this.node___.attributes___) {
          return this.node___.attributes___[attribName] || null;
        }
        return null;
      }
      var value = bridal.getAttribute(this.node___, attribName);
      if ('string' !== typeof value) { return value; }
      switch (atype) {
        case html4.atype.ID:
        case html4.atype.IDREF:
        case html4.atype.IDREFS:
          if (!value) { return null; }
          var n = idSuffix.length;
          var len = value.length;
          var end = len - n;
          if (end > 0 && idSuffix === value.substring(end, len)) {
            return value.substring(0, end);
          }
          return null;
        default:
          if ('' === value) {
            // IE creates attribute nodes for any attribute in the HTML schema
            // so even when they are deleted, there will be a value, usually
            // the empty string.
            var attr = bridal.getAttributeNode(this.node___, attribName);
            if (attr && !attr.specified) { return null; }
          }
          return value;
      }
    };
    TameElement.prototype.getAttributeNode = function (name) {
      return new TameBackedAttributeNode(this, name);
    };
    TameElement.prototype.hasAttribute = function (attribName) {
      attribName = String(attribName).toLowerCase();
      var tagName = this.node___.tagName.toLowerCase();
      var atype = getAttributeType(tagName, attribName);
      if (atype === void 0) {
        // Unrecognized attribute; use virtual map
        return !!(
            this.node___.attributes___ &&
            ___.hasOwnProp(this.node___.attributes___, attribName));
      } else {
        return bridal.hasAttribute(this.node___, attribName);
      }
    };
    TameElement.prototype.setAttribute = function (attribName, value) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      attribName = String(attribName).toLowerCase();
      var tagName = this.node___.tagName.toLowerCase();
      var atype = getAttributeType(tagName, attribName);
      if (atype === void 0) {
        // Unrecognized attribute; use virtual map
        if (!this.node___.attributes___) { this.node___.attributes___ = {}; }
        this.node___.attributes___[attribName] = String(value);
      } else {
        var sanitizedValue = rewriteAttribute(
            tagName, attribName, atype, value);
        if (sanitizedValue !== null) {
          bridal.setAttribute(this.node___, attribName, sanitizedValue);
        }
      }
      return value;
    };
    TameElement.prototype.removeAttribute = function (attribName) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      attribName = String(attribName).toLowerCase();
      var tagName = this.node___.tagName.toLowerCase();
      var atype = getAttributeType(tagName, attribName);
      if (atype === void 0) {
        // Unrecognized attribute; use virtual map
        if (this.node___.attributes___) {
          delete this.node___.attributes___[attribName];
        }
      } else {
        this.node___.removeAttribute(attribName);
      }
    };
    TameElement.prototype.getBoundingClientRect = function () {
      var elRect = bridal.getBoundingClientRect(this.node___);
      var vbody = bridal.getBoundingClientRect(this.getOwnerDocument().body___);
      var vbodyLeft = vbody.left, vbodyTop = vbody.top;
      return ({
                top: elRect.top - vbodyTop,
                left: elRect.left - vbodyLeft,
                right: elRect.right - vbodyLeft,
                bottom: elRect.bottom - vbodyTop
              });
    };
    TameElement.prototype.getClassName = function () {
      return this.getAttribute('class') || '';
    };
    TameElement.prototype.setClassName = function (classes) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return this.setAttribute('class', String(classes));
    };
    TameElement.prototype.getTitle = function () {
      return this.getAttribute('title') || '';
    };
    TameElement.prototype.setTitle = function (classes) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return this.setAttribute('title', String(classes));
    };
    TameElement.prototype.getDir = function () {
      return this.getAttribute('dir') || '';
    };
    TameElement.prototype.setDir = function (classes) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return this.setAttribute('dir', String(classes));
    };
    TameElement.prototype.getTagName = TameBackedNode.prototype.getNodeName;
    TameElement.prototype.getInnerHTML = function () {
      var tagName = this.node___.tagName.toLowerCase();
      if (!html4.ELEMENTS.hasOwnProperty(tagName)) {
        return '';  // unknown node
      }
      var flags = html4.ELEMENTS[tagName];
      var innerHtml = this.node___.innerHTML;
      if (flags & html4.eflags.CDATA) {
        innerHtml = html.escapeAttrib(innerHtml);
      } else if (flags & html4.eflags.RCDATA) {
        // Make sure we return PCDATA.
        // For RCDATA we only need to escape & if they're not part of an entity.
        innerHtml = html.normalizeRCData(innerHtml);
      } else {
        // If we blessed the resulting HTML, then this would round trip better
        // but it would still not survive appending, and it would propagate
        // event handlers where the setter of innerHTML does not expect it to.
        innerHtml = tameInnerHtml(innerHtml);
      }
      return innerHtml;
    };
    TameElement.prototype.setInnerHTML = function (htmlFragment) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      var tagName = this.node___.tagName.toLowerCase();
      if (!html4.ELEMENTS.hasOwnProperty(tagName)) { throw new Error(); }
      var flags = html4.ELEMENTS[tagName];
      if (flags & html4.eflags.UNSAFE) { throw new Error(); }
      var sanitizedHtml;
      if (flags & html4.eflags.RCDATA) {
        sanitizedHtml = html.normalizeRCData(String(htmlFragment || ''));
      } else {
        sanitizedHtml = (htmlFragment instanceof Html
                        ? safeHtml(htmlFragment)
                        : sanitizeHtml(String(htmlFragment || '')));
      }
      this.node___.innerHTML = sanitizedHtml;
      return htmlFragment;
    };
    TameElement.prototype.setStyle = function (style) {
      this.setAttribute('style', style);
      return this.getStyle();
    };
    TameElement.prototype.getStyle = function () {
      return new TameStyle(this.node___.style, this.editable___);
    };
    TameElement.prototype.updateStyle = function (style) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      var cssPropertiesAndValues = cssSealerUnsealerPair.unseal(style);
      if (!cssPropertiesAndValues) { throw new Error(); }

      var styleNode = this.node___.style;
      for (var i = 0; i < cssPropertiesAndValues.length; i += 2) {
        var propName = cssPropertiesAndValues[i];
        var propValue = cssPropertiesAndValues[i + 1];
        // If the propertyName differs between DOM and CSS, there will
        // be a semicolon between the two.
        // E.g., 'background-color;backgroundColor'
        // See CssTemplate.toPropertyValueList.
        var semi = propName.indexOf(';');
        if (semi >= 0) { propName = propName.substring(semi + 1); }
        styleNode[propName] = propValue;
      }
    };

    TameElement.prototype.getOffsetParent = function () {
      return tameRelatedNode(this.node___.offsetParent, this.editable___);
    };
    TameElement.prototype.getGeometryDelegate___ = function () {
      return this.node___;
    };
    TameElement.prototype.toString = function () {
      return '<' + this.node___.tagName + '>';
    };
    TameElement.prototype.addEventListener = tameAddEventListener;
    TameElement.prototype.removeEventListener = tameRemoveEventListener;
    ___.ctor(TameElement, TameBackedNode, 'TameElement');
    ___.all2(
       ___.grantTypedGeneric, TameElement.prototype,
       ['addEventListener', 'removeEventListener',
        'getAttribute', 'setAttribute',
        'removeAttribute', 'hasAttribute',
        'getAttributeNode',
        'getBoundingClientRect',
        'getClassName', 'setClassName', 'getId', 'setId',
        'getInnerHTML', 'setInnerHTML', 'updateStyle', 'getStyle', 'setStyle',
        'getTagName']);

    cajita.forOwnKeys({
      clientWidth: {
        get: function () { return this.getGeometryDelegate___().clientWidth; }
      },
      clientHeight: {
        get: function () { return this.getGeometryDelegate___().clientHeight; }
      },
      offsetLeft: {
        get: function () { return this.getGeometryDelegate___().offsetLeft; }
      },
      offsetTop: {
        get: function () { return this.getGeometryDelegate___().offsetTop; }
      },
      offsetWidth: {
        get: function () { return this.getGeometryDelegate___().offsetWidth; }
      },
      offsetHeight: {
        get: function () { return this.getGeometryDelegate___().offsetHeight; }
      },
      scrollLeft: {
        get: function () { return this.getGeometryDelegate___().scrollLeft; },
        set: function (x) {
          if (!this.editable___) { throw new Error(NOT_EDITABLE); }
          this.getGeometryDelegate___().scrollLeft = +x;
          return x;
        }
      },
      scrollTop: {
        get: function () { return this.getGeometryDelegate___().scrollTop; },
        set: function (y) {
          if (!this.editable___) { throw new Error(NOT_EDITABLE); }
          this.getGeometryDelegate___().scrollTop = +y;
          return y;
        }
      },
      scrollWidth: {
        get: function () { return this.getGeometryDelegate___().scrollWidth; }
      },
      scrollHeight: {
        get: function () { return this.getGeometryDelegate___().scrollHeight; }
      }
    }, ___.func(function (propertyName, def) {
      var setter = def.set || propertyOnlyHasGetter;
      ___.useGetHandler(TameElement.prototype, propertyName, def.get);
      ___.useSetHandler(TameElement.prototype, propertyName, setter);
      ___.useGetHandler(TamePseudoElement.prototype, propertyName, def.get);
      ___.useSetHandler(TamePseudoElement.prototype, propertyName, setter);
    }));

    // Register set handlers for onclick, onmouseover, etc.
    (function () {
      var attrNameRe = /:(.*)/;
      for (var html4Attrib in html4.ATTRIBS) {
        if (html4.atype.SCRIPT === html4.ATTRIBS[html4Attrib]) {
          (function (attribName) {
            ___.useSetHandler(
                TameElement.prototype,
                attribName,
                function eventHandlerSetter(listener) {
                  if (!this.editable___) { throw new Error(NOT_EDITABLE); }
                  if (!listener) {  // Clear the current handler
                    this.node___[attribName] = null;
                  } else {
                    // This handler cannot be copied from one node to another
                    // which is why getters are not yet supported.
                    this.node___[attribName] = makeEventHandlerWrapper(
                        this.node___, listener);
                  }
                  return listener;
                });
           })(html4Attrib.match(attrNameRe)[1]);
        }
      }
    })();

    function TameAElement(node, editable) {
      TameElement.call(this, node, editable, editable);
      classUtils.exportFields(this, ['href']);
    }
    classUtils.extend(TameAElement, TameElement);
    nodeClasses.HTMLAnchorElement = TameAElement;
    TameAElement.prototype.focus = function () {
      this.node___.focus();
    };
    TameAElement.prototype.getHref = function () {
      return this.node___.href;
    };
    TameAElement.prototype.setHref = function (href) {
      this.setAttribute('href', href);
      return href;
    };
    ___.ctor(TameAElement, TameElement, 'TameAElement');
    ___.all2(___.grantTypedGeneric, TameAElement.prototype,
             ['getHref', 'setHref', 'focus']);

    // http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-40002357
    function TameFormElement(node, editable) {
      TameElement.call(this, node, editable, editable);
      this.length = node.length;
      classUtils.exportFields(
          this,
          ['action', 'elements', 'enctype', 'method', 'target']);
    }
    classUtils.extend(TameFormElement, TameElement);
    nodeClasses.HTMLFormElement = TameFormElement;
    TameFormElement.prototype.submit = function () {
      return this.node___.submit();
    };
    TameFormElement.prototype.reset = function () {
      return this.node___.reset();
    };
    TameFormElement.prototype.getAction = function () {
      return this.getAttribute('action') || '';
    };
    TameFormElement.prototype.setAction = function (newVal) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return this.setAttribute('action', String(newVal));
    };
    TameFormElement.prototype.getElements = function () {
      return tameNodeList(this.node___.elements, this.editable___, 'name');
    };
    TameFormElement.prototype.getEnctype = function () {
      return this.getAttribute('enctype') || '';
    };
    TameFormElement.prototype.setEnctype = function (newVal) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return this.setAttribute('enctype', String(newVal));
    };
    TameFormElement.prototype.getMethod = function () {
      return this.getAttribute('method') || '';
    };
    TameFormElement.prototype.setMethod = function (newVal) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return this.setAttribute('method', String(newVal));
    };
    TameFormElement.prototype.getTarget = function () {
      return this.getAttribute('target') || '';
    };
    TameFormElement.prototype.setTarget = function (newVal) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return this.setAttribute('target', String(newVal));
    };
    TameFormElement.prototype.reset = function () {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.reset();
    };
    TameFormElement.prototype.submit = function () {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.submit();
    };
    ___.ctor(TameFormElement, TameElement, 'TameFormElement');
    ___.all2(___.grantTypedGeneric, TameFormElement.prototype,
             ['getElements', 'reset', 'submit']);


    function TameInputElement(node, editable) {
      TameElement.call(this, node, editable, editable);
      classUtils.exportFields(
          this,
          ['form', 'value', 'defaultValue',
           'checked', 'disabled', 'readOnly',
           'options', 'selected', 'selectedIndex',
           'name', 'accessKey', 'tabIndex', 'text',
           'defaultChecked', 'defaultSelected', 'maxLength',
           'size', 'type', 'index', 'label',
           'multiple', 'cols', 'rows']);
    }
    classUtils.extend(TameInputElement, TameElement);
    nodeClasses.HTMLInputElement = TameInputElement;
    TameInputElement.prototype.getChecked = function () {
      return this.node___.checked;
    };
    TameInputElement.prototype.setChecked = function (checked) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return (this.node___.checked = !!checked);
    };
    TameInputElement.prototype.getValue = function () {
      // For <option> elements, Firefox returns a value even when no value
      // attribute is present, using the contained text, but IE does not.
      var value = this.node___.value;
      return value === null || value === void 0 ? null : String(value);
    };
    TameInputElement.prototype.setValue = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.value = (
          newValue === null || newValue === void 0 ? '' : '' + newValue);
      return newValue;
    };
    TameInputElement.prototype.getDefaultValue = function () {
      var value = this.node___.defaultValue;
      return value === null || value === void 0 ? null : String(value);
    };
    TameInputElement.prototype.setDefaultValue = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.defaultValue = (
          newValue === null || newValue === void 0 ? '' : '' + newValue);
      return newValue;
    };
    TameInputElement.prototype.focus = function () {
      this.node___.focus();
    };
    TameInputElement.prototype.blur = function () {
      this.node___.blur();
    };
    TameInputElement.prototype.select = function () {
      this.node___.select();
    };
    TameInputElement.prototype.getForm = function () {
      return tameRelatedNode(this.node___.form, this.editable___);
    };
    TameInputElement.prototype.getDisabled = function () {
      return this.node___.disabled;
    };
    TameInputElement.prototype.setDisabled = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.disabled = newValue;
      return newValue;
    };
    TameInputElement.prototype.getReadOnly = function () {
      return this.node___.readOnly;
    };
    TameInputElement.prototype.setReadOnly = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.readOnly = newValue;
      return newValue;
    };
    TameInputElement.prototype.getOptions = function () {
      return tameNodeList(this.node___.options, this.editable___, 'name');
    };
    TameInputElement.prototype.getDefaultSelected = function () {
      return this.node___.defaultSelected;
    };
    TameInputElement.prototype.setDefaultSelected = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.defaultSelected = !!newValue;
      return newValue;
    };
    TameInputElement.prototype.getSelected = function () {
      return this.node___.selected;
    };
    TameInputElement.prototype.setSelected = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.selected = newValue;
      return newValue;
    };
    TameInputElement.prototype.getSelectedIndex = function () {
      return this.node___.selectedIndex;
    };
    TameInputElement.prototype.setSelectedIndex = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.selectedIndex = (newValue | 0);
      return newValue;
    };
    TameInputElement.prototype.getName = function () {
      return this.node___.name;
    };
    TameInputElement.prototype.setName = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.name = newValue;
      return newValue;
    };
    TameInputElement.prototype.getAccessKey = function () {
      return this.node___.accessKey;
    };
    TameInputElement.prototype.setAccessKey = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.accessKey = newValue;
      return newValue;
    };
    TameInputElement.prototype.getTabIndex = function () {
      return this.node___.tabIndex;
    };
    TameInputElement.prototype.getText = function () {
        return String(this.node___.text);
    };
    TameInputElement.prototype.setTabIndex = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.tabIndex = newValue;
      return newValue;
    };
    TameInputElement.prototype.getDefaultChecked = function () {
      return this.node___.defaultChecked;
    };
    TameInputElement.prototype.setDefaultChecked = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.defaultChecked = newValue;
      return newValue;
    };
    TameInputElement.prototype.getMaxLength = function () {
      return this.node___.maxLength;
    };
    TameInputElement.prototype.setMaxLength = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.maxLength = newValue;
      return newValue;
    };
    TameInputElement.prototype.getSize = function () {
      return this.node___.size;
    };
    TameInputElement.prototype.setSize = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.size = newValue;
      return newValue;
    };
    TameInputElement.prototype.getType = function () {
      return String(this.node___.type);
    };
    TameInputElement.prototype.setType = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.type = newValue;
      return newValue;
    };
    TameInputElement.prototype.getIndex = function () {
      return this.node___.index;
    };
    TameInputElement.prototype.setIndex = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.index = newValue;
      return newValue;
    };
    TameInputElement.prototype.getLabel = function () {
      return this.node___.label;
    };
    TameInputElement.prototype.setLabel = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.label = newValue;
      return newValue;
    };
    TameInputElement.prototype.getMultiple = function () {
      return this.node___.multiple;
    };
    TameInputElement.prototype.setMultiple = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.multiple = newValue;
      return newValue;
    };
    TameInputElement.prototype.getCols = function () {
      return this.node___.cols;
    };
    TameInputElement.prototype.setCols = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.cols = newValue;
      return newValue;
    };
    TameInputElement.prototype.getRows = function () {
      return this.node___.rows;
    };
    TameInputElement.prototype.setRows = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.rows = newValue;
      return newValue;
    };
    ___.ctor(TameInputElement, TameElement, 'TameInputElement');
    ___.all2(___.grantTypedGeneric, TameInputElement.prototype,
             ['getValue', 'setValue', 'focus', 'getForm', 'getType', 'select']);


    function TameImageElement(node, editable) {
      TameElement.call(this, node, editable, editable);
      classUtils.exportFields(this, ['src', 'alt']);
    }
    classUtils.extend(TameImageElement, TameElement);
    nodeClasses.HTMLImageElement = TameImageElement;
    TameImageElement.prototype.getSrc = function () {
      return this.node___.src;
    };
    TameImageElement.prototype.setSrc = function (src) {
      this.setAttribute('src', src);
      return src;
    };
    TameImageElement.prototype.getAlt = function () {
      return this.node___.alt;
    };
    TameImageElement.prototype.setAlt = function (alt) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.alt = String(alt);
      return alt;
    };
    ___.ctor(TameImageElement, TameElement, 'TameImageElement');
    ___.all2(___.grantTypedGeneric, TameImageElement.prototype,
             ['getSrc', 'setSrc', 'getAlt', 'setAlt']);

    /**
     * A script element wrapper that allows setting of a src that has been
     * rewritten by a URL policy, but not modifying of textual content.
     */
    function TameScriptElement(node, editable) {
      // Make the child list immutable so that text content can't be added
      // or removed.
      TameElement.call(this, node, editable, false);
      classUtils.exportFields(this, ['src']);
    }
    classUtils.extend(TameScriptElement, TameElement);
    nodeClasses.HTMLScriptElement = TameScriptElement;
    TameScriptElement.prototype.getSrc = function () {
      return this.node___.src;
    };
    TameScriptElement.prototype.setSrc = function (src) {
      this.setAttribute('src', src);
      return src;
    };
    ___.ctor(TameScriptElement, TameElement, 'TameScriptElement');


    function TameTableCompElement(node, editable) {
      TameElement.call(this, node, editable, editable);
      classUtils.exportFields(
          this,
          ['colSpan','cells','rowSpan','rows','rowIndex','align',
           'vAlign','nowrap']);
    }
    classUtils.extend(TameTableCompElement, TameElement);
    TameTableCompElement.prototype.getColSpan = function () {
      return this.node___.colSpan;
    };
    TameTableCompElement.prototype.setColSpan = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.colSpan = newValue;
      return newValue;
    };
    TameTableCompElement.prototype.getCells = function () {
      return tameNodeList(this.node___.cells, this.editable___);
    };
    TameTableCompElement.prototype.getRowSpan = function () {
      return this.node___.rowSpan;
    };
    TameTableCompElement.prototype.setRowSpan = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.rowSpan = newValue;
      return newValue;
    };
    TameTableCompElement.prototype.getRows = function () {
      return tameNodeList(this.node___.rows, this.editable___);
    };
    TameTableCompElement.prototype.getRowIndex = function () {
      return this.node___.rowIndex;
    };
    TameTableCompElement.prototype.getAlign = function () {
      return this.node___.align;
    };
    TameTableCompElement.prototype.setAlign = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.align = newValue;
      return newValue;
    };
    TameTableCompElement.prototype.getVAlign = function () {
      return this.node___.vAlign;
    };
    TameTableCompElement.prototype.setVAlign = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.vAlign = newValue;
      return newValue;
    };
    TameTableCompElement.prototype.getNowrap = function () {
      return this.node___.nowrap;
    };
    TameTableCompElement.prototype.setNowrap = function (newValue) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.nowrap = newValue;
      return newValue;
    };
    ___.ctor(TameTableCompElement, TameElement, 'TameTableCompElement');


    function TameTableElement(node, editable) {
      TameTableCompElement.call(this, node, editable);
      classUtils.exportFields(this, ['tBodies','tHead','tFoot']);
    }
    classUtils.extend(TameTableElement, TameTableCompElement);
    nodeClasses.HTMLTableElement = TameTableElement;
    TameTableElement.prototype.getTBodies = function () {
      return tameNodeList(this.node___.tBodies, this.editable___);
    };
    TameTableElement.prototype.getTHead = function () {
      return tameNode(this.node___.tHead, this.editable___);
    };
    TameTableElement.prototype.getTFoot = function () {
      return tameNode(this.node___.tFoot, this.editable___);
    };
    TameTableElement.prototype.createTHead = function () {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return tameNode(this.node___.createTHead(), this.editable___);
    };
    TameTableElement.prototype.deleteTHead = function () {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.deleteTHead();
    };
    TameTableElement.prototype.createTFoot = function () {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return tameNode(this.node___.createTFoot(), this.editable___);
    };
    TameTableElement.prototype.deleteTFoot = function () {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      this.node___.deleteTFoot();
    };
    ___.ctor(TameTableElement, TameTableCompElement, 'TameTableElement');
    ___.all2(___.grantTypedGeneric, TameTableElement.prototype,
             ['createTHead', 'deleteTHead','createTFoot', 'deleteTFoot']);

    function tameEvent(event) {
      if (event.tamed___) { return event.tamed___; }
      return event.tamed___ = new TameEvent(event);
    }

    function TameEvent(event) {
      this.event___ = event;
      ___.stamp(tameEventTrademark, this, true);
      classUtils.exportFields(
          this,
          ['type', 'target', 'pageX', 'pageY', 'altKey',
           'ctrlKey', 'metaKey', 'shiftKey', 'button',
           'screenX', 'screenY',
           'currentTarget', 'relatedTarget',
           'fromElement', 'toElement',
           'srcElement',
           'clientX', 'clientY', 'keyCode', 'which']);
    }
    nodeClasses.Event = TameEvent;
    TameEvent.prototype.getType = function () {
      return bridal.untameEventType(String(this.event___.type));
    };
    TameEvent.prototype.getTarget = function () {
      var event = this.event___;
      return tameRelatedNode(event.target || event.srcElement, true);
    };
    TameEvent.prototype.getSrcElement = function () {
      return tameRelatedNode(this.event___.srcElement, true);
    };
    TameEvent.prototype.getCurrentTarget = function () {
      var e = this.event___;
      return tameRelatedNode(e.currentTarget, true);
    };
    TameEvent.prototype.getRelatedTarget = function () {
      var e = this.event___;
      var t = e.relatedTarget;
      if (!t) {
        if (e.type === 'mouseout') {
          t = e.toElement;
        } else if (e.type === 'mouseover') {
          t = e.fromElement;
        }
      }
      return tameRelatedNode(t, true);
    };
    TameEvent.prototype.getFromElement = function () {
      return tameRelatedNode(this.event___.fromElement, true);
    };
    TameEvent.prototype.getToElement = function () {
      return tameRelatedNode(this.event___.toElement, true);
    };
    TameEvent.prototype.getPageX = function () {
      return Number(this.event___.pageX);
    };
    TameEvent.prototype.getPageY = function () {
      return Number(this.event___.pageY);
    };
    TameEvent.prototype.stopPropagation = function () {
      // TODO(mikesamuel): make sure event doesn't propagate to dispatched
      // events for this gadget only.
      // But don't allow it to stop propagation to the container.
      if (this.event___.stopPropagation) {
        this.event___.stopPropagation();
      } else {
        this.event___.cancelBubble = true;
      }
    };
    TameEvent.prototype.preventDefault = function () {
      // TODO(mikesamuel): make sure event doesn't propagate to dispatched
      // events for this gadget only.
      // But don't allow it to stop propagation to the container.
      if (this.event___.preventDefault) {
        this.event___.preventDefault();
      } else {
        this.event___.returnValue = false;
      }
    };
    TameEvent.prototype.getAltKey = function () {
      return Boolean(this.event___.altKey);
    };
    TameEvent.prototype.getCtrlKey = function () {
      return Boolean(this.event___.ctrlKey);
    };
    TameEvent.prototype.getMetaKey = function () {
      return Boolean(this.event___.metaKey);
    };
    TameEvent.prototype.getShiftKey = function () {
      return Boolean(this.event___.shiftKey);
    };
    TameEvent.prototype.getButton = function () {
      var e = this.event___;
      return e.button && Number(e.button);
    };
    TameEvent.prototype.getClientX = function () {
      return Number(this.event___.clientX);
    };
    TameEvent.prototype.getClientY = function () {
      return Number(this.event___.clientY);
    };
    TameEvent.prototype.getScreenX = function () {
      return Number(this.event___.screenX);
    };
    TameEvent.prototype.getScreenY = function () {
      return Number(this.event___.screenY);
    };
    TameEvent.prototype.getWhich = function () {
      var w = this.event___.which;
      return w && Number(w);
    };
    TameEvent.prototype.getKeyCode = function () {
      var kc = this.event___.keyCode;
      return kc && Number(kc);
    };
    TameEvent.prototype.toString = function () { return '[Fake Event]'; };
    ___.ctor(TameEvent, void 0, 'TameEvent');
    ___.all2(___.grantTypedGeneric, TameEvent.prototype,
             ['getType', 'getTarget', 'getPageX', 'getPageY', 'stopPropagation',
              'getAltKey', 'getCtrlKey', 'getMetaKey', 'getShiftKey',
              'getButton', 'getClientX', 'getClientY',
              'getScreenX', 'getScreenY',
              'getRelatedTarget',
              'getFromElement', 'getToElement',
              'getSrcElement',
              'preventDefault',
              'getKeyCode', 'getWhich']);

    function TameCustomHTMLEvent(event) {
      TameEvent.call(this, event);
      this.properties___ = {};
    }
    classUtils.extend(TameCustomHTMLEvent, TameEvent);
    TameCustomHTMLEvent.prototype.initEvent
        = function (type, bubbles, cancelable) {
      bridal.initEvent(this.event___, type, bubbles, cancelable);
    };
    TameCustomHTMLEvent.prototype.handleRead___ = function (name) {
      name = String(name);
      if (endsWith__.test(name)) { return void 0; }
      var handlerName = name + '_getter___';
      if (this[handlerName]) {
        return this[handlerName]();
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName]();
      }
      if (___.hasOwnProp(this.event___.properties___, name)) {
        return this.event___.properties___[name];
      } else {
        return void 0;
      }
    };
    TameCustomHTMLEvent.prototype.handleCall___ = function (name, args) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      var handlerName = name + '_handler___';
      if (this[handlerName]) {
        return this[handlerName].call(this, args);
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName].call(this, args);
      }
      if (___.hasOwnProp(this.event___.properties___, name)) {
        return this.event___.properties___[name].call(this, args);
      } else {
        throw new TypeError(name + ' is not a function.');
      }
    };
    TameCustomHTMLEvent.prototype.handleSet___ = function (name, val) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      var handlerName = name + '_setter___';
      if (this[handlerName]) {
        return this[handlerName](val);
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName](val);
      }
      if (!this.event___.properties___) {
        this.event___.properties___ = {};
      }
      this[name + '_canEnum___'] = true;
      return this.event___.properties___[name] = val;
    };
    TameCustomHTMLEvent.prototype.handleDelete___ = function (name) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      var handlerName = name + '_deleter___';
      if (this[handlerName]) {
        return this[handlerName]();
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName]();
      }
      if (this.event___.properties___) {
        return (
            delete this.event___.properties___[name]
            && delete this[name + '_canEnum___']);
      } else {
        return true;
      }
    };
    TameCustomHTMLEvent.prototype.handleEnum___ = function (ownFlag) {
      // TODO(metaweta): Add code to list all the other handled stuff we know
      // about.
      if (this.event___.properties___) {
        return cajita.allKeys(this.event___.properties___);
      }
      return [];
    };
    TameCustomHTMLEvent.prototype.toString = function () {
      return '[Fake CustomEvent]';
    };
    ___.grantTypedGeneric(TameCustomHTMLEvent.prototype, 'initEvent');
    ___.ctor(TameCustomHTMLEvent, TameEvent, 'TameCustomHTMLEvent');

    /**
     * Return a fake node list containing tamed nodes.
     * @param {Array.<TameNode>} array of tamed nodes.
     * @return an array that duck types to a node list.
     */
    function fakeNodeList(array) {
      array.item = ___.frozenFunc(function(i) { return array[i]; });
      return cajita.freeze(array);
    }

    function TameHTMLDocument(doc, body, domain, editable) {
      TamePseudoNode.call(this, editable);
      this.doc___ = doc;
      this.body___ = body;
      this.domain___ = domain;
      this.onLoadListeners___ = [];
      var tameDoc = this;

      var tameBody = tameNode(body, editable);
      this.tameBody___ = tameBody;
      // TODO(mikesamuel): create a proper class for BODY, HEAD, and HTML along
      // with all the other specialized node types.
      var tameBodyElement = new TamePseudoElement(
          'BODY',
          this,
          function () { return tameNodeList(body.childNodes, editable); },
          function () { return tameHtmlElement; },
          function () { return tameInnerHtml(body.innerHTML); },
          tameBody,
          editable);
      cajita.forOwnKeys(
          { appendChild: 0, removeChild: 0, insertBefore: 0, replaceChild: 0 },
          ___.frozenFunc(function (k) {
            tameBodyElement[k] = tameBody[k].bind(tameBody);
            ___.grantFunc(tameBodyElement, k);
          }));

      var title = doc.createTextNode(body.getAttribute('title') || '');
      var tameTitleElement = new TamePseudoElement(
          'TITLE',
          this,
          function () { return [tameNode(title, false)]; },
          function () { return tameHeadElement; },
          function () { return html.escapeAttrib(title.nodeValue); },
          null,
          editable);
      var tameHeadElement = new TamePseudoElement(
          'HEAD',
          this,
          function () { return [tameTitleElement]; },
          function () { return tameHtmlElement; },
          function () {
            return '<title>' + tameTitleElement.getInnerHTML() + '</title>';
          },
          null,
          editable);
      var tameHtmlElement = new TamePseudoElement(
          'HTML',
          this,
          function () { return [tameHeadElement, tameBodyElement]; },
          function () { return tameDoc; },
          function () {
            return ('<head>' + tameHeadElement.getInnerHTML + '<\/head><body>'
                    + tameBodyElement.getInnerHTML() + '<\/body>');
          },
          tameBody,
          editable);
      if (body.contains) {  // typeof is 'object' on IE
        tameHtmlElement.contains = function (other) {
          cajita.guard(tameNodeTrademark, other);
          var otherNode = other.node___;
          return body.contains(otherNode);
        };
        ___.grantFunc(tameHtmlElement, 'contains');
      }
      if ('function' === typeof body.compareDocumentPosition) {
        /**
         * Speced in <a href="http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition">DOM-Level-3</a>.
         */
        tameHtmlElement.compareDocumentPosition = function (other) {
          cajita.guard(tameNodeTrademark, other);
          var otherNode = other.node___;
          if (!otherNode) { return 0; }
          var bitmask = +body.compareDocumentPosition(otherNode);
          // To avoid leaking information about the relative positioning of
          // different roots, if neither contains the other, then we mask out
          // the preceding/following bits.
          // 0x18 is (CONTAINS | CONTAINED).
          // 0x1f is all the bits documented at
          // http://www.w3.org/TR/DOM-Level-3-Core/core.html#DocumentPosition
          // except IMPLEMENTATION_SPECIFIC.
          // 0x01 is DISCONNECTED.
          /*
          if (!(bitmask & 0x18)) {
            // TODO: If they are not under the same virtual doc root, return
            // DOCUMENT_POSITION_DISCONNECTED instead of leaking information
            // about PRECEEDED | FOLLOWING.
          }
          */
          return bitmask & 0x1f;
        };
        if (!___.hasOwnProp(tameHtmlElement, 'contains')) {
          // http://www.quirksmode.org/blog/archives/2006/01/contains_for_mo.html
          tameHtmlElement.contains = (function (other) {
            var docPos = this.compareDocumentPosition(other);
            return !(!(docPos & 0x10) && docPos);
          }).bind(tameHtmlElement);
          ___.grantFunc(tameHtmlElement, 'contains');
        }
        ___.grantFunc(tameHtmlElement, 'compareDocumentPosition');
      }
      this.documentElement___ = tameHtmlElement;
      classUtils.exportFields(
          this, ['documentElement', 'body', 'title', 'domain']);
    }
    classUtils.extend(TameHTMLDocument, TamePseudoNode);
    nodeClasses.HTMLDocument = TameHTMLDocument;
    TameHTMLDocument.prototype.getNodeType = function () { return 9; };
    TameHTMLDocument.prototype.getNodeName
        = function () { return '#document'; };
    TameHTMLDocument.prototype.getNodeValue = function () { return null; };
    TameHTMLDocument.prototype.getChildNodes
        = function () { return [this.documentElement___]; };
    TameHTMLDocument.prototype.getAttributes = function () { return []; };
    TameHTMLDocument.prototype.getParentNode = function () { return null; };
    TameHTMLDocument.prototype.getElementsByTagName = function (tagName) {
      tagName = String(tagName).toLowerCase();
      switch (tagName) {
        case 'body': return fakeNodeList([ this.getBody() ]);
        case 'head': return fakeNodeList([ this.getHead() ]);
        case 'title': return fakeNodeList([ this.getTitle() ]);
        case 'html': return fakeNodeList([ this.getDocumentElement() ]);
        default:
          return tameGetElementsByTagName(
              this.body___, tagName, this.editable___);
      }
    };
    TameHTMLDocument.prototype.getDocumentElement = function () {
      return this.documentElement___;
    };
    TameHTMLDocument.prototype.getBody = function () {
      return this.documentElement___.getLastChild();
    };
    TameHTMLDocument.prototype.getHead = function () {
      return this.documentElement___.getFirstChild();
    };
    TameHTMLDocument.prototype.getTitle = function () {
      return this.getHead().getFirstChild();
    };
    TameHTMLDocument.prototype.getDomain = function () {
      return this.domain___;
    };
    TameHTMLDocument.prototype.getElementsByClassName = function (className) {
      return tameGetElementsByClassName(
          this.body___, className, this.editable___);
    };
    TameHTMLDocument.prototype.addEventListener =
        function (name, listener, useCapture) {
          return this.tameBody___.addEventListener(name, listener, useCapture);
        };
    TameHTMLDocument.prototype.removeEventListener =
        function (name, listener, useCapture) {
          return this.tameBody___.removeEventListener(
              name, listener, useCapture);
        };
    TameHTMLDocument.prototype.createComment = function (text) {
      return new TameCommentNode(this.doc___.createComment(" "), true);
    };
    TameHTMLDocument.prototype.createDocumentFragment = function () {
      return new TameBackedNode(this.doc___.createDocumentFragment(),
                                this.editable___);
    };
    TameHTMLDocument.prototype.createElement = function (tagName) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      tagName = String(tagName).toLowerCase();
      if (!html4.ELEMENTS.hasOwnProperty(tagName)) {
        throw new Error(UNKNOWN_TAGNAME + "[" + tagName + "]");
      }
      var flags = html4.ELEMENTS[tagName];
      // Script exemption allows dynamic loading of proxied scripts.
      if ((flags & html4.eflags.UNSAFE) && !(flags & html4.eflags.SCRIPT)) {
        cajita.log(UNSAFE_TAGNAME + "[" + tagName + "]: no action performed");
        return null;
      }
      var newEl = this.doc___.createElement(tagName);
      if (elementPolicies.hasOwnProperty(tagName)) {
        var attribs = elementPolicies[tagName]([]);
        if (attribs) {
          for (var i = 0; i < attribs.length; i += 2) {
            bridal.setAttribute(newEl, attribs[i], attribs[i + 1]);
          }
        }
      }
      return tameNode(newEl, true);
    };
    TameHTMLDocument.prototype.createTextNode = function (text) {
      if (!this.editable___) { throw new Error(NOT_EDITABLE); }
      return tameNode(this.doc___.createTextNode(
          text !== null && text !== void 0 ? '' + text : ''), true);
    };
    TameHTMLDocument.prototype.getElementById = function (id) {
      id += idSuffix;
      var node = this.doc___.getElementById(id);
      return tameNode(node, this.editable___);
    };
    TameHTMLDocument.prototype.toString = function () {
      return '[Fake Document]';
    };
    TameHTMLDocument.prototype.write = function (text) {
      // TODO(mikesamuel): Needs implementation
      cajita.log('Called document.write() with: ' + text);
    };
    // http://www.w3.org/TR/DOM-Level-2-Events/events.html
    // #Events-DocumentEvent-createEvent
    TameHTMLDocument.prototype.createEvent = function (type) {
      type = String(type);
      if (type !== 'HTMLEvents') {
        // See https://developer.mozilla.org/en/DOM/document.createEvent#Notes
        // for a long list of event ypes.
        // See http://www.w3.org/TR/DOM-Level-2-Events/events.html
        // #Events-eventgroupings
        // for the DOM2 list.
        throw new Error('Unrecognized event type ' + type);
      }
      var document = this.doc___;
      var rawEvent;
      if (document.createEvent) {
        rawEvent = document.createEvent(type);
      } else {
        rawEvent = document.createEventObject();
        rawEvent.eventType = 'ondataavailable';
      }
      var tamedEvent = new TameCustomHTMLEvent(rawEvent);
      rawEvent.tamed___ = tamedEvent;
      return tamedEvent;
    };
    TameHTMLDocument.prototype.getOwnerDocument = function () {
      return null;
    };
    // Called by the html-emitter when the virtual document has been loaded.
    TameHTMLDocument.prototype.signalLoaded___ = function () {
      var listeners = this.onLoadListeners___;
      this.onLoadListeners___ = [];
      for (var i = 0, n = listeners.length; i < n; ++i) {
        (function (listener) {
          var listenerFn = ___.asFunc(listener);
          setTimeout(function () { listenerFn.call(cajita.USELESS); }, 0);
        })(listeners[i]);
      }
    };

    ___.ctor(TameHTMLDocument, TamePseudoNode, 'TameHTMLDocument');
    ___.all2(___.grantTypedGeneric, TameHTMLDocument.prototype,
             ['addEventListener', 'removeEventListener',
              'createComment', 'createDocumentFragment',
              'createElement', 'createEvent', 'createTextNode',
              'getElementById', 'getElementsByClassName',
              'getElementsByTagName',
              'write']);


    imports.tameNode___ = tameNode;
    imports.tameEvent___ = tameEvent;
    imports.blessHtml___ = blessHtml;
    imports.blessCss___ = function (var_args) {
      var arr = [];
      for (var i = 0, n = arguments.length; i < n; ++i) {
        arr[i] = arguments[i];
      }
      return cssSealerUnsealerPair.seal(arr);
    };
    imports.htmlAttr___ = function (s) {
      return html.escapeAttrib(String(s || ''));
    };
    imports.html___ = safeHtml;
    imports.rewriteUri___ = function (uri, mimeType) {
      var s = rewriteAttribute(null, null, html4.atype.URI, uri);
      if (!s) { throw new Error(); }
      return s;
    };
    imports.suffix___ = function (nmtokens) {
      var p = String(nmtokens).replace(/^\s+|\s+$/g, '').split(/\s+/g);
      var out = [];
      for (var i = 0; i < p.length; ++i) {
        var nmtoken = rewriteAttribute(null, null, html4.atype.ID, p[i]);
        if (!nmtoken) { throw new Error(nmtokens); }
        out.push(nmtoken);
      }
      return out.join(' ');
    };
    imports.ident___ = function (nmtokens) {
      var p = String(nmtokens).replace(/^\s+|\s+$/g, '').split(/\s+/g);
      var out = [];
      for (var i = 0; i < p.length; ++i) {
        var nmtoken = rewriteAttribute(null, null, html4.atype.CLASSES, p[i]);
        if (!nmtoken) { throw new Error(nmtokens); }
        out.push(nmtoken);
      }
      return out.join(' ');
    };
    // Maps a lower-cased style property name, e.g. background-image,
    // to a style object property name, e.g. backgroundImage, so that
    // it can be used as a style object property name as in
    // myHtmlElement.style['backgroundImage'].
    var canonicalStylePropertyNames = {};
    // Maps style property names, e.g. cssFloat, to property names, e.g. float.
    var cssPropertyNames = [];

    cajita.forOwnKeys(css.properties,
                      ___.frozenFunc(function (cssPropertyName, pattern) {
      var baseStylePropertyName = cssPropertyName.replace(
          /-([a-z])/g, function (_, letter) { return letter.toUpperCase(); });
      var canonStylePropertyName = baseStylePropertyName;
      cssPropertyNames[baseStylePropertyName]
          = cssPropertyNames[canonStylePropertyName]
          = cssPropertyName;
      if (css.alternates.hasOwnProperty(canonStylePropertyName)) {
        var alts = css.alternates[canonStylePropertyName];
        for (var i = alts.length; --i >= 0;) {
          cssPropertyNames[alts[i]] = cssPropertyName;
          // Handle oddities like cssFloat/styleFloat.
          if (alts[i] in document.documentElement.style
              && !(canonStylePropertyName in document.documentElement.style)) {
            canonStylePropertyName = alts[i];
          }
        }
      }
      canonicalStylePropertyNames[cssPropertyName] = canonStylePropertyName;
    }));

    function TameStyle(style, editable) {
      this.style___ = style;
      this.editable___ = editable;
      ___.grantCall(this, 'getPropertyValue');
    }
    nodeClasses.Style = TameStyle;
    TameStyle.prototype.allowProperty___ = function (cssPropertyName) {
      return css.properties.hasOwnProperty(cssPropertyName);
    };
    TameStyle.prototype.handleRead___ = function (stylePropertyName) {
      if (!this.style___
          || !cssPropertyNames.hasOwnProperty(stylePropertyName)) {
        return void 0;
      }
      var cssPropertyName = cssPropertyNames[stylePropertyName];
      if (!this.allowProperty___(cssPropertyName)) { return void 0; }
      var canonName = canonicalStylePropertyNames[cssPropertyName];
      return String(this.style___[canonName] || '');
    };
    TameStyle.prototype.getPropertyValue = function (cssPropertyName) {
      cssPropertyName = String(cssPropertyName || '').toLowerCase();
      if (!this.allowProperty___(cssPropertyName)) { return ''; }
      var canonName = canonicalStylePropertyNames[cssPropertyName];
      return String(this.style___[canonName] || '');
    };
    TameStyle.prototype.handleSet___ = function (stylePropertyName, value) {
      if (!this.editable___) { throw new Error('style not editable'); }
      if (!cssPropertyNames.hasOwnProperty(stylePropertyName)) {
        throw new Error('Unknown CSS property name ' + stylePropertyName);
      }
      var cssPropertyName = cssPropertyNames[stylePropertyName];
      if (!this.allowProperty___(cssPropertyName)) { return void 0; }
      var pattern = css.properties[cssPropertyName];
      if (!pattern) { throw new Error('style not editable'); }
      var val = '' + (value || '');
      // CssPropertyPatterns.java only allows styles of the form
      // url("...").  See the BUILTINS definition for the "uri" symbol.
      val = val.replace(
          /\burl\s*\(\s*\"([^\"]*)\"\s*\)/gi,
          function (_, url) {
            var decodedUrl = decodeCssString(url);
            var rewrittenUrl = uriCallback
                ? uriCallback.rewrite(decodedUrl, 'image/*')
                : null;
            if (!rewrittenUrl) {
              rewrittenUrl = 'about:blank';
            }
            return 'url("'
                + rewrittenUrl.replace(
                    /[\"\'\{\}\(\):\\]/g,
                    function (ch) {
                      return '\\' + ch.charCodeAt(0).toString(16) + ' ';
                    })
                + '")';
          });
      if (val && !pattern.test(val + ' ')) {
        throw new Error('bad value `' + val + '` for CSS property '
                        + stylePropertyName);
      }
      var canonName = canonicalStylePropertyNames[cssPropertyName];
      this.style___[canonName] = val;
      return value;
    };
    TameStyle.prototype.toString = function () { return '[Fake Style]'; };

    function TameComputedStyle(style) {
      TameStyle.call(this, style, false);
    }
    classUtils.extend(TameComputedStyle, TameStyle);
    TameComputedStyle.prototype.allowProperty___ = function (cssPropertyName) {
      return css.COMPUTED_STYLE_WHITELIST.hasOwnProperty(cssPropertyName);
    };
    TameComputedStyle.prototype.toString = function () {
      return '[Fake Computed Style]';
    };

    nodeClasses.XMLHttpRequest = domitaModules.TameXMLHttpRequest(
        domitaModules.XMLHttpRequestCtor(
            window.XMLHttpRequest,
            window.ActiveXObject),
        uriCallback);

    /**
     * given a number, outputs the equivalent css text.
     * @param {number} num
     * @return {string} an CSS representation of a number suitable for both html
     *    attribs and plain text.
     */
    imports.cssNumber___ = function (num) {
      if ('number' === typeof num && isFinite(num) && !isNaN(num)) {
        return '' + num;
      }
      throw new Error(num);
    };
    /**
     * given a number as 24 bits of RRGGBB, outputs a properly formatted CSS
     * color.
     * @param {number} num
     * @return {string} a CSS representation of num suitable for both html
     *    attribs and plain text.
     */
    imports.cssColor___ = function (color) {
      // TODO: maybe whitelist the color names defined for CSS if the arg is a
      // string.
      if ('number' !== typeof color || (color != (color | 0))) {
        throw new Error(color);
      }
      var hex = '0123456789abcdef';
      return '#' + hex.charAt((color >> 20) & 0xf)
          + hex.charAt((color >> 16) & 0xf)
          + hex.charAt((color >> 12) & 0xf)
          + hex.charAt((color >> 8) & 0xf)
          + hex.charAt((color >> 4) & 0xf)
          + hex.charAt(color & 0xf);
    };
    imports.cssUri___ = function (uri, mimeType) {
      var s = rewriteAttribute(null, null, html4.atype.URI, uri);
      if (!s) { throw new Error(); }
      return s;
    };

    /**
     * Create a CSS stylesheet with the given text and append it to the DOM.
     * @param {string} cssText a well-formed stylesheet production.
     */
    imports.emitCss___ = function (cssText) {
      this.getCssContainer___().appendChild(
          bridal.createStylesheet(document, cssText));
    };
    /** The node to which gadget stylesheets should be added. */
    imports.getCssContainer___ = function () {
      return document.getElementsByTagName('head')[0];
    };

    if (!/^-/.test(idSuffix)) {
      throw new Error('id suffix "' + idSuffix + '" must start with "-"');
    }
    var idClass = idSuffix.substring(1);
    /** A per-gadget class used to separate style rules. */
    imports.getIdClass___ = function () {
      return idClass;
    };

    // TODO(mikesamuel): remove these, and only expose them via window once
    // Valija works
    imports.setTimeout = tameSetTimeout;
    imports.setInterval = tameSetInterval;
    imports.clearTimeout = tameClearTimeout;
    imports.clearInterval = tameClearInterval;

    var tameDocument = new TameHTMLDocument(
        document,
        pseudoBodyNode,
        String(optPseudoWindowLocation.hostname || 'nosuchhost.fake'),
        true);
    imports.document = tameDocument;

    // TODO(mikesamuel): figure out a mechanism by which the container can
    // specify the gadget's apparent URL.
    // See http://www.whatwg.org/specs/web-apps/current-work/multipage/history.html#location0
    var tameLocation = ___.primFreeze({
      toString: ___.frozenFunc(function () { return tameLocation.href; }),
      href: String(optPseudoWindowLocation.href || 'http://nosuchhost.fake/'),
      hash: String(optPseudoWindowLocation.hash || ''),
      host: String(optPseudoWindowLocation.host || 'nosuchhost.fake'),
      hostname: String(optPseudoWindowLocation.hostname || 'nosuchhost.fake'),
      pathname: String(optPseudoWindowLocation.pathname || '/'),
      port: String(optPseudoWindowLocation.port || ''),
      protocol: String(optPseudoWindowLocation.protocol || 'http:'),
      search: String(optPseudoWindowLocation.search || '')
      });

    // See spec at http://www.whatwg.org/specs/web-apps/current-work/multipage/browsers.html#navigator
    var tameNavigator = ___.primFreeze({
      appCodeName: 'Caja',
      appName: 'Sandbox',
      appVersion: '1.0',  // Should we expose the user's Locale here?
      language: '',  // Should we expose the user's Locale here?
      platform: 'Caja',
      oscpu: 'Caja',
      vendor: '',
      vendorSub: '',
      product: 'Caja',
      productSub: '',
      userAgent: 'Caja/1.0'
      });

    /**
     * Set of allowed pseudo elements as described at
     * http://www.w3.org/TR/CSS2/selector.html#q20
     */
    var PSEUDO_ELEMENT_WHITELIST = {
      // after and before disallowed since they can leak information about
      // arbitrary ancestor nodes.
      'first-letter': true,
      'first-line': true
    };

    /**
     * See http://www.whatwg.org/specs/web-apps/current-work/multipage/browsers.html#window for the full API.
     */
    function TameWindow() {
      this.properties___ = {};
    }

    /**
     * An <a href=
     * href=http://www.w3.org/TR/DOM-Level-2-Views/views.html#Views-AbstractView
     * >AbstractView</a> implementation that exposes styling, positioning, and
     * sizing information about the current document's pseudo-body.
     * <p>
     * The AbstractView spec specifies very little in its IDL description, but
     * mozilla defines it thusly:<blockquote>
     *   document.defaultView is generally a reference to the window object
     *   for the document, however that is not defined in the specification
     *   and can't be relied upon for all host environments, particularly as
     *   not all browsers implement it.
     * </blockquote>
     * <p>
     * We can't provide access to the tamed window directly from document
     * since it is the global scope of valija code, and so access to another
     * module's tamed window provides an unbounded amount of authority.
     * <p>
     * Instead, we expose styling, positioning, and sizing properties
     * via this class.  All of this authority is already available from the
     * document.
     */
    function TameDefaultView() {
      // TODO(mikesamuel): Implement in terms of
      //     http://www.w3.org/TR/cssom-view/#the-windowview-interface
      // TODO: expose a read-only version of the document
      this.document = tameDocument;
      // Exposing an editable default view that pointed to a read-only
      // tameDocument via document.defaultView would allow escalation of
      // authority.
      assert(tameDocument.editable___);
      ___.grantRead(this, 'document');
    }

    cajita.forOwnKeys({
      document: tameDocument,
      location: tameLocation,
      navigator: tameNavigator,
      setTimeout: tameSetTimeout,
      setInterval: tameSetInterval,
      clearTimeout: tameClearTimeout,
      clearInterval: tameClearInterval,
      addEventListener: ___.frozenFunc(function (name, listener, useCapture) {
        if (name === 'load') {
          ___.asFunc(listener);
          tameDocument.onLoadListeners___.push(listener);
        }
      }),
      removeEventListener: ___.frozenFunc(function (name, listener, useCapture) {
        var listeners = tameDocument.onLoadListeners___;
        var k = 0;
        for (var i = 0, n = listeners.length; i < n; ++i) {
          listeners[i - k] = listeners[i];
          if (listeners[i] === listener) {
            ++k;
          }
        }
        listeners.length -= k;
      }),
      dispathEvent: ___.frozenFunc(function (evt) {
        // TODO(ihab.awad): Implement
      })
    }, ___.func(function (propertyName, value) {
      TameWindow.prototype[propertyName] = value;
      ___.grantRead(TameWindow.prototype, propertyName);
    }));
    cajita.forOwnKeys({
      scrollBy: ___.frozenFunc(
          function (dx, dy) {
            // The window is always auto scrollable, so make the apparent window
            // body scrollable if the gadget tries to scroll it.
            if (dx || dy) { makeScrollable(tameDocument.body___); }
            tameScrollBy(tameDocument.body___, dx, dy);
          }),
      scrollTo: ___.frozenFunc(
          function (x, y) {
            // The window is always auto scrollable, so make the apparent window
            // body scrollable if the gadget tries to scroll it.
            makeScrollable(tameDocument.body___);
            tameScrollTo(tameDocument.body___, x, y);
          }),
      resizeTo: ___.frozenFunc(
          function (w, h) {
            tameResizeTo(tameDocument.body___, w, h);
          }),
      resizeBy: ___.frozenFunc(
          function (dw, dh) {
            tameResizeBy(tameDocument.body___, dw, dh);
          }),
      /** A partial implementation of getComputedStyle. */
      getComputedStyle: ___.frozenFunc(
          // Pseudo elements are suffixes like :first-line which constrain to
          // a portion of the element's content as defined at
          // http://www.w3.org/TR/CSS2/selector.html#q20
          function (tameElement, pseudoElement) {
            cajita.guard(tameNodeTrademark, tameElement);
            // Coerce all nullish values to undefined, since that is the value
            // for unspecified parameters.
            // Per bug 973: pseudoElement should be null according to the
            // spec, but mozilla docs contradict this.
            // From https://developer.mozilla.org/En/DOM:window.getComputedStyle
            //     pseudoElt is a string specifying the pseudo-element to match.
            //     Should be an empty string for regular elements.
            pseudoElement = (pseudoElement === null || pseudoElement === void 0
                             || '' === pseudoElement)
                ? void 0 : String(pseudoElement).toLowerCase();
            if (pseudoElement !== void 0
                && !PSEUDO_ELEMENT_WHITELIST.hasOwnProperty(pseudoElement)) {
              throw new Error('Bad pseudo class ' + pseudoElement);
            }
            // No need to check editable since computed styles are readonly.

            var rawNode = tameElement.node___;
            if (rawNode.currentStyle && pseudoElement === void 0) {
              return new TameComputedStyle(rawNode.currentStyle);
            } else if (window.getComputedStyle) {
              var rawStyleNode = window.getComputedStyle(
                  tameElement.node___, pseudoElement);
              return new TameComputedStyle(rawStyleNode);
            } else {
              throw new Error(
                  'Computed style not available for pseudo element '
                  + pseudoElement);
            }
          })

      // NOT PROVIDED
      // event: a global on IE.  We always define it in scopes that can handle
      //        events.
      // opera: defined only on Opera.
    }, ___.func(function (propertyName, value) {
      TameWindow.prototype[propertyName] = value;
      ___.grantRead(TameWindow.prototype, propertyName);
      TameDefaultView.prototype[propertyName] = value;
      ___.grantRead(TameDefaultView.prototype, propertyName);
    }));
    TameWindow.prototype.handleRead___ = function (name) {
      name = String(name);
      if (endsWith__.test(name)) { return void 0; }
      var handlerName = name + '_getter___';
      if (this[handlerName]) {
        return this[handlerName]();
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName]();
      }
      if (___.hasOwnProp(this, name)) {
        return this[name];
      } else {
        return void 0;
      }
    };
    TameWindow.prototype.handleSet___ = function (name, val) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      var handlerName = name + '_setter___';
      if (this[handlerName]) {
        return this[handlerName](val);
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName](val);
      }
      this[name + '_canEnum___'] = true;
      this[name + '_canRead___'] = true;
      return this[name] = val;
    };
    TameWindow.prototype.handleDelete___ = function (name) {
      name = String(name);
      if (endsWith__.test(name)) { throw new Error(INVALID_SUFFIX); }
      var handlerName = name + '_deleter___';
      if (this[handlerName]) {
        return this[handlerName]();
      }
      handlerName = handlerName.toLowerCase();
      if (this[handlerName]) {
        return this[handlerName]();
      }
      return (
          delete this[name]
          && delete this[name + '_canEnum___']
          && delete this[name + '_canRead___']);
    };
    TameWindow.prototype.handleEnum___ = function (ownFlag) {
      // TODO(metaweta): Add code to list all the other handled stuff we know
      // about.
      return cajita.allKeys(this);
    };

    var tameWindow = new TameWindow();
    var tameDefaultView = new TameDefaultView(tameDocument.editable___);

    function propertyOnlyHasGetter(_) {
      throw new TypeError('setting a property that only has a getter');
    }
    cajita.forOwnKeys({
      // We define all the window positional properties relative to
      // the fake body element to maintain the illusion that the fake
      // document is completely defined by the nodes under the fake body.
      clientLeft: {
        get: function () { return tameDocument.body___.clientLeft; }
      },
      clientTop: {
        get: function () { return tameDocument.body___.clientTop; }
      },
      clientHeight: {
        get: function () { return tameDocument.body___.clientHeight; }
      },
      clientWidth: {
        get: function () { return tameDocument.body___.clientWidth; }
      },
      offsetLeft: {
        get: function () { return tameDocument.body___.offsetLeft; }
      },
      offsetTop: {
        get: function () { return tameDocument.body___.offsetTop; }
      },
      offsetHeight: {
        get: function () { return tameDocument.body___.offsetHeight; }
      },
      offsetWidth: {
        get: function () { return tameDocument.body___.offsetWidth; }
      },
      // page{X,Y}Offset appear only as members of window, not on all elements
      // but http://www.howtocreate.co.uk/tutorials/javascript/browserwindow
      // says that they are identical to the scrollTop/Left on all browsers but
      // old versions of Safari.
      pageXOffset: {
        get: function () { return tameDocument.body___.scrollLeft; }
      },
      pageYOffset: {
        get: function () { return tameDocument.body___.scrollTop; }
      },
      scrollLeft: {
        get: function () { return tameDocument.body___.scrollLeft; },
        set: function (x) { tameDocument.body___.scrollLeft = +x; return x; }
      },
      scrollTop: {
        get: function () { return tameDocument.body___.scrollTop; },
        set: function (y) { tameDocument.body___.scrollTop = +y; return y; }
      },
      scrollHeight: {
        get: function () { return tameDocument.body___.scrollHeight; }
      },
      scrollWidth: {
        get: function () { return tameDocument.body___.scrollWidth; }
      }
    }, ___.func(function (propertyName, def) {
      var setter = def.set || propertyOnlyHasGetter;
      // TODO(mikesamuel): define on prototype.
      ___.useGetHandler(tameWindow, propertyName, def.get);
      ___.useSetHandler(tameWindow, propertyName, setter);
      ___.useGetHandler(tameDefaultView, propertyName, def.get);
      ___.useSetHandler(tameDefaultView, propertyName, setter);
      var tameBody = tameDocument.getBody();
      ___.useGetHandler(tameBody, propertyName, def.get);
      ___.useSetHandler(tameBody, propertyName, setter);
      var tameDocEl = tameDocument.getDocumentElement();
      ___.useGetHandler(tameDocEl, propertyName, def.get);
      ___.useSetHandler(tameDocEl, propertyName, setter);
    }));

    cajita.forOwnKeys({
      innerHeight: function () { return tameDocument.body___.clientHeight; },
      innerWidth: function () { return tameDocument.body___.clientWidth; },
      outerHeight: function () { return tameDocument.body___.clientHeight; },
      outerWidth: function () { return tameDocument.body___.clientWidth; }
    }, ___.func(function (propertyName, handler) {
      // TODO(mikesamuel): define on prototype.
      ___.useGetHandler(tameWindow, propertyName, handler);
      ___.useGetHandler(tameDefaultView, propertyName, handler);
    }));

    // Attach reflexive properties to 'window' object
    var windowProps = ['top', 'self', 'opener', 'parent', 'window'];
    var wpLen = windowProps.length;
    for (var i = 0; i < wpLen; ++i) {
      var prop = windowProps[i];
      tameWindow[prop] = tameWindow;
      ___.grantRead(tameWindow, prop);
    }

    if (tameDocument.editable___) {
      tameDocument.defaultView = tameDefaultView;
      ___.grantRead(tameDocument, 'defaultView');
    }

    // Iterate over all node classes, assigning them to the Window object
    // under their DOM Level 2 standard name.
    cajita.forOwnKeys(nodeClasses, ___.func(function(name, ctor) {
      ___.primFreeze(ctor);
      tameWindow[name] = ctor;
      ___.grantRead(tameWindow, name);
    }));

    // TODO(ihab.awad): Build a more sophisticated virtual class hierarchy by
    // creating a table of actual subclasses and instantiating tame nodes by
    // table lookups. This will allow the client code to see a truly consistent
    // DOM class hierarchy.
    var defaultNodeClasses = [
      'HTMLAppletElement',
      'HTMLAreaElement',
      'HTMLBaseElement',
      'HTMLBaseFontElement',
      'HTMLBodyElement',
      'HTMLBRElement',
      'HTMLButtonElement',
      'HTMLDirectoryElement',
      'HTMLDivElement',
      'HTMLDListElement',
      'HTMLFieldSetElement',
      'HTMLFontElement',
      'HTMLFrameElement',
      'HTMLFrameSetElement',
      'HTMLHeadElement',
      'HTMLHeadingElement',
      'HTMLHRElement',
      'HTMLHtmlElement',
      'HTMLIFrameElement',
      'HTMLIsIndexElement',
      'HTMLLabelElement',
      'HTMLLegendElement',
      'HTMLLIElement',
      'HTMLLinkElement',
      'HTMLMapElement',
      'HTMLMenuElement',
      'HTMLMetaElement',
      'HTMLModElement',
      'HTMLObjectElement',
      'HTMLOListElement',
      'HTMLOptGroupElement',
      'HTMLOptionElement',
      'HTMLParagraphElement',
      'HTMLParamElement',
      'HTMLPreElement',
      'HTMLQuoteElement',
      'HTMLScriptElement',
      'HTMLSelectElement',
      'HTMLStyleElement',
      'HTMLTableCaptionElement',
      'HTMLTableCellElement',
      'HTMLTableColElement',
      'HTMLTableElement',
      'HTMLTableRowElement',
      'HTMLTableSectionElement',
      'HTMLTextAreaElement',
      'HTMLTitleElement',
      'HTMLUListElement'
    ];

    var defaultNodeClassCtor = ___.primFreeze(TameElement);
    for (var i = 0; i < defaultNodeClasses.length; i++) {
      tameWindow[defaultNodeClasses[i]] = defaultNodeClassCtor;
      ___.grantRead(tameWindow, defaultNodeClasses[i]);
    }

    var outers = imports.outers;
    if (___.isJSONContainer(outers)) {
      // For Valija, attach use the window object as outers.
      cajita.forOwnKeys(outers, ___.func(function(k, v) {
        if (!(k in tameWindow)) {
          tameWindow[k] = v;
          ___.grantRead(tameWindow, k);
        }
      }));
      imports.outers = tameWindow;
    } else {
      imports.window = tameWindow;
    }
  }

  return attachDocumentStub;
})();

/**
 * Function called from rewritten event handlers to dispatch an event safely.
 */
function plugin_dispatchEvent___(thisNode, event, pluginId, handler) {
  event = (event || window.event);
  var sig = String(handler).match(/^function\b[^\)]*\)/);
  cajita.log(
      'Dispatch ' + (event && event.type) +
      'event thisNode=' + thisNode + ', ' +
      'event=' + event + ', ' +
      'pluginId=' + pluginId + ', ' +
      'handler=' + (sig ? sig[0] : handler));
  var imports = ___.getImports(pluginId);
  switch (typeof handler) {
    case 'string':
      handler = imports[handler];
      break;
    case 'function': case 'object':
      break;
    default:
      throw new Error(
          'Expected function as event handler, not ' + typeof handler);
  }
  if (___.startCallerStack) { ___.startCallerStack(); }
  imports.isProcessingEvent___ = true;
  try {
    return ___.callPub(
        handler, 'call',
        [___.USELESS,
         imports.tameEvent___(event),
         imports.tameNode___(thisNode, true)
         ]);
  } catch (ex) {
    if (ex && ex.cajitaStack___ && 'undefined' !== (typeof console)) {
      console.error('Event dispatch %s: %s',
          handler, ___.unsealCallerStack(ex.cajitaStack___).join('\n'));
    }
    throw ex;
  } finally {
    imports.isProcessingEvent___ = false;
  }
}