text: // calculate.jquery, from https://github.com/pablobm/calculate.jquery
  // Licensed under MIT
  (function ($) {
  
    //
    // jQuery hook
    //
  
    $.fn.calculate = function (stringOrFunction, opts) {
      var opts = $.extend({}, $.calculate.defaults(), opts || {});
      var api = new Api(this, opts);
  
      if (typeof stringOrFunction === 'string') {
        api.formula(stringOrFunction);
      }
      else {
        stringOrFunction.bind(this).call(stringOrFunction, api);
      }
  
      return this;
    }
  
  
    //
    // Global settings
    //
  
    $.calculate = {
      inputParser: function(rawVal) { return rawVal },
  
      outputFormatter: function(result) { return result },
  
      options: function(opts) {
        if (opts.inputParser) {
          this.inputParser = opts.inputParser;
        }
        if (opts.outputFormatter) {
          this.outputFormatter = opts.outputFormatter;
        }
      },
  
      defaults: function() {
        return {
          inputParser: this.inputParser,
          outputFormatter: this.outputFormatter
        }
      }
    }
  
  
    //
    // API
    //
  
    function Api(base, opts) {
      this.base = $(base);
      this.details = null;
      this.opts = opts || {};
      this.events = [];
    }
    Api.prototype = {
      formula: function(val) {
        priv.compile(this, val);
        priv.updateEvents(this);
        this.run();
      },
  
      run: function() {
        var that = this;
        this.base.each(function() {
          that.runOne($(this));
        });
      },
  
      runOne: function(base) {
        var values = readValues(this.details.operands, base, this.opts.inputParser);
        var result = this.compiled.eval(values);
        var formattedResult = this.opts.outputFormatter(result);
        base.find(this.details.resultSelector)
          .val(formattedResult)
          .trigger('change');
      }
    }
  
  
    //
    // Private "methods"
    //
  
    var priv = {
      compile: function(self, formula) {
        self.details = parseFormula(formula);
        var tree = math.parse(self.details.rside);
        self.compiled = tree.compile(math);
      },
  
      updateEvents: function(self) {
        priv.removeEvents(self);
        priv.setEvents(self);
      },
  
      removeEvents: function(self) {
        self.events.forEach(function(evt) {
          evt.query.off(evt.name, evt.handler);
        });
        self.events = [];
      },
  
      setEvents: function(self) {
        Object.keys(self.details.operands).forEach(function(selector) {
          self.base.each(function() {
            var singleBase = $(this);
            var field = singleBase.find(selector);
            priv.setEvent(
              self,
              field,
              'change',
              function() { self.runOne(singleBase) }
            );
          });
        });
      },
  
      setEvent: function(self, query, name, handler) {
        var evt = {
          query: query,
          name: name,
          handler: handler
        }
        evt.query.on(evt.name, evt.handler);
        self.events.push(evt);
      }
    };
  
  
    //
    // Utility functions
    //
  
    // Receives a string such as '{{.foo}} = {{.bar}} - {{.baz}}'.
    // Returns an object such as:
    //   formula.operands => { '.bar': 'B', '.baz': 'C' }
    //   formula.resultSelector => '.foo'
    //   formula.rside => 'B - C'
    function parseFormula(str) {
      var formula = {
        operands: {},
      };
  
      var sides = str.match(/^\s*([^=\s]+)\s*=\s*(.*)$/);
      formula.rside = assignVariablesToSelectors(sides[2], formula.operands);
      formula.resultSelector = readFirstSelector(sides[1]);
  
      return formula;
    }
  
    var formulaSelectorsRegexp = '\{\{([^\}]+)\}\}';
  
    function assignVariablesToSelectors(formula, dict) {
      var re = new RegExp(formulaSelectorsRegexp, 'g');
      return formula.replace(re, function (_, capture) {
        var existing = dict[capture];
        if (existing) {
          return existing;
        }
        return dict[capture] = generateVarName();
      });
    }
  
    function readFirstSelector(formula) {
      var re = new RegExp(formulaSelectorsRegexp);
      return re.exec(formula)[1];
    }
  
  
    // Simply returning letters in order for now
    var generateVarName_count = 0;
    function generateVarName(index) {
      return 'X' + (generateVarName_count++);
    }
  
    // Receives:
    //   * operands: { '.foo': 'A', '.bar': 'B', '.baz': 'C' }
    //   * element: base for DOM search
    // Returns {'A': '23.5', 'B': '12.8', C: '3'}
    function readValues(operands, element, inputParser) {
      var values = {};
      var base = $(element);
      $.each(asPairs(operands), function() {
        var selector = this[0];
        var varname = this[1];
        var matches = base.find(selector);
        values[varname] = matches.toArray().reduce(function(sum, el) {
          var rawVal = $(el).val();
          return sum + inputParser(rawVal)*1;
        }, 0);
      });
      return values;
    }
  
    // Receive an object and return key/value pairs
    function asPairs(obj) {
      var pairs = [];
      Object.keys(obj).forEach(function (key) {
        var value = obj[key];
        pairs.push([key, value]);
      });
      return pairs;
    }
  
  })(jQuery);