loganbek / Mutation Summary

// ==UserScript==
// @namespace       https://openuserjs.org/users/parlor.trickss
// @exclude         *

// ==UserLibrary==
// @name            Mutation Summary
// @description     Mutation Summary is a JavaScript library that makes observing changes to the DOM fast, easy and safe.
// @copyright       2011, Google Inc.
// @license         Apache-2.0
// @version         1.0
// @homepageURL     https://github.com/rafaelw/mutation-summary

// ==/UserScript==

// ==/UserLibrary==

// ==OpenUserJS==
// @author parlor.trickss
// ==/OpenUseJs==

// Copyright 2011 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.
var __extends = this.__extends || function (d, b) {
  for (var p in b)
    if (b.hasOwnProperty(p)) d[p] = b[p];

  function __() {
    this.constructor = d;
  }
  __.prototype = b.prototype;
  d.prototype = new __();
};
var MutationObserverCtor;
if (typeof WebKitMutationObserver !== 'undefined')
  MutationObserverCtor = WebKitMutationObserver;
else
  MutationObserverCtor = MutationObserver;
if (MutationObserverCtor === undefined) {
  console.error('DOM Mutation Observers are required.');
  console.error('https://developer.mozilla.org/en-US/docs/DOM/MutationObserver');
  throw Error('DOM Mutation Observers are required');
}
var NodeMap = (function () {
  function NodeMap() {
    this.nodes = [];
    this.values = [];
  }
  NodeMap.prototype.isIndex = function (s) {
    return +s === s >>> 0;
  };
  NodeMap.prototype.nodeId = function (node) {
    var id = node[NodeMap.ID_PROP];
    if (!id)
      id = node[NodeMap.ID_PROP] = NodeMap.nextId_++;
    return id;
  };
  NodeMap.prototype.set = function (node, value) {
    var id = this.nodeId(node);
    this.nodes[id] = node;
    this.values[id] = value;
  };
  NodeMap.prototype.get = function (node) {
    var id = this.nodeId(node);
    return this.values[id];
  };
  NodeMap.prototype.has = function (node) {
    return this.nodeId(node) in this.nodes;
  };
  NodeMap.prototype.delete = function (node) {
    var id = this.nodeId(node);
    delete this.nodes[id];
    this.values[id] = undefined;
  };
  NodeMap.prototype.keys = function () {
    var nodes = [];
    for (var id in this.nodes) {
      if (!this.isIndex(id))
        continue;
      nodes.push(this.nodes[id]);
    }
    return nodes;
  };
  NodeMap.ID_PROP = '__mutation_summary_node_map_id__';
  NodeMap.nextId_ = 1;
  return NodeMap;
})();
/**
 *  var reachableMatchableProduct = [
 *  //  STAYED_OUT,  ENTERED,     STAYED_IN,   EXITED
 *    [ STAYED_OUT,  STAYED_OUT,  STAYED_OUT,  STAYED_OUT ], // STAYED_OUT
 *    [ STAYED_OUT,  ENTERED,     ENTERED,     STAYED_OUT ], // ENTERED
 *    [ STAYED_OUT,  ENTERED,     STAYED_IN,   EXITED     ], // STAYED_IN
 *    [ STAYED_OUT,  STAYED_OUT,  EXITED,      EXITED     ]  // EXITED
 *  ];
 */
var Movement;
(function (Movement) {
  Movement[Movement["STAYED_OUT"] = 0] = "STAYED_OUT";
  Movement[Movement["ENTERED"] = 1] = "ENTERED";
  Movement[Movement["STAYED_IN"] = 2] = "STAYED_IN";
  Movement[Movement["REPARENTED"] = 3] = "REPARENTED";
  Movement[Movement["REORDERED"] = 4] = "REORDERED";
  Movement[Movement["EXITED"] = 5] = "EXITED";
})(Movement || (Movement = {}));

function enteredOrExited(changeType) {
  return changeType === Movement.ENTERED || changeType === Movement.EXITED;
}
var NodeChange = (function () {
  function NodeChange(node, childList, attributes, characterData, oldParentNode, added, attributeOldValues, characterDataOldValue) {
    if (childList === void 0) {
      childList = false;
    }
    if (attributes === void 0) {
      attributes = false;
    }
    if (characterData === void 0) {
      characterData = false;
    }
    if (oldParentNode === void 0) {
      oldParentNode = null;
    }
    if (added === void 0) {
      added = false;
    }
    if (attributeOldValues === void 0) {
      attributeOldValues = null;
    }
    if (characterDataOldValue === void 0) {
      characterDataOldValue = null;
    }
    this.node = node;
    this.childList = childList;
    this.attributes = attributes;
    this.characterData = characterData;
    this.oldParentNode = oldParentNode;
    this.added = added;
    this.attributeOldValues = attributeOldValues;
    this.characterDataOldValue = characterDataOldValue;
    this.isCaseInsensitive =
      this.node.nodeType === Node.ELEMENT_NODE &&
      this.node instanceof HTMLElement &&
      this.node.ownerDocument instanceof HTMLDocument;
  }
  NodeChange.prototype.getAttributeOldValue = function (name) {
    if (!this.attributeOldValues)
      return undefined;
    if (this.isCaseInsensitive)
      name = name.toLowerCase();
    return this.attributeOldValues[name];
  };
  NodeChange.prototype.getAttributeNamesMutated = function () {
    var names = [];
    if (!this.attributeOldValues)
      return names;
    for (var name in this.attributeOldValues) {
      names.push(name);
    }
    return names;
  };
  NodeChange.prototype.attributeMutated = function (name, oldValue) {
    this.attributes = true;
    this.attributeOldValues = this.attributeOldValues || {};
    if (name in this.attributeOldValues)
      return;
    this.attributeOldValues[name] = oldValue;
  };
  NodeChange.prototype.characterDataMutated = function (oldValue) {
    if (this.characterData)
      return;
    this.characterData = true;
    this.characterDataOldValue = oldValue;
  };
  // Note: is it possible to receive a removal followed by a removal. This
  // can occur if the removed node is added to an non-observed node, that
  // node is added to the observed area, and then the node removed from
  // it.
  NodeChange.prototype.removedFromParent = function (parent) {
    this.childList = true;
    if (this.added || this.oldParentNode)
      this.added = false;
    else
      this.oldParentNode = parent;
  };
  NodeChange.prototype.insertedIntoParent = function () {
    this.childList = true;
    this.added = true;
  };
  // An node's oldParent is
  //   -its present parent, if its parentNode was not changed.
  //   -null if the first thing that happened to it was an add.
  //   -the node it was removed from if the first thing that happened to it
  //      was a remove.
  NodeChange.prototype.getOldParent = function () {
    if (this.childList) {
      if (this.oldParentNode)
        return this.oldParentNode;
      if (this.added)
        return null;
    }
    return this.node.parentNode;
  };
  return NodeChange;
})();
var ChildListChange = (function () {
  function ChildListChange() {
    this.added = new NodeMap();
    this.removed = new NodeMap();
    this.maybeMoved = new NodeMap();
    this.oldPrevious = new NodeMap();
    this.moved = undefined;
  }
  return ChildListChange;
})();
var TreeChanges = (function (_super) {
  __extends(TreeChanges, _super);

  function TreeChanges(rootNode, mutations) {
    _super.call(this);
    this.rootNode = rootNode;
    this.reachableCache = undefined;
    this.wasReachableCache = undefined;
    this.anyParentsChanged = false;
    this.anyAttributesChanged = false;
    this.anyCharacterDataChanged = false;
    for (var m = 0; m < mutations.length; m++) {
      var mutation = mutations[m];
      switch (mutation.type) {
        case 'childList':
          this.anyParentsChanged = true;
          for (var i = 0; i < mutation.removedNodes.length; i++) {
            var node = mutation.removedNodes[i];
            this.getChange(node).removedFromParent(mutation.target);
          }
          for (var i = 0; i < mutation.addedNodes.length; i++) {
            var node = mutation.addedNodes[i];
            this.getChange(node).insertedIntoParent();
          }
          break;
        case 'attributes':
          this.anyAttributesChanged = true;
          var change = this.getChange(mutation.target);
          change.attributeMutated(mutation.attributeName, mutation.oldValue);
          break;
        case 'characterData':
          this.anyCharacterDataChanged = true;
          var change = this.getChange(mutation.target);
          change.characterDataMutated(mutation.oldValue);
          break;
      }
    }
  }
  TreeChanges.prototype.getChange = function (node) {
    var change = this.get(node);
    if (!change) {
      change = new NodeChange(node);
      this.set(node, change);
    }
    return change;
  };
  TreeChanges.prototype.getOldParent = function (node) {
    var change = this.get(node);
    return change ? change.getOldParent() : node.parentNode;
  };
  TreeChanges.prototype.getIsReachable = function (node) {
    if (node === this.rootNode)
      return true;
    if (!node)
      return false;
    this.reachableCache = this.reachableCache || new NodeMap();
    var isReachable = this.reachableCache.get(node);
    if (isReachable === undefined) {
      isReachable = this.getIsReachable(node.parentNode);
      this.reachableCache.set(node, isReachable);
    }
    return isReachable;
  };
  // A node wasReachable if its oldParent wasReachable.
  TreeChanges.prototype.getWasReachable = function (node) {
    if (node === this.rootNode)
      return true;
    if (!node)
      return false;
    this.wasReachableCache = this.wasReachableCache || new NodeMap();
    var wasReachable = this.wasReachableCache.get(node);
    if (wasReachable === undefined) {
      wasReachable = this.getWasReachable(this.getOldParent(node));
      this.wasReachableCache.set(node, wasReachable);
    }
    return wasReachable;
  };
  TreeChanges.prototype.reachabilityChange = function (node) {
    if (this.getIsReachable(node)) {
      return this.getWasReachable(node) ?
        Movement.STAYED_IN : Movement.ENTERED;
    }
    return this.getWasReachable(node) ?
      Movement.EXITED : Movement.STAYED_OUT;
  };
  return TreeChanges;
})(NodeMap);
var MutationProjection = (function () {
  // TOOD(any)
  function MutationProjection(rootNode, mutations, selectors, calcReordered, calcOldPreviousSibling) {
    this.rootNode = rootNode;
    this.mutations = mutations;
    this.selectors = selectors;
    this.calcReordered = calcReordered;
    this.calcOldPreviousSibling = calcOldPreviousSibling;
    this.treeChanges = new TreeChanges(rootNode, mutations);
    this.entered = [];
    this.exited = [];
    this.stayedIn = new NodeMap();
    this.visited = new NodeMap();
    this.childListChangeMap = undefined;
    this.characterDataOnly = undefined;
    this.matchCache = undefined;
    this.processMutations();
  }
  MutationProjection.prototype.processMutations = function () {
    if (!this.treeChanges.anyParentsChanged &&
      !this.treeChanges.anyAttributesChanged)
      return;
    var changedNodes = this.treeChanges.keys();
    for (var i = 0; i < changedNodes.length; i++) {
      this.visitNode(changedNodes[i], undefined);
    }
  };
  MutationProjection.prototype.visitNode = function (node, parentReachable) {
    if (this.visited.has(node))
      return;
    this.visited.set(node, true);
    var change = this.treeChanges.get(node);
    var reachable = parentReachable;
    // node inherits its parent's reachability change unless
    // its parentNode was mutated.
    if ((change && change.childList) || reachable == undefined)
      reachable = this.treeChanges.reachabilityChange(node);
    if (reachable === Movement.STAYED_OUT)
      return;
    // Cache match results for sub-patterns.
    this.matchabilityChange(node);
    if (reachable === Movement.ENTERED) {
      this.entered.push(node);
    }
    else if (reachable === Movement.EXITED) {
      this.exited.push(node);
      this.ensureHasOldPreviousSiblingIfNeeded(node);
    }
    else if (reachable === Movement.STAYED_IN) {
      var movement = Movement.STAYED_IN;
      if (change && change.childList) {
        if (change.oldParentNode !== node.parentNode) {
          movement = Movement.REPARENTED;
          this.ensureHasOldPreviousSiblingIfNeeded(node);
        }
        else if (this.calcReordered && this.wasReordered(node)) {
          movement = Movement.REORDERED;
        }
      }
      this.stayedIn.set(node, movement);
    }
    if (reachable === Movement.STAYED_IN)
      return;
    // reachable === ENTERED || reachable === EXITED.
    for (var child = node.firstChild; child; child = child.nextSibling) {
      this.visitNode(child, reachable);
    }
  };
  MutationProjection.prototype.ensureHasOldPreviousSiblingIfNeeded = function (node) {
    if (!this.calcOldPreviousSibling)
      return;
    this.processChildlistChanges();
    var parentNode = node.parentNode;
    var nodeChange = this.treeChanges.get(node);
    if (nodeChange && nodeChange.oldParentNode)
      parentNode = nodeChange.oldParentNode;
    var change = this.childListChangeMap.get(parentNode);
    if (!change) {
      change = new ChildListChange();
      this.childListChangeMap.set(parentNode, change);
    }
    if (!change.oldPrevious.has(node)) {
      change.oldPrevious.set(node, node.previousSibling);
    }
  };
  MutationProjection.prototype.getChanged = function (summary, selectors, characterDataOnly) {
    this.selectors = selectors;
    this.characterDataOnly = characterDataOnly;
    for (var i = 0; i < this.entered.length; i++) {
      var node = this.entered[i];
      var matchable = this.matchabilityChange(node);
      if (matchable === Movement.ENTERED || matchable === Movement.STAYED_IN)
        summary.added.push(node);
    }
    var stayedInNodes = this.stayedIn.keys();
    for (var i = 0; i < stayedInNodes.length; i++) {
      var node = stayedInNodes[i];
      var matchable = this.matchabilityChange(node);
      if (matchable === Movement.ENTERED) {
        summary.added.push(node);
      }
      else if (matchable === Movement.EXITED) {
        summary.removed.push(node);
      }
      else if (matchable === Movement.STAYED_IN && (summary.reparented || summary.reordered)) {
        var movement = this.stayedIn.get(node);
        if (summary.reparented && movement === Movement.REPARENTED)
          summary.reparented.push(node);
        else if (summary.reordered && movement === Movement.REORDERED)
          summary.reordered.push(node);
      }
    }
    for (var i = 0; i < this.exited.length; i++) {
      var node = this.exited[i];
      var matchable = this.matchabilityChange(node);
      if (matchable === Movement.EXITED || matchable === Movement.STAYED_IN)
        summary.removed.push(node);
    }
  };
  MutationProjection.prototype.getOldParentNode = function (node) {
    var change = this.treeChanges.get(node);
    if (change && change.childList)
      return change.oldParentNode ? change.oldParentNode : null;
    var reachabilityChange = this.treeChanges.reachabilityChange(node);
    if (reachabilityChange === Movement.STAYED_OUT || reachabilityChange === Movement.ENTERED)
      throw Error('getOldParentNode requested on invalid node.');
    return node.parentNode;
  };
  MutationProjection.prototype.getOldPreviousSibling = function (node) {
    var parentNode = node.parentNode;
    var nodeChange = this.treeChanges.get(node);
    if (nodeChange && nodeChange.oldParentNode)
      parentNode = nodeChange.oldParentNode;
    var change = this.childListChangeMap.get(parentNode);
    if (!change)
      throw Error('getOldPreviousSibling requested on invalid node.');
    return change.oldPrevious.get(node);
  };
  MutationProjection.prototype.getOldAttribute = function (element, attrName) {
    var change = this.treeChanges.get(element);
    if (!change || !change.attributes)
      throw Error('getOldAttribute requested on invalid node.');
    var value = change.getAttributeOldValue(attrName);
    if (value === undefined)
      throw Error('getOldAttribute requested for unchanged attribute name.');
    return value;
  };
  MutationProjection.prototype.attributeChangedNodes = function (includeAttributes) {
    if (!this.treeChanges.anyAttributesChanged)
      return {}; // No attributes mutations occurred.
    var attributeFilter;
    var caseInsensitiveFilter;
    if (includeAttributes) {
      attributeFilter = {};
      caseInsensitiveFilter = {};
      for (var i = 0; i < includeAttributes.length; i++) {
        var attrName = includeAttributes[i];
        attributeFilter[attrName] = true;
        caseInsensitiveFilter[attrName.toLowerCase()] = attrName;
      }
    }
    var result = {};
    var nodes = this.treeChanges.keys();
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      var change = this.treeChanges.get(node);
      if (!change.attributes)
        continue;
      if (Movement.STAYED_IN !== this.treeChanges.reachabilityChange(node) ||
        Movement.STAYED_IN !== this.matchabilityChange(node)) {
        continue;
      }
      var element = node;
      var changedAttrNames = change.getAttributeNamesMutated();
      for (var j = 0; j < changedAttrNames.length; j++) {
        var attrName = changedAttrNames[j];
        if (attributeFilter &&
          !attributeFilter[attrName] &&
          !(change.isCaseInsensitive && caseInsensitiveFilter[attrName])) {
          continue;
        }
        var oldValue = change.getAttributeOldValue(attrName);
        if (oldValue === element.getAttribute(attrName))
          continue;
        if (caseInsensitiveFilter && change.isCaseInsensitive)
          attrName = caseInsensitiveFilter[attrName];
        result[attrName] = result[attrName] || [];
        result[attrName].push(element);
      }
    }
    return result;
  };
  MutationProjection.prototype.getOldCharacterData = function (node) {
    var change = this.treeChanges.get(node);
    if (!change || !change.characterData)
      throw Error('getOldCharacterData requested on invalid node.');
    return change.characterDataOldValue;
  };
  MutationProjection.prototype.getCharacterDataChanged = function () {
    if (!this.treeChanges.anyCharacterDataChanged)
      return []; // No characterData mutations occurred.
    var nodes = this.treeChanges.keys();
    var result = [];
    for (var i = 0; i < nodes.length; i++) {
      var target = nodes[i];
      if (Movement.STAYED_IN !== this.treeChanges.reachabilityChange(target))
        continue;
      var change = this.treeChanges.get(target);
      if (!change.characterData ||
        target.textContent == change.characterDataOldValue)
        continue;
      result.push(target);
    }
    return result;
  };
  MutationProjection.prototype.computeMatchabilityChange = function (selector, el) {
    if (!this.matchCache)
      this.matchCache = [];
    if (!this.matchCache[selector.uid])
      this.matchCache[selector.uid] = new NodeMap();
    var cache = this.matchCache[selector.uid];
    var result = cache.get(el);
    if (result === undefined) {
      result = selector.matchabilityChange(el, this.treeChanges.get(el));
      cache.set(el, result);
    }
    return result;
  };
  MutationProjection.prototype.matchabilityChange = function (node) {
    var _this = this;
    // TODO(rafaelw): Include PI, CDATA?
    // Only include text nodes.
    if (this.characterDataOnly) {
      switch (node.nodeType) {
        case Node.COMMENT_NODE:
        case Node.TEXT_NODE:
          return Movement.STAYED_IN;
        default:
          return Movement.STAYED_OUT;
      }
    }
    // No element filter. Include all nodes.
    if (!this.selectors)
      return Movement.STAYED_IN;
    // Element filter. Exclude non-elements.
    if (node.nodeType !== Node.ELEMENT_NODE)
      return Movement.STAYED_OUT;
    var el = node;
    var matchChanges = this.selectors.map(function (selector) {
      return _this.computeMatchabilityChange(selector, el);
    });
    var accum = Movement.STAYED_OUT;
    var i = 0;
    while (accum !== Movement.STAYED_IN && i < matchChanges.length) {
      switch (matchChanges[i]) {
        case Movement.STAYED_IN:
          accum = Movement.STAYED_IN;
          break;
        case Movement.ENTERED:
          if (accum === Movement.EXITED)
            accum = Movement.STAYED_IN;
          else
            accum = Movement.ENTERED;
          break;
        case Movement.EXITED:
          if (accum === Movement.ENTERED)
            accum = Movement.STAYED_IN;
          else
            accum = Movement.EXITED;
          break;
      }
      i++;
    }
    return accum;
  };
  MutationProjection.prototype.getChildlistChange = function (el) {
    var change = this.childListChangeMap.get(el);
    if (!change) {
      change = new ChildListChange();
      this.childListChangeMap.set(el, change);
    }
    return change;
  };
  MutationProjection.prototype.processChildlistChanges = function () {
    if (this.childListChangeMap)
      return;
    this.childListChangeMap = new NodeMap();
    for (var i = 0; i < this.mutations.length; i++) {
      var mutation = this.mutations[i];
      if (mutation.type != 'childList')
        continue;
      if (this.treeChanges.reachabilityChange(mutation.target) !== Movement.STAYED_IN &&
        !this.calcOldPreviousSibling)
        continue;
      var change = this.getChildlistChange(mutation.target);
      var oldPrevious = mutation.previousSibling;

      function recordOldPrevious(node, previous) {
        if (!node ||
          change.oldPrevious.has(node) ||
          change.added.has(node) ||
          change.maybeMoved.has(node))
          return;
        if (previous &&
          (change.added.has(previous) ||
            change.maybeMoved.has(previous)))
          return;
        change.oldPrevious.set(node, previous);
      }
      for (var j = 0; j < mutation.removedNodes.length; j++) {
        var node = mutation.removedNodes[j];
        recordOldPrevious(node, oldPrevious);
        if (change.added.has(node)) {
          change.added.delete(node);
        }
        else {
          change.removed.set(node, true);
          change.maybeMoved.delete(node);
        }
        oldPrevious = node;
      }
      recordOldPrevious(mutation.nextSibling, oldPrevious);
      for (var j = 0; j < mutation.addedNodes.length; j++) {
        var node = mutation.addedNodes[j];
        if (change.removed.has(node)) {
          change.removed.delete(node);
          change.maybeMoved.set(node, true);
        }
        else {
          change.added.set(node, true);
        }
      }
    }
  };
  MutationProjection.prototype.wasReordered = function (node) {
    if (!this.treeChanges.anyParentsChanged)
      return false;
    this.processChildlistChanges();
    var parentNode = node.parentNode;
    var nodeChange = this.treeChanges.get(node);
    if (nodeChange && nodeChange.oldParentNode)
      parentNode = nodeChange.oldParentNode;
    var change = this.childListChangeMap.get(parentNode);
    if (!change)
      return false;
    if (change.moved)
      return change.moved.get(node);
    change.moved = new NodeMap();
    var pendingMoveDecision = new NodeMap();

    function isMoved(node) {
      if (!node)
        return false;
      if (!change.maybeMoved.has(node))
        return false;
      var didMove = change.moved.get(node);
      if (didMove !== undefined)
        return didMove;
      if (pendingMoveDecision.has(node)) {
        didMove = true;
      }
      else {
        pendingMoveDecision.set(node, true);
        didMove = getPrevious(node) !== getOldPrevious(node);
      }
      if (pendingMoveDecision.has(node)) {
        pendingMoveDecision.delete(node);
        change.moved.set(node, didMove);
      }
      else {
        didMove = change.moved.get(node);
      }
      return didMove;
    }
    var oldPreviousCache = new NodeMap();

    function getOldPrevious(node) {
      var oldPrevious = oldPreviousCache.get(node);
      if (oldPrevious !== undefined)
        return oldPrevious;
      oldPrevious = change.oldPrevious.get(node);
      while (oldPrevious &&
        (change.removed.has(oldPrevious) || isMoved(oldPrevious))) {
        oldPrevious = getOldPrevious(oldPrevious);
      }
      if (oldPrevious === undefined)
        oldPrevious = node.previousSibling;
      oldPreviousCache.set(node, oldPrevious);
      return oldPrevious;
    }
    var previousCache = new NodeMap();

    function getPrevious(node) {
      if (previousCache.has(node))
        return previousCache.get(node);
      var previous = node.previousSibling;
      while (previous && (change.added.has(previous) || isMoved(previous)))
        previous = previous.previousSibling;
      previousCache.set(node, previous);
      return previous;
    }
    change.maybeMoved.keys().forEach(isMoved);
    return change.moved.get(node);
  };
  return MutationProjection;
})();
var Summary = (function () {
  function Summary(projection, query) {
    var _this = this;
    this.projection = projection;
    this.added = [];
    this.removed = [];
    this.reparented = query.all || query.element || query.characterData ? [] : undefined;
    this.reordered = query.all ? [] : undefined;
    projection.getChanged(this, query.elementFilter, query.characterData);
    if (query.all || query.attribute || query.attributeList) {
      var filter = query.attribute ? [query.attribute] : query.attributeList;
      var attributeChanged = projection.attributeChangedNodes(filter);
      if (query.attribute) {
        this.valueChanged = attributeChanged[query.attribute] || [];
      }
      else {
        this.attributeChanged = attributeChanged;
        if (query.attributeList) {
          query.attributeList.forEach(function (attrName) {
            if (!_this.attributeChanged.hasOwnProperty(attrName))
              _this.attributeChanged[attrName] = [];
          });
        }
      }
    }
    if (query.all || query.characterData) {
      var characterDataChanged = projection.getCharacterDataChanged();
      if (query.characterData)
        this.valueChanged = characterDataChanged;
      else
        this.characterDataChanged = characterDataChanged;
    }
    if (this.reordered)
      this.getOldPreviousSibling = projection.getOldPreviousSibling.bind(projection);
  }
  Summary.prototype.getOldParentNode = function (node) {
    return this.projection.getOldParentNode(node);
  };
  Summary.prototype.getOldAttribute = function (node, name) {
    return this.projection.getOldAttribute(node, name);
  };
  Summary.prototype.getOldCharacterData = function (node) {
    return this.projection.getOldCharacterData(node);
  };
  Summary.prototype.getOldPreviousSibling = function (node) {
    return this.projection.getOldPreviousSibling(node);
  };
  return Summary;
})();
// TODO(rafaelw): Allow ':' and '.' as valid name characters.
var validNameInitialChar = /[a-zA-Z_]+/;
var validNameNonInitialChar = /[a-zA-Z0-9_\-]+/;
// TODO(rafaelw): Consider allowing backslash in the attrValue.
// TODO(rafaelw): There's got a to be way to represent this state machine
// more compactly???
function escapeQuotes(value) {
  return '"' + value.replace(/"/, '\\\"') + '"';
}
var Qualifier = (function () {
  function Qualifier() {}
  Qualifier.prototype.matches = function (oldValue) {
    if (oldValue === null)
      return false;
    if (this.attrValue === undefined)
      return true;
    if (!this.contains)
      return this.attrValue == oldValue;
    var tokens = oldValue.split(' ');
    for (var i = 0; i < tokens.length; i++) {
      if (this.attrValue === tokens[i])
        return true;
    }
    return false;
  };
  Qualifier.prototype.toString = function () {
    if (this.attrName === 'class' && this.contains)
      return '.' + this.attrValue;
    if (this.attrName === 'id' && !this.contains)
      return '#' + this.attrValue;
    if (this.contains)
      return '[' + this.attrName + '~=' + escapeQuotes(this.attrValue) + ']';
    if ('attrValue' in this)
      return '[' + this.attrName + '=' + escapeQuotes(this.attrValue) + ']';
    return '[' + this.attrName + ']';
  };
  return Qualifier;
})();
var Selector = (function () {
  function Selector() {
    this.uid = Selector.nextUid++;
    this.qualifiers = [];
  }
  Object.defineProperty(Selector.prototype, "caseInsensitiveTagName", {
    get: function () {
      return this.tagName.toUpperCase();
    },
    enumerable: true,
    configurable: true
  });
  Object.defineProperty(Selector.prototype, "selectorString", {
    get: function () {
      return this.tagName + this.qualifiers.join('');
    },
    enumerable: true,
    configurable: true
  });
  Selector.prototype.isMatching = function (el) {
    return el[Selector.matchesSelector](this.selectorString);
  };
  Selector.prototype.wasMatching = function (el, change, isMatching) {
    if (!change || !change.attributes)
      return isMatching;
    var tagName = change.isCaseInsensitive ? this.caseInsensitiveTagName : this.tagName;
    if (tagName !== '*' && tagName !== el.tagName)
      return false;
    var attributeOldValues = [];
    var anyChanged = false;
    for (var i = 0; i < this.qualifiers.length; i++) {
      var qualifier = this.qualifiers[i];
      var oldValue = change.getAttributeOldValue(qualifier.attrName);
      attributeOldValues.push(oldValue);
      anyChanged = anyChanged || (oldValue !== undefined);
    }
    if (!anyChanged)
      return isMatching;
    for (var i = 0; i < this.qualifiers.length; i++) {
      var qualifier = this.qualifiers[i];
      var oldValue = attributeOldValues[i];
      if (oldValue === undefined)
        oldValue = el.getAttribute(qualifier.attrName);
      if (!qualifier.matches(oldValue))
        return false;
    }
    return true;
  };
  Selector.prototype.matchabilityChange = function (el, change) {
    var isMatching = this.isMatching(el);
    if (isMatching)
      return this.wasMatching(el, change, isMatching) ? Movement.STAYED_IN : Movement.ENTERED;
    else
      return this.wasMatching(el, change, isMatching) ? Movement.EXITED : Movement.STAYED_OUT;
  };
  Selector.parseSelectors = function (input) {
    var selectors = [];
    var currentSelector;
    var currentQualifier;

    function newSelector() {
      if (currentSelector) {
        if (currentQualifier) {
          currentSelector.qualifiers.push(currentQualifier);
          currentQualifier = undefined;
        }
        selectors.push(currentSelector);
      }
      currentSelector = new Selector();
    }

    function newQualifier() {
      if (currentQualifier)
        currentSelector.qualifiers.push(currentQualifier);
      currentQualifier = new Qualifier();
    }
    var WHITESPACE = /\s/;
    var valueQuoteChar;
    var SYNTAX_ERROR = 'Invalid or unsupported selector syntax.';
    var SELECTOR = 1;
    var TAG_NAME = 2;
    var QUALIFIER = 3;
    var QUALIFIER_NAME_FIRST_CHAR = 4;
    var QUALIFIER_NAME = 5;
    var ATTR_NAME_FIRST_CHAR = 6;
    var ATTR_NAME = 7;
    var EQUIV_OR_ATTR_QUAL_END = 8;
    var EQUAL = 9;
    var ATTR_QUAL_END = 10;
    var VALUE_FIRST_CHAR = 11;
    var VALUE = 12;
    var QUOTED_VALUE = 13;
    var SELECTOR_SEPARATOR = 14;
    var state = SELECTOR;
    var i = 0;
    while (i < input.length) {
      var c = input[i++];
      switch (state) {
        case SELECTOR:
          if (c.match(validNameInitialChar)) {
            newSelector();
            currentSelector.tagName = c;
            state = TAG_NAME;
            break;
          }
          if (c == '*') {
            newSelector();
            currentSelector.tagName = '*';
            state = QUALIFIER;
            break;
          }
          if (c == '.') {
            newSelector();
            newQualifier();
            currentSelector.tagName = '*';
            currentQualifier.attrName = 'class';
            currentQualifier.contains = true;
            state = QUALIFIER_NAME_FIRST_CHAR;
            break;
          }
          if (c == '#') {
            newSelector();
            newQualifier();
            currentSelector.tagName = '*';
            currentQualifier.attrName = 'id';
            state = QUALIFIER_NAME_FIRST_CHAR;
            break;
          }
          if (c == '[') {
            newSelector();
            newQualifier();
            currentSelector.tagName = '*';
            currentQualifier.attrName = '';
            state = ATTR_NAME_FIRST_CHAR;
            break;
          }
          if (c.match(WHITESPACE))
            break;
          throw Error(SYNTAX_ERROR);
        case TAG_NAME:
          if (c.match(validNameNonInitialChar)) {
            currentSelector.tagName += c;
            break;
          }
          if (c == '.') {
            newQualifier();
            currentQualifier.attrName = 'class';
            currentQualifier.contains = true;
            state = QUALIFIER_NAME_FIRST_CHAR;
            break;
          }
          if (c == '#') {
            newQualifier();
            currentQualifier.attrName = 'id';
            state = QUALIFIER_NAME_FIRST_CHAR;
            break;
          }
          if (c == '[') {
            newQualifier();
            currentQualifier.attrName = '';
            state = ATTR_NAME_FIRST_CHAR;
            break;
          }
          if (c.match(WHITESPACE)) {
            state = SELECTOR_SEPARATOR;
            break;
          }
          if (c == ',') {
            state = SELECTOR;
            break;
          }
          throw Error(SYNTAX_ERROR);
        case QUALIFIER:
          if (c == '.') {
            newQualifier();
            currentQualifier.attrName = 'class';
            currentQualifier.contains = true;
            state = QUALIFIER_NAME_FIRST_CHAR;
            break;
          }
          if (c == '#') {
            newQualifier();
            currentQualifier.attrName = 'id';
            state = QUALIFIER_NAME_FIRST_CHAR;
            break;
          }
          if (c == '[') {
            newQualifier();
            currentQualifier.attrName = '';
            state = ATTR_NAME_FIRST_CHAR;
            break;
          }
          if (c.match(WHITESPACE)) {
            state = SELECTOR_SEPARATOR;
            break;
          }
          if (c == ',') {
            state = SELECTOR;
            break;
          }
          throw Error(SYNTAX_ERROR);
        case QUALIFIER_NAME_FIRST_CHAR:
          if (c.match(validNameInitialChar)) {
            currentQualifier.attrValue = c;
            state = QUALIFIER_NAME;
            break;
          }
          throw Error(SYNTAX_ERROR);
        case QUALIFIER_NAME:
          if (c.match(validNameNonInitialChar)) {
            currentQualifier.attrValue += c;
            break;
          }
          if (c == '.') {
            newQualifier();
            currentQualifier.attrName = 'class';
            currentQualifier.contains = true;
            state = QUALIFIER_NAME_FIRST_CHAR;
            break;
          }
          if (c == '#') {
            newQualifier();
            currentQualifier.attrName = 'id';
            state = QUALIFIER_NAME_FIRST_CHAR;
            break;
          }
          if (c == '[') {
            newQualifier();
            state = ATTR_NAME_FIRST_CHAR;
            break;
          }
          if (c.match(WHITESPACE)) {
            state = SELECTOR_SEPARATOR;
            break;
          }
          if (c == ',') {
            state = SELECTOR;
            break;
          }
          throw Error(SYNTAX_ERROR);
        case ATTR_NAME_FIRST_CHAR:
          if (c.match(validNameInitialChar)) {
            currentQualifier.attrName = c;
            state = ATTR_NAME;
            break;
          }
          if (c.match(WHITESPACE))
            break;
          throw Error(SYNTAX_ERROR);
        case ATTR_NAME:
          if (c.match(validNameNonInitialChar)) {
            currentQualifier.attrName += c;
            break;
          }
          if (c.match(WHITESPACE)) {
            state = EQUIV_OR_ATTR_QUAL_END;
            break;
          }
          if (c == '~') {
            currentQualifier.contains = true;
            state = EQUAL;
            break;
          }
          if (c == '=') {
            currentQualifier.attrValue = '';
            state = VALUE_FIRST_CHAR;
            break;
          }
          if (c == ']') {
            state = QUALIFIER;
            break;
          }
          throw Error(SYNTAX_ERROR);
        case EQUIV_OR_ATTR_QUAL_END:
          if (c == '~') {
            currentQualifier.contains = true;
            state = EQUAL;
            break;
          }
          if (c == '=') {
            currentQualifier.attrValue = '';
            state = VALUE_FIRST_CHAR;
            break;
          }
          if (c == ']') {
            state = QUALIFIER;
            break;
          }
          if (c.match(WHITESPACE))
            break;
          throw Error(SYNTAX_ERROR);
        case EQUAL:
          if (c == '=') {
            currentQualifier.attrValue = '';
            state = VALUE_FIRST_CHAR;
            break;
          }
          throw Error(SYNTAX_ERROR);
        case ATTR_QUAL_END:
          if (c == ']') {
            state = QUALIFIER;
            break;
          }
          if (c.match(WHITESPACE))
            break;
          throw Error(SYNTAX_ERROR);
        case VALUE_FIRST_CHAR:
          if (c.match(WHITESPACE))
            break;
          if (c == '"' || c == "'") {
            valueQuoteChar = c;
            state = QUOTED_VALUE;
            break;
          }
          currentQualifier.attrValue += c;
          state = VALUE;
          break;
        case VALUE:
          if (c.match(WHITESPACE)) {
            state = ATTR_QUAL_END;
            break;
          }
          if (c == ']') {
            state = QUALIFIER;
            break;
          }
          if (c == "'" || c == '"')
            throw Error(SYNTAX_ERROR);
          currentQualifier.attrValue += c;
          break;
        case QUOTED_VALUE:
          if (c == valueQuoteChar) {
            state = ATTR_QUAL_END;
            break;
          }
          currentQualifier.attrValue += c;
          break;
        case SELECTOR_SEPARATOR:
          if (c.match(WHITESPACE))
            break;
          if (c == ',') {
            state = SELECTOR;
            break;
          }
          throw Error(SYNTAX_ERROR);
      }
    }
    switch (state) {
      case SELECTOR:
      case TAG_NAME:
      case QUALIFIER:
      case QUALIFIER_NAME:
      case SELECTOR_SEPARATOR:
        // Valid end states.
        newSelector();
        break;
      default:
        throw Error(SYNTAX_ERROR);
    }
    if (!selectors.length)
      throw Error(SYNTAX_ERROR);
    return selectors;
  };
  Selector.nextUid = 1;
  Selector.matchesSelector = (function () {
    var element = document.createElement('div');
    if (typeof element['webkitMatchesSelector'] === 'function')
      return 'webkitMatchesSelector';
    if (typeof element['mozMatchesSelector'] === 'function')
      return 'mozMatchesSelector';
    if (typeof element['msMatchesSelector'] === 'function')
      return 'msMatchesSelector';
    return 'matchesSelector';
  })();
  return Selector;
})();
var attributeFilterPattern = /^([a-zA-Z:_]+[a-zA-Z0-9_\-:\.]*)$/;

function validateAttribute(attribute) {
  if (typeof attribute != 'string')
    throw Error('Invalid request opion. attribute must be a non-zero length string.');
  attribute = attribute.trim();
  if (!attribute)
    throw Error('Invalid request opion. attribute must be a non-zero length string.');
  if (!attribute.match(attributeFilterPattern))
    throw Error('Invalid request option. invalid attribute name: ' + attribute);
  return attribute;
}

function validateElementAttributes(attribs) {
  if (!attribs.trim().length)
    throw Error('Invalid request option: elementAttributes must contain at least one attribute.');
  var lowerAttributes = {};
  var attributes = {};
  var tokens = attribs.split(/\s+/);
  for (var i = 0; i < tokens.length; i++) {
    var name = tokens[i];
    if (!name)
      continue;
    var name = validateAttribute(name);
    var nameLower = name.toLowerCase();
    if (lowerAttributes[nameLower])
      throw Error('Invalid request option: observing multiple case variations of the same attribute is not supported.');
    attributes[name] = true;
    lowerAttributes[nameLower] = true;
  }
  return Object.keys(attributes);
}

function elementFilterAttributes(selectors) {
  var attributes = {};
  selectors.forEach(function (selector) {
    selector.qualifiers.forEach(function (qualifier) {
      attributes[qualifier.attrName] = true;
    });
  });
  return Object.keys(attributes);
}
var MutationSummary = (function () {
  function MutationSummary(opts) {
    var _this = this;
    this.connected = false;
    this.options = MutationSummary.validateOptions(opts);
    this.observerOptions = MutationSummary.createObserverOptions(this.options.queries);
    this.root = this.options.rootNode;
    this.callback = this.options.callback;
    this.elementFilter = Array.prototype.concat.apply([], this.options.queries.map(function (query) {
      return query.elementFilter ? query.elementFilter : [];
    }));
    if (!this.elementFilter.length)
      this.elementFilter = undefined;
    this.calcReordered = this.options.queries.some(function (query) {
      return query.all;
    });
    this.queryValidators = []; // TODO(rafaelw): Shouldn't always define this.
    if (MutationSummary.createQueryValidator) {
      this.queryValidators = this.options.queries.map(function (query) {
        return MutationSummary.createQueryValidator(_this.root, query);
      });
    }
    this.observer = new MutationObserverCtor(function (mutations) {
      _this.observerCallback(mutations);
    });
    this.reconnect();
  }
  MutationSummary.createObserverOptions = function (queries) {
    var observerOptions = {
      childList: true,
      subtree: true
    };
    var attributeFilter;

    function observeAttributes(attributes) {
      if (observerOptions.attributes && !attributeFilter)
        return; // already observing all.
      observerOptions.attributes = true;
      observerOptions.attributeOldValue = true;
      if (!attributes) {
        // observe all.
        attributeFilter = undefined;
        return;
      }
      // add to observed.
      attributeFilter = attributeFilter || {};
      attributes.forEach(function (attribute) {
        attributeFilter[attribute] = true;
        attributeFilter[attribute.toLowerCase()] = true;
      });
    }
    queries.forEach(function (query) {
      if (query.characterData) {
        observerOptions.characterData = true;
        observerOptions.characterDataOldValue = true;
        return;
      }
      if (query.all) {
        observeAttributes();
        observerOptions.characterData = true;
        observerOptions.characterDataOldValue = true;
        return;
      }
      if (query.attribute) {
        observeAttributes([query.attribute.trim()]);
        return;
      }
      var attributes = elementFilterAttributes(query.elementFilter).concat(query.attributeList || []);
      if (attributes.length)
        observeAttributes(attributes);
    });
    if (attributeFilter)
      observerOptions.attributeFilter = Object.keys(attributeFilter);
    return observerOptions;
  };
  MutationSummary.validateOptions = function (options) {
    for (var prop in options) {
      if (!(prop in MutationSummary.optionKeys))
        throw Error('Invalid option: ' + prop);
    }
    if (typeof options.callback !== 'function')
      throw Error('Invalid options: callback is required and must be a function');
    if (!options.queries || !options.queries.length)
      throw Error('Invalid options: queries must contain at least one query request object.');
    var opts = {
      callback: options.callback,
      rootNode: options.rootNode || document,
      observeOwnChanges: !!options.observeOwnChanges,
      oldPreviousSibling: !!options.oldPreviousSibling,
      queries: []
    };
    for (var i = 0; i < options.queries.length; i++) {
      var request = options.queries[i];
      // all
      if (request.all) {
        if (Object.keys(request).length > 1)
          throw Error('Invalid request option. all has no options.');
        opts.queries.push({
          all: true
        });
        continue;
      }
      // attribute
      if ('attribute' in request) {
        var query = {
          attribute: validateAttribute(request.attribute)
        };
        query.elementFilter = Selector.parseSelectors('*[' + query.attribute + ']');
        if (Object.keys(request).length > 1)
          throw Error('Invalid request option. attribute has no options.');
        opts.queries.push(query);
        continue;
      }
      // element
      if ('element' in request) {
        var requestOptionCount = Object.keys(request).length;
        var query = {
          element: request.element,
          elementFilter: Selector.parseSelectors(request.element)
        };
        if (request.hasOwnProperty('elementAttributes')) {
          query.attributeList = validateElementAttributes(request.elementAttributes);
          requestOptionCount--;
        }
        if (requestOptionCount > 1)
          throw Error('Invalid request option. element only allows elementAttributes option.');
        opts.queries.push(query);
        continue;
      }
      // characterData
      if (request.characterData) {
        if (Object.keys(request).length > 1)
          throw Error('Invalid request option. characterData has no options.');
        opts.queries.push({
          characterData: true
        });
        continue;
      }
      throw Error('Invalid request option. Unknown query request.');
    }
    return opts;
  };
  MutationSummary.prototype.createSummaries = function (mutations) {
    if (!mutations || !mutations.length)
      return [];
    var projection = new MutationProjection(this.root, mutations, this.elementFilter, this.calcReordered, this.options.oldPreviousSibling);
    var summaries = [];
    for (var i = 0; i < this.options.queries.length; i++) {
      summaries.push(new Summary(projection, this.options.queries[i]));
    }
    return summaries;
  };
  MutationSummary.prototype.checkpointQueryValidators = function () {
    this.queryValidators.forEach(function (validator) {
      if (validator)
        validator.recordPreviousState();
    });
  };
  MutationSummary.prototype.runQueryValidators = function (summaries) {
    this.queryValidators.forEach(function (validator, index) {
      if (validator)
        validator.validate(summaries[index]);
    });
  };
  MutationSummary.prototype.changesToReport = function (summaries) {
    return summaries.some(function (summary) {
      var summaryProps = ['added', 'removed', 'reordered', 'reparented',
        'valueChanged', 'characterDataChanged'
      ];
      if (summaryProps.some(function (prop) {
          return summary[prop] && summary[prop].length;
        }))
        return true;
      if (summary.attributeChanged) {
        var attrNames = Object.keys(summary.attributeChanged);
        var attrsChanged = attrNames.some(function (attrName) {
          return !!summary.attributeChanged[attrName].length;
        });
        if (attrsChanged)
          return true;
      }
      return false;
    });
  };
  MutationSummary.prototype.observerCallback = function (mutations) {
    if (!this.options.observeOwnChanges)
      this.observer.disconnect();
    var summaries = this.createSummaries(mutations);
    this.runQueryValidators(summaries);
    if (this.options.observeOwnChanges)
      this.checkpointQueryValidators();
    if (this.changesToReport(summaries))
      this.callback(summaries);
    // disconnect() may have been called during the callback.
    if (!this.options.observeOwnChanges && this.connected) {
      this.checkpointQueryValidators();
      this.observer.observe(this.root, this.observerOptions);
    }
  };
  MutationSummary.prototype.reconnect = function () {
    if (this.connected)
      throw Error('Already connected');
    this.observer.observe(this.root, this.observerOptions);
    this.connected = true;
    this.checkpointQueryValidators();
  };
  MutationSummary.prototype.takeSummaries = function () {
    if (!this.connected)
      throw Error('Not connected');
    var summaries = this.createSummaries(this.observer.takeRecords());
    return this.changesToReport(summaries) ? summaries : undefined;
  };
  MutationSummary.prototype.disconnect = function () {
    var summaries = this.takeSummaries();
    this.observer.disconnect();
    this.connected = false;
    return summaries;
  };
  MutationSummary.NodeMap = NodeMap; // exposed for use in TreeMirror.
  MutationSummary.parseElementFilter = Selector.parseSelectors; // exposed for testing.
  MutationSummary.optionKeys = {
    'callback': true,
    'queries': true,
    'rootNode': true,
    'oldPreviousSibling': true,
    'observeOwnChanges': true
  };
  return MutationSummary;
})();