shithub: mycel

ref: dfab56b94698b9ebb5291931a9f67c6cebac8881
dir: /domino-lib/Node.js/

View raw version
"use strict";
module.exports = Node;

var EventTarget = require('./EventTarget');
var LinkedList = require('./LinkedList');
var NodeUtils = require('./NodeUtils');
var utils = require('./utils');

// All nodes have a nodeType and an ownerDocument.
// Once inserted, they also have a parentNode.
// This is an abstract class; all nodes in a document are instances
// of a subtype, so all the properties are defined by more specific
// constructors.
function Node() {
  EventTarget.call(this);
  this.parentNode = null;
  this._nextSibling = this._previousSibling = this;
  this._index = undefined;
}

var ELEMENT_NODE                = Node.ELEMENT_NODE = 1;
var ATTRIBUTE_NODE              = Node.ATTRIBUTE_NODE = 2;
var TEXT_NODE                   = Node.TEXT_NODE = 3;
var CDATA_SECTION_NODE          = Node.CDATA_SECTION_NODE = 4;
var ENTITY_REFERENCE_NODE       = Node.ENTITY_REFERENCE_NODE = 5;
var ENTITY_NODE                 = Node.ENTITY_NODE = 6;
var PROCESSING_INSTRUCTION_NODE = Node.PROCESSING_INSTRUCTION_NODE = 7;
var COMMENT_NODE                = Node.COMMENT_NODE = 8;
var DOCUMENT_NODE               = Node.DOCUMENT_NODE = 9;
var DOCUMENT_TYPE_NODE          = Node.DOCUMENT_TYPE_NODE = 10;
var DOCUMENT_FRAGMENT_NODE      = Node.DOCUMENT_FRAGMENT_NODE = 11;
var NOTATION_NODE               = Node.NOTATION_NODE = 12;

var DOCUMENT_POSITION_DISCONNECTED            = Node.DOCUMENT_POSITION_DISCONNECTED = 0x01;
var DOCUMENT_POSITION_PRECEDING               = Node.DOCUMENT_POSITION_PRECEDING = 0x02;
var DOCUMENT_POSITION_FOLLOWING               = Node.DOCUMENT_POSITION_FOLLOWING = 0x04;
var DOCUMENT_POSITION_CONTAINS                = Node.DOCUMENT_POSITION_CONTAINS = 0x08;
var DOCUMENT_POSITION_CONTAINED_BY            = Node.DOCUMENT_POSITION_CONTAINED_BY = 0x10;
var DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20;

Node.prototype = Object.create(EventTarget.prototype, {

  // Node that are not inserted into the tree inherit a null parent

  // XXX: the baseURI attribute is defined by dom core, but
  // a correct implementation of it requires HTML features, so
  // we'll come back to this later.
  baseURI: { get: utils.nyi },

  parentElement: { get: function() {
    return (this.parentNode && this.parentNode.nodeType===ELEMENT_NODE) ? this.parentNode : null;
  }},

  hasChildNodes: { value: utils.shouldOverride },

  firstChild: { get: utils.shouldOverride },

  lastChild: { get: utils.shouldOverride },

  previousSibling: { get: function() {
    var parent = this.parentNode;
    if (!parent) return null;
    if (this === parent.firstChild) return null;
    return this._previousSibling;
  }},

  nextSibling: { get: function() {
    var parent = this.parentNode, next = this._nextSibling;
    if (!parent) return null;
    if (next === parent.firstChild) return null;
    return next;
  }},

  textContent: {
    // Should override for DocumentFragment/Element/Attr/Text/PI/Comment
    get: function() { return null; },
    set: function(v) { /* do nothing */ },
  },

  _countChildrenOfType: { value: function(type) {
    var sum = 0;
    for (var kid = this.firstChild; kid !== null; kid = kid.nextSibling) {
      if (kid.nodeType === type) sum++;
    }
    return sum;
  }},

  _ensureInsertValid: { value: function _ensureInsertValid(node, child, isPreinsert) {
    var parent = this, i, kid;
    if (!node.nodeType) throw new TypeError('not a node');
    // 1. If parent is not a Document, DocumentFragment, or Element
    // node, throw a HierarchyRequestError.
    switch (parent.nodeType) {
    case DOCUMENT_NODE:
    case DOCUMENT_FRAGMENT_NODE:
    case ELEMENT_NODE:
      break;
    default: utils.HierarchyRequestError();
    }
    // 2. If node is a host-including inclusive ancestor of parent,
    // throw a HierarchyRequestError.
    if (node.isAncestor(parent)) utils.HierarchyRequestError();
    // 3. If child is not null and its parent is not parent, then
    // throw a NotFoundError. (replaceChild omits the 'child is not null'
    // and throws a TypeError here if child is null.)
    if (child !== null || !isPreinsert) {
      if (child.parentNode !== parent) utils.NotFoundError();
    }
    // 4. If node is not a DocumentFragment, DocumentType, Element,
    // Text, ProcessingInstruction, or Comment node, throw a
    // HierarchyRequestError.
    switch (node.nodeType) {
    case DOCUMENT_FRAGMENT_NODE:
    case DOCUMENT_TYPE_NODE:
    case ELEMENT_NODE:
    case TEXT_NODE:
    case PROCESSING_INSTRUCTION_NODE:
    case COMMENT_NODE:
      break;
    default: utils.HierarchyRequestError();
    }
    // 5. If either node is a Text node and parent is a document, or
    // node is a doctype and parent is not a document, throw a
    // HierarchyRequestError.
    // 6. If parent is a document, and any of the statements below, switched
    // on node, are true, throw a HierarchyRequestError.
    if (parent.nodeType === DOCUMENT_NODE) {
      switch (node.nodeType) {
      case TEXT_NODE:
        utils.HierarchyRequestError();
        break;
      case DOCUMENT_FRAGMENT_NODE:
        // 6a1. If node has more than one element child or has a Text
        // node child.
        if (node._countChildrenOfType(TEXT_NODE) > 0)
          utils.HierarchyRequestError();
        switch (node._countChildrenOfType(ELEMENT_NODE)) {
        case 0:
          break;
        case 1:
          // 6a2. Otherwise, if node has one element child and either
          // parent has an element child, child is a doctype, or child
          // is not null and a doctype is following child. [preinsert]
          // 6a2. Otherwise, if node has one element child and either
          // parent has an element child that is not child or a
          // doctype is following child. [replaceWith]
          if (child !== null /* always true here for replaceWith */) {
            if (isPreinsert && child.nodeType === DOCUMENT_TYPE_NODE)
              utils.HierarchyRequestError();
            for (kid = child.nextSibling; kid !== null; kid = kid.nextSibling) {
              if (kid.nodeType === DOCUMENT_TYPE_NODE)
                utils.HierarchyRequestError();
            }
          }
          i = parent._countChildrenOfType(ELEMENT_NODE);
          if (isPreinsert) {
            // "parent has an element child"
            if (i > 0)
              utils.HierarchyRequestError();
          } else {
            // "parent has an element child that is not child"
            if (i > 1 || (i === 1 && child.nodeType !== ELEMENT_NODE))
              utils.HierarchyRequestError();
          }
          break;
        default: // 6a1, continued. (more than one Element child)
          utils.HierarchyRequestError();
        }
        break;
      case ELEMENT_NODE:
        // 6b. parent has an element child, child is a doctype, or
        // child is not null and a doctype is following child. [preinsert]
        // 6b. parent has an element child that is not child or a
        // doctype is following child. [replaceWith]
        if (child !== null /* always true here for replaceWith */) {
          if (isPreinsert && child.nodeType === DOCUMENT_TYPE_NODE)
            utils.HierarchyRequestError();
          for (kid = child.nextSibling; kid !== null; kid = kid.nextSibling) {
            if (kid.nodeType === DOCUMENT_TYPE_NODE)
              utils.HierarchyRequestError();
          }
        }
        i = parent._countChildrenOfType(ELEMENT_NODE);
        if (isPreinsert) {
          // "parent has an element child"
          if (i > 0)
            utils.HierarchyRequestError();
        } else {
          // "parent has an element child that is not child"
          if (i > 1 || (i === 1 && child.nodeType !== ELEMENT_NODE))
            utils.HierarchyRequestError();
        }
        break;
      case DOCUMENT_TYPE_NODE:
        // 6c. parent has a doctype child, child is non-null and an
        // element is preceding child, or child is null and parent has
        // an element child. [preinsert]
        // 6c. parent has a doctype child that is not child, or an
        // element is preceding child. [replaceWith]
        if (child === null) {
          if (parent._countChildrenOfType(ELEMENT_NODE))
            utils.HierarchyRequestError();
        } else {
          // child is always non-null for [replaceWith] case
          for (kid = parent.firstChild; kid !== null; kid = kid.nextSibling) {
            if (kid === child) break;
            if (kid.nodeType === ELEMENT_NODE)
              utils.HierarchyRequestError();
          }
        }
        i = parent._countChildrenOfType(DOCUMENT_TYPE_NODE);
        if (isPreinsert) {
          // "parent has an doctype child"
          if (i > 0)
            utils.HierarchyRequestError();
        } else {
          // "parent has an doctype child that is not child"
          if (i > 1 || (i === 1 && child.nodeType !== DOCUMENT_TYPE_NODE))
            utils.HierarchyRequestError();
        }
        break;
      }
    } else {
      // 5, continued: (parent is not a document)
      if (node.nodeType === DOCUMENT_TYPE_NODE) utils.HierarchyRequestError();
    }
  }},

  insertBefore: { value: function insertBefore(node, child) {
    var parent = this;
    // 1. Ensure pre-insertion validity
    parent._ensureInsertValid(node, child, true);
    // 2. Let reference child be child.
    var refChild = child;
    // 3. If reference child is node, set it to node's next sibling
    if (refChild === node) { refChild = node.nextSibling; }
    // 4. Adopt node into parent's node document.
    parent.doc.adoptNode(node);
    // 5. Insert node into parent before reference child.
    node._insertOrReplace(parent, refChild, false);
    // 6. Return node
    return node;
  }},


  appendChild: { value: function(child) {
    // This invokes _appendChild after doing validity checks.
    return this.insertBefore(child, null);
  }},

  _appendChild: { value: function(child) {
    child._insertOrReplace(this, null, false);
  }},

  removeChild: { value: function removeChild(child) {
    var parent = this;
    if (!child.nodeType) throw new TypeError('not a node');
    if (child.parentNode !== parent) utils.NotFoundError();
    child.remove();
    return child;
  }},

  // To replace a `child` with `node` within a `parent` (this)
  replaceChild: { value: function replaceChild(node, child) {
    var parent = this;
    // Ensure validity (slight differences from pre-insertion check)
    parent._ensureInsertValid(node, child, false);
    // Adopt node into parent's node document.
    if (node.doc !== parent.doc) {
      // XXX adoptNode has side-effect of removing node from its parent
      // and generating a mutation event, thus causing the _insertOrReplace
      // to generate two deletes and an insert instead of a 'move'
      // event.  It looks like the new MutationObserver stuff avoids
      // this problem, but for now let's only adopt (ie, remove `node`
      // from its parent) here if we need to.
      parent.doc.adoptNode(node);
    }
    // Do the replace.
    node._insertOrReplace(parent, child, true);
    return child;
  }},

  // See: http://ejohn.org/blog/comparing-document-position/
  contains: { value: function contains(node) {
    if (node === null) { return false; }
    if (this === node) { return true; /* inclusive descendant */ }
    /* jshint bitwise: false */
    return (this.compareDocumentPosition(node) &
            DOCUMENT_POSITION_CONTAINED_BY) !== 0;
  }},

  compareDocumentPosition: { value: function compareDocumentPosition(that){
    // Basic algorithm for finding the relative position of two nodes.
    // Make a list the ancestors of each node, starting with the
    // document element and proceeding down to the nodes themselves.
    // Then, loop through the lists, looking for the first element
    // that differs.  The order of those two elements give the
    // order of their descendant nodes.  Or, if one list is a prefix
    // of the other one, then that node contains the other.

    if (this === that) return 0;

    // If they're not owned by the same document or if one is rooted
    // and one is not, then they're disconnected.
    if (this.doc !== that.doc ||
      this.rooted !== that.rooted)
      return (DOCUMENT_POSITION_DISCONNECTED +
          DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC);

    // Get arrays of ancestors for this and that
    var these = [], those = [];
    for(var n = this; n !== null; n = n.parentNode) these.push(n);
    for(n = that; n !== null; n = n.parentNode) those.push(n);
    these.reverse();  // So we start with the outermost
    those.reverse();

    if (these[0] !== those[0]) // No common ancestor
      return (DOCUMENT_POSITION_DISCONNECTED +
          DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC);

    n = Math.min(these.length, those.length);
    for(var i = 1; i < n; i++) {
      if (these[i] !== those[i]) {
        // We found two different ancestors, so compare
        // their positions
        if (these[i].index < those[i].index)
          return DOCUMENT_POSITION_FOLLOWING;
        else
          return DOCUMENT_POSITION_PRECEDING;
      }
    }

    // If we get to here, then one of the nodes (the one with the
    // shorter list of ancestors) contains the other one.
    if (these.length < those.length)
      return (DOCUMENT_POSITION_FOLLOWING +
          DOCUMENT_POSITION_CONTAINED_BY);
    else
      return (DOCUMENT_POSITION_PRECEDING +
          DOCUMENT_POSITION_CONTAINS);
  }},

  isSameNode: {value : function isSameNode(node) {
    return this === node;
  }},


  // This method implements the generic parts of node equality testing
  // and defers to the (non-recursive) type-specific isEqual() method
  // defined by subclasses
  isEqualNode: { value: function isEqualNode(node) {
    if (!node) return false;
    if (node.nodeType !== this.nodeType) return false;

    // Check type-specific properties for equality
    if (!this.isEqual(node)) return false;

    // Now check children for number and equality
    for (var c1 = this.firstChild, c2 = node.firstChild;
         c1 && c2;
         c1 = c1.nextSibling, c2 = c2.nextSibling) {
      if (!c1.isEqualNode(c2)) return false;
    }
    return c1 === null && c2 === null;
  }},

  // This method delegates shallow cloning to a clone() method
  // that each concrete subclass must implement
  cloneNode: { value: function(deep) {
    // Clone this node
    var clone = this.clone();

    // Handle the recursive case if necessary
    if (deep) {
      for (var kid = this.firstChild; kid !== null; kid = kid.nextSibling) {
        clone._appendChild(kid.cloneNode(true));
      }
    }

    return clone;
  }},

  lookupPrefix: { value: function lookupPrefix(ns) {
    var e;
    if (ns === '' || ns === null || ns === undefined) return null;
    switch(this.nodeType) {
    case ELEMENT_NODE:
      return this._lookupNamespacePrefix(ns, this);
    case DOCUMENT_NODE:
      e = this.documentElement;
      return e ? e.lookupPrefix(ns) : null;
    case ENTITY_NODE:
    case NOTATION_NODE:
    case DOCUMENT_FRAGMENT_NODE:
    case DOCUMENT_TYPE_NODE:
      return null;
    case ATTRIBUTE_NODE:
      e = this.ownerElement;
      return e ? e.lookupPrefix(ns) : null;
    default:
      e = this.parentElement;
      return e ? e.lookupPrefix(ns) : null;
    }
  }},


  lookupNamespaceURI: {value: function lookupNamespaceURI(prefix) {
    if (prefix === '' || prefix === undefined) { prefix = null; }
    var e;
    switch(this.nodeType) {
    case ELEMENT_NODE:
      return utils.shouldOverride();
    case DOCUMENT_NODE:
      e = this.documentElement;
      return e ? e.lookupNamespaceURI(prefix) : null;
    case ENTITY_NODE:
    case NOTATION_NODE:
    case DOCUMENT_TYPE_NODE:
    case DOCUMENT_FRAGMENT_NODE:
      return null;
    case ATTRIBUTE_NODE:
      e = this.ownerElement;
      return e ? e.lookupNamespaceURI(prefix) : null;
    default:
      e = this.parentElement;
      return e ? e.lookupNamespaceURI(prefix) : null;
    }
  }},

  isDefaultNamespace: { value: function isDefaultNamespace(ns) {
    if (ns === '' || ns === undefined) { ns = null; }
    var defaultNamespace = this.lookupNamespaceURI(null);
    return (defaultNamespace === ns);
  }},

  // Utility methods for nodes.  Not part of the DOM

  // Return the index of this node in its parent.
  // Throw if no parent, or if this node is not a child of its parent
  index: { get: function() {
    var parent = this.parentNode;
    if (this === parent.firstChild) return 0; // fast case
    var kids = parent.childNodes;
    if (this._index === undefined || kids[this._index] !== this) {
      // Ensure that we don't have an O(N^2) blowup if none of the
      // kids have defined indices yet and we're traversing via
      // nextSibling or previousSibling
      for (var i=0; i<kids.length; i++) {
        kids[i]._index = i;
      }
      utils.assert(kids[this._index] === this);
    }
    return this._index;
  }},

  // Return true if this node is equal to or is an ancestor of that node
  // Note that nodes are considered to be ancestors of themselves
  isAncestor: { value: function(that) {
    // If they belong to different documents, then they're unrelated.
    if (this.doc !== that.doc) return false;
    // If one is rooted and one isn't then they're not related
    if (this.rooted !== that.rooted) return false;

    // Otherwise check by traversing the parentNode chain
    for(var e = that; e; e = e.parentNode) {
      if (e === this) return true;
    }
    return false;
  }},

  // DOMINO Changed the behavior to conform with the specs. See:
  // https://groups.google.com/d/topic/mozilla.dev.platform/77sIYcpdDmc/discussion
  ensureSameDoc: { value: function(that) {
    if (that.ownerDocument === null) {
      that.ownerDocument = this.doc;
    }
    else if(that.ownerDocument !== this.doc) {
      utils.WrongDocumentError();
    }
  }},

  removeChildren: { value: utils.shouldOverride },

  // Insert this node as a child of parent before the specified child,
  // or insert as the last child of parent if specified child is null,
  // or replace the specified child with this node, firing mutation events as
  // necessary
  _insertOrReplace: { value: function _insertOrReplace(parent, before, isReplace) {
    var child = this, before_index, i;

    if (child.nodeType === DOCUMENT_FRAGMENT_NODE && child.rooted) {
      utils.HierarchyRequestError();
    }

    /* Ensure index of `before` is cached before we (possibly) remove it. */
    if (parent._childNodes) {
      before_index = (before === null) ? parent._childNodes.length :
        before.index; /* ensure _index is cached */

      // If we are already a child of the specified parent, then
      // the index may have to be adjusted.
      if (child.parentNode === parent) {
        var child_index = child.index;
        // If the child is before the spot it is to be inserted at,
        // then when it is removed, the index of that spot will be
        // reduced.
        if (child_index < before_index) {
          before_index--;
        }
      }
    }

    // Delete the old child
    if (isReplace) {
      if (before.rooted) before.doc.mutateRemove(before);
      before.parentNode = null;
    }

    var n = before;
    if (n === null) { n = parent.firstChild; }

    // If both the child and the parent are rooted, then we want to
    // transplant the child without uprooting and rerooting it.
    var bothRooted = child.rooted && parent.rooted;
    if (child.nodeType === DOCUMENT_FRAGMENT_NODE) {
      var spliceArgs = [0, isReplace ? 1 : 0], next;
      for (var kid = child.firstChild; kid !== null; kid = next) {
        next = kid.nextSibling;
        spliceArgs.push(kid);
        kid.parentNode = parent;
      }
      var len = spliceArgs.length;
      // Add all nodes to the new parent, overwriting the old child
      if (isReplace) {
        LinkedList.replace(n, len > 2 ? spliceArgs[2] : null);
      } else if (len > 2 && n !== null) {
        LinkedList.insertBefore(spliceArgs[2], n);
      }
      if (parent._childNodes) {
        spliceArgs[0] = (before === null) ?
          parent._childNodes.length : before._index;
        parent._childNodes.splice.apply(parent._childNodes, spliceArgs);
        for (i=2; i<len; i++) {
          spliceArgs[i]._index = spliceArgs[0] + (i - 2);
        }
      } else if (parent._firstChild === before) {
        if (len > 2) {
          parent._firstChild = spliceArgs[2];
        } else if (isReplace) {
          parent._firstChild = null;
        }
      }
      // Remove all nodes from the document fragment
      if (child._childNodes) {
        child._childNodes.length = 0;
      } else {
        child._firstChild = null;
      }
      // Call the mutation handlers
      // Use spliceArgs since the original array has been destroyed. The
      // liveness guarantee requires us to clone the array so that
      // references to the childNodes of the DocumentFragment will be empty
      // when the insertion handlers are called.
      if (parent.rooted) {
        parent.modify();
        for (i = 2; i < len; i++) {
          parent.doc.mutateInsert(spliceArgs[i]);
        }
      }
    } else {
      if (before === child) { return; }
      if (bothRooted) {
        // Remove the child from its current position in the tree
        // without calling remove(), since we don't want to uproot it.
        child._remove();
      } else if (child.parentNode) {
        child.remove();
      }

      // Insert it as a child of its new parent
      child.parentNode = parent;
      if (isReplace) {
        LinkedList.replace(n, child);
        if (parent._childNodes) {
          child._index = before_index;
          parent._childNodes[before_index] = child;
        } else if (parent._firstChild === before) {
          parent._firstChild = child;
        }
      } else {
        if (n !== null) {
          LinkedList.insertBefore(child, n);
        }
        if (parent._childNodes) {
          child._index = before_index;
          parent._childNodes.splice(before_index, 0, child);
        } else if (parent._firstChild === before) {
          parent._firstChild = child;
        }
      }
      if (bothRooted) {
        parent.modify();
        // Generate a move mutation event
        parent.doc.mutateMove(child);
      } else if (parent.rooted) {
        parent.modify();
        parent.doc.mutateInsert(child);
      }
    }
  }},


  // Return the lastModTime value for this node. (For use as a
  // cache invalidation mechanism. If the node does not already
  // have one, initialize it from the owner document's modclock
  // property. (Note that modclock does not return the actual
  // time; it is simply a counter incremented on each document
  // modification)
  lastModTime: { get: function() {
    if (!this._lastModTime) {
      this._lastModTime = this.doc.modclock;
    }
    return this._lastModTime;
  }},

  // Increment the owner document's modclock and use the new
  // value to update the lastModTime value for this node and
  // all of its ancestors. Nodes that have never had their
  // lastModTime value queried do not need to have a
  // lastModTime property set on them since there is no
  // previously queried value to ever compare the new value
  // against, so only update nodes that already have a
  // _lastModTime property.
  modify: { value: function() {
    if (this.doc.modclock) { // Skip while doc.modclock == 0
      var time = ++this.doc.modclock;
      for(var n = this; n; n = n.parentElement) {
        if (n._lastModTime) {
          n._lastModTime = time;
        }
      }
    }
  }},

  // This attribute is not part of the DOM but is quite helpful.
  // It returns the document with which a node is associated.  Usually
  // this is the ownerDocument. But ownerDocument is null for the
  // document object itself, so this is a handy way to get the document
  // regardless of the node type
  doc: { get: function() {
    return this.ownerDocument || this;
  }},


  // If the node has a nid (node id), then it is rooted in a document
  rooted: { get: function() {
    return !!this._nid;
  }},

  normalize: { value: function() {
    var next;
    for (var child=this.firstChild; child !== null; child=next) {
      next = child.nextSibling;

      if (child.normalize) {
        child.normalize();
      }

      if (child.nodeType !== Node.TEXT_NODE) {
        continue;
      }

      if (child.nodeValue === "") {
        this.removeChild(child);
        continue;
      }

      var prevChild = child.previousSibling;
      if (prevChild === null) {
        continue;
      } else if (prevChild.nodeType === Node.TEXT_NODE) {
        // merge this with previous and remove the child
        prevChild.appendData(child.nodeValue);
        this.removeChild(child);
      }
    }
  }},

  // Convert the children of a node to an HTML string.
  // This is used by the innerHTML getter
  // The serialization spec is at:
  // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#serializing-html-fragments
  //
  // The serialization logic is intentionally implemented in a separate
  // `NodeUtils` helper instead of the more obvious choice of a private
  // `_serializeOne()` method on the `Node.prototype` in order to avoid
  // the megamorphic `this._serializeOne` property access, which reduces
  // performance unnecessarily. If you need specialized behavior for a
  // certain subclass, you'll need to implement that in `NodeUtils`.
  // See https://github.com/fgnass/domino/pull/142 for more information.
  serialize: { value: function() {
    var s = '';
    for (var kid = this.firstChild; kid !== null; kid = kid.nextSibling) {
      s += NodeUtils.serializeOne(kid, this);
    }
    return s;
  }},

  // Non-standard, but often useful for debugging.
  outerHTML: {
    get: function() {
      return NodeUtils.serializeOne(this, { nodeType: 0 });
    },
    set: utils.nyi,
  },

  // mirror node type properties in the prototype, so they are present
  // in instances of Node (and subclasses)
  ELEMENT_NODE:                { value: ELEMENT_NODE },
  ATTRIBUTE_NODE:              { value: ATTRIBUTE_NODE },
  TEXT_NODE:                   { value: TEXT_NODE },
  CDATA_SECTION_NODE:          { value: CDATA_SECTION_NODE },
  ENTITY_REFERENCE_NODE:       { value: ENTITY_REFERENCE_NODE },
  ENTITY_NODE:                 { value: ENTITY_NODE },
  PROCESSING_INSTRUCTION_NODE: { value: PROCESSING_INSTRUCTION_NODE },
  COMMENT_NODE:                { value: COMMENT_NODE },
  DOCUMENT_NODE:               { value: DOCUMENT_NODE },
  DOCUMENT_TYPE_NODE:          { value: DOCUMENT_TYPE_NODE },
  DOCUMENT_FRAGMENT_NODE:      { value: DOCUMENT_FRAGMENT_NODE },
  NOTATION_NODE:               { value: NOTATION_NODE },

  DOCUMENT_POSITION_DISCONNECTED: { value: DOCUMENT_POSITION_DISCONNECTED },
  DOCUMENT_POSITION_PRECEDING:    { value: DOCUMENT_POSITION_PRECEDING },
  DOCUMENT_POSITION_FOLLOWING:    { value: DOCUMENT_POSITION_FOLLOWING },
  DOCUMENT_POSITION_CONTAINS:     { value: DOCUMENT_POSITION_CONTAINS },
  DOCUMENT_POSITION_CONTAINED_BY: { value: DOCUMENT_POSITION_CONTAINED_BY },
  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: { value: DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC },
});