<commit_before>"use strict";

var _ = require("underscore");
var $$ = require("substance-application").$$;
var Fragmenter = require("substance-util").Fragmenter;
var Annotator = require("substance-document").Annotator;

var NodeView = require("../node").View;

function _getAnnotationBehavior(doc) {
  var annotationBehavior = doc.getAnnotationBehavior();
  if (!annotationBehavior) {
    throw new Error("Missing AnnotationBehavior.");
  }
  return annotationBehavior;
}

var __id__ = 0;

var TextView = function(node, renderer, options) {
  NodeView.call(this, node);
  options = options || {};

  // NOTE: left for debugging purposes
  this.__id__ = __id__++;
  // console.log("Creating text view...", this.__id__);

  this.property = options.property || "content";
  this.propertyPath = options.propertyPath || [this.node.id, this.property];

  this.$el.addClass('content-node text');

  if (node.type === "text") {
    this.$el.addClass("text-node");
  }

  // If TextView is used to display a custom property,
  // we don't have an id. Only full-fledged text nodes
  // have id's.
  if (options.property) {
    // Note: currently NodeView sets the id. In this mode the must not be set
    // as we are displaying a textish property of a node, not a text node.
    // IMO it is ok to have the id set by default, as it is the 99% case.
    this.$el.removeAttr('id');
    this.$el.removeClass('content-node');
    this.$el.addClass(options.property);
  }

  this.$el.attr('data-path', this.property);

  this._annotations = {};
  this.annotationBehavior = _getAnnotationBehavior(node.document);

  // Note: due to (nested) annotations this DOM node is fragmented
  // into several child nodes which contain a primitive DOM TextNodes.
  // We wrap each of these nodes into a Fragment object.
  // A Fragment object offers the chance to override things like the
  // interpreted length or the manipulation behavior.
  this._fragments = [];
};

var _findTextEl;

TextView.Prototype = function() {

  var __super__ = NodeView.prototype;
  // Rendering
  // =============================
  //

  this.render = function(enhancer) {
    __super__.render.call(this, enhancer);

    this.renderContent();

    return this;
  };

  this.renderContent = function() {
    // console.log("TextView.renderContent", this.__id__);
    this.content.innerHTML = "";
    this._annotations = this.node.document.getIndex("annotations").get(this.propertyPath);
    this.renderWithAnnotations(this._annotations);

    // EXPERIMENTAL: trying to fix some issues that we think other implementations handle
    // with a trailing

    var br = window.document.createElement("BR");
    this.content.appendChild(br);

    var text = this.getText();
    // console.log("TextView.renderContent() add empty?", this.__id__, this.propertyPath, text.length);
    if (text.length === 0 && !this.__hasFocus) {
      this.content.classList.add('empty');
    }
  };

  this.onBlur = function() {
    var text = this.getText();
    this.__hasFocus = false;
    if (!text || text.length === 0) {
      this.content.classList.add('empty');
    } else {
      this.content.classList.remove('empty');
    }
  };

  this.onFocus = function() {
    // console.log("TextView.onFocus", this.__id__, this.propertyPath);
    this.__hasFocus = true;
    this.content.classList.remove('empty');
  };

  this.insert = function(pos, str) {
    // console.log("TextView.insert",pos, str, this.__id__);
    var result = this._lookupPostion(pos);
    var frag = result[0];
    var textNode = frag.el;
    var offset = result[1];

    var text = textNode.textContent;
    text = text.substring(0, offset) + str + text.substring(offset);
    textNode.textContent = text;

    // update the cached fragment positions
    for (var i = frag.index+1; i < this._fragments.length; i++) {
      this._fragments[i].charPos += str.length;
    }
  };

  this.delete = function(pos, length) {
    // console.log("TextView.delete", pos, length, this.__id__);
    if (length === 0) {
      console.log("FIXME: received empty deletion which could be avoided.")
      return;
    }

    var result = this._lookupPostion(pos, "delete");
    var frag = result[0];
    var textNode = frag.el;
    var offset = result[1];

    var text = textNode.textContent;

    // can not do this incrementally if it is a greater delete
    if (offset+length > text.length) {
      this.renderContent();
      return;
    }

    text = text.substring(0, offset) + text.substring(offset+length);
    textNode.textContent = text;

    // update the cached fragment positions
    for (var i = frag.index+1; i < this._fragments.length; i++) {
      this._fragments[i].charPos -= length;
    }
  };

  // Lookup a fragment for the given position.
  // ----
  // For insertions, the annotation level is considered on annotation boundaries,
  // i.e., if the annotation is exclusive, then the outer element/fragment is returned.
  // For deletions the annotation exclusivity is not important
  // i.e., the position belongs to the next fragment
  this._lookupPostion = function(pos, is_delete) {
    var frag, l;
    for (var i = 0; i < this._fragments.length; i++) {
      frag = this._fragments[i];
      l = frag.getLength();

      // right in the middle of a fragment
      if (pos < l) {
        return [frag, pos];
      }
      // the position is not within this fragment
      else if (pos > l) {
        pos -= l;
      }
      // ... at the boundary we consider the element's level
      else {
        var next = this._fragments[i+1];
        // if the element level of the next fragment is lower then we put the cursor there
        if (next && next.level < frag.level || is_delete) {
          return [next, 0];
        }
        // otherwise we leave the cursor in the current fragment
        else {
          return [frag, l];
        }
      }
    }
    return [frag, l];
  };

  this.onNodeUpdate = function(op) {
    if (_.isEqual(op.path, this.propertyPath)) {
      // console.log("Updating text view: ", op);
      if (op.type === "update") {
        var update = op.diff;
        if (update.isInsert()) {
          this.insert(update.pos, update.str);
          return true;
        } else if (update.isDelete()) {
          this.delete(update.pos, update.str.length);
          return true;
        }
      } else if (op.type === "set") {
        this.renderContent();
        return true;
      }
    }
    return false;
  };

  this.onGraphUpdate = function(op, graph, options) {

    // chronicled operations need to be rendered non-incrementally
    if(options && options["chronicle"]) {
      // console.log("non incremental update as it is a chronicled op");
      this.renderContent();
      return true;
    }

    // Call super handler and return if that has processed the operation already
    if (__super__.onGraphUpdate.call(this, op)) {
      return true;
    }

    // Otherwise deal with annotation changes
    if (Annotator.changesAnnotations(this.node.document, op, this.propertyPath)) {
      if (op.type === "create" || op.type === "delete" ||
          op.path[1] === "path" || op.path[1] === "range") {

        // NOTE: the last condition applies to all annotation range updates
        // However, due to the incremental nature of this implementation
        // it is not necessary to trigger a full rerender when content has been
        // changed incrementally.
        if (op.data && op.data["incremental"]) {
          // console.log("... change is incremental");
          return false;
        }

        // console.log("Rerendering TextView due to annotation update", op);
        this.renderContent();
        return true;
      }
    }

    return false;
  };

  this.createAnnotationElement = function(entry) {
    var el;
    if (entry.type === "link") {
      el = $$('a.annotation.'+entry.type, {
        id: entry.id,
        href: this.node.document.get(entry.id).url // "http://zive.at"
      });
    } else {
      el = $$('span.annotation.'+entry.type, {
        id: entry.id
      });
    }

    return el;
  };

  this.getText = function() {
    return this.node.document.get(this.propertyPath);
  };

  this.renderWithAnnotations = function(annotations) {
    var self = this;
    var text = this.getText();
    var fragment = window.document.createDocumentFragment();

    // this splits the text and annotations into smaller pieces
    // which is necessary to generate proper HTML.
    var fragmenter = new Fragmenter(this.annotationBehavior.levels);

    this._fragments = [];

    var _entry = null;
    var _index = 0;
    var _charPos = 0;
    var _level  = 0;

    fragmenter.onText = function(context, text) {
      var el = window.document.createTextNode(text);

      // Note: we create the data structures to allow lookup fragments
      // for coordinate mapping and incremental changes
      // TODO: to override the Fragment behavior we would need to override this
      self._fragments.push(new TextView.DefaultFragment(el, _index++, _charPos, _level));
      _charPos += text.length;
      context.appendChild(el);
    };

    fragmenter.onEnter = function(entry, parentContext) {
      _entry = entry;
      _level++;
      var el = self.createAnnotationElement(entry);
      parentContext.appendChild(el);
      return el;
    };

    fragmenter.onExit = function() {
      _level--;
    };

    // this calls onText and onEnter in turns...
    fragmenter.start(fragment, text, annotations);

    // set the content
    this.content.innerHTML = "";
    this.content.appendChild(fragment);
  };

  // Free the memory
  // --------

  this.dispose = function() {
    this.stopListening();
  };
};

TextView.Prototype.prototype = NodeView.prototype;
TextView.prototype = new TextView.Prototype();

var _unshiftAll = function(arr, other) {
  for (var i = 0; i < other.length; i++) {
    arr.unshift(other[i]);
  }
};

_findTextEl = function(el, pos) {
  var childNodes = [];
  _unshiftAll(childNodes, el.childNodes);

  while(childNodes.length) {
    var next = childNodes.shift();
    if (next.nodeType === window.Node.TEXT_NODE) {
      var t = next.textContent;
      if (t.length >= pos) {
        return [next, pos];
      } else {
        pos -= t.length;
      }
    } else {
      _unshiftAll(childNodes, next.childNodes);
    }
  }
};

TextView.Fragment = function(el, index, charPos, level) {
  this.el = el;

  // the position in the fragments array
  this.index = index;

  this.charPos = charPos;

  // Note: the level is used to determine the behavior at element boundaries.
  // Basiscally, for character positions at the boundaries, a manipulation is done
  // in the node with lower level.
  this.level = level;

};

TextView.Fragment.Prototype = function() {
  this.getLength = function() {
    throw new Error("This method is abstract");
  };
};
TextView.Fragment.prototype = new TextView.Fragment.Prototype();

TextView.DefaultFragment = function() {
  TextView.Fragment.apply(this, arguments);
};
TextView.DefaultFragment.Prototype = function() {
  this.getLength = function() {
    return this.el.length;
  };
};
TextView.DefaultFragment.Prototype.prototype = TextView.Fragment.prototype;
TextView.DefaultFragment.prototype = new TextView.DefaultFragment.Prototype();

module.exports = TextView;
<commit_msg>Also react not updates in TextView when propertyPath matches.<commit_after>"use strict";

var _ = require("underscore");
var $$ = require("substance-application").$$;
var Fragmenter = require("substance-util").Fragmenter;
var Annotator = require("substance-document").Annotator;

var NodeView = require("../node").View;

function _getAnnotationBehavior(doc) {
  var annotationBehavior = doc.getAnnotationBehavior();
  if (!annotationBehavior) {
    throw new Error("Missing AnnotationBehavior.");
  }
  return annotationBehavior;
}

var __id__ = 0;

var TextView = function(node, renderer, options) {
  NodeView.call(this, node);
  options = options || {};

  // NOTE: left for debugging purposes
  this.__id__ = __id__++;
  // console.log("Creating text view...", this.__id__);

  this.property = options.property || "content";
  this.propertyPath = options.propertyPath || [this.node.id, this.property];

  this.$el.addClass('content-node text');

  if (node.type === "text") {
    this.$el.addClass("text-node");
  }

  // If TextView is used to display a custom property,
  // we don't have an id. Only full-fledged text nodes
  // have id's.
  if (options.property) {
    // Note: currently NodeView sets the id. In this mode the must not be set
    // as we are displaying a textish property of a node, not a text node.
    // IMO it is ok to have the id set by default, as it is the 99% case.
    this.$el.removeAttr('id');
    this.$el.removeClass('content-node');
    this.$el.addClass(options.property);
  }

  this.$el.attr('data-path', this.property);

  this._annotations = {};
  this.annotationBehavior = _getAnnotationBehavior(node.document);

  // Note: due to (nested) annotations this DOM node is fragmented
  // into several child nodes which contain a primitive DOM TextNodes.
  // We wrap each of these nodes into a Fragment object.
  // A Fragment object offers the chance to override things like the
  // interpreted length or the manipulation behavior.
  this._fragments = [];
};

var _findTextEl;

TextView.Prototype = function() {

  var __super__ = NodeView.prototype;
  // Rendering
  // =============================
  //

  this.render = function(enhancer) {
    __super__.render.call(this, enhancer);

    this.renderContent();

    return this;
  };

  this.renderContent = function() {
    // console.log("TextView.renderContent", this.__id__);
    this.content.innerHTML = "";
    this._annotations = this.node.document.getIndex("annotations").get(this.propertyPath);
    this.renderWithAnnotations(this._annotations);

    // EXPERIMENTAL: trying to fix some issues that we think other implementations handle
    // with a trailing

    var br = window.document.createElement("BR");
    this.content.appendChild(br);

    var text = this.getText();
    // console.log("TextView.renderContent() add empty?", this.__id__, this.propertyPath, text.length);
    if (text.length === 0 && !this.__hasFocus) {
      this.content.classList.add('empty');
    }
  };

  this.onBlur = function() {
    var text = this.getText();
    this.__hasFocus = false;
    if (!text || text.length === 0) {
      this.content.classList.add('empty');
    } else {
      this.content.classList.remove('empty');
    }
  };

  this.onFocus = function() {
    // console.log("TextView.onFocus", this.__id__, this.propertyPath);
    this.__hasFocus = true;
    this.content.classList.remove('empty');
  };

  this.insert = function(pos, str) {
    // console.log("TextView.insert",pos, str, this.__id__);
    var result = this._lookupPostion(pos);
    var frag = result[0];
    var textNode = frag.el;
    var offset = result[1];

    var text = textNode.textContent;
    text = text.substring(0, offset) + str + text.substring(offset);
    textNode.textContent = text;

    // update the cached fragment positions
    for (var i = frag.index+1; i < this._fragments.length; i++) {
      this._fragments[i].charPos += str.length;
    }
  };

  this.delete = function(pos, length) {
    // console.log("TextView.delete", pos, length, this.__id__);
    if (length === 0) {
      console.log("FIXME: received empty deletion which could be avoided.")
      return;
    }

    var result = this._lookupPostion(pos, "delete");
    var frag = result[0];
    var textNode = frag.el;
    var offset = result[1];

    var text = textNode.textContent;

    // can not do this incrementally if it is a greater delete
    if (offset+length > text.length) {
      this.renderContent();
      return;
    }

    text = text.substring(0, offset) + text.substring(offset+length);
    textNode.textContent = text;

    // update the cached fragment positions
    for (var i = frag.index+1; i < this._fragments.length; i++) {
      this._fragments[i].charPos -= length;
    }
  };

  // Lookup a fragment for the given position.
  // ----
  // For insertions, the annotation level is considered on annotation boundaries,
  // i.e., if the annotation is exclusive, then the outer element/fragment is returned.
  // For deletions the annotation exclusivity is not important
  // i.e., the position belongs to the next fragment
  this._lookupPostion = function(pos, is_delete) {
    var frag, l;
    for (var i = 0; i < this._fragments.length; i++) {
      frag = this._fragments[i];
      l = frag.getLength();

      // right in the middle of a fragment
      if (pos < l) {
        return [frag, pos];
      }
      // the position is not within this fragment
      else if (pos > l) {
        pos -= l;
      }
      // ... at the boundary we consider the element's level
      else {
        var next = this._fragments[i+1];
        // if the element level of the next fragment is lower then we put the cursor there
        if (next && next.level < frag.level || is_delete) {
          return [next, 0];
        }
        // otherwise we leave the cursor in the current fragment
        else {
          return [frag, l];
        }
      }
    }
    return [frag, l];
  };

  this.onNodeUpdate = function(op) {
    if (_.isEqual(op.path, this.propertyPath)) {
      // console.log("Updating text view: ", op);
      if (op.type === "update") {
        var update = op.diff;
        if (update.isInsert()) {
          this.insert(update.pos, update.str);
          return true;
        } else if (update.isDelete()) {
          this.delete(update.pos, update.str.length);
          return true;
        }
      } else if (op.type === "set") {
        this.renderContent();
        return true;
      }
    }
    return false;
  };

  this.onGraphUpdate = function(op, graph, options) {

    // chronicled operations need to be rendered non-incrementally
    if(options && options["chronicle"]) {
      // console.log("non incremental update as it is a chronicled op");
      this.renderContent();
      return true;
    }

    // Call super handler and return if that has processed the operation already
    if (__super__.onGraphUpdate.call(this, op)) {
      return true;
    }

    if (_.isEqual(op.path, this.propertyPath)) {
      this.onNodeUpdate(op);
      return true;
    }

    // Otherwise deal with annotation changes
    if (Annotator.changesAnnotations(this.node.document, op, this.propertyPath)) {
      if (op.type === "create" || op.type === "delete" ||
          op.path[1] === "path" || op.path[1] === "range") {

        // NOTE: the last condition applies to all annotation range updates
        // However, due to the incremental nature of this implementation
        // it is not necessary to trigger a full rerender when content has been
        // changed incrementally.
        if (op.data && op.data["incremental"]) {
          // console.log("... change is incremental");
          return false;
        }

        // console.log("Rerendering TextView due to annotation update", op);
        this.renderContent();
        return true;
      }
    }

    return false;
  };

  this.createAnnotationElement = function(entry) {
    var el;
    if (entry.type === "link") {
      el = $$('a.annotation.'+entry.type, {
        id: entry.id,
        href: this.node.document.get(entry.id).url // "http://zive.at"
      });
    } else {
      el = $$('span.annotation.'+entry.type, {
        id: entry.id
      });
    }

    return el;
  };

  this.getText = function() {
    return this.node.document.get(this.propertyPath);
  };

  this.renderWithAnnotations = function(annotations) {
    var self = this;
    var text = this.getText();
    var fragment = window.document.createDocumentFragment();

    // this splits the text and annotations into smaller pieces
    // which is necessary to generate proper HTML.
    var fragmenter = new Fragmenter(this.annotationBehavior.levels);

    this._fragments = [];

    var _entry = null;
    var _index = 0;
    var _charPos = 0;
    var _level  = 0;

    fragmenter.onText = function(context, text) {
      var el = window.document.createTextNode(text);

      // Note: we create the data structures to allow lookup fragments
      // for coordinate mapping and incremental changes
      // TODO: to override the Fragment behavior we would need to override this
      self._fragments.push(new TextView.DefaultFragment(el, _index++, _charPos, _level));
      _charPos += text.length;
      context.appendChild(el);
    };

    fragmenter.onEnter = function(entry, parentContext) {
      _entry = entry;
      _level++;
      var el = self.createAnnotationElement(entry);
      parentContext.appendChild(el);
      return el;
    };

    fragmenter.onExit = function() {
      _level--;
    };

    // this calls onText and onEnter in turns...
    fragmenter.start(fragment, text, annotations);

    // set the content
    this.content.innerHTML = "";
    this.content.appendChild(fragment);
  };

  // Free the memory
  // --------

  this.dispose = function() {
    this.stopListening();
  };
};

TextView.Prototype.prototype = NodeView.prototype;
TextView.prototype = new TextView.Prototype();

var _unshiftAll = function(arr, other) {
  for (var i = 0; i < other.length; i++) {
    arr.unshift(other[i]);
  }
};

_findTextEl = function(el, pos) {
  var childNodes = [];
  _unshiftAll(childNodes, el.childNodes);

  while(childNodes.length) {
    var next = childNodes.shift();
    if (next.nodeType === window.Node.TEXT_NODE) {
      var t = next.textContent;
      if (t.length >= pos) {
        return [next, pos];
      } else {
        pos -= t.length;
      }
    } else {
      _unshiftAll(childNodes, next.childNodes);
    }
  }
};

TextView.Fragment = function(el, index, charPos, level) {
  this.el = el;

  // the position in the fragments array
  this.index = index;

  this.charPos = charPos;

  // Note: the level is used to determine the behavior at element boundaries.
  // Basiscally, for character positions at the boundaries, a manipulation is done
  // in the node with lower level.
  this.level = level;

};

TextView.Fragment.Prototype = function() {
  this.getLength = function() {
    throw new Error("This method is abstract");
  };
};
TextView.Fragment.prototype = new TextView.Fragment.Prototype();

TextView.DefaultFragment = function() {
  TextView.Fragment.apply(this, arguments);
};
TextView.DefaultFragment.Prototype = function() {
  this.getLength = function() {
    return this.el.length;
  };
};
TextView.DefaultFragment.Prototype.prototype = TextView.Fragment.prototype;
TextView.DefaultFragment.prototype = new TextView.DefaultFragment.Prototype();

module.exports = TextView;
<|endoftext|>


<commit_before>"use strict"

const math = require('../../../index');
const MathResolveChecks = require('./MathResolveChecks');
const NodeCreator = require('./NodeCreator');
const NodeStatus = require('./NodeStatus');
const NodeType = require('./NodeType');
const MathChangeTypes = require('./MathChangeTypes');

// For storing polynomial terms and performing operations on them.
// Has a name, an exponent, and a coefficient. Note that this doesn't include
// constants.
// These expressions can be translated into a PolynomialTerm: x^2, 2y, z, 3x/5
// These expressions would not: 4, x^(3+4), 2+x, 3*7, x-z
class PolynomialTerm {
  constructor(node) {
    if (!PolynomialTerm.isPolynomialTerm(node)) {
     throw Error("PolynomialTerm constructor called with a node that is not" +
                 " a polynomial term.");
    }

    // Default values
    this.numeratorCoeff = 0;   // for fraction coefficients, the value on top
    this.denominatorCoeff = 0; // for fraction coefficients, the bottom value
    this.coeff = 1;            // the coefficient (might not be an integer)
    this.exp = 1;              // the exponent

    if (NodeType.operator(node)) {
      if (node.op === "^") {
        const symbolTerm = node.args[0];
        const exponentTerm = node.args[1];
        // TODO: support exponents that aren't constants, too
        this.name = symbolTerm.name;
        this.exp = parseFloat(exponentTerm.value);
      }
      // it's "*" ie it has a coefficient
      else if (node.op === "*") {
        this.coeff = parseFloat(node.args[0].value);
        const nonCoefficientTerm = new PolynomialTerm(node.args[1]);
        this.name = nonCoefficientTerm.name;
        this.exp = nonCoefficientTerm.exp;
      }
      // this means there's a fraction coefficient
      else if (node.op === "/") {
        this.denominatorCoeff = parseFloat(node.args[1].value);
        const numeratorTerm = new PolynomialTerm(node.args[0]);
        this.exp = numeratorTerm.exp;
        this.name = numeratorTerm.name;
        this.numeratorCoeff = numeratorTerm.coeff;
        this.coeff = this.numeratorCoeff/this.denominatorCoeff;
      } else {
        throw Error("Unsupported operatation for polynomial term: " + node.op);
      }
    }
    else if (NodeType.unaryMinus(node) && isPolynomialTerm(node.args[0], false)) {
      const polyTerm = new PolynomialTerm(node.args[0]);
      this.coeff = -1;
      this.exp = polyTerm.exp;
      this.name = polyTerm.name;
    }
    else if (NodeType.symbol(node)) {
      this.name = node.name;
    }
    else {
      throw Error("Unsupported node type: " + node.type);
    }
  }
  hasFractionCoeff() {
    return this.numeratorCoeff && this.denominatorCoeff;
  }
}

// Returns if the node represents an expression that can be considered a term.
// e.g. x^2, 2y, z, 3x/5 are all terms. 4, 2+x, 3*7, x-z are all not terms.
// If allowCoeff is false, 2y or 2z^2 would return false
PolynomialTerm.isPolynomialTerm = function(node, allowCoeff = true,
                                                 onlyConstantExponent = true) {
  if (NodeType.operator(node)) {
    if (node.op === "^") {
      if (node.args[0].type !== 'SymbolNode') {
        return false;
      }
      const exponentNode = node.args[1];
      if (onlyConstantExponent) {
        return exponentNode.type === 'ConstantNode';
      } else {
        return MathResolveChecks.resolvesToConstant(exponentNode);
      }
    }
    if (node.op === "*") {
      return (allowCoeff &&
              node.implicit &&
              node.args[0].type === 'ConstantNode' && // the coefficient
              PolynomialTerm.isPolynomialTerm(node.args[1], false));
    }
    else if (node.op === "/") {
      return (allowCoeff &&
              node.args[1].type === "ConstantNode" && // the denominator
              PolynomialTerm.isPolynomialTerm(node.args[0]));
    }
    return false;
  }
  else if (NodeType.parenthesis(node) || NodeType.constant(node)) {
    return false;
  }
  else if (NodeType.symbol(node)) {
    return true;
  }
  else {
    throw Error("Unsupported node type: " + node.type);
  }
}

// Combines polynomial terms for an operation node of type + or *
// Returns a NodeStatus object.
PolynomialTerm.combinePolynomialTerms = function(node) {
  if (node.op === "+" && canAddLikeTermPolynomialNodes(node.args)) {
    let combinedNode = addlikeTermPolynomialNodes(node.args);
    return new NodeStatus(
      combinedNode, true, MathChangeTypes.ADD_POLYNOMIAL_TERMS);
  }
  else if (node.op === "*" && canMultiplyLikeTermPolynomialNodes(node.args)) {
    let combinedNode = multiplyLikeTermPolynomialNodes(node.args);
    return new NodeStatus(
      combinedNode, true, MathChangeTypes.MULT_POLYNOMIAL_TERMS);
  }
  else {
    return new NodeStatus(node);
  }
}

// Multiplies a constant node by a polynomial node and returns the result
// in a NodeStatus object. If the node's arguments aren't a constant and
// polynomial node, returns a NO_CHANGE NodeStatus object.
PolynomialTerm.multiplyConstantAndPolynomialTerm = function(node) {
  if (!canMultiplyConstantAndPolynomial(node)) {
    return new NodeStatus(node);
  }

  let constNode, polyNode, newNode;
  if (node.args[0].type === 'ConstantNode') {
    constNode = node.args[0];
    polyNode = node.args[1];
  } else {
    constNode = node.args[1];
    polyNode = node.args[0];
  }

  // If it already has a coefficient, multiply that by the constant
  if (polynomialTermHasCoefficient(polyNode)) {
    const oldCoeffValue = parseFloat(polyNode.args[0].value);
    const symbolNode = polyNode.args[1]; // the term without its coefficient
    const constNodeValue = parseFloat(constNode.value);
    const newCoeffNode = NodeCreator.constant(oldCoeff * constNodeValue);
    newNode = NodeCreator.operator("*", [newCoeffNode, symbolNode], true);
  }
  // If there was no coefficient, the constant becomes the coefficient
  else {
    newNode = NodeCreator.operator("*", [constNode, polyNode], true);
  }
  return new NodeStatus(newNode, true, MathChangeTypes.MULT_POLY_BY_CONST);
}

// Simplifies a polynomial term with a fraction as its coefficients.
// e.g. 2x/4 --> x/2    10x/5 --> 2x
// returns the new simplified node in a NodeStatus object
PolynomialTerm.simplifyPolynomialFraction = function(node) {
  if (!canSimplifyPolynomialFraction(node)) {
    return new NodeStatus(node);
  }
  const polyTerm = new PolynomialTerm(node);
  // The gcd is what we're dividing the numerator and denominator by.
  const gcd = math.gcd(polyTerm.numeratorCoeff, polyTerm.denominatorCoeff);

  // update the constant in the numerator term
  const newNumeratorCoeff = polyTerm.numeratorCoeff/gcd;
  // the first operand of a division node is what comes before the / symbol
  // ie the numerator (e.g. for 2x/4 the numeratorTerm is 2x)
  let numeratorTerm = node.args[0];
  if (newNumeratorCoeff === 1) {
    // numeratorTerm will have two args. numeratorTerm.args[0] is the
    // coefficient, which is multiplied by numeratorTerm.args[1], the symbol
    // part. This will get rid of the coefficient constant, since it's 1 now.
    // e.g. 2x/4 --> gcd is 2 --> new numerator coefficient is 2/2 = 1
    // --> numerator term is just x (not 1x)
    numeratorTerm = numeratorTerm.args[1];
  } else {
    // If the new coefficient isn't 1, replace the old coefficient
    numeratorTerm.args[0] = NodeCreator.constant(newNumeratorCoeff);
  }
  const newDenominatorCoeff = polyTerm.denominatorCoeff/gcd;

  let simplifiedNode;
  if (newDenominatorCoeff === 1) {
    simplifiedNode = numeratorTerm;
  } else {
    simplifiedNode = NodeCreator.operator('/', [
      numeratorTerm,
      NodeCreator.constant(newDenominatorCoeff)
    ]);
  }
  return new NodeStatus(simplifiedNode, true, MathChangeTypes.DIVIDE_POLY_TERM);
}

// Returns true if the node is an operation node with parameters that are
// polynomial terms that can be combined in some way.
PolynomialTerm.canCombinePolynomialTerms = function(node) {
  return ((node.op === "+" && canAddLikeTermPolynomialNodes(node.args)) ||
          (node.op === "*" && canMultiplyLikeTermPolynomialNodes(node.args)) ||
          canMultiplyConstantAndPolynomial(node));
}

// Adds a list of nodes that are polynomial terms. Returns a node.
function addlikeTermPolynomialNodes(nodes) {
  if (!canAddLikeTermPolynomialNodes(nodes)) {
    throw Error("Can't add two polynomial terms of different types");
  }

  // add up the coefficients
  const coefficientList = nodes.map(n => {
    let p = new PolynomialTerm(n);
    return p.coeff;
  });
  const newCoefficient = coefficientList.reduce((a,b) => a+b, 0);

  // Polynomial terms that can be added together must share the same symbol
  // name and exponent. Get that name and exponent from the first term
  const firstTerm = new PolynomialTerm(nodes[0]);
  const exponent = firstTerm.exp;
  const name = firstTerm.name;

  // if the exponent is 1, that means there's no exponent
  if (exponent === 1) {
    return NodeCreator.operator("*",
      [NodeCreator.constant(newCoefficient), NodeCreator.symbol(name)], true);
  } else {
    const coefficientNode =  NodeCreator.constant(newCoefficient);
    const symbolNode = NodeCreator.operator("^", [
        NodeCreator.symbol(name),
        NodeCreator.constant(exponent)]);
    return NodeCreator.operator("*", [coefficientNode, symbolNode], true);
  }
}

// Multiplies a list of nodes that are polynomial like terms. Returns a node.
// The polynomial nodes should *not* have coefficients. (multiplying
// coefficients is handled in collecting like terms for multiplication)
function multiplyLikeTermPolynomialNodes(nodes) {
  if (!canMultiplyLikeTermPolynomialNodes(nodes)) {
    throw Error("Can't multiply like terms - terms are not alike");
  }

  // If we're multiplying polynomial nodes together, they all share the same
  // symbol name. Get that name from the first node.
  const termName = new PolynomialTerm(nodes[0]).name;

  // The new exponent will be a sum of exponents (an operation, wrapped in
  // parens) e.g. x^(3+4+5)
  const exponentNodeList = nodes.map(n => { // map exponents to constant nodes
      let p = new PolynomialTerm(n);
      return NodeCreator.constant(p.exp);
    });
  const newExponent = NodeCreator.parenthesis(
    NodeCreator.operator("+", exponentNodeList));
  return NodeCreator.operator("^", [NodeCreator.symbol(termName), newExponent]);
}

// Returns true if the nodes are polynomial terms that can be added together.
function canAddLikeTermPolynomialNodes(nodes) {
  if (!nodes.every(n => PolynomialTerm.isPolynomialTerm(n))) {
    return false;
  }
  if (nodes.length === 1) {
    return false;
  }

  const firstTerm = new PolynomialTerm(nodes[0]);
  const restTerms = nodes.slice(1).map(n => new PolynomialTerm(n));
  // to add terms, they must have the same symbol name *and* exponent degree
  const shareNameAndExponent = restTerms.every(term => {
    return firstTerm.name === term.name && firstTerm.exp === term.exp;
  });
  return shareNameAndExponent;
}

// Returns true if the nodes are symbolic terms with the same symbol
// and no coefficients.
function canMultiplyLikeTermPolynomialNodes(nodes) {
  if (!nodes.every(n => PolynomialTerm.isPolynomialTerm(n))) {
    return false;
  }
  if (nodes.length === 1) {
    return false;
  }
  const polynomialTerms = nodes.map(n => new PolynomialTerm(n));
  // all coefficients must be 1 (ie there's no coefficient term)
  if (!polynomialTerms.every(polyTerm => polyTerm.coeff === 1)) {
    return false;
  }

  const firstTerm = polynomialTerms[0];
  const restTerms = polynomialTerms.slice(1);
  // they're considered like terms if they have the same symbol name
  return restTerms.every(term => firstTerm.name === term.name);
}

// Returns true if the expression is a multiplication between a constant
// and polynomial without a coefficient.
function canMultiplyConstantAndPolynomial(node) {
  // implicit multiplication doesn't count as multiplication here, since it
  // represents a single term.
  if (node.op !== '*' || node.implicit) {
    return false;
  }
  if (node.args.length !== 2) {
    return false;
  }
  return ((PolynomialTerm.isPolynomialTerm(node.args[0], false) &&
            node.args[1].type === 'ConstantNode') ||
          (PolynomialTerm.isPolynomialTerm(node.args[1], false) &&
            node.args[0].type === 'ConstantNode'));
}

// Returns true if polyNode is a node that represents a polynomial term
// and has a coefficent node.
function polynomialTermHasCoefficient(polyNode) {
  return PolynomialTerm.isPolynomialTerm(polyNode) &&
    polyNode.op === '*';
}

// Some polynomial terms have a fraction coefficient e.g. 2x/4
// This returns true if we can simplify that fraction.
// e.g. 2x/4 and 10x/5 return true, 4x/7 would return false
function canSimplifyPolynomialFraction (node) {
  if (!PolynomialTerm.isPolynomialTerm(node)) {
    return false;
  }
  const polyTerm = new PolynomialTerm(node);
  if (!(polyTerm.numeratorCoeff && polyTerm.denominatorCoeff)) {
    return false;
  }
  return math.gcd(polyTerm.numeratorCoeff, polyTerm.denominatorCoeff) !== 1;
}

module.exports = PolynomialTerm;
<commit_msg>in progress<commit_after>"use strict"

const math = require('../../../index');
const MathResolveChecks = require('./MathResolveChecks');
const NodeCreator = require('./NodeCreator');
const NodeStatus = require('./NodeStatus');
const NodeType = require('./NodeType');
const MathChangeTypes = require('./MathChangeTypes');

// For storing polynomial terms and performing operations on them.
// Has a name, an exponent, and a coefficient. Note that this doesn't include
// constants.
// These expressions can be translated into a PolynomialTerm: x^2, 2y, z, 3x/5
// These expressions would not: 4, x^(3+4), 2+x, 3*7, x-z
class PolynomialTerm {
  constructor(node) {
    if (!PolynomialTerm.isPolynomialTerm(node)) {
     throw Error("PolynomialTerm constructor called with a node that is not" +
                 " a polynomial term.");
    }

    // Default values
    this.numeratorCoeff = 0;   // for fraction coefficients, the value on top
    this.denominatorCoeff = 0; // for fraction coefficients, the bottom value
    this.coeff = 1;            // the coefficient (might not be an integer)
    this.exp = 1;              // the exponent

    if (NodeType.operator(node)) {
      if (node.op === "^") {
        const symbolTerm = node.args[0];
        const exponentTerm = node.args[1];
        // TODO: support exponents that aren't constants, too
        this.name = symbolTerm.name;
        this.exp = parseFloat(exponentTerm.value);
      }
      // it's "*" ie it has a coefficient
      else if (node.op === "*") {
        this.coeff = parseFloat(node.args[0].value);
        const nonCoefficientTerm = new PolynomialTerm(node.args[1]);
        this.name = nonCoefficientTerm.name;
        this.exp = nonCoefficientTerm.exp;
      }
      // this means there's a fraction coefficient
      else if (node.op === "/") {
        this.denominatorCoeff = parseFloat(node.args[1].value);
        const numeratorTerm = new PolynomialTerm(node.args[0]);
        this.exp = numeratorTerm.exp;
        this.name = numeratorTerm.name;
        this.numeratorCoeff = numeratorTerm.coeff;
        this.coeff = this.numeratorCoeff/this.denominatorCoeff;
      } else {
        throw Error("Unsupported operatation for polynomial term: " + node.op);
      }
    }
    else if (NodeType.unaryMinus(node) && isPolynomialTerm(node.args[0], false)) {
      const polyTerm = new PolynomialTerm(node.args[0]);
      this.coeff = -1;
      this.exp = polyTerm.exp;
      this.name = polyTerm.name;
    }
    else if (NodeType.symbol(node)) {
      this.name = node.name;
    }
    else {
      throw Error("Unsupported node type: " + node.type);
    }
  }
  hasFractionCoeff() {
    return this.numeratorCoeff && this.denominatorCoeff;
  }
}

// Returns if the node represents an expression that can be considered a term.
// e.g. x^2, 2y, z, 3x/5 are all terms. 4, 2+x, 3*7, x-z are all not terms.
// If allowCoeff is false, 2y or 2z^2 would return false
PolynomialTerm.isPolynomialTerm = function(node, allowCoeff = true,
                                                 onlyConstantExponent = true) {
  if (NodeType.operator(node)) {
    if (node.op === "^") {
      if (NodeType.symbol(node.args[0])) {
        return false;
      }
      const exponentNode = node.args[1];
      if (onlyConstantExponent) {
        return NodeType.constant(exponentNode);
      } else {
        return MathResolveChecks.resolvesToConstant(exponentNode);
      }
    }
    if (node.op === "*") {
      return (allowCoeff &&
              node.implicit &&
              NodeType.constant(node.args[0]) && // the coefficient
              PolynomialTerm.isPolynomialTerm(node.args[1], false));
    }
    else if (node.op === "/") {
      return (allowCoeff &&
              NodeType.constant(node.args[1]) && // the denominator
              PolynomialTerm.isPolynomialTerm(node.args[0]));
    }
    return false;
  }
  else if (NodeType.parenthesis(node) || NodeType.constant(node)) {
    return false;
  }
  else if (NodeType.symbol(node)) {
    return true;
  }
  else {
    throw Error("Unsupported node type: " + node.type);
  }
}

// Combines polynomial terms for an operation node of type + or *
// Returns a NodeStatus object.
PolynomialTerm.combinePolynomialTerms = function(node) {
  if (node.op === "+" && canAddLikeTermPolynomialNodes(node.args)) {
    let combinedNode = addlikeTermPolynomialNodes(node.args);
    return new NodeStatus(
      combinedNode, true, MathChangeTypes.ADD_POLYNOMIAL_TERMS);
  }
  else if (node.op === "*" && canMultiplyLikeTermPolynomialNodes(node.args)) {
    let combinedNode = multiplyLikeTermPolynomialNodes(node.args);
    return new NodeStatus(
      combinedNode, true, MathChangeTypes.MULT_POLYNOMIAL_TERMS);
  }
  else {
    return new NodeStatus(node);
  }
}

// Multiplies a constant node by a polynomial node and returns the result
// in a NodeStatus object. If the node's arguments aren't a constant and
// polynomial node, returns a NO_CHANGE NodeStatus object.
PolynomialTerm.multiplyConstantAndPolynomialTerm = function(node) {
  if (!canMultiplyConstantAndPolynomial(node)) {
    return new NodeStatus(node);
  }

  let constNode, polyNode, newNode;
  if (node.args[0].type === 'ConstantNode') {
    constNode = node.args[0];
    polyNode = node.args[1];
  } else {
    constNode = node.args[1];
    polyNode = node.args[0];
  }

  // If it already has a coefficient, multiply that by the constant
  if (polynomialTermHasCoefficient(polyNode)) {
    const oldCoeffValue = parseFloat(polyNode.args[0].value);
    const symbolNode = polyNode.args[1]; // the term without its coefficient
    const constNodeValue = parseFloat(constNode.value);
    const newCoeffNode = NodeCreator.constant(oldCoeff * constNodeValue);
    newNode = NodeCreator.operator("*", [newCoeffNode, symbolNode], true);
  }
  // If there was no coefficient, the constant becomes the coefficient
  else {
    newNode = NodeCreator.operator("*", [constNode, polyNode], true);
  }
  return new NodeStatus(newNode, true, MathChangeTypes.MULT_POLY_BY_CONST);
}

// Simplifies a polynomial term with a fraction as its coefficients.
// e.g. 2x/4 --> x/2    10x/5 --> 2x
// returns the new simplified node in a NodeStatus object
PolynomialTerm.simplifyPolynomialFraction = function(node) {
  if (!canSimplifyPolynomialFraction(node)) {
    return new NodeStatus(node);
  }
  const polyTerm = new PolynomialTerm(node);
  // The gcd is what we're dividing the numerator and denominator by.
  const gcd = math.gcd(polyTerm.numeratorCoeff, polyTerm.denominatorCoeff);

  // update the constant in the numerator term
  const newNumeratorCoeff = polyTerm.numeratorCoeff/gcd;
  // the first operand of a division node is what comes before the / symbol
  // ie the numerator (e.g. for 2x/4 the numeratorTerm is 2x)
  let numeratorTerm = node.args[0];
  if (newNumeratorCoeff === 1) {
    // numeratorTerm will have two args. numeratorTerm.args[0] is the
    // coefficient, which is multiplied by numeratorTerm.args[1], the symbol
    // part. This will get rid of the coefficient constant, since it's 1 now.
    // e.g. 2x/4 --> gcd is 2 --> new numerator coefficient is 2/2 = 1
    // --> numerator term is just x (not 1x)
    numeratorTerm = numeratorTerm.args[1];
  } else {
    // If the new coefficient isn't 1, replace the old coefficient
    numeratorTerm.args[0] = NodeCreator.constant(newNumeratorCoeff);
  }
  const newDenominatorCoeff = polyTerm.denominatorCoeff/gcd;

  let simplifiedNode;
  if (newDenominatorCoeff === 1) {
    simplifiedNode = numeratorTerm;
  } else {
    simplifiedNode = NodeCreator.operator('/', [
      numeratorTerm,
      NodeCreator.constant(newDenominatorCoeff)
    ]);
  }
  return new NodeStatus(simplifiedNode, true, MathChangeTypes.DIVIDE_POLY_TERM);
}

// Returns true if the node is an operation node with parameters that are
// polynomial terms that can be combined in some way.
PolynomialTerm.canCombinePolynomialTerms = function(node) {
  return ((node.op === "+" && canAddLikeTermPolynomialNodes(node.args)) ||
          (node.op === "*" && canMultiplyLikeTermPolynomialNodes(node.args)) ||
          canMultiplyConstantAndPolynomial(node));
}

// Adds a list of nodes that are polynomial terms. Returns a node.
function addlikeTermPolynomialNodes(nodes) {
  if (!canAddLikeTermPolynomialNodes(nodes)) {
    throw Error("Can't add two polynomial terms of different types");
  }

  // add up the coefficients
  const coefficientList = nodes.map(n => {
    let p = new PolynomialTerm(n);
    return p.coeff;
  });
  const newCoefficient = coefficientList.reduce((a,b) => a+b, 0);

  // Polynomial terms that can be added together must share the same symbol
  // name and exponent. Get that name and exponent from the first term
  const firstTerm = new PolynomialTerm(nodes[0]);
  const exponent = firstTerm.exp;
  const name = firstTerm.name;

  // if the exponent is 1, that means there's no exponent
  if (exponent === 1) {
    return NodeCreator.operator("*",
      [NodeCreator.constant(newCoefficient), NodeCreator.symbol(name)], true);
  } else {
    const coefficientNode =  NodeCreator.constant(newCoefficient);
    const symbolNode = NodeCreator.operator("^", [
        NodeCreator.symbol(name),
        NodeCreator.constant(exponent)]);
    return NodeCreator.operator("*", [coefficientNode, symbolNode], true);
  }
}

// Multiplies a list of nodes that are polynomial like terms. Returns a node.
// The polynomial nodes should *not* have coefficients. (multiplying
// coefficients is handled in collecting like terms for multiplication)
function multiplyLikeTermPolynomialNodes(nodes) {
  if (!canMultiplyLikeTermPolynomialNodes(nodes)) {
    throw Error("Can't multiply like terms - terms are not alike");
  }

  // If we're multiplying polynomial nodes together, they all share the same
  // symbol name. Get that name from the first node.
  const termName = new PolynomialTerm(nodes[0]).name;

  // The new exponent will be a sum of exponents (an operation, wrapped in
  // parens) e.g. x^(3+4+5)
  const exponentNodeList = nodes.map(n => { // map exponents to constant nodes
      let p = new PolynomialTerm(n);
      return NodeCreator.constant(p.exp);
    });
  const newExponent = NodeCreator.parenthesis(
    NodeCreator.operator("+", exponentNodeList));
  return NodeCreator.operator("^", [NodeCreator.symbol(termName), newExponent]);
}

// Returns true if the nodes are polynomial terms that can be added together.
function canAddLikeTermPolynomialNodes(nodes) {
  if (!nodes.every(n => PolynomialTerm.isPolynomialTerm(n))) {
    return false;
  }
  if (nodes.length === 1) {
    return false;
  }

  const firstTerm = new PolynomialTerm(nodes[0]);
  const restTerms = nodes.slice(1).map(n => new PolynomialTerm(n));
  // to add terms, they must have the same symbol name *and* exponent degree
  const shareNameAndExponent = restTerms.every(term => {
    return firstTerm.name === term.name && firstTerm.exp === term.exp;
  });
  return shareNameAndExponent;
}

// Returns true if the nodes are symbolic terms with the same symbol
// and no coefficients.
function canMultiplyLikeTermPolynomialNodes(nodes) {
  if (!nodes.every(n => PolynomialTerm.isPolynomialTerm(n))) {
    return false;
  }
  if (nodes.length === 1) {
    return false;
  }
  const polynomialTerms = nodes.map(n => new PolynomialTerm(n));
  // all coefficients must be 1 (ie there's no coefficient term)
  if (!polynomialTerms.every(polyTerm => polyTerm.coeff === 1)) {
    return false;
  }

  const firstTerm = polynomialTerms[0];
  const restTerms = polynomialTerms.slice(1);
  // they're considered like terms if they have the same symbol name
  return restTerms.every(term => firstTerm.name === term.name);
}

// Returns true if the expression is a multiplication between a constant
// and polynomial without a coefficient.
function canMultiplyConstantAndPolynomial(node) {
  // implicit multiplication doesn't count as multiplication here, since it
  // represents a single term.
  if (node.op !== '*' || node.implicit) {
    return false;
  }
  if (node.args.length !== 2) {
    return false;
  }
  return ((PolynomialTerm.isPolynomialTerm(node.args[0], false) &&
            node.args[1].type === 'ConstantNode') ||
          (PolynomialTerm.isPolynomialTerm(node.args[1], false) &&
            node.args[0].type === 'ConstantNode'));
}

// Returns true if polyNode is a node that represents a polynomial term
// and has a coefficent node.
function polynomialTermHasCoefficient(polyNode) {
  return PolynomialTerm.isPolynomialTerm(polyNode) &&
    polyNode.op === '*';
}

// Some polynomial terms have a fraction coefficient e.g. 2x/4
// This returns true if we can simplify that fraction.
// e.g. 2x/4 and 10x/5 return true, 4x/7 would return false
function canSimplifyPolynomialFraction (node) {
  if (!PolynomialTerm.isPolynomialTerm(node)) {
    return false;
  }
  const polyTerm = new PolynomialTerm(node);
  if (!(polyTerm.numeratorCoeff && polyTerm.denominatorCoeff)) {
    return false;
  }
  return math.gcd(polyTerm.numeratorCoeff, polyTerm.denominatorCoeff) !== 1;
}

module.exports = PolynomialTerm;
<|endoftext|>


<commit_before>/*
    This program is distributed under the terms of the MIT license.
    Please see the LICENSE file for details.

    Copyright 2006-2008, OGG, LLC
*/

/* jslint configuration: */
/*global document, window, setTimeout, clearTimeout, console,
    XMLHttpRequest, ActiveXObject,
    Base64, MD5,
    Strophe, $build, $msg, $iq, $pres */

/** File: strophe.js
 *  A JavaScript library to enable XMPP over Websocket in Strophejs.
 *
 *  This file implements XMPP over WebSockets for Strophejs.
 *  If a Connection is established with a Websocket url (ws://...)
 *  Strophe will use WebSockets instead of BOSH.
 *  WebSocket support implemented by Andreas Guth (guth@dbis.rwth-aachen.de)
 *  For more information on XMPP-over WebSocket see this RFC draft:
 *  http://tools.ietf.org/html/draft-moffitt-xmpp-over-websocket-01
 */

/** PrivateConstructor: Strophe.Websocket
 *  Create and initialize a Strophe.WebSocket object.
 *  Currently only sets the connection Object.
 *
 *  Parameters:
 *    (Strophe.Connection) connection - The Strophe.Connection that will use WebSockets.
 *
 *  Returns:
 *    A new Strophe.WebSocket object.
 */
Strophe.Websocket = function(connection) {
    this._conn = connection;
};

Strophe.Websocket.prototype = {
    /** PrivateFunction: _buildStream
     *  _Private_ helper function to generate the <stream> start tag for WebSockets
     *
     *  Returns:
     *    A Strophe.Builder with a <stream> element.
     */
    _buildStream: function ()
    {
        return $build("stream:stream", {
            "to": this._conn.domain,
            "xmlns": Strophe.NS.CLIENT,
            "xmlns:stream": Strophe.NS.STREAM,
            "version": '1.0'
        });
    },

    /** PrivateFunction: _check_streamerror
     * _Private_ checks a message for stream:error
     *
     *  Parameters:
     *    (Strophe.Request) bodyWrap - The received stanza.
     *    connectstatus - The ConnectStatus that will be set on error.
     *  Returns:
     *     true if there was a streamerror, false otherwise.
     */
    _check_streamerror: function (bodyWrap, connectstatus) {
        var errors = bodyWrap.getElementsByTagName("stream:error");
        if (errors.length === 0) {
            return false;
        }
        var error = errors[0];

        var condition = "";
        var text = "";

        ns = "urn:ietf:params:xml:ns:xmpp-streams";
        for (i = 0; i < error.childNodes.length; i++) {
            e = error.childNodes[i];
            if (e.getAttribute("xmlns") != ns)
                break;
            if (e.nodeName == "text")
                text = e.textContent;
            else
                condition = e.nodeName;
        }

        errorString = "WebSocket stream error: ";

        if (condition)
            errorString += condition;
        else
            errorString += "unknown";

        if (text)
            errorString += " - " + condition;

        Strophe.error(errorString);

        // close the connection on stream_error
        this._conn._changeConnectStatus(connectstatus, condition);
        this._conn._doDisconnect();
        return true;
    },

    /** PrivateFunction: _connect
     *  _Private_ function called by Strophe.Connection.connect
     *
     *  Creates a WebSocket for a connection and assigns Callbacks to it.
     *  Does nothing if there already is a WebSocket.
     */
    _connect: function () {
        // Ensure that there is no open WebSocket from a previous Connection.
        this._closeSocket();

        // Create the new WobSocket
        this.socket = new WebSocket(this._conn.service, "xmpp");
        this.socket.onopen = this._onOpen.bind(this);
        this.socket.onerror = this._onError.bind(this);
        this.socket.onclose = this._onClose.bind(this);
        this.socket.onmessage = this._connect_cb_wrapper.bind(this);
    },

    /** PrivateFunction: _connect_cb
     *  _Private_ function called by Strophe.Connection._connect_cb
     *
     * checks for stream:error
     *
     *  Parameters:
     *    (Strophe.Request) bodyWrap - The received stanza.
     */
    _connect_cb: function(bodyWrap) {
        var error = this._check_streamerror(bodyWrap, Strophe.Status.CONNFAIL);
        if (error) {
            return Strophe.Status.CONNFAIL;
        }
    },

    /** PrivateFunction: _connect_cb_wrapper
     * _Private_ function that handles the first connection messages.
     *
     * On receiving an opening stream tag this callback replaces itself with the real
     * message handler. On receiving a stream error the connection is terminated.
     */
    _connect_cb_wrapper: function(message) {
        //Inject namespaces into stream tags. has to be done because no SAX parser is used.
        var string = message.data.replace(/<stream:([a-z]*)>/, "<stream:$1 xmlns:stream='http://etherx.jabber.org/streams'>");
        //Make the initial stream:stream selfclosing to parse it without a SAX parser.
        string = string.replace(/<stream:stream (.*[^/])>/, "<stream:stream $1/>");

        var parser = new DOMParser();
        var elem = parser.parseFromString(string, "text/xml").documentElement;

        if (elem.nodeName != "stream:stream") {
            this.socket.onmessage = this._onMessage.bind(this);
            elem = this._conn._bodyWrap(elem).tree();
            this._conn._connect_cb(elem);
        } else {
            this._conn.xmlInput(elem);
            this._conn.rawInput(Strophe.serialize(elem));
            //_connect_cb will check for stream:error and disconnect on error
            this._connect_cb(elem)
        }
    },

    /** PrivateFunction: _disconnect
     *  _Private_ function called by Strophe.Connection.disconnect
     *
     *  Disconnects and sends a last stanza if one is given
     *
     *  Parameters:
     *    (Request) pres - This stanza will be sent before disconnecting.
     */
    _disconnect: function (pres)
    {
        if (this.socket.readyState !== WebSocket.CLOSED) {
            if (pres) {
                this._conn.send(pres);
            }
            var close = '</stream:stream>';
            this._conn.xmlOutput(this._conn._bodyWrap(document.createElement("stream:stream")));
            this._conn.rawOutput(close);
            try {
                this.socket.send(close);
            } catch (e) {
                Strophe.info("Couldn't send closing stream tag.");
            }

            this._conn._doDisconnect();
        }
    },

    /** PrivateFunction: _doDisconnect
     *  _Private_ function to disconnect.
     *
     *  Just closes the Socket for WebSockets
     */
    _doDisconnect: function ()
    {
        Strophe.info("WebSockets _doDisconnect was called");
        this._closeSocket();
    },

    /** PrivateFunction: _closeSocket
     *  _Private_ function to close the WebSocket.
     *
     *  Closes the socket if it is still open and deletes it
     */
    _closeSocket: function ()
    {
        if (this.socket) { try {
            this.socket.close();
        } catch (e) {} }
        this.socket = null;
    },

    /** PrivateFunction: _emptyQueue
     * _Private_ function to check if the message queue is empty.
     *
     *  Returns:
     *    True, because WebSocket messages are send immediately after queueing.
     */
    _emptyQueue: function ()
    {
        return true;
    },

    /** PrivateFunction: _onClose
     * _Private_ function to handle websockets closing.
     *
     * Nothing to do here for WebSockets
     */
    _onClose: function(event) {
        if(this._conn.connected && !this._conn.disconnecting) {
            Strophe.error("Websocket closed unexcectedly");
            this._conn._doDisconnect();
        } else {
            Strophe.info("Websocket closed");
        }
    },

    /** PrivateFunction: _no_auth_received
     *
     * Called on stream start/restart when no stream:features
     * has been received.
     */
    _no_auth_received: function (_callback)
    {
        Strophe.error("Server did not send any auth methods")
        this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Server did not send any auth methods");
        if (_callback) {
            _callback = _callback.bind(this._conn);
            _callback();
        }
        this._conn._doDisconnect();
    },

    /** PrivateFunction: _onDisconnectTimeout
     *  _Private_ timeout handler for handling non-graceful disconnection.
     *
     *  This does nothing for WebSockets
     */
    _onDisconnectTimeout: function () {},

    /** PrivateFunction: _onError
     * _Private_ function to handle websockets errors.
     *
     * Parameters:
     * (Object) error - The websocket error.
     */
    _onError: function(error) {
        Strophe.error("Websocket error " + error);
    },

    /** PrivateFunction: _onIdle
     *  _Private_ function called by Strophe.Connection._onIdle
     *
     *  sends all queued stanzas
     */
    _onIdle: function () {
        var data = this._conn._data;
        if (data.length > 0 && !this._conn.paused) {
            for (i = 0; i < data.length; i++) {
                if (data[i] !== null) {
                    if (data[i] === "restart") {
                        var stanza = this._buildStream();
                        var rawStanza = this._removeClosingTag(stanza)
                    } else {
                        var stanza = data[i];
                        var rawStanza = Strophe.serialize(stanza)
                    }
                    this._conn.xmlOutput(stanza);
                    this._conn.rawOutput(rawStanza);
                    this.socket.send(rawStanza);
                }
            }
            this._conn._data = [];
        }
    },

    /** PrivateFunction: _onMessage
     * _Private_ function to handle websockets messages.
     *
     * This function parses each of the messages as if they are full documents. [TODO : We may actually want to use a SAX Push parser].
     *
     * Since all XMPP traffic starts with "<stream:stream version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='3697395463' from='SERVER'>"
     * The first stanza will always fail to be parsed...
     * Addtionnaly, the seconds stanza will always be a <stream:features> with the stream NS defined in the previous stanza... so we need to 'force' the inclusion of the NS in this stanza!
     *
     * Parameters:
     * (string) message - The websocket message.
     */
    _onMessage: function(message) {
        // check for closing stream
        if (message.data === "</stream:stream>") {
            var close = "</stream:stream>";
            this._conn.rawInput(close);
            this._conn.xmlInput(this._conn._bodyWrap(document.createElement("stream:stream")));
            if (!this._conn.disconnecting) {
                this._conn._doDisconnect();
            }
            return;
        }
        var string = message.data;
        if (string.search("xmlns:stream") == -1) {
            //Inject namespaces into stream tags if they are missing. Has to be done because no SAX parser is used.
            string = string.replace(/<stream:([^>]*)>/, "<stream:$1 xmlns:stream='http://etherx.jabber.org/streams'>");
        }
        //Make the initial stream:stream selfclosing to parse it without a SAX parser.
        string = string.replace(/<stream:stream (.*[^/])>/, "<stream:stream $1/>");

        parser = new DOMParser();
        var elem = parser.parseFromString(string, "text/xml").documentElement;

        var elem = this._conn._bodyWrap(elem).tree();

        if (this._check_streamerror(elem, Strophe.Status.ERROR)) {
            return;
        }

        //handle unavailable presence stanza before disconnecting
        if (this._conn.disconnecting &&
                elem.firstChild.nodeName === "presence" &&
                elem.firstChild.getAttribute("type") === "unavailable") {
            this._conn.xmlInput(elem);
            this._conn.rawInput(Strophe.serialize(elem));
            // if we are already disconnecting we will ignore the unavailable stanza and
            // wait for the </stream:stream> tag before we close the connection
            return;
        } else {
            this._conn._dataRecv(elem);
        }
    },

    /** PrivateFunction: _onOpen
     * _Private_ function to handle websockets connection setup.
     *
     * The opening stream tag is sent here.
     */
    _onOpen: function() {
        Strophe.info("Websocket open");
        var start = this._buildStream();
        this._conn.xmlOutput(start);

        var startString = this._removeClosingTag(start);
        this._conn.rawOutput(startString);
        this.socket.send(startString);
    },

    /** PrivateFunction: _removeClosingTag
     *  _Private_ function to Make the first <stream:stream> non-selfclosing
     *
     *  Parameters:
     *      (Object) elem - The <stream:stream> tag.
     *
     *  Returns:
     *      The stream:stream tag as String
     */
    _removeClosingTag: function(elem) {
        var string = Strophe.serialize(elem);
        string = string.replace(/<(stream:stream .*[^/])\/>$/, "<$1>");
        return string;
    },

    /** PrivateFunction: _reqToData
     * _Private_ function to get a stanza out of a request.
     *
     * WebSockets don't use requests, so the passed argument is just returned.
     *
     *  Parameters:
     *    (Object) stanza - The stanza.
     *
     *  Returns:
     *    The stanza that was passed.
     */
    _reqToData: function (stanza)
    {
        return stanza;
    },

    /** PrivateFunction: _send
     *  _Private_ part of the Connection.send function for WebSocket
     *
     * Just flushes the messages that are in the queue
     */
    _send: function () {
        this._conn.flush();
    },

    /** PrivateFunction: _sendRestart
     *
     *  Send an xmpp:restart stanza.
     */
    _sendRestart: function ()
    {
        clearTimeout(this._conn._idleTimeout);
        this._conn._onIdle.bind(this._conn)();
    }
};
<commit_msg>call _doDisconnect even if WebSocket is not closed<commit_after>/*
    This program is distributed under the terms of the MIT license.
    Please see the LICENSE file for details.

    Copyright 2006-2008, OGG, LLC
*/

/* jslint configuration: */
/*global document, window, setTimeout, clearTimeout, console,
    XMLHttpRequest, ActiveXObject,
    Base64, MD5,
    Strophe, $build, $msg, $iq, $pres */

/** File: strophe.js
 *  A JavaScript library to enable XMPP over Websocket in Strophejs.
 *
 *  This file implements XMPP over WebSockets for Strophejs.
 *  If a Connection is established with a Websocket url (ws://...)
 *  Strophe will use WebSockets instead of BOSH.
 *  WebSocket support implemented by Andreas Guth (guth@dbis.rwth-aachen.de)
 *  For more information on XMPP-over WebSocket see this RFC draft:
 *  http://tools.ietf.org/html/draft-moffitt-xmpp-over-websocket-01
 */

/** PrivateConstructor: Strophe.Websocket
 *  Create and initialize a Strophe.WebSocket object.
 *  Currently only sets the connection Object.
 *
 *  Parameters:
 *    (Strophe.Connection) connection - The Strophe.Connection that will use WebSockets.
 *
 *  Returns:
 *    A new Strophe.WebSocket object.
 */
Strophe.Websocket = function(connection) {
    this._conn = connection;
};

Strophe.Websocket.prototype = {
    /** PrivateFunction: _buildStream
     *  _Private_ helper function to generate the <stream> start tag for WebSockets
     *
     *  Returns:
     *    A Strophe.Builder with a <stream> element.
     */
    _buildStream: function ()
    {
        return $build("stream:stream", {
            "to": this._conn.domain,
            "xmlns": Strophe.NS.CLIENT,
            "xmlns:stream": Strophe.NS.STREAM,
            "version": '1.0'
        });
    },

    /** PrivateFunction: _check_streamerror
     * _Private_ checks a message for stream:error
     *
     *  Parameters:
     *    (Strophe.Request) bodyWrap - The received stanza.
     *    connectstatus - The ConnectStatus that will be set on error.
     *  Returns:
     *     true if there was a streamerror, false otherwise.
     */
    _check_streamerror: function (bodyWrap, connectstatus) {
        var errors = bodyWrap.getElementsByTagName("stream:error");
        if (errors.length === 0) {
            return false;
        }
        var error = errors[0];

        var condition = "";
        var text = "";

        ns = "urn:ietf:params:xml:ns:xmpp-streams";
        for (i = 0; i < error.childNodes.length; i++) {
            e = error.childNodes[i];
            if (e.getAttribute("xmlns") != ns)
                break;
            if (e.nodeName == "text")
                text = e.textContent;
            else
                condition = e.nodeName;
        }

        errorString = "WebSocket stream error: ";

        if (condition)
            errorString += condition;
        else
            errorString += "unknown";

        if (text)
            errorString += " - " + condition;

        Strophe.error(errorString);

        // close the connection on stream_error
        this._conn._changeConnectStatus(connectstatus, condition);
        this._conn._doDisconnect();
        return true;
    },

    /** PrivateFunction: _connect
     *  _Private_ function called by Strophe.Connection.connect
     *
     *  Creates a WebSocket for a connection and assigns Callbacks to it.
     *  Does nothing if there already is a WebSocket.
     */
    _connect: function () {
        // Ensure that there is no open WebSocket from a previous Connection.
        this._closeSocket();

        // Create the new WobSocket
        this.socket = new WebSocket(this._conn.service, "xmpp");
        this.socket.onopen = this._onOpen.bind(this);
        this.socket.onerror = this._onError.bind(this);
        this.socket.onclose = this._onClose.bind(this);
        this.socket.onmessage = this._connect_cb_wrapper.bind(this);
    },

    /** PrivateFunction: _connect_cb
     *  _Private_ function called by Strophe.Connection._connect_cb
     *
     * checks for stream:error
     *
     *  Parameters:
     *    (Strophe.Request) bodyWrap - The received stanza.
     */
    _connect_cb: function(bodyWrap) {
        var error = this._check_streamerror(bodyWrap, Strophe.Status.CONNFAIL);
        if (error) {
            return Strophe.Status.CONNFAIL;
        }
    },

    /** PrivateFunction: _connect_cb_wrapper
     * _Private_ function that handles the first connection messages.
     *
     * On receiving an opening stream tag this callback replaces itself with the real
     * message handler. On receiving a stream error the connection is terminated.
     */
    _connect_cb_wrapper: function(message) {
        //Inject namespaces into stream tags. has to be done because no SAX parser is used.
        var string = message.data.replace(/<stream:([a-z]*)>/, "<stream:$1 xmlns:stream='http://etherx.jabber.org/streams'>");
        //Make the initial stream:stream selfclosing to parse it without a SAX parser.
        string = string.replace(/<stream:stream (.*[^/])>/, "<stream:stream $1/>");

        var parser = new DOMParser();
        var elem = parser.parseFromString(string, "text/xml").documentElement;

        if (elem.nodeName != "stream:stream") {
            this.socket.onmessage = this._onMessage.bind(this);
            elem = this._conn._bodyWrap(elem).tree();
            this._conn._connect_cb(elem);
        } else {
            this._conn.xmlInput(elem);
            this._conn.rawInput(Strophe.serialize(elem));
            //_connect_cb will check for stream:error and disconnect on error
            this._connect_cb(elem)
        }
    },

    /** PrivateFunction: _disconnect
     *  _Private_ function called by Strophe.Connection.disconnect
     *
     *  Disconnects and sends a last stanza if one is given
     *
     *  Parameters:
     *    (Request) pres - This stanza will be sent before disconnecting.
     */
    _disconnect: function (pres)
    {
        if (this.socket.readyState !== WebSocket.CLOSED) {
            if (pres) {
                this._conn.send(pres);
            }
            var close = '</stream:stream>';
            this._conn.xmlOutput(this._conn._bodyWrap(document.createElement("stream:stream")));
            this._conn.rawOutput(close);
            try {
                this.socket.send(close);
            } catch (e) {
                Strophe.info("Couldn't send closing stream tag.");
            }
        }

        this._conn._doDisconnect();
    },

    /** PrivateFunction: _doDisconnect
     *  _Private_ function to disconnect.
     *
     *  Just closes the Socket for WebSockets
     */
    _doDisconnect: function ()
    {
        Strophe.info("WebSockets _doDisconnect was called");
        this._closeSocket();
    },

    /** PrivateFunction: _closeSocket
     *  _Private_ function to close the WebSocket.
     *
     *  Closes the socket if it is still open and deletes it
     */
    _closeSocket: function ()
    {
        if (this.socket) { try {
            this.socket.close();
        } catch (e) {} }
        this.socket = null;
    },

    /** PrivateFunction: _emptyQueue
     * _Private_ function to check if the message queue is empty.
     *
     *  Returns:
     *    True, because WebSocket messages are send immediately after queueing.
     */
    _emptyQueue: function ()
    {
        return true;
    },

    /** PrivateFunction: _onClose
     * _Private_ function to handle websockets closing.
     *
     * Nothing to do here for WebSockets
     */
    _onClose: function(event) {
        if(this._conn.connected && !this._conn.disconnecting) {
            Strophe.error("Websocket closed unexcectedly");
            this._conn._doDisconnect();
        } else {
            Strophe.info("Websocket closed");
        }
    },

    /** PrivateFunction: _no_auth_received
     *
     * Called on stream start/restart when no stream:features
     * has been received.
     */
    _no_auth_received: function (_callback)
    {
        Strophe.error("Server did not send any auth methods")
        this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Server did not send any auth methods");
        if (_callback) {
            _callback = _callback.bind(this._conn);
            _callback();
        }
        this._conn._doDisconnect();
    },

    /** PrivateFunction: _onDisconnectTimeout
     *  _Private_ timeout handler for handling non-graceful disconnection.
     *
     *  This does nothing for WebSockets
     */
    _onDisconnectTimeout: function () {},

    /** PrivateFunction: _onError
     * _Private_ function to handle websockets errors.
     *
     * Parameters:
     * (Object) error - The websocket error.
     */
    _onError: function(error) {
        Strophe.error("Websocket error " + error);
    },

    /** PrivateFunction: _onIdle
     *  _Private_ function called by Strophe.Connection._onIdle
     *
     *  sends all queued stanzas
     */
    _onIdle: function () {
        var data = this._conn._data;
        if (data.length > 0 && !this._conn.paused) {
            for (i = 0; i < data.length; i++) {
                if (data[i] !== null) {
                    if (data[i] === "restart") {
                        var stanza = this._buildStream();
                        var rawStanza = this._removeClosingTag(stanza)
                    } else {
                        var stanza = data[i];
                        var rawStanza = Strophe.serialize(stanza)
                    }
                    this._conn.xmlOutput(stanza);
                    this._conn.rawOutput(rawStanza);
                    this.socket.send(rawStanza);
                }
            }
            this._conn._data = [];
        }
    },

    /** PrivateFunction: _onMessage
     * _Private_ function to handle websockets messages.
     *
     * This function parses each of the messages as if they are full documents. [TODO : We may actually want to use a SAX Push parser].
     *
     * Since all XMPP traffic starts with "<stream:stream version='1.0' xml:lang='en' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='3697395463' from='SERVER'>"
     * The first stanza will always fail to be parsed...
     * Addtionnaly, the seconds stanza will always be a <stream:features> with the stream NS defined in the previous stanza... so we need to 'force' the inclusion of the NS in this stanza!
     *
     * Parameters:
     * (string) message - The websocket message.
     */
    _onMessage: function(message) {
        // check for closing stream
        if (message.data === "</stream:stream>") {
            var close = "</stream:stream>";
            this._conn.rawInput(close);
            this._conn.xmlInput(this._conn._bodyWrap(document.createElement("stream:stream")));
            if (!this._conn.disconnecting) {
                this._conn._doDisconnect();
            }
            return;
        }
        var string = message.data;
        if (string.search("xmlns:stream") == -1) {
            //Inject namespaces into stream tags if they are missing. Has to be done because no SAX parser is used.
            string = string.replace(/<stream:([^>]*)>/, "<stream:$1 xmlns:stream='http://etherx.jabber.org/streams'>");
        }
        //Make the initial stream:stream selfclosing to parse it without a SAX parser.
        string = string.replace(/<stream:stream (.*[^/])>/, "<stream:stream $1/>");

        parser = new DOMParser();
        var elem = parser.parseFromString(string, "text/xml").documentElement;

        var elem = this._conn._bodyWrap(elem).tree();

        if (this._check_streamerror(elem, Strophe.Status.ERROR)) {
            return;
        }

        //handle unavailable presence stanza before disconnecting
        if (this._conn.disconnecting &&
                elem.firstChild.nodeName === "presence" &&
                elem.firstChild.getAttribute("type") === "unavailable") {
            this._conn.xmlInput(elem);
            this._conn.rawInput(Strophe.serialize(elem));
            // if we are already disconnecting we will ignore the unavailable stanza and
            // wait for the </stream:stream> tag before we close the connection
            return;
        } else {
            this._conn._dataRecv(elem);
        }
    },

    /** PrivateFunction: _onOpen
     * _Private_ function to handle websockets connection setup.
     *
     * The opening stream tag is sent here.
     */
    _onOpen: function() {
        Strophe.info("Websocket open");
        var start = this._buildStream();
        this._conn.xmlOutput(start);

        var startString = this._removeClosingTag(start);
        this._conn.rawOutput(startString);
        this.socket.send(startString);
    },

    /** PrivateFunction: _removeClosingTag
     *  _Private_ function to Make the first <stream:stream> non-selfclosing
     *
     *  Parameters:
     *      (Object) elem - The <stream:stream> tag.
     *
     *  Returns:
     *      The stream:stream tag as String
     */
    _removeClosingTag: function(elem) {
        var string = Strophe.serialize(elem);
        string = string.replace(/<(stream:stream .*[^/])\/>$/, "<$1>");
        return string;
    },

    /** PrivateFunction: _reqToData
     * _Private_ function to get a stanza out of a request.
     *
     * WebSockets don't use requests, so the passed argument is just returned.
     *
     *  Parameters:
     *    (Object) stanza - The stanza.
     *
     *  Returns:
     *    The stanza that was passed.
     */
    _reqToData: function (stanza)
    {
        return stanza;
    },

    /** PrivateFunction: _send
     *  _Private_ part of the Connection.send function for WebSocket
     *
     * Just flushes the messages that are in the queue
     */
    _send: function () {
        this._conn.flush();
    },

    /** PrivateFunction: _sendRestart
     *
     *  Send an xmpp:restart stanza.
     */
    _sendRestart: function ()
    {
        clearTimeout(this._conn._idleTimeout);
        this._conn._onIdle.bind(this._conn)();
    }
};
<|endoftext|>


<commit_before>// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Provides utility methods to render soy template.
 * @author chrishenry@google.com (Chris Henry)
 */

goog.provide('goog.soy');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');
goog.require('goog.dom.safe');
goog.require('goog.html.SafeHtml');
goog.require('goog.soy.data.SanitizedContent');

/**
 * A structural interface for injected data.
 *
 *
Soy generated code contributes optional properties.
 *
 * @record
 */
goog.soy.IjData = function() {};

/**
 * Helper typedef for ij parameters.  This is what soy generates.
 * @private
 * @typedef {!goog.soy.IjData|!Object<string, *>}
 */
goog.soy.CompatibleIj_;

// TODO(b/36644846): remove the second half of the function type union
/**
 * Type definition for strict Soy templates. Very useful when passing a template
 * as an argument.
 * @typedef {function(?, ?goog.soy.CompatibleIj_=):
 * !goog.soy.data.SanitizedContent| function(?, null=, ?Object<string, *>=):
 * !goog.soy.data.SanitizedContent}
 */
goog.soy.StrictTemplate;

// TODO(b/36644846): remove the second half of the function type union
/**
 * Type definition for strict Soy HTML templates. Very useful when passing
 * a template as an argument.
 * @typedef {function(?, ?goog.soy.CompatibleIj_=):
 * !goog.soy.data.SanitizedHtml| function(?, null=, ?Object<string, *>=):
 * !goog.soy.data.SanitizedHtml}
 */
goog.soy.StrictHtmlTemplate;

/**
 * Type definition for text templates.
 * @typedef {function(?=, ?goog.soy.CompatibleIj_=):string}
 */
goog.soy.TextTemplate;

/**
 * Sets the processed template as the innerHTML of an element. It is recommended
 * to use this helper function instead of directly setting innerHTML in your
 * hand-written code, so that it will be easier to audit the code for cross-site
 * scripting vulnerabilities.
 *
 * @param {?Element} element The element whose content we are rendering into.
 * @param {!goog.soy.data.SanitizedContent} templateResult The processed
 *     template of kind HTML or TEXT (which will be escaped).
 * @template ARG_TYPES
 */
goog.soy.renderHtml = function(element, templateResult) {
  goog.dom.safe.unsafeSetInnerHtmlDoNotUseOrElse(
      goog.asserts.assert(element),
      goog.soy.ensureTemplateOutputHtml_(templateResult));
};

// TODO(b/36644846): remove the second half of the function type union
/**
 * Renders a Soy template and then set the output string as
 * the innerHTML of an element. It is recommended to use this helper function
 * instead of directly setting innerHTML in your hand-written code, so that it
 * will be easier to audit the code for cross-site scripting vulnerabilities.
 *
 * @param {Element} element The element whose content we are rendering into.
 * @param {?function(ARG_TYPES, ?goog.soy.CompatibleIj_=):*|
 *     ?function(ARG_TYPES, null=, Object<string, *>=):*} template
 *     The Soy template defining the element's content.
 * @param {ARG_TYPES=} opt_templateData The data for the template.
 * @param {Object=} opt_injectedData The injected data for the template.
 * @template ARG_TYPES
 */
goog.soy.renderElement = function(
    element, template, opt_templateData, opt_injectedData) {
  // Soy template parameter is only nullable for historical reasons.
  goog.asserts.assert(template, 'Soy template may not be null.');
  var html = goog.soy.ensureTemplateOutputHtml_(template(
      opt_templateData || goog.soy.defaultTemplateData_, undefined,
      opt_injectedData));
  goog.dom.safe.unsafeSetInnerHtmlDoNotUseOrElse(
      goog.asserts.assert(element), html);
};

// TODO(b/36644846): remove the second half of the function type union
/**
 * Renders a Soy template into a single node or a document
 * fragment. If the rendered HTML string represents a single node, then that
 * node is returned (note that this is *not* a fragment, despite them name of
 * the method). Otherwise a document fragment is returned containing the
 * rendered nodes.
 *
 * @param {
 *     ?function(ARG_TYPES,
 * ?goog.soy.CompatibleIj_=):!goog.soy.data.SanitizedContent|
 *     ?function(ARG_TYPES, null=, Object<string, *>=):
 *     goog.soy.data.SanitizedContent} template The Soy template defining the
 *     element's content. The kind of the template must be "html" or "text".
 * @param {ARG_TYPES=} opt_templateData The data for the template.
 * @param {Object=} opt_injectedData The injected data for the template.
 * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper used to
 *     create DOM nodes; defaults to `goog.dom.getDomHelper`.
 * @return {!Node} The resulting node or document fragment.
 * @template ARG_TYPES
 */
goog.soy.renderAsFragment = function(
    template, opt_templateData, opt_injectedData, opt_domHelper) {
  // Soy template parameter is only nullable for historical reasons.
  goog.asserts.assert(template, 'Soy template may not be null.');
  var dom = opt_domHelper || goog.dom.getDomHelper();
  var output = template(
      opt_templateData || goog.soy.defaultTemplateData_, undefined,
      opt_injectedData);
  var html = goog.soy.ensureTemplateOutputHtml_(output);
  goog.soy.assertFirstTagValid_(html.getTypedStringValue());
  return dom.safeHtmlToNode(html);
};

// TODO(b/36644846): remove the second half of the function type union
/**
 * Renders a Soy template into a single node. If the rendered
 * HTML string represents a single node, then that node is returned. Otherwise,
 * a DIV element is returned containing the rendered nodes.
 *
 * @param {?function(ARG_TYPES, ?goog.soy.CompatibleIj_=):*|
 *     ?function(ARG_TYPES, null=, Object<string, *>=):*} template
 *     The Soy template defining the element's content.
 * @param {ARG_TYPES=} opt_templateData The data for the template.
 * @param {Object=} opt_injectedData The injected data for the template.
 * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper used to
 *     create DOM nodes; defaults to `goog.dom.getDomHelper`.
 * @return {!Element} Rendered template contents, wrapped in a parent DIV
 *     element if necessary.
 * @template ARG_TYPES
 */
goog.soy.renderAsElement = function(
    template, opt_templateData, opt_injectedData, opt_domHelper) {
  // Soy template parameter is only nullable for historical reasons.
  goog.asserts.assert(template, 'Soy template may not be null.');
  return goog.soy.convertToElement_(
      template(
          opt_templateData || goog.soy.defaultTemplateData_, undefined,
          opt_injectedData),
      opt_domHelper);
};

/**
 * Converts a processed Soy template into a single node. If the rendered
 * HTML string represents a single node, then that node is returned. Otherwise,
 * a DIV element is returned containing the rendered nodes.
 *
 * @param {!goog.soy.data.SanitizedContent} templateResult The processed
 *     template of kind HTML or TEXT (which will be escaped).
 * @param {?goog.dom.DomHelper=} opt_domHelper The DOM helper used to
 *     create DOM nodes; defaults to `goog.dom.getDomHelper`.
 * @return {!Element} Rendered template contents, wrapped in a parent DIV
 *     element if necessary.
 */
goog.soy.convertToElement = function(templateResult, opt_domHelper) {
  return goog.soy.convertToElement_(templateResult, opt_domHelper);
};

/**
 * Non-strict version of `goog.soy.convertToElement`.
 *
 * @param {*} templateResult The processed template.
 * @param {?goog.dom.DomHelper=} opt_domHelper The DOM helper used to
 *     create DOM nodes; defaults to `goog.dom.getDomHelper`.
 * @return {!Element} Rendered template contents, wrapped in a parent DIV
 *     element if necessary.
 * @private
 */
goog.soy.convertToElement_ = function(templateResult, opt_domHelper) {
  var dom = opt_domHelper || goog.dom.getDomHelper();
  var wrapper = dom.createElement(goog.dom.TagName.DIV);
  var html = goog.soy.ensureTemplateOutputHtml_(templateResult);
  goog.soy.assertFirstTagValid_(html.getTypedStringValue());
  goog.dom.safe.unsafeSetInnerHtmlDoNotUseOrElse(wrapper, html);

  // If the template renders as a single element, return it.
  if (wrapper.childNodes.length == 1) {
    var firstChild = wrapper.firstChild;
    if (firstChild.nodeType == goog.dom.NodeType.ELEMENT) {
      return /** @type {!Element} */ (firstChild);
    }
  }

  // Otherwise, return the wrapper DIV.
  return wrapper;
};

/**
 * Ensures the result is "safe" to insert as HTML.
 *
 * In the case the argument is a SanitizedContent object, it either must
 * already be of kind HTML, or if it is kind="text", the output will be HTML
 * escaped.
 *
 * @param {*} templateResult The template result.
 * @return {!goog.html.SafeHtml} The assumed-safe HTML output string.
 * @private
 */
goog.soy.ensureTemplateOutputHtml_ = function(templateResult) {
  // Note we allow everything that isn't an object, because some non-escaping
  // templates end up returning non-strings if their only print statement is a
  // non-escaped argument, plus some unit tests spoof templates.
  // TODO(gboyer): Track down and fix these cases.
  if (!goog.isObject(templateResult)) {
    return goog.html.SafeHtml.htmlEscape(String(templateResult));
  }

  // Allow SanitizedContent of kind HTML.
  if (templateResult instanceof goog.soy.data.SanitizedContent) {
    return templateResult.toSafeHtml();
  }

  goog.asserts.fail(
      'Soy template output is unsafe for use as HTML: ' + templateResult);

  // In production, return a safe string, rather than failing hard.
  return goog.html.SafeHtml.htmlEscape('zSoyz');
};

/**
 * Checks that the rendered HTML does not start with an invalid tag that would
 * likely cause unexpected output from renderAsElement or renderAsFragment.
 * See {@link http://www.w3.org/TR/html5/semantics.html#semantics} for reference
 * as to which HTML elements can be parents of each other.
 * @param {string} html The output of a template.
 * @private
 */
goog.soy.assertFirstTagValid_ = function(html) {
  if (goog.asserts.ENABLE_ASSERTS) {
    var matches = html.match(goog.soy.INVALID_TAG_TO_RENDER_);
    goog.asserts.assert(
        !matches, 'This template starts with a %s, which ' +
            'cannot be a child of a
, as required by soy internals. ' +
            'Consider using goog.soy.renderElement instead.\nTemplate output: %s',
        matches && matches[0], html);
  }
};

/**
 * A pattern to find templates that cannot be rendered by renderAsElement or
 * renderAsFragment, as these elements cannot exist as the child of a
.
 * @type {!RegExp}
 * @private
 */
goog.soy.INVALID_TAG_TO_RENDER_ =
    /^<(body|caption|col|colgroup|head|html|tr|td|th|tbody|thead|tfoot)>/i;

/**
 * Immutable object that is passed into templates that are rendered
 * without any data.
 * @private @const
 */
goog.soy.defaultTemplateData_ = {};
<commit_msg>Remove the type union from goog.soy.StrictHtmlTemplate and goog.soy.StrictTemplate<commit_after>// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Provides utility methods to render soy template.
 * @author chrishenry@google.com (Chris Henry)
 */

goog.provide('goog.soy');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');
goog.require('goog.dom.safe');
goog.require('goog.html.SafeHtml');
goog.require('goog.soy.data.SanitizedContent');

/**
 * A structural interface for injected data.
 *
 *
Soy generated code contributes optional properties.
 *
 * @record
 */
goog.soy.IjData = function() {};

/**
 * Helper typedef for ij parameters.  This is what soy generates.
 * @private
 * @typedef {!goog.soy.IjData|!Object<string, *>}
 */
goog.soy.CompatibleIj_;

/**
 * Type definition for strict Soy templates. Very useful when passing a template
 * as an argument.
 * @typedef {function(?=,
 * ?goog.soy.CompatibleIj_=):(string|!goog.soy.data.SanitizedContent)}
 */
goog.soy.StrictTemplate;

/**
 * Type definition for strict Soy HTML templates. Very useful when passing
 * a template as an argument.
 * @typedef {function(?=,
 * ?goog.soy.CompatibleIj_=):!goog.soy.data.SanitizedHtml}
 */
goog.soy.StrictHtmlTemplate;

/**
 * Type definition for text templates.
 * @typedef {function(?=, ?goog.soy.CompatibleIj_=):string}
 */
goog.soy.TextTemplate;

/**
 * Sets the processed template as the innerHTML of an element. It is recommended
 * to use this helper function instead of directly setting innerHTML in your
 * hand-written code, so that it will be easier to audit the code for cross-site
 * scripting vulnerabilities.
 *
 * @param {?Element} element The element whose content we are rendering into.
 * @param {!goog.soy.data.SanitizedContent} templateResult The processed
 *     template of kind HTML or TEXT (which will be escaped).
 * @template ARG_TYPES
 */
goog.soy.renderHtml = function(element, templateResult) {
  goog.dom.safe.unsafeSetInnerHtmlDoNotUseOrElse(
      goog.asserts.assert(element),
      goog.soy.ensureTemplateOutputHtml_(templateResult));
};

// TODO(b/36644846): remove the second half of the function type union
/**
 * Renders a Soy template and then set the output string as
 * the innerHTML of an element. It is recommended to use this helper function
 * instead of directly setting innerHTML in your hand-written code, so that it
 * will be easier to audit the code for cross-site scripting vulnerabilities.
 *
 * @param {Element} element The element whose content we are rendering into.
 * @param {?function(ARG_TYPES, ?goog.soy.CompatibleIj_=):*|
 *     ?function(ARG_TYPES, null=, Object<string, *>=):*} template
 *     The Soy template defining the element's content.
 * @param {ARG_TYPES=} opt_templateData The data for the template.
 * @param {Object=} opt_injectedData The injected data for the template.
 * @template ARG_TYPES
 */
goog.soy.renderElement = function(
    element, template, opt_templateData, opt_injectedData) {
  // Soy template parameter is only nullable for historical reasons.
  goog.asserts.assert(template, 'Soy template may not be null.');
  var html = goog.soy.ensureTemplateOutputHtml_(template(
      opt_templateData || goog.soy.defaultTemplateData_, undefined,
      opt_injectedData));
  goog.dom.safe.unsafeSetInnerHtmlDoNotUseOrElse(
      goog.asserts.assert(element), html);
};

// TODO(b/36644846): remove the second half of the function type union
/**
 * Renders a Soy template into a single node or a document
 * fragment. If the rendered HTML string represents a single node, then that
 * node is returned (note that this is *not* a fragment, despite them name of
 * the method). Otherwise a document fragment is returned containing the
 * rendered nodes.
 *
 * @param {
 *     ?function(ARG_TYPES,
 * ?goog.soy.CompatibleIj_=):!goog.soy.data.SanitizedContent|
 *     ?function(ARG_TYPES, null=, Object<string, *>=):
 *     goog.soy.data.SanitizedContent} template The Soy template defining the
 *     element's content. The kind of the template must be "html" or "text".
 * @param {ARG_TYPES=} opt_templateData The data for the template.
 * @param {Object=} opt_injectedData The injected data for the template.
 * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper used to
 *     create DOM nodes; defaults to `goog.dom.getDomHelper`.
 * @return {!Node} The resulting node or document fragment.
 * @template ARG_TYPES
 */
goog.soy.renderAsFragment = function(
    template, opt_templateData, opt_injectedData, opt_domHelper) {
  // Soy template parameter is only nullable for historical reasons.
  goog.asserts.assert(template, 'Soy template may not be null.');
  var dom = opt_domHelper || goog.dom.getDomHelper();
  var output = template(
      opt_templateData || goog.soy.defaultTemplateData_, undefined,
      opt_injectedData);
  var html = goog.soy.ensureTemplateOutputHtml_(output);
  goog.soy.assertFirstTagValid_(html.getTypedStringValue());
  return dom.safeHtmlToNode(html);
};

// TODO(b/36644846): remove the second half of the function type union
/**
 * Renders a Soy template into a single node. If the rendered
 * HTML string represents a single node, then that node is returned. Otherwise,
 * a DIV element is returned containing the rendered nodes.
 *
 * @param {?function(ARG_TYPES, ?goog.soy.CompatibleIj_=):*|
 *     ?function(ARG_TYPES, null=, Object<string, *>=):*} template
 *     The Soy template defining the element's content.
 * @param {ARG_TYPES=} opt_templateData The data for the template.
 * @param {Object=} opt_injectedData The injected data for the template.
 * @param {goog.dom.DomHelper=} opt_domHelper The DOM helper used to
 *     create DOM nodes; defaults to `goog.dom.getDomHelper`.
 * @return {!Element} Rendered template contents, wrapped in a parent DIV
 *     element if necessary.
 * @template ARG_TYPES
 */
goog.soy.renderAsElement = function(
    template, opt_templateData, opt_injectedData, opt_domHelper) {
  // Soy template parameter is only nullable for historical reasons.
  goog.asserts.assert(template, 'Soy template may not be null.');
  return goog.soy.convertToElement_(
      template(
          opt_templateData || goog.soy.defaultTemplateData_, undefined,
          opt_injectedData),
      opt_domHelper);
};

/**
 * Converts a processed Soy template into a single node. If the rendered
 * HTML string represents a single node, then that node is returned. Otherwise,
 * a DIV element is returned containing the rendered nodes.
 *
 * @param {!goog.soy.data.SanitizedContent} templateResult The processed
 *     template of kind HTML or TEXT (which will be escaped).
 * @param {?goog.dom.DomHelper=} opt_domHelper The DOM helper used to
 *     create DOM nodes; defaults to `goog.dom.getDomHelper`.
 * @return {!Element} Rendered template contents, wrapped in a parent DIV
 *     element if necessary.
 */
goog.soy.convertToElement = function(templateResult, opt_domHelper) {
  return goog.soy.convertToElement_(templateResult, opt_domHelper);
};

/**
 * Non-strict version of `goog.soy.convertToElement`.
 *
 * @param {*} templateResult The processed template.
 * @param {?goog.dom.DomHelper=} opt_domHelper The DOM helper used to
 *     create DOM nodes; defaults to `goog.dom.getDomHelper`.
 * @return {!Element} Rendered template contents, wrapped in a parent DIV
 *     element if necessary.
 * @private
 */
goog.soy.convertToElement_ = function(templateResult, opt_domHelper) {
  var dom = opt_domHelper || goog.dom.getDomHelper();
  var wrapper = dom.createElement(goog.dom.TagName.DIV);
  var html = goog.soy.ensureTemplateOutputHtml_(templateResult);
  goog.soy.assertFirstTagValid_(html.getTypedStringValue());
  goog.dom.safe.unsafeSetInnerHtmlDoNotUseOrElse(wrapper, html);

  // If the template renders as a single element, return it.
  if (wrapper.childNodes.length == 1) {
    var firstChild = wrapper.firstChild;
    if (firstChild.nodeType == goog.dom.NodeType.ELEMENT) {
      return /** @type {!Element} */ (firstChild);
    }
  }

  // Otherwise, return the wrapper DIV.
  return wrapper;
};

/**
 * Ensures the result is "safe" to insert as HTML.
 *
 * In the case the argument is a SanitizedContent object, it either must
 * already be of kind HTML, or if it is kind="text", the output will be HTML
 * escaped.
 *
 * @param {*} templateResult The template result.
 * @return {!goog.html.SafeHtml} The assumed-safe HTML output string.
 * @private
 */
goog.soy.ensureTemplateOutputHtml_ = function(templateResult) {
  // Note we allow everything that isn't an object, because some non-escaping
  // templates end up returning non-strings if their only print statement is a
  // non-escaped argument, plus some unit tests spoof templates.
  // TODO(gboyer): Track down and fix these cases.
  if (!goog.isObject(templateResult)) {
    return goog.html.SafeHtml.htmlEscape(String(templateResult));
  }

  // Allow SanitizedContent of kind HTML.
  if (templateResult instanceof goog.soy.data.SanitizedContent) {
    return templateResult.toSafeHtml();
  }

  goog.asserts.fail(
      'Soy template output is unsafe for use as HTML: ' + templateResult);

  // In production, return a safe string, rather than failing hard.
  return goog.html.SafeHtml.htmlEscape('zSoyz');
};

/**
 * Checks that the rendered HTML does not start with an invalid tag that would
 * likely cause unexpected output from renderAsElement or renderAsFragment.
 * See {@link http://www.w3.org/TR/html5/semantics.html#semantics} for reference
 * as to which HTML elements can be parents of each other.
 * @param {string} html The output of a template.
 * @private
 */
goog.soy.assertFirstTagValid_ = function(html) {
  if (goog.asserts.ENABLE_ASSERTS) {
    var matches = html.match(goog.soy.INVALID_TAG_TO_RENDER_);
    goog.asserts.assert(
        !matches, 'This template starts with a %s, which ' +
            'cannot be a child of a
, as required by soy internals. ' +
            'Consider using goog.soy.renderElement instead.\nTemplate output: %s',
        matches && matches[0], html);
  }
};

/**
 * A pattern to find templates that cannot be rendered by renderAsElement or
 * renderAsFragment, as these elements cannot exist as the child of a
.
 * @type {!RegExp}
 * @private
 */
goog.soy.INVALID_TAG_TO_RENDER_ =
    /^<(body|caption|col|colgroup|head|html|tr|td|th|tbody|thead|tfoot)>/i;

/**
 * Immutable object that is passed into templates that are rendered
 * without any data.
 * @private @const
 */
goog.soy.defaultTemplateData_ = {};
<|endoftext|>