<commit_before>module.exports = (function() {
  var wechat = require('wechat');
  var express = require('express');
  var _ = require('lodash');

  var API = require('./api.js');
  var api = null;	// API caller
  var self = null;	// point to WeChat itself
  var default_conf = {
    port: 80,
    CONST: {
      ERR: '啊啦，服务器傲娇啦~~~~(>_<)~~~~ 。请稍后重试~！',
      NO_RESULT: '抱歉，暂未找到相关词条，不妨试试其他关键词~？'
    }
  };

  /*
   * Base class for all WeChat server-side applications serving wikis on huiji
   *
   * Parameter *config* is required, defined as {
   *   name, required, name of your wiki site. More precisely, name is the
   *   first part of the url of your wiki site. E.g., 'lotr' is the name for
   *   http://lotr.huiji.wiki
   *
   *   wechat, a dict, required, credential for your wechat app, defined as {
   *     token, required,
   *     appid, required,
   *     encodingAESKey, required
   *   }
   *   For more information, please check out Wechat's official document:
   *   http://mp.weixin.qq.com/wiki/home/index.html
   *
   *   port, int, optional, indicates which port to be listened. You can pass
   *   it into start() instead when calling to start the server or just ignore
   *   it and a default port 80 will be used.
   * }
   */
  var WeChat = function(config) {
    /*
     * *config* is required,
     *   config.name,
     *   config.wechat,
     *   config.wechat.token,
     *   config.wechat.appid,
     *   config.wechat.encodingAESKey
     * All of the above are required.
     */
    if (!config) return;
    if (!config.name || !config.wechat) return;
    var wechat_required = _.every(['token', 'appid', 'encodingAESKey'], function(n) {
      return _.has(config.wechat, n);
    });
    if (!wechat_required) return;

    this.conf = default_conf;
    _.assign(this.conf, config);

    this.url = this._url();
    api = new API(this.url);

    // used for hack
    this._hack_key = [];
    this._hack_value = [];

    // used for keyword
    this._keywords_key = [];
    this._keywords_func = [];

    this.app = express();
    this.app.use('', wechat(config.wechat)
      .text(this.handlerText)
      .image(this.handlerImage)
      .voice(this.handlerVoice)
      .video(this.handlerVideo)
      .location(this.handlerLocation)
      .link(this.handlerLink)
      .event(this.handlerEvent)
      .middlewarify());
    // handlerXXX() functions need such self to point to WeChat instance itself
    self = this;
  };

  WeChat.prototype = {
    /*
     * Start wechat server
     *
     * *port*, which port to listen to, optinal. If not set, will look up
     * conf.port and if still not set, 80 by default.
     */
    start: function(port) {
      var port = port || this.conf.port || 80;
      var server = this.app.listen(port, function() {
        // TODO: log
        console.log("WeChat server for %s starts...", this.url);
      });
      server.on('error', function(err) {
        // TODO: log
      });
    },
    /*************************************************************************
     * Handle different type of requests and give response                   *
     *************************************************************************/
    /*
     * Handle text message
     *
     * Default behavior of handlerText() will be as followed,
     *   1. hack(), translate user's input in order to , e.g., avoid typo,
     *      support new keywords without failing legacy ones, explicitly
     *      redirection out-of-wiki-site, etc.
     *      Users should call addHack() to add various hack-cases.
     *   2. keyword(), handle special keywords returning specific responses.
     *      Users should call addKeyword() to add various keyword-cases.
     *      NOTICE: All intercepted keywords will not be passed into post-
     *              process.
     *   3. Return detail information of the page queried if user's input get
     *      a precise hit; Otherwise, list of search results will be returned.
     */
    handlerText: function(msg, req, res, next) {
      var text = msg.Content || '';
      console.log("Text message recv: %s", text);
      // 1. hack()
      text = self.hack(text);
      // 2. keyword()
      var handled = self.keyword(text);
      if (handled !== false) {
        res.reply(handled);
      } else {
        api.details({
          titles: [ text ],
          // A sole message-with-pic requires a 320px-wide picture
          size: 320
        }, function(err, data) {
          if (err) return self._err(err, res);
          if (data.length == 0) {
            // Message is not a precise title of any page.
            // Try search.
            api.search({
              key: text,
              limit: 10,  //  TODO: could be configured
              target: 'default',  //  TODO: could be configured, or not...
            }, function(err, data) {
              if (err) return self._err(err, res);
              if (data.length == 0) return self._no_result(res);
              // Get details of these result pages
              api.details({
                titles: data,
                // TODO: size == 320 or one more API call?
                size: 320
              }, function(err, data) {
                if (err) return self._err(err, res);
                res.reply(_.map(data, function(detail) {
                  return self._single(detail);
                }));
              });
            });
          } else {
            res.reply([ self._single(data[0]) ]);
          }
        });
      }
    },
    handlerImage: function(msg, req, res, next) {
    },
    handlerVoice: function(msg, req, res, next) {
    },
    handlerVideo: function(msg, req, res, next) {
    },
    handlerLocation: function(msg, req, res, next) {
    },
    handlerLink: function(msg, req, res, next) {
    },
    handlerEvent: function(msg, req, res, next) {
    },
    /*
     * Hack users' input for various reasons, e.g. avoid typo, support new
     * keywords without failing legacy ones, explicitly redirection
     * out-of-wiki-site, etc.
     *
     * *msg*, required, users' input
     *
     * Users should call addHack() before the wechat server starts.
     * hack() will lookup _hack_key and return corresponding value in
     * _hack_value if there is a hit. Otherwise, return what is passed in.
     *
     * By default, hack() will be called at the beginning of handlerText().
     */
    hack: function(msg) {
      if (msg == undefined || msg == '') msg = '';
      else msg = '' + msg;
      var hacked = false;
      var res = undefined;

      function hit(value) {
        res = value;
        hacked = true;
        return false;
      }

      _.forEach(this._hack_key, function(key, index) {
        var value = this._hack_value[index];
        if (typeof(key) == 'string') {
          if (msg == key) return hit(value);
        } else if (key instanceof RegExp) {
          if (key.test(msg)) return hit(value);
        } else if (typeof(key) == 'function') {
          ret = key(msg);
          if (ret === true) return hit(value);
          if (ret !== false) return hit(ret);
        }
      }, this);
      if (hacked) {
        console.log('hacked %s => %s', msg, res);
        return res;
      } else return msg;
    },
    /*
     * Add hack-case to support hack().
     *
     * Following types of hack-case are supported,
     *   1. <string, string>, e.g., '囧恩' => '琼恩'
     *   2. <RegExp, string>, e.g., /^囧/ => '琼恩'
     *   3. <array, string>, e.g., ['囧', '囧恩', '穷恩'] => '琼恩'
     *   4. <dict object, undefined>, e.g.,
     *        { '囧': '琼恩', '囧恩': '琼恩' } => undefined (value not used)
     *   5. <function, string>, users' input will be filtered by function.
     *        Those whose return value is true will be hacked; Else not.
     *        However, this hack-case also support dynamic hacking. If the
     *        result of a return is not false and is not true either, use
     *        the return value as the hacked result.
     *   6. <array, undefined>, e.g.,
     *        [{ 'key': '囧恩', 'value': '琼恩' }] => undefined
     *        addHack() every elements in the array. Each element should be an
     *        object containing only two fields: key & value.
     */
    addHack: function(key, value) {
      if (key == undefined || key == null || _.isNaN(key) || _.isEqual(key, {})
        || _.isEqual(key, []) || key == '')
        return;
      if (key instanceof RegExp || typeof(key) == 'function') {
        this._hack_key.push(key);
        this._hack_value.push(value);
      } else if (_.isArray(key)) {
        if (value === undefined) {
          _.forEach(key, function(k) {
            this.addHack(k.key, k.value);
          }, this);
        } else {
          _.forEach(key, function(k) {
            this.addHack(k, value);
          }, this);
        }
      } else if (typeof(key) == 'object') {
        _.forEach(key, function(v, k) {
          this._hack_key.push(k);
          this._hack_value.push(v);
        }, this);
      } else {
        key = '' + key;
        this._hack_key.push(key);
        this._hack_value.push(value);
      }
    },
    /*
     * Handle special keyword-cases
     *
     * *msg*, required, users' input

     * Such special keyword-cases will be handled only in functions added by
     * addKeyword() and will not be passed to following process.
     *
     * Return response if handled; false otherwise.
     */
    keyword: function(msg) {
      if (!msg && msg != 0) return false;
      var handled = false;
      var res = undefined;

      function hit(func) {
        res = func(msg);
        handled = true;
        return false;
      }

      _.forEach(this._keywords_key, function(key, index) {
        var func = this._keywords_func[index];
        if (typeof(key) == 'string') {
          if (msg == key) return hit(func);
        } else if (key instanceof RegExp) {
          if (key.test(msg)) return hit(func);
        } else if (typeof(key) == 'function') {
          ret = key(msg);
          if (ret === true) return hit(func);
          if (ret !== false) return hit(ret);
        }
      }, this);
      if (handled) {
        console.log('keyword %s hit.', msg);
        return res;
      } else return false;
    },
    /*
     * Add keyword-case to support keyword().
     *
     * To provide dynamic keyword functionality (supposed to be rare case),
     * the following code is similar to code of addHack().
     * *key* is similar to those for addHack(). However,
     * *func* is a function which accepts users' input that produces the
     * output and responds to client directly.
     *
     * handlerText() will handle keyword-cases after hack(). Those inputs that
     * hit a keyword-case will be handled only in corresponding *func* and will
     * not be passed to following process.
     */
    addKeyword: function(key, func) {
      if (key == undefined || key == null || _.isNaN(key) || _.isEqual(key, {})
        || _.isEqual(key, []) || key == '')
        return;
      if (key instanceof RegExp || typeof(key) == 'function') {
        this._keywords_key.push(key);
        this._keywords_func.push(func);
      } else if (_.isArray(key)) {
        if (func === undefined) {
          _.forEach(key, function(k) {
            this.addKeyword(k.key, k.func);
          }, this);
        } else {
          _.forEach(key, function(k) {
            this.addKeyword(k, func);
          }, this);
        }
      } else if (typeof(key) == 'object') {
        _.forEach(key, function(f, k) {
          this._keywords_key.push(k);
          this._keywords_func.push(f);
        }, this);
      } else {
        key = '' + key;
        this._keywords_key.push(key);
        this._keywords_func.push(func);
      }
    },
    /*
     * Get url of the wiki site
     */
    _url: function() {
      return 'http://' + this.conf.name + '.huiji.wiki';
    },
    /*
     * Get page url on the wiki site
     */
    _page_url: function(title) {
      var base = this.url || this._url();
      return base + '/wiki/' + title;
    },
    /*
     * Called when an error occurs. Respond plain text conf.CONST.ERR to
     * client by default.
     *
     * *err*, message of the error, will be logged.
     * *res*, will call res.reply() to respond to client.
     */
    _err: function(err, res) {
      console.log(err);
      res.reply(this.conf.CONST.ERR);
      return;
    },
    /*
     * Called when no results to respond. Respond plain text
     * conf.CONST.NO_RESULT to client by default.
     *
     * *res*, will call res.reply() to respond to client.
     */
    _no_result: function(res) {
      console.log('NO RESULTS');
      res.reply(this.conf.CONST.NO_RESULT);
      return;
    },
    /*
     * Form wechat single message according to the result returned by API
     */
    _single: function(res) {
      // handle thumbnail
      var picurl = '';
      // TODO: further process to thumbnail according to its size
      if (!res.thumbnail) {
        // TODO: if no thumbnail exists, a default pic should be returned.
      } else {
        picurl = res.thumbnail.source;
      }
      return {
        title: res.title,
        description: res.extract,
        url: this._page_url(res.title),
        picurl: picurl
      };
    }
  };

  return WeChat;
}());
<commit_msg>order of searching results is preserved now.<commit_after>module.exports = (function() {
  var wechat = require('wechat');
  var express = require('express');
  var _ = require('lodash');

  var API = require('./api.js');
  var api = null;	// API caller
  var self = null;	// point to WeChat itself
  var default_conf = {
    port: 80,
    CONST: {
      ERR: '啊啦，服务器傲娇啦~~~~(>_<)~~~~ 。请稍后重试~！',
      NO_RESULT: '抱歉，暂未找到相关词条，不妨试试其他关键词~？'
    }
  };

  /*
   * Base class for all WeChat server-side applications serving wikis on huiji
   *
   * Parameter *config* is required, defined as {
   *   name, required, name of your wiki site. More precisely, name is the
   *   first part of the url of your wiki site. E.g., 'lotr' is the name for
   *   http://lotr.huiji.wiki
   *
   *   wechat, a dict, required, credential for your wechat app, defined as {
   *     token, required,
   *     appid, required,
   *     encodingAESKey, required
   *   }
   *   For more information, please check out Wechat's official document:
   *   http://mp.weixin.qq.com/wiki/home/index.html
   *
   *   port, int, optional, indicates which port to be listened. You can pass
   *   it into start() instead when calling to start the server or just ignore
   *   it and a default port 80 will be used.
   * }
   */
  var WeChat = function(config) {
    /*
     * *config* is required,
     *   config.name,
     *   config.wechat,
     *   config.wechat.token,
     *   config.wechat.appid,
     *   config.wechat.encodingAESKey
     * All of the above are required.
     */
    if (!config) return;
    if (!config.name || !config.wechat) return;
    var wechat_required = _.every(['token', 'appid', 'encodingAESKey'], function(n) {
      return _.has(config.wechat, n);
    });
    if (!wechat_required) return;

    this.conf = default_conf;
    _.assign(this.conf, config);

    this.url = this._url();
    api = new API(this.url);

    // used for hack
    this._hack_key = [];
    this._hack_value = [];

    // used for keyword
    this._keywords_key = [];
    this._keywords_func = [];

    this.app = express();
    this.app.use('', wechat(config.wechat)
      .text(this.handlerText)
      .image(this.handlerImage)
      .voice(this.handlerVoice)
      .video(this.handlerVideo)
      .location(this.handlerLocation)
      .link(this.handlerLink)
      .event(this.handlerEvent)
      .middlewarify());
    // handlerXXX() functions need such self to point to WeChat instance itself
    self = this;
  };

  WeChat.prototype = {
    /*
     * Start wechat server
     *
     * *port*, which port to listen to, optinal. If not set, will look up
     * conf.port and if still not set, 80 by default.
     */
    start: function(port) {
      var port = port || this.conf.port || 80;
      var server = this.app.listen(port, function() {
        console.log("WeChat server for %s starts...", self.url);
      });
      server.on('error', function(err) {
        console.log(err);
      });
    },
    /*************************************************************************
     * Handle different type of requests and give response                   *
     *************************************************************************/
    /*
     * Handle text message
     *
     * Default behavior of handlerText() will be as followed,
     *   1. hack(), translate user's input in order to , e.g., avoid typo,
     *      support new keywords without failing legacy ones, explicitly
     *      redirection out-of-wiki-site, etc.
     *      Users should call addHack() to add various hack-cases.
     *   2. keyword(), handle special keywords returning specific responses.
     *      Users should call addKeyword() to add various keyword-cases.
     *      NOTICE: All intercepted keywords will not be passed into post-
     *              process.
     *   3. Return detail information of the page queried if user's input get
     *      a precise hit; Otherwise, list of search results will be returned.
     */
    handlerText: function(msg, req, res, next) {
      var text = msg.Content || '';
      console.log("Text message recv: %s", text);
      // 1. hack()
      text = self.hack(text);
      // 2. keyword()
      var handled = self.keyword(text);
      if (handled !== false) {
        res.reply(handled);
      } else {
        api.details({
          titles: [ text ],
          // A sole message-with-pic requires a 320px-wide picture
          size: 320
        }, function(err, data) {
          if (err) return self._err(err, res);
          if (data.length == 0) {
            // Message is not a precise title of any page.
            // Try search.
            api.search({
              key: text,
              limit: 10,  //  TODO: could be configured
              target: 'default',  //  TODO: could be configured, or not...
            }, function(err, titles) {
              if (err) return self._err(err, res);
              if (titles.length == 0) return self._no_result(res);
              // Get details of these result pages
              api.details({
                titles: titles,
                // TODO: size == 320 or one more API call?
                size: 320
              }, function(err, data) {
                if (err) return self._err(err, res);
                // Preserve order of the searching results strictly according
                // to the order in titles
                var results = [];
                _.forEach(data, function(detail) {
                  var index = titles.indexOf(detail.title);
                  results[index] = self._single(detail);
                });
                res.reply(_.filter(results));
              });
            });
          } else {
            res.reply([ self._single(data[0]) ]);
          }
        });
      }
    },
    handlerImage: function(msg, req, res, next) {
    },
    handlerVoice: function(msg, req, res, next) {
    },
    handlerVideo: function(msg, req, res, next) {
    },
    handlerLocation: function(msg, req, res, next) {
    },
    handlerLink: function(msg, req, res, next) {
    },
    handlerEvent: function(msg, req, res, next) {
    },
    /*
     * Hack users' input for various reasons, e.g. avoid typo, support new
     * keywords without failing legacy ones, explicitly redirection
     * out-of-wiki-site, etc.
     *
     * *msg*, required, users' input
     *
     * Users should call addHack() before the wechat server starts.
     * hack() will lookup _hack_key and return corresponding value in
     * _hack_value if there is a hit. Otherwise, return what is passed in.
     *
     * By default, hack() will be called at the beginning of handlerText().
     */
    hack: function(msg) {
      if (msg == undefined || msg == '') msg = '';
      else msg = '' + msg;
      var hacked = false;
      var res = undefined;

      function hit(value) {
        res = value;
        hacked = true;
        return false;
      }

      _.forEach(this._hack_key, function(key, index) {
        var value = this._hack_value[index];
        if (typeof(key) == 'string') {
          if (msg == key) return hit(value);
        } else if (key instanceof RegExp) {
          if (key.test(msg)) return hit(value);
        } else if (typeof(key) == 'function') {
          ret = key(msg);
          if (ret === true) return hit(value);
          if (ret !== false) return hit(ret);
        }
      }, this);
      if (hacked) {
        console.log('hacked %s => %s', msg, res);
        return res;
      } else return msg;
    },
    /*
     * Add hack-case to support hack().
     *
     * Following types of hack-case are supported,
     *   1. <string, string>, e.g., '囧恩' => '琼恩'
     *   2. <RegExp, string>, e.g., /^囧/ => '琼恩'
     *   3. <array, string>, e.g., ['囧', '囧恩', '穷恩'] => '琼恩'
     *   4. <dict object, undefined>, e.g.,
     *        { '囧': '琼恩', '囧恩': '琼恩' } => undefined (value not used)
     *   5. <function, string>, users' input will be filtered by function.
     *        Those whose return value is true will be hacked; Else not.
     *        However, this hack-case also support dynamic hacking. If the
     *        result of a return is not false and is not true either, use
     *        the return value as the hacked result.
     *   6. <array, undefined>, e.g.,
     *        [{ 'key': '囧恩', 'value': '琼恩' }] => undefined
     *        addHack() every elements in the array. Each element should be an
     *        object containing only two fields: key & value.
     */
    addHack: function(key, value) {
      if (key == undefined || key == null || _.isNaN(key) || _.isEqual(key, {})
        || _.isEqual(key, []) || key == '')
        return;
      if (key instanceof RegExp || typeof(key) == 'function') {
        this._hack_key.push(key);
        this._hack_value.push(value);
      } else if (_.isArray(key)) {
        if (value === undefined) {
          _.forEach(key, function(k) {
            this.addHack(k.key, k.value);
          }, this);
        } else {
          _.forEach(key, function(k) {
            this.addHack(k, value);
          }, this);
        }
      } else if (typeof(key) == 'object') {
        _.forEach(key, function(v, k) {
          this._hack_key.push(k);
          this._hack_value.push(v);
        }, this);
      } else {
        key = '' + key;
        this._hack_key.push(key);
        this._hack_value.push(value);
      }
    },
    /*
     * Handle special keyword-cases
     *
     * *msg*, required, users' input

     * Such special keyword-cases will be handled only in functions added by
     * addKeyword() and will not be passed to following process.
     *
     * Return response if handled; false otherwise.
     */
    keyword: function(msg) {
      if (!msg && msg != 0) return false;
      var handled = false;
      var res = undefined;

      function hit(func) {
        res = func(msg);
        handled = true;
        return false;
      }

      _.forEach(this._keywords_key, function(key, index) {
        var func = this._keywords_func[index];
        if (typeof(key) == 'string') {
          if (msg == key) return hit(func);
        } else if (key instanceof RegExp) {
          if (key.test(msg)) return hit(func);
        } else if (typeof(key) == 'function') {
          ret = key(msg);
          if (ret === true) return hit(func);
          if (ret !== false) return hit(ret);
        }
      }, this);
      if (handled) {
        console.log('keyword %s hit.', msg);
        return res;
      } else return false;
    },
    /*
     * Add keyword-case to support keyword().
     *
     * To provide dynamic keyword functionality (supposed to be rare case),
     * the following code is similar to code of addHack().
     * *key* is similar to those for addHack(). However,
     * *func* is a function which accepts users' input that produces the
     * output and responds to client directly.
     *
     * handlerText() will handle keyword-cases after hack(). Those inputs that
     * hit a keyword-case will be handled only in corresponding *func* and will
     * not be passed to following process.
     */
    addKeyword: function(key, func) {
      if (key == undefined || key == null || _.isNaN(key) || _.isEqual(key, {})
        || _.isEqual(key, []) || key == '')
        return;
      if (key instanceof RegExp || typeof(key) == 'function') {
        this._keywords_key.push(key);
        this._keywords_func.push(func);
      } else if (_.isArray(key)) {
        if (func === undefined) {
          _.forEach(key, function(k) {
            this.addKeyword(k.key, k.func);
          }, this);
        } else {
          _.forEach(key, function(k) {
            this.addKeyword(k, func);
          }, this);
        }
      } else if (typeof(key) == 'object') {
        _.forEach(key, function(f, k) {
          this._keywords_key.push(k);
          this._keywords_func.push(f);
        }, this);
      } else {
        key = '' + key;
        this._keywords_key.push(key);
        this._keywords_func.push(func);
      }
    },
    /*
     * Get url of the wiki site
     */
    _url: function() {
      return 'http://' + this.conf.name + '.huiji.wiki';
    },
    /*
     * Get page url on the wiki site
     */
    _page_url: function(title) {
      var base = this.url || this._url();
      return base + '/wiki/' + title;
    },
    /*
     * Called when an error occurs. Respond plain text conf.CONST.ERR to
     * client by default.
     *
     * *err*, message of the error, will be logged.
     * *res*, will call res.reply() to respond to client.
     */
    _err: function(err, res) {
      console.log(err);
      res.reply(this.conf.CONST.ERR);
      return;
    },
    /*
     * Called when no results to respond. Respond plain text
     * conf.CONST.NO_RESULT to client by default.
     *
     * *res*, will call res.reply() to respond to client.
     */
    _no_result: function(res) {
      console.log('NO RESULTS');
      res.reply(this.conf.CONST.NO_RESULT);
      return;
    },
    /*
     * Form wechat single message according to the result returned by API
     */
    _single: function(res) {
      // handle thumbnail
      var picurl = '';
      // TODO: further process to thumbnail according to its size
      if (!res.thumbnail) {
        // TODO: if no thumbnail exists, a default pic should be returned.
      } else {
        picurl = res.thumbnail.source;
      }
      return {
        title: res.title,
        description: res.extract,
        url: this._page_url(res.title),
        picurl: picurl
      };
    }
  };

  return WeChat;
}());
<|endoftext|>


<commit_before>
/**
 * @fileoverview google_tests.js has unit tests for the functions in
 *    google_services.js.
 * @author dld@google.com (DrakeDiedrich)
 *
 * Copyright 2013 Google Inc. 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.
 */

test('googleName', function() {
  equal(googleName
      ('mail.google.com', 'mail/u/0/?shva=1#search/qunit'),
      'mail.google.com',
      'mail.google.com');
  equal(googleName
      ('www.google.com', 'calendar/render?tab=mc'),
      'www.google.com/calendar',
      'www.google.com/calendar');
  equal(googleName
      ('www.google.com', '#hl=en'),
      'www.google.com',
      'www.google.com/#');
  equal(googleName
      ('www.google.com', 'search?q=css image'),
      'www.google.com/search',
      'www.google.com/search');
  equal(googleName
      ('goto.google.com', 'redir'),
      'goto.google.com',
      'goto.google.com/redir');

  // everything under these domains is just counted as one service
  var domain_services = ['youtube.com', 'youtube-nocookie.com',
    'youtu.be', 'ytimg.com',
    'doubleclick.net',
    'googlesyndication.com', 'admob.com',
    'googletagservices.com',
    'googleadservices.com',
    'google-analytics.com', 'urchin.com',
    'googletagmanager.com',
    'gstatic.com',
    'goo.gl', 'g.co',
    'googlecommerce.com',
    'android.com',
    'googleapis.com',
    'appspot.com', 'withgoogle.com',
    'blogger.com', 'blogblog.com',
    'blogspot.com',
    'googlecode.com', 'googlesource.com',
    'googlegroups.com',
    'googleusercontent.com', 'ggpht.com',
    'googledrive.com',
    'gmail.com', 'googlemail.com',
    'googleitahosted.com', 'itasoftware.com',
    'widevine.tv', 'widevine.com',
    'keyhole.com',
    'google.net', 'google.org'
  ];
  for (var i = 0; i < domain_services.length; i++) {
    equal(googleName
        ('www.' + domain_services[i], 'path?args'),
        domain_services[i],
        domain_services[i]);
  }
  equal(googleName
      ('service.company.com', 'path'),
      null,
      '!service.company.com');
});

<commit_msg>Make sure fix for hashtags works for this case too<commit_after>
/**
 * @fileoverview google_tests.js has unit tests for the functions in
 *    google_services.js.
 * @author dld@google.com (DrakeDiedrich)
 *
 * Copyright 2013 Google Inc. 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.
 */

test('googleName', function() {
  equal(googleName
      ('mail.google.com', 'mail/u/0/?shva=1#search/qunit'),
      'mail.google.com',
      'mail.google.com');
  equal(googleName
      ('www.google.com', 'calendar/render?tab=mc'),
      'www.google.com/calendar',
      'www.google.com/calendar');
  equal(googleName
      ('www.google.com', '#hl=en'),
      'www.google.com',
      'www.google.com/#');
  equal(googleName
      ('www.google.com', 'voice#inbox'),
      'www.google.com/voice',
      'www.google.com/voice#inbox');
  equal(googleName
      ('www.google.com', 'search?q=css image'),
      'www.google.com/search',
      'www.google.com/search');
  equal(googleName
      ('goto.google.com', 'redir'),
      'goto.google.com',
      'goto.google.com/redir');

  // everything under these domains is just counted as one service
  var domain_services = ['youtube.com', 'youtube-nocookie.com',
    'youtu.be', 'ytimg.com',
    'doubleclick.net',
    'googlesyndication.com', 'admob.com',
    'googletagservices.com',
    'googleadservices.com',
    'google-analytics.com', 'urchin.com',
    'googletagmanager.com',
    'gstatic.com',
    'goo.gl', 'g.co',
    'googlecommerce.com',
    'android.com',
    'googleapis.com',
    'appspot.com', 'withgoogle.com',
    'blogger.com', 'blogblog.com',
    'blogspot.com',
    'googlecode.com', 'googlesource.com',
    'googlegroups.com',
    'googleusercontent.com', 'ggpht.com',
    'googledrive.com',
    'gmail.com', 'googlemail.com',
    'googleitahosted.com', 'itasoftware.com',
    'widevine.tv', 'widevine.com',
    'keyhole.com',
    'google.net', 'google.org'
  ];
  for (var i = 0; i < domain_services.length; i++) {
    equal(googleName
        ('www.' + domain_services[i], 'path?args'),
        domain_services[i],
        domain_services[i]);
  }
  equal(googleName
      ('service.company.com', 'path'),
      null,
      '!service.company.com');
});

<|endoftext|>


<commit_before>/**
 * Created by Pieterjan Lambrecht on 12/06/15.
 */

function WindowJS (options) {
    var that = this;
    if(options) {
        // Get a reference to body DOM element
        this.body = document.getElementsByTagName("body")[0];
        // Create outer window div
        this._window = this.createElement("article", "windowjs-window");
        this._window.style.position = "absolute";
        if("title" in options || "close_button" in options) {
            this._header = this.createElement("header", "windowjs-header");
            this.addToWindow(this._header);
        }
        // Add title div
        if("title" in options) {
            this._title = this.createElement("div", "windowjs-title", options.title);
            this.addToHeader(this._title);
        }
        // Add a close button and make closeable
        if("close_button" in options) {
            this._close = this.createElement("div", "windowjs-close", options.close_button);
            this._close.onclick = function() {
                that.close();
            }
            this.addToHeader(this._close);
        }
        // Make draggable if necessary
        if("draggable" in options && options.draggable === true) {
            this.draggable = options.draggable;
            this.attachDrag();
        }
        // Attach an id to the window
        if("id" in options) {
            this._id = options.id;
        } else {
            this._id = "windowjs-" + (document.getElementsByClassName("windowjs-window").length + 1);
        }
        this._window.id = this._id;
        // Add some main content to the window
        if("content" in options){
            this._body = this.createElement("main", "windowjs-body", options.content);
            this.addToWindow(this._body);
        }
        // Add a parent div for our window
        if("parent" in options) {
            this.parent = options.parent;
        } else {
            this.parent = this.body;
        }
    } else {
        // We need options to do something
        throw "No options provided, skipping creation of window";
    }
}

WindowJS.prototype.createElement = function(tag, className, html) {
    var el = document.createElement(tag);
    if(className) {
        el.className = className;
    }
    if(html) {
        el.innerHTML = html;
    }
    return el;
};

// Add a DOM element to our window
WindowJS.prototype.addToWindow = function(el) {
    this._window.appendChild(el);
};

WindowJS.prototype.addToHeader = function(el) {
    this._header.appendChild(el);
};

// Remove the window from the DOM
WindowJS.prototype.close = function() {
    this._window.parentNode.removeChild(this._window);
};

// Add the window to the DOM
WindowJS.prototype.show = function() {
    if(this.parent) {
        this.parent.appendChild(this._window);
    } else {
        throw "WindowJS instance has no valid parent div";
    }
};

// Add drag functionality, attached to the title div
WindowJS.prototype.attachDrag = function() {
    var that = this;
    this._window.ondragstart = function() {
        return false;
    };
    this._title.onmousedown = function() {
        document.onmousemove = function(ev) {
            var pos = that._title.getBoundingClientRect();
            var x = (ev.clientX - that._title.clientHeight/2)+"px";
            var y = (ev.clientY - that._title.clientWidth/2)+"px";
            that._window.style.left = x;
            that._window.style.top = y;
        }
    };
    this._title.onmouseup = function() {
        document.onmousemove = null;
    };
};<commit_msg>[enhancement] positioning of windows<commit_after>/**
 * Created by Pieterjan Lambrecht on 12/06/15.
 */

function WindowJS (options) {
    var that = this;
    if(options) {
        // Get a reference to body DOM element
        this.body = document.getElementsByTagName("body")[0];
        // Create outer window div
        this._window = this._createElement("article", "windowjs-window");
        this._window.style.position = "absolute";
        if("title" in options || "close_button" in options) {
            this._header = this._createElement("header", "windowjs-header");
            this._addToWindow(this._header);
        }
        // Add title div
        if("title" in options) {
            this._title = this._createElement("div", "windowjs-title", options.title);
            this._addToHeader(this._title);
        }
        // Add a close button and make closeable
        if("close_button" in options) {
            this._close = this._createElement("div", "windowjs-close", options.close_button);
            this._close.onclick = function() {
                that.close();
            };
            this._addToHeader(this._close);
        }
        // Make draggable if necessary
        if("draggable" in options && options.draggable === true) {
            this.draggable = options.draggable;
            this._attachDrag();
        }
        // Attach an id to the window
        if("id" in options) {
            this._id = options.id;
        } else {
            this._id = "windowjs-" + (document.getElementsByClassName("windowjs-window").length + 1);
        }
        this._window.id = this._id;
        // Add some main content to the window
        if("content" in options){
            this._body = this._createElement("main", "windowjs-body", options.content);
            this._addToWindow(this._body);
        }
        // Add a parent div for our window
        if("parent" in options) {
            this.parent = options.parent;
        } else {
            this.parent = this.body;
        }
    } else {
        // We need options to do something
        throw "No options provided, skipping creation of window";
    }
}

WindowJS.prototype._createElement = function(tag, className, html) {
    var el = document.createElement(tag);
    if(className) {
        el.className = className;
    }
    if(html) {
        el.innerHTML = html;
    }
    return el;
};

// Add a DOM element to our window
WindowJS.prototype._addToWindow = function(el) {
    this._window.appendChild(el);
};

WindowJS.prototype._addToHeader = function(el) {
    this._header.appendChild(el);
};

// Remove the window from the DOM
WindowJS.prototype.close = function() {
    this._window.parentNode.removeChild(this._window);
};

// Add the window to the DOM
WindowJS.prototype.show = function() {
    if(this.parent) {
        this.parent.appendChild(this._window);
    } else {
        throw "WindowJS instance has no valid parent div";
    }
};

// Add drag functionality, attached to the title div
WindowJS.prototype._attachDrag = function() {
    var that = this;
    this._window.ondragstart = function() {
        return false;
    };
    this._title.onmousedown = function() {
        document.onmousemove = function(ev) {
            var x = (ev.clientX - that._title.clientHeight/2)+"px";
            var y = (ev.clientY - that._title.clientWidth/2)+"px";
            that._setPosition({top: x, left: y});
        };
    };
    this._title.onmouseup = function() {
        document.onmousemove = null;
    };
};

WindowJS.prototype._setPosition = function(position) {
    if("left" in position){
        this._window.style.left = position.left;
    }
    if("top" in position) {
        this._window.style.top = position.top;
    }
    if("bottom" in position) {
        this._window.style.bottom = position.bottom;
    }
    if("right" in position) {
        this._window.style.right = position.right;
    }
};

// Get height of window, including off screen pixels
WindowJS.prototype._getHeight = function() {
  return this._window.scrollHeight;
};

// Get width of window, including off screen pixels
WindowJS.prototype._getWidth = function() {
    return this._window.scrollWidth;
};

WindowJS.prototype._getPosition = function() {
    var pos = this._window.getBoundingClientRect();
    return {
        top: pos.top,
        left: pos.left,
        bottom: pos.bottom,
        right: pos.right
    };
};

WindowJS.prototype.below = function(other, spacing) {
    var top = other._getPosition().bottom + spacing;
    var left = other._getPosition().left;
    this._setPosition({top: top, left: left});
};

WindowJS.prototype.above = function(other, spacing) {
    var bottom = other._getPosition().top + spacing;
    var left = other._getPosition().left;
    this._setPosition({bottom: bottom, left: left});
};

WindowJS.prototype.leftOf = function(other, spacing) {
    var right = other._getPosition().left + spacing;
    var top = other._getPosition().top;
    this._setPosition({top: top, right: right});
};

WindowJS.prototype.rightOf = function(other, spacing) {
    var left = other._getPosition().right + spacing;
    var top = other._getPosition().top;
    this._setPosition({left: left, top: top});
};<|endoftext|>


<commit_before>(function main() {

  $('#newly-released-data-btn').on('click', function () {
    if ($('#newly-released-data').hasClass('hidden')) {
      $('#newly-released-data-btn').html('');
      $('#newly-released-data').removeClass('hidden');
      $('#newly-released-data-btn').html('');
    } else {
      $('#newly-released-data-btn').html('');
      $('#newly-released-data').addClass('hidden');
      $('#newly-released-data-btn').html('');
    }
  });

  $('.orgportals-filters').on('change', function () {
    var url = $(this).val();

    if (url) {
      window.location = url + '#search-data';
    }

    return false;
  });

  var fontUrl = 'https://mmwebfonts.comquas.com/fonts/?font=zawgyi';

  $('head').append('<link rel="stylesheet" href="' + fontUrl + '" />');

  _setActiveLanguage();

  /*
   * Set the active language in the language picker
   * based on the active locale
   */
  function _setActiveLanguage() {
    var languageSelector = $('.language-selector');
    var currentLanguage = $('html').attr('lang');
    var languageElement;

    if (currentLanguage === 'en') {
      languageElement = languageSelector.find('li')[0];
    } else {
      languageElement = languageSelector.find('li')[1];
    }

    if (languageElement) {
      languageElement.className = 'active';
    }
  }

  var downloadAsBtn = $('.snapshot-dashboard-menu');

  downloadAsBtn.on('click', function(event) {
    var target = event.target;
    var dataAttribute = target.getAttribute('data-download-as');
    var renderedCanvas;

    // Scroll the window to top, in order to capture the entire screen
    window.scrollTo(0, 0);

    _hideElementsBeforeDownload();

    html2canvas($('body')[0]).then(function(canvas) {
      var image = canvas.toDataURL('image/png');
      var doc;

      if (dataAttribute === 'pdf') {
        doc = new jsPDF('portrait', 'mm', [document.body.offsetWidth / 3.85, document.body.offsetHeight * 0.9]);
        doc.addImage(canvas, 'PNG', 0, 0, 0, 0);
        doc.save('download.pdf');
      } else if (dataAttribute === 'image') {
        Canvas2Image.saveAsPNG(canvas);
      }

      _showElementsAfterDownload();
    });
  });

  var heroMap = $('.hero-map-wrap');
  var newData = $('.new-data');
  var allData = $('.all-data');

  function _hideElementsBeforeDownload() {
    downloadAsBtn.parent().toggleClass('open');
    heroMap.hide();
    newData.hide();
    allData.hide();
  }

  function _showElementsAfterDownload() {
    heroMap.show();
    newData.show();
    allData.show();
  }

  var mediaContainer = $('div[data-section="media"]');

  mediaContainer.on('click', function onMediaContainerClick(event) {
    var target = $(event.target);
    var graphDescription = target.siblings('.graph-description');

    if (target.hasClass('download-graph-btn')) {
      graphDescription.hide();
      // target.hide();

      html2canvas(target.parent('.graph-container')[0]).then(function(canvas) {
        graphDescription.show();
        // target.show();
        Canvas2Image.saveAsPNG(canvas);
      });
    }
  });

})();

function toggleResources(resourceId) {
  $('#' + resourceId).toggleClass('hidden');
}
<commit_msg>Improve making a snapshot of a subdashboard<commit_after>(function main() {

  $('#newly-released-data-btn').on('click', function () {
    if ($('#newly-released-data').hasClass('hidden')) {
      $('#newly-released-data-btn').html('');
      $('#newly-released-data').removeClass('hidden');
      $('#newly-released-data-btn').html('');
    } else {
      $('#newly-released-data-btn').html('');
      $('#newly-released-data').addClass('hidden');
      $('#newly-released-data-btn').html('');
    }
  });

  $('.orgportals-filters').on('change', function () {
    var url = $(this).val();

    if (url) {
      window.location = url + '#search-data';
    }

    return false;
  });

  var fontUrl = 'https://mmwebfonts.comquas.com/fonts/?font=zawgyi';

  $('head').append('<link rel="stylesheet" href="' + fontUrl + '" />');

  _setActiveLanguage();

  /*
   * Set the active language in the language picker
   * based on the active locale
   */
  function _setActiveLanguage() {
    var languageSelector = $('.language-selector');
    var currentLanguage = $('html').attr('lang');
    var languageElement;

    if (currentLanguage === 'en') {
      languageElement = languageSelector.find('li')[0];
    } else {
      languageElement = languageSelector.find('li')[1];
    }

    if (languageElement) {
      languageElement.className = 'active';
    }
  }

  var snapshotDashboardMenu = $('.snapshot-dashboard-menu');

  snapshotDashboardMenu.on('click', function(event) {
    var target = event.target;
    var dataAttribute = target.getAttribute('data-download-as');
    var renderedCanvas;

    // Scroll the window to top, in order to capture the entire screen
    window.scrollTo(0, 0);

    _hideElementsBeforeDownload();

    html2canvas($('body')[0]).then(function(canvas) {
      var image = canvas.toDataURL('image/png');
      var doc;

      if (dataAttribute === 'pdf') {
        doc = new jsPDF('portrait', 'mm', [document.body.offsetWidth / 3.85, document.body.offsetHeight * 0.9]);
        doc.addImage(canvas, 'PNG', 0, 0, 0, 0);
        doc.save('download.pdf');
      } else if (dataAttribute === 'image') {
        Canvas2Image.saveAsPNG(canvas);
      }

      _showElementsAfterDownload();
    });
  });

  var heroMap = $('.hero-map-wrap');
  var newData = $('.new-data');
  var allData = $('.all-data');
  var goDownArrows = $('.go-down-arrow');
  var downloadAsBtn = $('#download-as-pdf');
  var mediaSection = $('[data-section="media"]');
  var downloadGraphBtns = $('.download-graph-btn');

  function _hideElementsBeforeDownload() {
    snapshotDashboardMenu.parent().toggleClass('open');
    heroMap.hide();
    newData.hide();
    allData.hide();
    goDownArrows.hide();
    downloadAsBtn.hide();
    downloadGraphBtns.hide();

    mediaSection.css('margin-bottom', '70px');
  }

  function _showElementsAfterDownload() {
    heroMap.show();
    newData.show();
    allData.show();
    goDownArrows.show();
    downloadAsBtn.show();
    downloadGraphBtns.show();

    mediaSection.css('margin-bottom', 'initial');
  }

  var mediaContainer = $('div[data-section="media"]');

  mediaContainer.on('click', function onMediaContainerClick(event) {
    var target = $(event.target);
    var graphDescription = target.siblings('.graph-description');

    if (target.hasClass('download-graph-btn')) {
      graphDescription.hide();
      // target.hide();

      html2canvas(target.parent('.graph-container')[0]).then(function(canvas) {
        graphDescription.show();
        // target.show();
        Canvas2Image.saveAsPNG(canvas);
      });
    }
  });

})();

function toggleResources(resourceId) {
  $('#' + resourceId).toggleClass('hidden');
}
<|endoftext|>


<commit_before>var path = require('path');
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    // No idea what this does.
    devtool: 'cheap-module-eval-source-map',
    // Seperates the application into chunks
    entry: {
        'polyfills': './src/polyfills.ts',
        'vendor': './src/vendor.js',
        'theme': './src/theme.js',
        'app': './src/main.js',
    },
    output: {
        path: path.resolve('dist'), //Serves it out of distribution folder
        publicPath: '/',
        filename: '[name].js',
        chunkFilename: '[id].chunk.js'
    },

    // Determines the naming convention
    resolve: {
        extensions: ['', '.js', '.ts']
    },

    module: {
        loaders: [
            {
                test: /\.ts$/,
                loaders: ['awesome-typescript-loader', 'angular2-template-loader']
            },
            {
                test: /\.html$/,
                loader: 'html'
            },
            {
                test: /\.css$/,
                exclude: helpers.root('src', 'app'),
                loader: ExtractTextPlugin.extract('style-loader', 'css-loader?sourceMap')
            },
            {
                test: /\.css$/,
                include: helpers.root('src', 'app'),
                loader: 'raw'
            },
            {
                test: /\.png$/,
                loader: 'url-loader?limit=100000'
            },
            {
                test: /\.(jpg|jpeg)$/,
                loader: 'file-loader'
            },
            {
                test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
                loader: 'url?limit=10000&mimetype=application/font-woff'
            },
            {
                test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
                loader: 'url?limit=10000&mimetype=application/octet-stream'
            },
            {
                test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
                loader: 'file'
            },
            {
                test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
                loader: 'url?limit=10000&mimetype=image/svg+xml'
            }
        ]
    },

    plugins: [
        // Seperates the application into chunks
        new webpack.optimize.CommonsChunkPlugin({
          name: ['polyfills', 'vendor', 'theme', 'app']
        }),
        // Pulls the css into seperate bundles
        new ExtractTextPlugin('[name].css'),
        // Injects the bundles into the index.html
        new HtmlWebpackPlugin({
          template: 'src/index.html'
        })
    ]
};
<commit_msg>remove helpers and renamed ts to js<commit_after>var path = require('path');
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    // No idea what this does.
    devtool: 'cheap-module-eval-source-map',
    // Seperates the application into chunks
    entry: {
        'polyfills': './src/polyfills.ts',
        'vendor': './src/vendor.ts',
        'theme': './src/theme.ts',
        'app': './src/main.ts',
    },
    output: {
        path: path.resolve('dist'), //Serves it out of distribution folder
        publicPath: '/',
        filename: '[name].js',
        chunkFilename: '[id].chunk.js'
    },

    // Determines the naming convention
    resolve: {
        extensions: ['', '.js', '.ts']
    },

    module: {
        loaders: [
            {
                test: /\.ts$/,
                loaders: ['awesome-typescript-loader', 'angular2-template-loader']
            },
            {
                test: /\.html$/,
                loader: 'html'
            },
            {
                test: /\.css$/,
                exclude: path.resolve('src', 'app'),
                loader: ExtractTextPlugin.extract('style-loader', 'css-loader?sourceMap')
            },
            {
                test: /\.css$/,
                include: path.resolve('src', 'app'),
                loader: 'raw'
            },
            {
                test: /\.png$/,
                loader: 'url-loader?limit=100000'
            },
            {
                test: /\.(jpg|jpeg)$/,
                loader: 'file-loader'
            },
            {
                test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
                loader: 'url?limit=10000&mimetype=application/font-woff'
            },
            {
                test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
                loader: 'url?limit=10000&mimetype=application/octet-stream'
            },
            {
                test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
                loader: 'file'
            },
            {
                test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
                loader: 'url?limit=10000&mimetype=image/svg+xml'
            }
        ]
    },

    plugins: [
        // Seperates the application into chunks
        new webpack.optimize.CommonsChunkPlugin({
          name: ['polyfills', 'vendor', 'theme', 'app']
        }),
        // Pulls the css into seperate bundles
        new ExtractTextPlugin('[name].css'),
        // Injects the bundles into the index.html
        new HtmlWebpackPlugin({
          template: 'src/index.html'
        })
    ]
};
<|endoftext|>


<commit_before>/* global m, document, Visibility */
'use strict';
var messages = messages || {};

/**
 * Messages component - model
 */
messages.Message = function(data) {
  this.id   = m.prop(data.id || null);
  this.type = m.prop(data.type || 'message');
  this.time = m.prop(data.time);
  this.user = m.prop(data.user || { name:'CHATBOT' });
  this.mess = m.prop(data.message);
  this.del  = m.prop(data.deleted || false);
};

/**
 * Messages component - storage model
 */
messages.MessagesList = function() {
  this.list = [];
  this.push = function(message) {
    if(Visibility.state() === 'hidden')
      document.title = 'Nouveau(x) message(s) !';
    this.list.push(message);
  };
  this.del = function(id) {
    var deferred = m.deferred();
    for(var i = 0; i < this.list.length; i++) {
      if(id == this.list[i].id()) {
        this.list.splice(i, 1);
        deferred.resolve();
      }
    }
    return deferred.promise;
  };
  this.messages = function() {
    return this.list;
  };
};<commit_msg>fix() : Nouveau message dans le titre si c'est un type message-bot<commit_after>/* global m, document, Visibility */
'use strict';
var messages = messages || {};

/**
 * Messages component - model
 */
messages.Message = function(data) {
  this.id   = m.prop(data.id || null);
  this.type = m.prop(data.type || 'message');
  this.time = m.prop(data.time);
  this.user = m.prop(data.user || { name:'CHATBOT' });
  this.mess = m.prop(data.message);
  this.del  = m.prop(data.deleted || false);
};

/**
 * Messages component - storage model
 */
messages.MessagesList = function() {
  this.list = [];
  this.push = function(message) {
    if(Visibility.state() === 'hidden' && message.type != 'message-bot')
      document.title = 'Nouveau(x) message(s) !';
    this.list.push(message);
  };
  this.del = function(id) {
    var deferred = m.deferred();
    for(var i = 0; i < this.list.length; i++) {
      if(id == this.list[i].id()) {
        this.list.splice(i, 1);
        deferred.resolve();
      }
    }
    return deferred.promise;
  };
  this.messages = function() {
    return this.list;
  };
};<|endoftext|>


<commit_before>import uuid from 'node-uuid';

import Mozilla from './uitour.js';

export const STORAGE_DURABILITY_KEY = '_storageDurability';

/**
 * Storage class that uses window.localStorage as it's backing store.
 */
export class LocalStorage {
  /**
   * @param    {string} prefix Prefix to append to all incoming keys.
   */
  constructor(prefix, { skipDurability = false }) {
    this.prefix = prefix;
    this.skipDurability = skipDurability;
  }

  async isDurable() {
    const durabilityStatus = localStorage.getItem(STORAGE_DURABILITY_KEY);
    return (parseInt(durabilityStatus, 10) >= 2);
  }

  _makeKey(key) {
    return `${this.prefix}-${key}`;
  }

  async getItem(key) {
    const storageIsDurable = await this.isDurable();
    if (!storageIsDurable && !this.skipDurability) {
      throw new Error('Storage durability unconfirmed');
    }
    const val = localStorage.getItem(this._makeKey(key));
    try {
      return JSON.parse(val);
    } catch (e) {
      return null;
    }
  }

  async setItem(key, value) {
    return localStorage.setItem(this._makeKey(key), JSON.stringify(value));
  }

  async removeItem(key) {
    return localStorage.removeItem(this._makeKey(key));
  }
}

/**
 * EventEmitter for Heartbeat events. Unlike normal event emitters,
 * Heartbeat events only occur once, and we guarantee that if a handler
 * is registered _after_ an event occurs, it will still be executed.
 */
export class HeartbeatEmitter {
  static EVENTS = [
    'NotificationOffered',
    'NotificationClosed',
    'LearnMore',
    'Voted',
    'TelemetrySent',
    'Engaged',
  ]

  constructor() {
    this.callbacks = {};
    this.eventData = {};
    for (const event of HeartbeatEmitter.EVENTS) {
      this.callbacks[event] = [];
      this.eventData[event] = null;
    }
  }

  on(eventName, callback) {
    if (HeartbeatEmitter.EVENTS.indexOf(eventName) === -1) {
      throw new Error(`${eventName} is an invalid Heartbeat event type.`);
    }

    this.callbacks[eventName].push(callback);

    // Call the callback if the event already happened.
    const data = this.eventData[eventName];
    if (data !== null) {
      callback(data);
    }
  }

  emit(eventName, data) {
    if (this.eventData[eventName] !== null) {
      throw new Error(`Cannot emit a Heartbeat event more than once: ${eventName}`);
    }

    this.eventData[eventName] = data;
    for (const callback of this.callbacks[eventName]) {
      callback(data);
    }
  }
}

/**
 * Implementation of the Normandy driver.
 */
export default class NormandyDriver {
  constructor(uitour = Mozilla.UITour) {
    this._uitour = uitour;
    this.setDurability();
  }

  setDurability() {
    let durability = parseInt(localStorage.getItem(STORAGE_DURABILITY_KEY), 10);
    if (isNaN(durability)) {
      durability = 0;
    }
    localStorage.setItem(STORAGE_DURABILITY_KEY, durability + 1);
  }

  _heartbeatCallbacks = {};
  registerCallbacks() {
    // Trigger heartbeat callbacks when the UITour tells us that Heartbeat
    // happened.
    this._uitour.observe((eventName, data) => {
      if (eventName.startsWith('Heartbeat:')) {
        const croppedEventName = eventName.slice(10); // Chop off "Heartbeat:"
        const callback = this._heartbeatCallbacks[data.flowId];
        if (callback !== undefined) {
          callback(croppedEventName, data);
        }
      }
    });
  }

  locale = document.documentElement.dataset.locale || navigator.language;

  _testingOverride = false;
  get testing() {
    return this._testingOverride || new URL(window.location.href).searchParams.has('testing');
  }
  set testing(value) {
    this._testingOverride = value;
  }

  _location = { countryCode: null };
  location() {
    return Promise.resolve(this._location);
  }

  log(message, level = 'debug') {
    if (level === 'debug' && !this.testing) {
      return;
    } else if (level === 'error') {
      console.error(message);
    } else {
      console.log(message);
    }
  }

  /**
   * Generate a fresh UUID
   * @return {String} Generated UUID
   */
  uuid() {
    return uuid.v4();
  }

  /**
   * Get a user id. If one doesn't exist yet, make one up and store it in local storage.
   * @return {String} A stored or generated UUID
   */
  get userId() {
    let userId = localStorage.getItem('userId');
    if (userId === null) {
      userId = uuid.v4();
      localStorage.setItem('userId', userId);
    }
    return userId;
  }

  createStorage(prefix) {
    return new LocalStorage(prefix, this.testing);
  }

  client() {
    return new Promise(resolve => {
      const client = {
        plugins: {},
      };

      // Populate plugin info.
      for (const plugin of navigator.plugins) {
        client.plugins[plugin.name] = {
          name: plugin.name,
          filename: plugin.filename,
          description: plugin.description,
          version: plugin.version,
        };
      }

      // Keys are UITour configs, functions are given the data
      // returned by UITour for that config.
      const wantedConfigs = {
        appinfo(data) {
          client.version = data.version;
          client.channel = data.defaultUpdateChannel;
          client.isDefaultBrowser = data.defaultBrowser;
          client.distribution = data.distribution;
        },
        selectedSearchEngine(data) {
          client.searchEngine = data.searchEngineIdentifier;
        },
        sync(data) {
          client.syncSetup = data.setup;
          client.syncDesktopDevices = data.desktopDevices || 0;
          client.syncMobileDevices = data.mobileDevices || 0;
          client.syncTotalDevices = data.totalDevices || 0;
        },
      };

      let retrievedConfigs = 0;
      const wantedConfigNames = Object.keys(wantedConfigs);
      wantedConfigNames.forEach(configName => {
        this._uitour.getConfiguration(configName, data => {
          wantedConfigs[configName](data);
          retrievedConfigs++;
          if (retrievedConfigs >= wantedConfigNames.length) {
            resolve(client);
          }
        });
      });
    });
  }

  saveHeartbeatFlow(data) {
    if (this.testing) {
      this.log('Pretending to send flow to Input');
      this.log(data);
      return Promise.resolve();
    }
    return fetch('https://input.mozilla.org/api/v2/hb/', {
      method: 'POST',
      body: JSON.stringify(data),
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
    });
  }

  showHeartbeat(options) {
    return new Promise(resolve => {
      const emitter = new HeartbeatEmitter();
      this._heartbeatCallbacks[options.flowId] = (eventName, data) => emitter.emit(eventName, data);

      // Positional arguments are overridden by the final options
      // argument, but they're still required so we pass them anyway.
      this._uitour.showHeartbeat(
        options.message,
        options.thanksMessage,
        options.flowId,
        options.postAnswerUrl,
        options.learnMoreMessage,
        options.learnMoreUrl,
        {
          engagementButtonLabel: options.engagementButtonLabel,
          surveyId: options.surveyId,
          surveyVersion: options.surveyVersion,
          testing: options.testing,
        }
      );

      resolve(emitter);
    });
  }
}
<commit_msg>selfrepair driver: Temporarily disable durability checking<commit_after>import uuid from 'node-uuid';

import Mozilla from './uitour.js';

export const STORAGE_DURABILITY_KEY = '_storageDurability';

/**
 * Storage class that uses window.localStorage as it's backing store.
 */
export class LocalStorage {
  /**
   * @param    {string} prefix Prefix to append to all incoming keys.
   */
  constructor(prefix, { skipDurability = false }) {
    this.prefix = prefix;
    this.skipDurability = skipDurability;
  }

  async isDurable() {
    const durabilityStatus = localStorage.getItem(STORAGE_DURABILITY_KEY);
    return (parseInt(durabilityStatus, 10) >= 2);
  }

  _makeKey(key) {
    return `${this.prefix}-${key}`;
  }

  async getItem(key) {
    const storageIsDurable = await this.isDurable();
    if (!storageIsDurable && !this.skipDurability) {
      throw new Error('Storage durability unconfirmed');
    }
    const val = localStorage.getItem(this._makeKey(key));
    try {
      return JSON.parse(val);
    } catch (e) {
      return null;
    }
  }

  async setItem(key, value) {
    return localStorage.setItem(this._makeKey(key), JSON.stringify(value));
  }

  async removeItem(key) {
    return localStorage.removeItem(this._makeKey(key));
  }
}

/**
 * EventEmitter for Heartbeat events. Unlike normal event emitters,
 * Heartbeat events only occur once, and we guarantee that if a handler
 * is registered _after_ an event occurs, it will still be executed.
 */
export class HeartbeatEmitter {
  static EVENTS = [
    'NotificationOffered',
    'NotificationClosed',
    'LearnMore',
    'Voted',
    'TelemetrySent',
    'Engaged',
  ]

  constructor() {
    this.callbacks = {};
    this.eventData = {};
    for (const event of HeartbeatEmitter.EVENTS) {
      this.callbacks[event] = [];
      this.eventData[event] = null;
    }
  }

  on(eventName, callback) {
    if (HeartbeatEmitter.EVENTS.indexOf(eventName) === -1) {
      throw new Error(`${eventName} is an invalid Heartbeat event type.`);
    }

    this.callbacks[eventName].push(callback);

    // Call the callback if the event already happened.
    const data = this.eventData[eventName];
    if (data !== null) {
      callback(data);
    }
  }

  emit(eventName, data) {
    if (this.eventData[eventName] !== null) {
      throw new Error(`Cannot emit a Heartbeat event more than once: ${eventName}`);
    }

    this.eventData[eventName] = data;
    for (const callback of this.callbacks[eventName]) {
      callback(data);
    }
  }
}

/**
 * Implementation of the Normandy driver.
 */
export default class NormandyDriver {
  constructor(uitour = Mozilla.UITour) {
    this._uitour = uitour;
    this.setDurability();
  }

  setDurability() {
    let durability = parseInt(localStorage.getItem(STORAGE_DURABILITY_KEY), 10);
    if (isNaN(durability)) {
      durability = 0;
    }
    localStorage.setItem(STORAGE_DURABILITY_KEY, durability + 1);
  }

  _heartbeatCallbacks = {};
  registerCallbacks() {
    // Trigger heartbeat callbacks when the UITour tells us that Heartbeat
    // happened.
    this._uitour.observe((eventName, data) => {
      if (eventName.startsWith('Heartbeat:')) {
        const croppedEventName = eventName.slice(10); // Chop off "Heartbeat:"
        const callback = this._heartbeatCallbacks[data.flowId];
        if (callback !== undefined) {
          callback(croppedEventName, data);
        }
      }
    });
  }

  locale = document.documentElement.dataset.locale || navigator.language;

  _testingOverride = false;
  get testing() {
    return this._testingOverride || new URL(window.location.href).searchParams.has('testing');
  }
  set testing(value) {
    this._testingOverride = value;
  }

  _location = { countryCode: null };
  location() {
    return Promise.resolve(this._location);
  }

  log(message, level = 'debug') {
    if (level === 'debug' && !this.testing) {
      return;
    } else if (level === 'error') {
      console.error(message);
    } else {
      console.log(message);
    }
  }

  /**
   * Generate a fresh UUID
   * @return {String} Generated UUID
   */
  uuid() {
    return uuid.v4();
  }

  /**
   * Get a user id. If one doesn't exist yet, make one up and store it in local storage.
   * @return {String} A stored or generated UUID
   */
  get userId() {
    let userId = localStorage.getItem('userId');
    if (userId === null) {
      userId = uuid.v4();
      localStorage.setItem('userId', userId);
    }
    return userId;
  }

  createStorage(prefix) {
    // TODO: Stop skipping durabilit checks after durability checking has
    // been deployed for 2+days to avoid interupting surveys.
    return new LocalStorage(prefix, { skipDurability: true });
  }

  client() {
    return new Promise(resolve => {
      const client = {
        plugins: {},
      };

      // Populate plugin info.
      for (const plugin of navigator.plugins) {
        client.plugins[plugin.name] = {
          name: plugin.name,
          filename: plugin.filename,
          description: plugin.description,
          version: plugin.version,
        };
      }

      // Keys are UITour configs, functions are given the data
      // returned by UITour for that config.
      const wantedConfigs = {
        appinfo(data) {
          client.version = data.version;
          client.channel = data.defaultUpdateChannel;
          client.isDefaultBrowser = data.defaultBrowser;
          client.distribution = data.distribution;
        },
        selectedSearchEngine(data) {
          client.searchEngine = data.searchEngineIdentifier;
        },
        sync(data) {
          client.syncSetup = data.setup;
          client.syncDesktopDevices = data.desktopDevices || 0;
          client.syncMobileDevices = data.mobileDevices || 0;
          client.syncTotalDevices = data.totalDevices || 0;
        },
      };

      let retrievedConfigs = 0;
      const wantedConfigNames = Object.keys(wantedConfigs);
      wantedConfigNames.forEach(configName => {
        this._uitour.getConfiguration(configName, data => {
          wantedConfigs[configName](data);
          retrievedConfigs++;
          if (retrievedConfigs >= wantedConfigNames.length) {
            resolve(client);
          }
        });
      });
    });
  }

  saveHeartbeatFlow(data) {
    if (this.testing) {
      this.log('Pretending to send flow to Input');
      this.log(data);
      return Promise.resolve();
    }
    return fetch('https://input.mozilla.org/api/v2/hb/', {
      method: 'POST',
      body: JSON.stringify(data),
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
    });
  }

  showHeartbeat(options) {
    return new Promise(resolve => {
      const emitter = new HeartbeatEmitter();
      this._heartbeatCallbacks[options.flowId] = (eventName, data) => emitter.emit(eventName, data);

      // Positional arguments are overridden by the final options
      // argument, but they're still required so we pass them anyway.
      this._uitour.showHeartbeat(
        options.message,
        options.thanksMessage,
        options.flowId,
        options.postAnswerUrl,
        options.learnMoreMessage,
        options.learnMoreUrl,
        {
          engagementButtonLabel: options.engagementButtonLabel,
          surveyId: options.surveyId,
          surveyVersion: options.surveyVersion,
          testing: options.testing,
        }
      );

      resolve(emitter);
    });
  }
}
<|endoftext|>


<commit_before>/*
 @author Zakai Hamilton
 @component KabHighlight
 */

screens.kab.highlight = function KabHighlight(me) {
    me.line = async function (session, line) {
        var hash = String(session.hash);
        var params = { hash, user: "$userId" };
        var highlight = await me.db.shared.highlight.find(params);
        let source = me.kab.text.clean(session.line);
        var classes = [];
        if (highlight) {
            classes.push("kab-term-highlight");
        }
        var styles = {
            "user-select": "none"
        };
        if (!line) {
            return "";
        }
        var value = line.match(/<p>(.*?)<\/p>/);
        if (value) {
            value = value[1];
        }
        else {
            value = line;
        }
        var html = me.ui.html.item({
            tag: "p",
            classes,
            styles,
            attributes: {
                ondblclick: "screens.kab.highlight.store(this)",
                hash,
                source
            },
            value
        });
        return html;
    };
    me.store = async function (element) {
        var user = "$userId";
        var name = "$userName";
        me.core.property.set(element, "ui.class.toggle", "kab-term-highlight");
        var highlight = me.core.property.get(element, "ui.class.kab-term-highlight");
        var hash = me.core.property.get(element, "ui.attribute.#hash");
        var source = me.core.property.get(element, "ui.attribute.#source");
        var data = await me.db.commentary.entry.find({
            "user": "$userId",
            "hash": hash
        });
        if (!data) {
            data = {};
        }
        data = Object.assign(data, {
            hash,
            user,
            name,
            source
        });
        if (highlight) {
            me.db.shared.highlight.use({
                "user": "$userId",
                "hash": hash
            }, data);
        }
        else {
            me.db.shared.highlight.remove({
                "user": "$userId"
            });
        }
    };
};
<commit_msg>bug fix<commit_after>/*
 @author Zakai Hamilton
 @component KabHighlight
 */

screens.kab.highlight = function KabHighlight(me) {
    me.line = async function (session, line) {
        var hash = String(session.hash);
        var params = { hash, user: "$userId" };
        var highlight = await me.db.shared.highlight.find(params);
        let source = me.kab.text.clean(session.line);
        var classes = [];
        if (highlight) {
            classes.push("kab-term-highlight");
        }
        var styles = {
            "user-select": "none"
        };
        if (!line) {
            return "";
        }
        var value = line.match(/<p>(.*?)<\/p>/);
        if (value) {
            value = value[1];
        }
        else {
            value = line;
        }
        var html = me.ui.html.item({
            tag: "p",
            classes,
            styles,
            attributes: {
                ondblclick: "screens.kab.highlight.store(this)",
                hash,
                source
            },
            value
        });
        return html;
    };
    me.store = async function (element) {
        var user = "$userId";
        var name = "$userName";
        me.core.property.set(element, "ui.class.toggle", "kab-term-highlight");
        var highlight = me.core.property.get(element, "ui.class.kab-term-highlight");
        var hash = me.core.property.get(element, "ui.attribute.#hash");
        var source = me.core.property.get(element, "ui.attribute.#source");
        var data = await me.db.shared.highlight.find({
            "user": "$userId",
            "hash": hash
        });
        if (!data) {
            data = {};
        }
        data = Object.assign(data, {
            hash,
            user,
            name,
            source
        });
        if (highlight) {
            me.db.shared.highlight.use({
                "user": "$userId",
                "hash": hash
            }, data);
        }
        else {
            me.db.shared.highlight.remove({
                "user": "$userId"
            });
        }
    };
};
<|endoftext|>