• atwiki
  • TEST
  • Simple Implementation of DOM Traversal in JavaScript

TEST

Simple Implementation of DOM Traversal in JavaScript

最終更新:

eriax

- view
管理者のみ編集可

制限

  • なし。

仕様上の注意

  • TreeWalker と違い、NodeIterator は参照ノードが除去されても文書木に残り、参照ノードを調節する。
  • NodeIterator は NodeFilter.FILTER_REJECT と FILTER_SKIP を同一に扱う。

ソースコード

function DOMObject(arg) {
  if (arguments.length > 0) {
    var name;
    for (name in arg) {
      if (arg.hasOwnProperty(name)) {
        this[name] = arg[name];
      }
    }
  }
}

(function () {
  this.constructor = DOMObject;
}).call(DOMObject.prototype);

////////////////////////////////////////////////////////////////////////

function DOMNodeFilter(arg) {
  if (arguments.length > 0) {
    if (arg) {
      DOMObject.apply(this, arguments);
    }
  }
};
DOMNodeFilter.prototype = new DOMObject;

(function () {
  this.constructor = DOMNodeFilter;
  this.acceptNode = null;
}).call(DOMNodeFilter.prototype);

(function () {
  // Constants returned by acceptNode
  this.FILTER_ACCEPT = 1;
  this.FILTER_REJECT = 2;
  this.FILTER_SKIP = 3;
  // Constants for whatToShow
  this.SHOW_ALL = 0xFFFFFFFF;
  this.SHOW_ELEMENT = 0x00000001;
  this.SHOW_ATTRIBUTE = 0x00000002;
  this.SHOW_TEXT = 0x00000004;
  this.SHOW_CDATA_SECTION = 0x00000008;
  this.SHOW_ENTITY_REFERENCE = 0x00000010;
  this.SHOW_ENTITY = 0x00000020;
  this.SHOW_PROCESSING_INSTRUCTION = 0x00000040;
  this.SHOW_COMMENT = 0x00000080;
  this.SHOW_DOCUMENT = 0x00000100;
  this.SHOW_DOCUMENT_TYPE = 0x00000200;
  this.SHOW_DOCUMENT_FRAGMENT = 0x00000400;
  this.SHOW_NOTATION = 0x00000800;
}).call(DOMNodeFilter);

////////////////////////////////////////////////////////////////////////

function DOMNodeIterator(arg) {
  if (arguments.length > 0) {
    if (arg) {
      DOMObject.apply(this, arguments);
    }
  }
}

DOMNodeIterator.prototype = new DOMObject;

(function () {
  this.constructor = DOMNodeIterator;
  this.root = null;
  this.whatToShow = null;
  this.filter = null;
  this.expandEntityReference = null;
  this['[CurrentRoute]'] = null;
}).call(DOMNodeIterator.prototype);

(function () {
  this['[GetChildIndex]'] = function (node) {
    var i = 0;
    var n = node;
    while ((n = n.previousSibling)) {
      i += 1;
    }
    return i;
  };
  this['[TraceRoute]'] = function (node) {
    var result = [];
    var root = this.root;
    while (node) {
      if (root === node) {
        result[result.length] = [node, 0];
        return result.reverse();
      }
      else {
        result[result.length] = [node, this['[GetChildIndex]'](node)];
        node = node.parentNode;
      }
    }
    return [];
  };
  this['[HasRoot]'] = function (node) {
    var root = this.root;
    var n = node;
    for (; n; n = n.parentNode) {
      if (n === root) {
        break;
      }
    }
    return Boolean(n);
  };
  this.nextNode = function () {
    if (!this.root) {
      throw new Error('already detached.'); // DOMException.INVALID_STATE_ERR
    }
    var root = this.root;
    var node = root;
    var route = this['[CurrentRoute]'];
    var stepCount = route.length;
    var n;
    switch (stepCount) {
    case 0:
      return null;
    case 1:
      var step0 = route[0];
      if (step0[0] === root && step0[1] === 1) {
        return null;
      }
      break;
    default:
      var step;
      var i;
      for (i = 1; i < stepCount; i++) {
        step = route[i];
        var cache = step[0];
        if (this['[HasRoot]'](cache)) { // cache still exists under the root
          node = cache;
          continue;
        }
        var cndex = step[1];
        var childNodes = node.childNodes;
        if (cndex < childNodes.length) {
          node = childNodes[cndex];
          break;
        }
        this['[CurrentRoute]'] = [
          [root, 1]
        ];
        return null;
      }
      break;
    }
    var accepted = null;
    var dir;
    while (true) {
      switch (this['[CallFilter]'](node)) {
      case DOMNodeFilter.FILTER_ACCEPT:
        accepted = node;
        // NodeIterators treat this as a synonym for FILTER_SKIP
      case DOMNodeFilter.FILTER_REJECT:
      case DOMNodeFilter.FILTER_SKIP:
        dir = 'firstChild';
        break;
      default:
        throw new Error;
      }
      if ((n = node[dir])) {
        node = n;
      }
      else {
        do {
          if (node === root) {
            node = null;
            break;
          }
          if ((n = node.nextSibling)) {
            node = n;
            break;
          }
        }
        while ((node = node.parentNode));
      }
      if (accepted) {
        if (node == null) { // out of visible list
          this['[CurrentRoute]'] = [
            [root, 1]
          ];
        }
        else {
          this['[CurrentRoute]'] = this['[TraceRoute]'](node); // A . [N]
        }
        return accepted;
      }
      if (node == null) { // out of visible list
        this['[CurrentRoute]'] = [
          [root, 1]
        ];
        return null;
      }
    }
  };
  this.previousNode = function () {
    if (!this.root) {
      throw new Error('already detached.'); // DOMException.INVALID_STATE_ERR
    }
    var root = this.root;
    var node = root;
    var route = this['[CurrentRoute]'];
    var stepCount = route.length;
    var n;
    switch (stepCount) {
    case 0:
      return null;
    case 1:
      var step0 = route[0];
      if (step0[0] === root && step0[1] === 0) {
        return null;
      }
      while ((n = node.lastChild)) {
        node = n;
      }
      break;
    default:
      var step;
      var i;
      for (i = 1; i < stepCount; i++) {
        step = route[i];
        var cache = step[0];
        if (this['[HasRoot]'](cache)) { // cache still exists under the root
          node = cache;
          continue;
        }
        var cndex = step[1];
        var childNodes = node.childNodes;
        if (cndex < childNodes.length) {
          node = childNodes[cndex];
          break;
        }
        this['[CurrentRoute]'] = [
          [root, 0]
        ];
        return null;
      }
      break;
    }
    while (true) {
      if ((n = node.previousSibling)) {
        for (node = n; true;) {
          if ((n = node.lastChild)) {
            node = n;
            continue;
          }
          break;
        }
      }
      else {
        node = node.parentNode;
        if (node == null) { // out of visible list
          this['[CurrentRoute]'] = [
            [root, 0]
          ];
          return null;
        }
      }
      switch (this['[CallFilter]'](node)) {
      case DOMNodeFilter.FILTER_ACCEPT:
        if (node === root) {
          this['[CurrentRoute]'] = [
            [root, 0]
          ];
        }
        else {
          this['[CurrentRoute]'] = this['[TraceRoute]'](node); // [P] . A
        }
        return node;
      case DOMNodeFilter.FILTER_REJECT:
      case DOMNodeFilter.FILTER_SKIP:
        break;
      default:
        throw new Error;
      }
    }
  };
  this.detach = function () {
    if (!this.root) {
      throw new Error('already detached.'); // DOMException.INVALID_STATE_ERR
    }
    this.root = null;
    this.whatToShow = null;
    this.filter = null;
    this.expandEntityReference = null;
  };
  this['[CallFilter]'] = function (node) {
    if (0 !== (this.whatToShow & (1 << node.nodeType - 1))) {
      var filter = this.filter;
      if (filter) {
        if ('function' === typeof filter.acceptNode) {
          return filter.acceptNode(node);
        }
        return filter.call(filter, node);
      }
      return DOMNodeFilter.FILTER_ACCEPT;
    }
    return DOMNodeFilter.FILTER_SKIP;
  };
}).call(DOMNodeIterator.prototype);

var as_DOM_DocumentTraversal_createNodeIterator = function (doc, root, whatToShow, filter, entityReferenceExpansion) {
  if (root == null) { // or undefined
    throw new Error; // DOMException.NOT_SUPPORTED_ERR(9)
  }
  var nt = new DOMNodeIterator({
    root: root,
    whatToShow: whatToShow,
    filter: filter,
    expandEntityReference: entityReferenceExpansion
  });
  nt['[CurrentRoute]'] = nt['[TraceRoute]'](root);
  return nt;
};

////////////////////////////////////////////////////////////////////////

function DOMTreeWalker(arg) {
  if (arguments.length > 0) {
    if (arg) {
      DOMObject.apply(this, arguments);
    }
  }
}

DOMTreeWalker.prototype = new DOMObject;

(function () {
  this.constructor = DOMTreeWalker;
  this.root = null;
  this.whatToShow = null;
  this.filter = null;
  this.expandEntityReference = null;
  this.currentNode = null;
}).call(DOMTreeWalker.prototype);

(function () {
  this['[HasRoot]'] = function (node) {
    var root = this.root;
    var n = node;
    for (; n; n = n.parentNode) {
      if (n === root) {
        break;
      }
    }
    return Boolean(n);
  };
  this['[CallFilter]'] = function (node) {
    if (0 !== (this.whatToShow & (1 << node.nodeType - 1))) {
      var filter = this.filter;
      if (filter) {
        if ('function' === typeof filter.acceptNode) {
          return filter.acceptNode(node);
        }
        return filter.call(filter, node);
      }
      return DOMNodeFilter.FILTER_ACCEPT;
    }
    return DOMNodeFilter.FILTER_SKIP;
  };
  this.nextNode = function () {
    var node = this.currentNode;
    if (!this['[HasRoot]'](node)) {
      return null;
    }
    var root = this.root;
    var dir = 'firstChild';
    var n;
    while (true) {
      if ((n = node[dir])) {
        node = n;
      }
      else {
        do {
          if (node === root) {
            node = null;
            break;
          }
          if ((n = node.nextSibling)) {
            node = n;
            break;
          }
        }
        while ((node = node.parentNode));
      }
      if (node == null) {
        return null;
      }
      switch (this['[CallFilter]'](node)) {
      case DOMNodeFilter.FILTER_ACCEPT:
        this.currentNode = node;
        return node;
      case DOMNodeFilter.FILTER_SKIP:
        dir = 'firstChild';
        break;
      case DOMNodeFilter.FILTER_REJECT:
        dir = 'nextSibling';
        break;
      default:
        throw new Error;
      }
    }
  };
  this.previousNode = function () {
    var node = this.currentNode;
    if (!this['[HasRoot]'](node)) {
      return null;
    }
    var root = this.root;
    var n;
    while (true) {
      if (node === root) {
        return null;
      }
      if ((n = node.previousSibling)) {
        for (node = n; true;) {
          if (this['[CallFilter]'](node) !== DOMNodeFilter.FILTER_REJECT && ((n = node.lastChild))) {
            node = n;
            continue;
          }
          break;
        }
      }
      else {
        node = node.parentNode;
        if (node == null) { // out of visible list
          return null;
        }
      }
      switch (this['[CallFilter]'](node)) {
      case DOMNodeFilter.FILTER_ACCEPT:
        this.currentNode = node;
        return node;
      case DOMNodeFilter.FILTER_SKIP:
      case DOMNodeFilter.FILTER_REJECT:
        break;
      default:
        throw new Error;
      }
    }
  };
  this.parentNode = function () {
    var node = this.currentNode;
    if (!this['[HasRoot]'](node)) {
      return null;
    }
    var root = this.root;
    var n;
    while (true) {
      if (node === root) {
        return null;
      }
      node = node.parentNode;
      if (node == null) { // out of visible list
        return null;
      }
      switch (this['[CallFilter]'](node)) {
      case DOMNodeFilter.FILTER_ACCEPT:
        this.currentNode = node;
        return node;
      case DOMNodeFilter.FILTER_SKIP:
      case DOMNodeFilter.FILTER_REJECT:
        break;
      default:
        throw new Error;
      }
    }
  };
  this.firstChild = function () {
    var node = this.currentNode;
    if (!this['[HasRoot]'](node)) {
      return null;
    }
    for (node = node.firstChild; node; node = node.nextSibling) {
      switch (this['[CallFilter]'](node)) {
      case DOMNodeFilter.FILTER_ACCEPT:
        this.currentNode = node;
        return node;
      case DOMNodeFilter.FILTER_SKIP:
      case DOMNodeFilter.FILTER_REJECT:
        break;
      default:
        throw new Error;
      }
    }
    return null;
  };
  this.lastChild = function () {
    var node = this.currentNode;
    if (!this['[HasRoot]'](node)) {
      return null;
    }
    for (node = node.lastChild; node; node = node.previousSibling) {
      switch (this['[CallFilter]'](node)) {
      case DOMNodeFilter.FILTER_ACCEPT:
        this.currentNode = node;
        return node;
      case DOMNodeFilter.FILTER_SKIP:
      case DOMNodeFilter.FILTER_REJECT:
        break;
      default:
        throw new Error;
      }
    }
    return null;
  };
  this.previousSibling = function () {
    var node = this.currentNode;
    if (!this['[HasRoot]'](node)) {
      return null;
    }
    while ((node = node.previousSibling)) {
      switch (this['[CallFilter]'](node)) {
      case DOMNodeFilter.FILTER_ACCEPT:
        this.currentNode = node;
        return node;
      case DOMNodeFilter.FILTER_SKIP:
      case DOMNodeFilter.FILTER_REJECT:
        break;
      default:
        throw new Error;
      }
    }
    return null;
  };
  this.nextSibling = function () {
    var node = this.currentNode;
    if (!this['[HasRoot]'](node)) {
      return null;
    }
    while ((node = node.nextSibling)) {
      switch (this['[CallFilter]'](node)) {
      case DOMNodeFilter.FILTER_ACCEPT:
        this.currentNode = node;
        return node;
      case DOMNodeFilter.FILTER_SKIP:
      case DOMNodeFilter.FILTER_REJECT:
        break;
      default:
        throw new Error;
      }
    }
    return null;
  };
}).call(DOMTreeWalker.prototype);

var as_DOM_DocumentTraversal_createTreeWalker = function (doc, root, whatToShow, filter, entityReferenceExpansion) {
  if (root == null) { // or undefined
    throw new Error; // DOMException.NOT_SUPPORTED_ERR(9)
  }
  var tw = new DOMTreeWalker({
    root: root,
    whatToShow: whatToShow,
    filter: filter,
    expandEntityReference: entityReferenceExpansion,
    currentNode: root
  });
  return tw;
};
  • 初出 2011-08-24
目安箱バナー