<commit_before>var util = require('./util');
var wrapper = util.wrapper;
var postJSON = util.postJSON;

/**
 * 获取用户基本信息
 * 详情请见：<http://mp.weixin.qq.com/wiki/index.php?title=获取用户基本信息>
 * Examples:
 * ```
 * api.getUser(openid, callback);
 * ```
 * Callback:
 *
 * - `err`, 调用失败时得到的异常
 * - `result`, 调用正常时得到的对象
 *
 * Result:
 * ```
 * {
 *  "subscribe": 1,
 *  "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M",
 *  "nickname": "Band",
 *  "sex": 1,
 *  "language": "zh_CN",
 *  "city": "广州",
 *  "province": "广东",
 *  "country": "中国",
 *  "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
 *  "subscribe_time": 1382694957
 * }
 * ```
 * @param {String} openid 用户的openid
 * @param {Function} callback 回调函数
 */
exports.getUser = function (openid, callback) {
  this.preRequest(this._getUser, arguments);
};

/*!
 * 获取用户基本信息的未封装版本
 */
exports._getUser = function (openid, callback) {
  // https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID
  var url = this.prefix + 'user/info?openid=' + openid + '&access_token=' + this.token.accessToken;
  this.request(url, {dataType: 'json'}, wrapper(callback));
};

/**
 * 获取关注者列表
 * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=获取关注者列表
 * Examples:
 * ```
 * api.getFollowers(callback);
 * // or
 * api.getFollowers(nextOpenid, callback);
 * ```
 * Callback:
 *
 * - `err`, 调用失败时得到的异常
 * - `result`, 调用正常时得到的对象
 *
 * Result:
 * ```
 * {
 *  "total":2,
 *  "count":2,
 *  "data":{
 *    "openid":["","OPENID1","OPENID2"]
 *  },
 *  "next_openid":"NEXT_OPENID"
 * }
 * ```
 * @param {String} nextOpenid 调用一次之后，传递回来的nextOpenid。第一次获取时可不填
 * @param {Function} callback 回调函数
 */
exports.getFollowers = function (nextOpenid, callback) {
  this.preRequest(this._getFollowers, arguments);
};

/*!
 * 获取关注者列表的未封装版本
 */
exports._getFollowers = function (nextOpenid, callback) {
  // https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID
  if (typeof nextOpenid === 'function') {
    callback = nextOpenid;
    nextOpenid = '';
  }
  var url = this.prefix + 'user/get?next_openid=' + nextOpenid + '&access_token=' + this.token.accessToken;
  this.request(url, {dataType: 'json'}, wrapper(callback));
};

/**
 * 设置用户备注名
 * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=设置用户备注名接口
 * Examples:
 * ```
 * api.updateRemark(openid, remark, callback);
 * ```
 * Callback:
 *
 * - `err`, 调用失败时得到的异常
 * - `result`, 调用正常时得到的对象
 *
 * Result:
 * ```
 * {
 *  "errcode":0,
 *  "errmsg":"ok"
 * }
 * ```
 * @param {String} openid 用户的openid
 * @param {String} remark 新的备注名，长度必须小于30字符
 * @param {Function} callback 回调函数
 */
exports.updateRemark = function (openid, remark, callback) {
  this.preRequest(this._updateRemark, arguments);
};

/*!
 * 设置用户备注名的未封装版本
 */
exports._updateRemark = function (openid, remark, callback) {
  // https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=ACCESS_TOKEN
  var url = this.prefix + 'user/info/updateremark?access_token=' + this.token.accessToken;
  var data = {
    openid: openid,
    remark: remark
  };
  this.request(url, postJSON(data), wrapper(callback));
};
<commit_msg>添加获取用户信息时候的语言设置<commit_after>var util = require('./util');
var wrapper = util.wrapper;
var postJSON = util.postJSON;

/**
 * 获取用户基本信息。可以设置lang，其中zh_CN 简体，zh_TW 繁体，en 英语。默认为en
 * 详情请见：<http://mp.weixin.qq.com/wiki/index.php?title=获取用户基本信息>
 * Examples:
 * ```
 * api.getUser(openid, callback);
 * api.getUser({openid: 'openid', lang: 'en'}, callback);
 * ```
 * Callback:
 *
 * - `err`, 调用失败时得到的异常
 * - `result`, 调用正常时得到的对象
 *
 * Result:
 * ```
 * {
 *  "subscribe": 1,
 *  "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M",
 *  "nickname": "Band",
 *  "sex": 1,
 *  "language": "zh_CN",
 *  "city": "广州",
 *  "province": "广东",
 *  "country": "中国",
 *  "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
 *  "subscribe_time": 1382694957
 * }
 * ```
 * @param {String|Object} options 用户的openid。或者配置选项，包含openid和lang两个属性。
 * @param {Function} callback 回调函数
 */
exports.getUser = function (options, callback) {
  this.preRequest(this._getUser, arguments);
};

/*!
 * 获取用户基本信息的未封装版本
 */
exports._getUser = function (options, callback) {
  if (typeof options !== 'object') {
    options = {
      openid: options,
      lang: 'en'
    };
  }
  // https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID
  var url = this.prefix + 'user/info?openid=' + options.openid + '&lang=' + options.lang + '&access_token=' + this.token.accessToken;
  this.request(url, {dataType: 'json'}, wrapper(callback));
};

/**
 * 获取关注者列表
 * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=获取关注者列表
 * Examples:
 * ```
 * api.getFollowers(callback);
 * // or
 * api.getFollowers(nextOpenid, callback);
 * ```
 * Callback:
 *
 * - `err`, 调用失败时得到的异常
 * - `result`, 调用正常时得到的对象
 *
 * Result:
 * ```
 * {
 *  "total":2,
 *  "count":2,
 *  "data":{
 *    "openid":["","OPENID1","OPENID2"]
 *  },
 *  "next_openid":"NEXT_OPENID"
 * }
 * ```
 * @param {String} nextOpenid 调用一次之后，传递回来的nextOpenid。第一次获取时可不填
 * @param {Function} callback 回调函数
 */
exports.getFollowers = function (nextOpenid, callback) {
  this.preRequest(this._getFollowers, arguments);
};

/*!
 * 获取关注者列表的未封装版本
 */
exports._getFollowers = function (nextOpenid, callback) {
  // https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID
  if (typeof nextOpenid === 'function') {
    callback = nextOpenid;
    nextOpenid = '';
  }
  var url = this.prefix + 'user/get?next_openid=' + nextOpenid + '&access_token=' + this.token.accessToken;
  this.request(url, {dataType: 'json'}, wrapper(callback));
};

/**
 * 设置用户备注名
 * 详细细节 http://mp.weixin.qq.com/wiki/index.php?title=设置用户备注名接口
 * Examples:
 * ```
 * api.updateRemark(openid, remark, callback);
 * ```
 * Callback:
 *
 * - `err`, 调用失败时得到的异常
 * - `result`, 调用正常时得到的对象
 *
 * Result:
 * ```
 * {
 *  "errcode":0,
 *  "errmsg":"ok"
 * }
 * ```
 * @param {String} openid 用户的openid
 * @param {String} remark 新的备注名，长度必须小于30字符
 * @param {Function} callback 回调函数
 */
exports.updateRemark = function (openid, remark, callback) {
  this.preRequest(this._updateRemark, arguments);
};

/*!
 * 设置用户备注名的未封装版本
 */
exports._updateRemark = function (openid, remark, callback) {
  // https://api.weixin.qq.com/cgi-bin/user/info/updateremark?access_token=ACCESS_TOKEN
  var url = this.prefix + 'user/info/updateremark?access_token=' + this.token.accessToken;
  var data = {
    openid: openid,
    remark: remark
  };
  this.request(url, postJSON(data), wrapper(callback));
};
<|endoftext|>


<commit_before>'use strict';

/*
 * @fileoverview Content system routing
 *
 */
const config = require('config');
const types = require('punchcard-content-types');
const multipart = require('connect-multiparty');
const uuid = require('uuid');
const _ = require('lodash');

const utils = require('../utils');
const database = require('../database');
const workflows = require('../workflows');
const schedule = require('../schedule');

const multipartMiddleware = multipart();

/*
 * Content Route Resolution
 *
 * @param {object} application - Express Application
 * @returns {object} - Configured Express Application
 */
const routes = application => {
  return new Promise(resolve => {
    const app = application;
    const references = app.get('references');

    /*
     * @name Content Home Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
    */
    app.get(`/${config.content.base}`, (req, res) => {
      res.render('content/home', {
        content: {
          home: config.content.home,
          base: config.content.base,
          types: req.content.types,
        },
      });
    });

    /*
     * @name Individual Content Type Landing Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type`, (req, res) => {
      return database
        .distinct(database.raw('ON (id) id'))
        .select('*')
        .from(`content-type--${req.content.type.id}`)
        .orderBy('id', 'DESC')
        .orderBy('revision', 'DESC').then(rws => {
          // add itentifier to each row
          const rows = utils.routes.identifier(rws, req.content.type);

          res.render('content/landing', {
            title: config.content.base,
            content: rows,
            type: req.content.type,
            config: config.content,
          });
        });
    });

    /*
     * @name Individual Content Type Add Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/${config.content.actions.add}`, (req, res, next) => {
      const errors = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].errors`, {});
      const values = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].content`, {});

      _.unset(req.session, 'form.content.add');

      const steps = _.cloneDeep(req.content.workflow).steps.reverse();
      const step = steps[steps.length - 1];

      return utils.fill(req.content.types, req.content.type, references, database).then(ct => {
        return types.only(req.params.type.toLowerCase(), values, [ct], config).then(merged => {
          return types.form(merged, errors, config).then(form => {
            res.render('content/add', {
              form,
              action: req.url.replace(config.content.actions.add, config.content.actions.save),
              type: ct,
              config: config.content,
              step,
            });
          });
        }).catch(e => {
          next(e);
        });
      });
    });

    /*
     * @name Individual Piece of Content
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/:id`, (req, res, next) => {
      return database
        .select('*')
        .from(`content-type--${req.content.type.id}`)
        .where('id', req.params.id)
        .orderBy('revision', 'DESC').then(rws => {
          if (rws.length < 1) {
            const err = {
              message: config.content.messages.missing.id.replace('%type', req.params.type).replace('%id', req.params.id),
              safe: `/${config.content.base}/${req.params.type}`,
              status: 404,
            };

            return next(err);
          }

          // add itentifier to each row
          const rows = utils.routes.identifier(rws, req.content.type);

          res.render('content/content', {
            title: config.content.messages.content.title.replace('%id', req.params.id),
            content: rows,
            type: req.content.type,
            workflow: _.cloneDeep(req.content.workflow).steps.reverse(),
            config: config.content,
          });

          return true;
        });
    });

    /*
     * @name Specific revision of an Individual Piece of Content
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/:id/:revision`, (req, res, next) => {
      return database
        .select('*')
        .from(`content-type--${req.content.type.id}`)
        .where('revision', req.params.revision)
        .orderBy('revision', 'DESC').then(rws => {
          if (rws.length < 1) {
            const err = {
              message: config.content.messages.missing.revision.replace('%revision', req.params.revision).replace('%type', req.params.type).replace('%id', req.params.id),
              safe: `/${config.content.base}/${req.params.type}`,
              status: 404,
            };

            return next(err);
          }

          // add itentifier to each row
          const rows = utils.routes.identifier(rws, req.content.type);

          res.render('content/content', {
            title: config.content.messages.revisions.title.replace('%revision', req.params.revision).replace('%id', req.params.id),
            content: rows,
            type: req.content.type,
            config: config.content,
          });

          return true;
        });
    });

    /*
     * @name Individual Content Type Edit Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/:id/:revision/${config.content.actions.edit}`, (req, res, next) => {
      const errors = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].errors`, {});
      const idSess = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].id`, {});
      const revisionSess = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].revision`, {});
      const values = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].content`, {});
      let data = {};

      const steps = _.cloneDeep(req.content.workflow).steps.reverse();

      // new revision, re-start workflow process
      const step = steps[steps.length - 1];

      _.unset(req.session, 'form.content.add');
      _.set(req.session, 'referrer', req.get('Referrer'));

      // something went wrong on save:
      if (Object.keys(values).length > 0) {
        // add the previous session data back in
        _.set(req.session, 'form.content.add', {
          [req.params.type.toLowerCase()]: {
            id: idSess,
            revision: revisionSess,
          },
        });

        return utils.fill(req.content.types, req.content.type, references, database).then(ct => {
          return types.only(req.params.type.toLowerCase(), values, [ct], config).then(merged => {
            return types.form(merged, errors, config).then(form => {
              res.render('content/add', {
                form,
                action: `/${config.content.base}/${req.params.type}/${config.content.actions.save}`,
                type: ct,
                step,
                config: config.content,
              });
            });
          }).catch(e => {
            next(e);
          });
        });
      }

      // eslint mad if no return, then mad at this else if it is there
      else { // eslint-disable-line no-else-return
        return utils.fill(req.content.types, req.content.type, references, database).then(ct => {
          // Search for the revision
          return database(`content-type--${ct.id}`).where({
            id: req.params.id,
            revision: req.params.revision,
          }).then(rows => {
            if (rows.length < 1) {
              const err = {
                message: config.content.messages.missing.revision.replace('%type', req.params.type).replace('%id', req.params.id).replace('%revision', req.params.revision),
                safe: `/${config.content.base}/${req.params.type}`,
                status: 404,
              };

              return next(err);
            }
            data = rows[0].value;

            // add session data for this content
            _.set(req.session, 'form.content.add', {
              [req.params.type.toLowerCase()]: {
                id: rows[0].id,
                revision: rows[0].revision,
              },
            });

            return types.only(ct.id, data, [ct], config);
          }).then(only => {
            return types.form(only, null, config);
          }).then(form => {
            res.render('content/add', {
              form,
              action: `/${config.content.base}/${req.params.type.toLowerCase()}/${config.content.actions.save}`,
              type: ct,
              data,
              step,
              config: config.content,
            });
          }).catch(e => {
            next(e);
          });
        });
      }
    });

    /*
     * @name Save - Post to Content Type
     * Save content type to db
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.post(`/${config.content.base}/:type/${config.content.actions.save}`, multipartMiddleware, (req, res, next) => {
      const referrer = _.get(req.session, 'referrer') || `/${config.content.base}/${req.params.type.toLowerCase()}`;
      let audits;
      let publishable = false;
      let check = 'publish';

      _.unset(req.session, 'referrer');

      if (req.body.submit === config.content.actions.new) {
        check = 'save';
      }

      // Validation
      const validated = types.form.validate(req.body, req.content.type, check);

      if (validated === true) {
        // Publishable
        if (check === 'publish') {
          publishable = true;
        }
        else if (types.form.validate(req.body, req.content.type, 'publish') === true) {
          publishable = true;
        }

        // id
        let id = uuid.v4();
        const sessionId = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].id`, '');
        if (sessionId) {
          id = sessionId;
        }

        // Sunrise/Sunset
        const sunrise = utils.time.iso(req.body['sunrise-date'], req.body['sunrise-time'], 'America/New_York');
        const sunset = utils.time.iso(req.body['sunset-date'], req.body['sunset-time'], 'America/New_York');

        // language
        let language = 'us-en';
        if (req.body.hasOwnProperty('language')) {
          language = req.body.language;
        }

        // approval step returns to first step after saving a new revision
        const steps = _.cloneDeep(req.content.workflow).steps.reverse();
        const approval = steps.length - 1;
        const request = _.clone(req, true);
        const rev = {
          approval,
        };
        const self = req.content.workflow.steps[approval].hasOwnProperty('self') && req.content.workflow.steps[approval].self === true;

        if (self) {
          rev.approval++;
        }

        if (check === 'publish') {
          if (self) {
            request.body['comment-textarea'] = 'Self published';
            request.body['action--select'] = 'self-published';
          }
          else {
            request.body['comment-textarea'] = 'content submitted';
            request.body['action--select'] = 'submit';
          }
        }

        audits = workflows.audits(rev, req.content.workflow, request);

        const data = utils.format(req.body);

        const insert = {
          id,
          language,
          sunrise,
          sunset,
          publishable,
          value: data,
          author: req.user.id,
        };

        return database(`content-type--${req.params.type.toLowerCase()}`).insert(_.merge({}, insert, audits)).returning('*').then(revision => {
          const latest = utils.routes.identifier(revision, req.content.type)[0];

          if (latest.publishable && latest.approval === 1) {
            return schedule(latest, req.content.type).then(() => {
              return latest;
            });
          }

          return latest;
        }).then((latest) => {
          _.set(req.session, 'form.content.recent', {
            [req.params.type.toLowerCase()]: {
              id: latest.id,
              revision: latest.revision,
            },
          });
          res.redirect(referrer);
        }).catch(e => {
          next(e);
        });
      }

      // eslint mad if no return, then mad at this else if it is there
      else { // eslint-disable-line no-else-return
        _.set(req.session, 'form.content.add', {
          [req.params.type.toLowerCase()]: {
            errors: validated,
            content: utils.format(req.body),
          },
        });
        res.redirect(req.get('Referrer'));
      }

      return true;
    });

    resolve(app);
  });
};

module.exports = routes;
<commit_msg>:bug: content route changes action<commit_after>'use strict';

/*
 * @fileoverview Content system routing
 *
 */
const config = require('config');
const types = require('punchcard-content-types');
const multipart = require('connect-multiparty');
const uuid = require('uuid');
const _ = require('lodash');

const utils = require('../utils');
const database = require('../database');
const workflows = require('../workflows');
const schedule = require('../schedule');

const multipartMiddleware = multipart();

/*
 * Content Route Resolution
 *
 * @param {object} application - Express Application
 * @returns {object} - Configured Express Application
 */
const routes = application => {
  return new Promise(resolve => {
    const app = application;
    const references = app.get('references');

    /*
     * @name Content Home Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
    */
    app.get(`/${config.content.base}`, (req, res) => {
      res.render('content/home', {
        content: {
          home: config.content.home,
          base: config.content.base,
          types: req.content.types,
        },
      });
    });

    /*
     * @name Individual Content Type Landing Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type`, (req, res) => {
      return database
        .distinct(database.raw('ON (id) id'))
        .select('*')
        .from(`content-type--${req.content.type.id}`)
        .orderBy('id', 'DESC')
        .orderBy('revision', 'DESC').then(rws => {
          // add itentifier to each row
          const rows = utils.routes.identifier(rws, req.content.type);

          res.render('content/landing', {
            title: config.content.base,
            content: rows,
            type: req.content.type,
            config: config.content,
          });
        });
    });

    /*
     * @name Individual Content Type Add Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/${config.content.actions.add}`, (req, res, next) => {
      const errors = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].errors`, {});
      const values = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].content`, {});

      _.unset(req.session, 'form.content.add');

      const steps = _.cloneDeep(req.content.workflow).steps.reverse();
      const step = steps[steps.length - 1];

      return utils.fill(req.content.types, req.content.type, references, database).then(ct => {
        return types.only(req.params.type.toLowerCase(), values, [ct], config).then(merged => {
          return types.form(merged, errors, config).then(form => {
            res.render('content/add', {
              form,
              action: `/${config.content.base}/${req.params.type.toLowerCase()}/${config.content.actions.save}`,
              type: ct,
              config: config.content,
              step,
            });
          });
        }).catch(e => {
          next(e);
        });
      });
    });

    /*
     * @name Individual Piece of Content
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/:id`, (req, res, next) => {
      return database
        .select('*')
        .from(`content-type--${req.content.type.id}`)
        .where('id', req.params.id)
        .orderBy('revision', 'DESC').then(rws => {
          if (rws.length < 1) {
            const err = {
              message: config.content.messages.missing.id.replace('%type', req.params.type).replace('%id', req.params.id),
              safe: `/${config.content.base}/${req.params.type}`,
              status: 404,
            };

            return next(err);
          }

          // add itentifier to each row
          const rows = utils.routes.identifier(rws, req.content.type);

          res.render('content/content', {
            title: config.content.messages.content.title.replace('%id', req.params.id),
            content: rows,
            type: req.content.type,
            workflow: _.cloneDeep(req.content.workflow).steps.reverse(),
            config: config.content,
          });

          return true;
        });
    });

    /*
     * @name Specific revision of an Individual Piece of Content
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/:id/:revision`, (req, res, next) => {
      return database
        .select('*')
        .from(`content-type--${req.content.type.id}`)
        .where('revision', req.params.revision)
        .orderBy('revision', 'DESC').then(rws => {
          if (rws.length < 1) {
            const err = {
              message: config.content.messages.missing.revision.replace('%revision', req.params.revision).replace('%type', req.params.type).replace('%id', req.params.id),
              safe: `/${config.content.base}/${req.params.type}`,
              status: 404,
            };

            return next(err);
          }

          // add itentifier to each row
          const rows = utils.routes.identifier(rws, req.content.type);

          res.render('content/content', {
            title: config.content.messages.revisions.title.replace('%revision', req.params.revision).replace('%id', req.params.id),
            content: rows,
            type: req.content.type,
            config: config.content,
          });

          return true;
        });
    });

    /*
     * @name Individual Content Type Edit Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/:id/:revision/${config.content.actions.edit}`, (req, res, next) => {
      const errors = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].errors`, {});
      const idSess = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].id`, {});
      const revisionSess = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].revision`, {});
      const values = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].content`, {});
      let data = {};

      const steps = _.cloneDeep(req.content.workflow).steps.reverse();

      // new revision, re-start workflow process
      const step = steps[steps.length - 1];

      _.unset(req.session, 'form.content.add');
      _.set(req.session, 'referrer', req.get('Referrer'));

      // something went wrong on save:
      if (Object.keys(values).length > 0) {
        // add the previous session data back in
        _.set(req.session, 'form.content.add', {
          [req.params.type.toLowerCase()]: {
            id: idSess,
            revision: revisionSess,
          },
        });

        return utils.fill(req.content.types, req.content.type, references, database).then(ct => {
          return types.only(req.params.type.toLowerCase(), values, [ct], config).then(merged => {
            return types.form(merged, errors, config).then(form => {
              res.render('content/add', {
                form,
                action: `/${config.content.base}/${req.params.type.toLowerCase()}/${config.content.actions.save}`,
                type: ct,
                step,
                config: config.content,
              });
            });
          }).catch(e => {
            next(e);
          });
        });
      }

      // eslint mad if no return, then mad at this else if it is there
      else { // eslint-disable-line no-else-return
        return utils.fill(req.content.types, req.content.type, references, database).then(ct => {
          // Search for the revision
          return database(`content-type--${ct.id}`).where({
            id: req.params.id,
            revision: req.params.revision,
          }).then(rows => {
            if (rows.length < 1) {
              const err = {
                message: config.content.messages.missing.revision.replace('%type', req.params.type).replace('%id', req.params.id).replace('%revision', req.params.revision),
                safe: `/${config.content.base}/${req.params.type}`,
                status: 404,
              };

              return next(err);
            }
            data = rows[0].value;

            // add session data for this content
            _.set(req.session, 'form.content.add', {
              [req.params.type.toLowerCase()]: {
                id: rows[0].id,
                revision: rows[0].revision,
              },
            });

            return types.only(ct.id, data, [ct], config);
          }).then(only => {
            return types.form(only, null, config);
          }).then(form => {
            res.render('content/add', {
              form,
              action: `/${config.content.base}/${req.params.type.toLowerCase()}/${config.content.actions.save}`,
              type: ct,
              data,
              step,
              config: config.content,
            });
          }).catch(e => {
            next(e);
          });
        });
      }
    });

    /*
     * @name Save - Post to Content Type
     * Save content type to db
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.post(`/${config.content.base}/:type/${config.content.actions.save}`, multipartMiddleware, (req, res, next) => {
      const referrer = _.get(req.session, 'referrer') || `/${config.content.base}/${req.params.type.toLowerCase()}`;
      let audits;
      let publishable = false;
      let check = 'publish';

      _.unset(req.session, 'referrer');

      if (req.body.submit === config.content.actions.new) {
        check = 'save';
      }

      // Validation
      const validated = types.form.validate(req.body, req.content.type, check);

      if (validated === true) {
        // Publishable
        if (check === 'publish') {
          publishable = true;
        }
        else if (types.form.validate(req.body, req.content.type, 'publish') === true) {
          publishable = true;
        }

        // id
        let id = uuid.v4();
        const sessionId = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].id`, '');
        if (sessionId) {
          id = sessionId;
        }

        // Sunrise/Sunset
        const sunrise = utils.time.iso(req.body['sunrise-date'], req.body['sunrise-time'], 'America/New_York');
        const sunset = utils.time.iso(req.body['sunset-date'], req.body['sunset-time'], 'America/New_York');

        // language
        let language = 'us-en';
        if (req.body.hasOwnProperty('language')) {
          language = req.body.language;
        }

        // approval step returns to first step after saving a new revision
        const steps = _.cloneDeep(req.content.workflow).steps.reverse();
        const approval = steps.length - 1;
        const request = _.clone(req, true);
        const rev = {
          approval,
        };
        const self = req.content.workflow.steps[approval].hasOwnProperty('self') && req.content.workflow.steps[approval].self === true;

        if (self) {
          rev.approval++;
        }

        if (check === 'publish') {
          if (self) {
            request.body['comment-textarea'] = 'Self published';
            request.body['action--select'] = 'self-published';
          }
          else {
            request.body['comment-textarea'] = 'content submitted';
            request.body['action--select'] = 'submit';
          }
        }

        audits = workflows.audits(rev, req.content.workflow, request);

        const data = utils.format(req.body);

        const insert = {
          id,
          language,
          sunrise,
          sunset,
          publishable,
          value: data,
          author: req.user.id,
        };

        return database(`content-type--${req.params.type.toLowerCase()}`).insert(_.merge({}, insert, audits)).returning('*').then(revision => {
          const latest = utils.routes.identifier(revision, req.content.type)[0];

          if (latest.publishable && latest.approval === 1) {
            return schedule(latest, req.content.type).then(() => {
              return latest;
            });
          }

          return latest;
        }).then((latest) => {
          _.set(req.session, 'form.content.recent', {
            [req.params.type.toLowerCase()]: {
              id: latest.id,
              revision: latest.revision,
            },
          });
          res.redirect(referrer);
        }).catch(e => {
          next(e);
        });
      }

      // eslint mad if no return, then mad at this else if it is there
      else { // eslint-disable-line no-else-return
        _.set(req.session, 'form.content.add', {
          [req.params.type.toLowerCase()]: {
            errors: validated,
            content: utils.format(req.body),
          },
        });
        res.redirect(req.get('Referrer'));
      }

      return true;
    });

    resolve(app);
  });
};

module.exports = routes;
<|endoftext|>


<commit_before>// Copyright 2014 Globo.com Player authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import {seekStringToSeconds} from 'base/utils'

import Playback from 'base/playback'
import template from 'base/template'
import Styler from 'base/styler'
import Browser from 'components/browser'
import Events from 'base/events'
import tagStyle from './public/style.scss'
import sourceHTML from './public/index.html'
import find from 'lodash.find'

const MIMETYPES = {
  'mp4': ["avc1.42E01E", "avc1.58A01E", "avc1.4D401E", "avc1.64001E", "mp4v.20.8", "mp4v.20.240", "mp4a.40.2"].map(
    (codec) => { return 'video/mp4; codecs="' + codec + ', mp4a.40.2"'}),
  'ogg': ['video/ogg; codecs="theora, vorbis"', 'video/ogg; codecs="dirac"', 'video/ogg; codecs="theora, speex"'],
  '3gpp': ['video/3gpp; codecs="mp4v.20.8, samr"'],
  'webm': ['video/webm; codecs="vp8, vorbis"'],
  'mkv': ['video/x-matroska; codecs="theora, vorbis"'],
  'm3u8': ['application/x-mpegurl']
}
MIMETYPES['ogv'] = MIMETYPES['ogg']
MIMETYPES['3gp'] = MIMETYPES['3gpp']

export default class HTML5Video extends Playback {
  get name() { return 'html5_video' }
  get tagName() { return 'video' }
  get template() { return template(sourceHTML) }

  get attributes() {
    return {
      'data-html5-video': ''
    }
  }

  get events() {
    return {
      'canplay': '_onCanPlay',
      'canplaythrough': '_handleBufferingEvents',
      'durationchange': '_onDurationChange',
      'ended': '_onEnded',
      'error': '_onError',
      'loadeddata': '_onLoadedData',
      'loadedmetadata': '_onLoadedMetadata',
      'pause': '_onPause',
      'playing': '_onPlaying',
      'progress': '_onProgress',
      'seeked': '_handleBufferingEvents',
      'seeking': '_handleBufferingEvents',
      'stalled': '_handleBufferingEvents',
      'timeupdate': '_onTimeUpdate',
      'waiting': '_onWaiting'
    }
  }

  /**
   * Determine if the playback has ended.
   * @property ended
   * @type Boolean
   */
  get ended() {
    return this.el.ended
  }

  /**
   * Determine if the playback is having to buffer in order for
   * playback to be smooth.
   * This is related to the PLAYBACK_BUFFERING and PLAYBACK_BUFFERFULL events
   * @property buffering
   * @type Boolean
   */
  get buffering() {
    return !!this._bufferingState
  }

  constructor(options) {
    super(options)
    this._loadStarted = false
    this._playheadMoving = false
    this._playheadMovingTimer = null
    this._stopped = false
    this._options = options
    this._setupSrc(options.src)
    this.el.loop = options.loop
    if (options.poster) {
      this.$el.attr("poster", options.poster)
    }
    this.el.autoplay = options.autoPlay
    if (Browser.isSafari) {
      this._setupSafari()
    } else {
      this.el.preload = options.preload ? options.preload: 'metadata'
    }
    // TODO should settings be private?
    this.settings = {default: ['seekbar']}
    this.settings.left = ["playpause", "position", "duration"]
    this.settings.right = ["fullscreen", "volume", "hd-indicator"]
  }

  /**
   * Sets the source url on the <video> element, and also the 'src' property.
   * @method setupSrc
   * @private
   * @param {String} srcUrl The source URL.
   */
  _setupSrc(srcUrl) {
    this._src = srcUrl
    this.el.src = srcUrl
  }

  _setupSafari() {
    this.el.preload = 'auto'
  }

  _onLoadedMetadata(e) {
    this._handleBufferingEvents()
    this.trigger(Events.PLAYBACK_LOADEDMETADATA, {duration: e.target.duration, data: e})
    this._updateSettings()
    var autoSeekFromUrl = typeof(this._options.autoSeekFromUrl) === "undefined" || this._options.autoSeekFromUrl
    if (this.getPlaybackType() !== Playback.LIVE && autoSeekFromUrl) {
      this._checkInitialSeek()
    }
  }

  _onDurationChange() {
    this._updateSettings()
    this._onTimeUpdate()
    // onProgress uses the duration
    this._onProgress()
  }

  _updateSettings() {
    // we can't figure out if hls resource is VoD or not until it is being loaded or duration has changed.
    // that's why we check it again and update media control accordingly.
    if (this.getPlaybackType() === Playback.VOD) {
      this.settings.left = ["playpause", "position", "duration"]
    } else {
      this.settings.left = ["playstop"]
    }
    this.settings.seekEnabled = this.isSeekEnabled()
    this.trigger(Events.PLAYBACK_SETTINGSUPDATE)
  }

  isSeekEnabled() {
    return isFinite(this.getDuration())
  }

  getPlaybackType() {
    return [0, undefined, Infinity].indexOf(this.el.duration) >= 0 ? Playback.LIVE : Playback.VOD
  }

  isHighDefinitionInUse() {
    return false
  }

  play() {
    this._stopped = false
    this._handleBufferingEvents()
    this.el.play()
  }

  pause() {
    this.el.pause()
  }

  stop() {
    this.pause()
    this._stopped = true
    this.el.currentTime = 0
    this._stopPlayheadMovingChecks()
    this._handleBufferingEvents()
    this.trigger(Events.PLAYBACK_STOP)
  }

  volume(value) {
    this.el.volume = value / 100
  }

  mute() {
    this.el.volume = 0
  }

  unmute() {
    this.el.volume = 1
  }

  isMuted() {
    return !!this.el.volume
  }

  isPlaying() {
    return !this.el.paused && !this.el.ended
  }

  get isReady() {
    return this._isReadyState
  }

  _startPlayheadMovingChecks() {
    if (this._playheadMovingTimer !== null) {
      return
    }
    this._playheadMovingTimeOnCheck = null
    this._determineIfPlayheadMoving()
    this._playheadMovingTimer = setInterval(this._determineIfPlayheadMoving.bind(this), 500)
  }

  _stopPlayheadMovingChecks() {
    if (this._playheadMovingTimer === null) {
      return
    }
    clearInterval(this._playheadMovingTimer)
    this._playheadMovingTimer = null
    this._playheadMoving = false
  }

  _determineIfPlayheadMoving() {
    var before = this._playheadMovingTimeOnCheck
    var now = this.el.currentTime
    this._playheadMoving = before !== now
    this._playheadMovingTimeOnCheck = now
    this._handleBufferingEvents()
  }

  // this seems to happen when the user is having to wait
  // for something to happen AFTER A USER INTERACTION
  // e.g the player might be buffering, but when `play()` is called
  // only at this point will this be called.
  // Or the user may seek somewhere but the new area requires buffering,
  // so it will fire then as well.
  // On devices where playing is blocked until requested with a user action,
  // buffering may start, but never finish until the user initiates a play,
  // but this only happens when play is actually requested
  _onWaiting() {
    this._loadStarted = true
    this._handleBufferingEvents()
  }

  // called after the first frame has loaded
  // note this doesn't fire on ios before the user has requested play
  // ideally the "loadstart" event would be used instead, but this fires
  // before a user has requested play on iOS, and also this is always fired
  // even if the preload setting is "none". In both these cases this causes
  // infinite buffering until the user does something which isn't great.
  _onLoadedData() {
    this._loadStarted = true
    this._handleBufferingEvents()
  }

  // note this doesn't fire on ios before user has requested play
  _onCanPlay() {
    this._handleBufferingEvents()
  }

  _onPlaying() {
    this._startPlayheadMovingChecks()
    this._handleBufferingEvents()
    this.trigger(Events.PLAYBACK_PLAY)
  }

  _onPause() {
    this._stopPlayheadMovingChecks()
    this._handleBufferingEvents()
    this.trigger(Events.PLAYBACK_PAUSE)
  }

  _onEnded() {
    this._handleBufferingEvents()
    this.trigger(Events.PLAYBACK_ENDED, this.name)
  }

  // The playback should be classed as buffering if the following are true:
  // - the ready state is less then HAVE_FUTURE_DATA or the playhead isn't moving and it should be
  // - the media hasn't "ended",
  // - the media hasn't been stopped
  // - loading has started
  _handleBufferingEvents() {
    var playheadShouldBeMoving = !this.el.ended && !this.el.paused
    var buffering = this._loadStarted && !this.el.ended && !this.stopped && ((playheadShouldBeMoving && !this._playheadMoving) || this.el.readyState < this.el.HAVE_FUTURE_DATA)
    if (this._bufferingState !== buffering) {
      this._bufferingState = buffering
      if (buffering) {
        this.trigger(Events.PLAYBACK_BUFFERING, this.name)
      }
      else {
        this.trigger(Events.PLAYBACK_BUFFERFULL, this.name)
      }
    }
  }

  _onError(event) {
    this.trigger(Events.PLAYBACK_ERROR, this.el.error, this.name)
  }

  destroy() {
    this.stop()
    this.el.src = ''
    this._src = null
    this.$el.remove()
  }

  seek(time) {
    this.el.currentTime = time
  }

  seekPercentage(percentage) {
    var time = this.el.duration * (percentage / 100)
    this.seek(time)
  }

  _checkInitialSeek() {
    var seekTime = seekStringToSeconds(window.location.href)
    if (seekTime !== 0) {
      this.seek(seekTime)
    }
  }

  getCurrentTime() {
    return this.el.currentTime
  }

  getDuration() {
    return this.el.duration
  }

  _onTimeUpdate() {
    this._handleBufferingEvents()
    if (this.getPlaybackType() === Playback.LIVE) {
      this.trigger(Events.PLAYBACK_TIMEUPDATE, {current: 1, total: 1}, this.name)
    } else {
      this.trigger(Events.PLAYBACK_TIMEUPDATE, {current: this.el.currentTime, total: this.el.duration}, this.name)
    }
  }

  _onProgress() {
    if (!this.el.buffered.length) {
      return
    }
    var bufferedPos = 0
    for (var i = 0;  i < this.el.buffered.length; i++) {
      if (this.el.currentTime >= this.el.buffered.start(i) && this.el.currentTime <= this.el.buffered.end(i)) {
        bufferedPos = i
        break
      }
    }
    this.trigger(Events.PLAYBACK_PROGRESS, {
      start: this.el.buffered.start(bufferedPos),
      current: this.el.buffered.end(bufferedPos),
      total: this.el.duration
    })
  }

  _typeFor(src) {
    var resourceParts = src.split('?')[0].match(/.*\.(.*)$/) || []
    var isHls = resourceParts.length > 1 && resourceParts[1] === "m3u8"
    return isHls ? 'application/vnd.apple.mpegurl' : 'video/mp4'
  }

  _ready() {
    if (this._isReadyState) {
      return
    }
    this._isReadyState = true
    this.trigger(Events.PLAYBACK_READY, this.name)
  }

  render() {
    var style = Styler.getStyleFor(tagStyle)

    this._src && this.$el.html(this.template({ src: this._src, type: this._typeFor(this._src) }))

    if (this._options.useVideoTagDefaultControls) {
      this.$el.attr('controls', 'controls')
    }

    if (this._options.disableVideoTagContextMenu) {
      this.$el.on("contextmenu", () => {
        return false
      })
    }

    this.$el.append(style)
    this._ready()
    return this
  }
}

HTML5Video._canPlay = function(type, mimeTypesByExtension, resourceUrl, mimeType) {
  var extension = (resourceUrl.split('?')[0].match(/.*\.(.*)$/) || [])[1]
  var mimeTypes = mimeType || mimeTypesByExtension[extension] || []
  mimeTypes = (mimeTypes.constructor === Array) ? mimeTypes : [mimeTypes]

  var media = document.createElement(type)
  return !!find(mimeTypes, (mediaType) => !!media.canPlayType(mediaType).replace(/no/, ''))
}

HTML5Video.canPlay = function(resourceUrl, mimeType) {
  return HTML5Video._canPlay('video', MIMETYPES, resourceUrl, mimeType)
}

module.exports = HTML5Video
<commit_msg>html5 video: fix InvalidStateError on IE11 when destroying playback<commit_after>// Copyright 2014 Globo.com Player authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import {seekStringToSeconds} from 'base/utils'

import Playback from 'base/playback'
import template from 'base/template'
import Styler from 'base/styler'
import Browser from 'components/browser'
import Events from 'base/events'
import tagStyle from './public/style.scss'
import sourceHTML from './public/index.html'
import find from 'lodash.find'

const MIMETYPES = {
  'mp4': ["avc1.42E01E", "avc1.58A01E", "avc1.4D401E", "avc1.64001E", "mp4v.20.8", "mp4v.20.240", "mp4a.40.2"].map(
    (codec) => { return 'video/mp4; codecs="' + codec + ', mp4a.40.2"'}),
  'ogg': ['video/ogg; codecs="theora, vorbis"', 'video/ogg; codecs="dirac"', 'video/ogg; codecs="theora, speex"'],
  '3gpp': ['video/3gpp; codecs="mp4v.20.8, samr"'],
  'webm': ['video/webm; codecs="vp8, vorbis"'],
  'mkv': ['video/x-matroska; codecs="theora, vorbis"'],
  'm3u8': ['application/x-mpegurl']
}
MIMETYPES['ogv'] = MIMETYPES['ogg']
MIMETYPES['3gp'] = MIMETYPES['3gpp']

export default class HTML5Video extends Playback {
  get name() { return 'html5_video' }
  get tagName() { return 'video' }
  get template() { return template(sourceHTML) }

  get attributes() {
    return {
      'data-html5-video': ''
    }
  }

  get events() {
    return {
      'canplay': '_onCanPlay',
      'canplaythrough': '_handleBufferingEvents',
      'durationchange': '_onDurationChange',
      'ended': '_onEnded',
      'error': '_onError',
      'loadeddata': '_onLoadedData',
      'loadedmetadata': '_onLoadedMetadata',
      'pause': '_onPause',
      'playing': '_onPlaying',
      'progress': '_onProgress',
      'seeked': '_handleBufferingEvents',
      'seeking': '_handleBufferingEvents',
      'stalled': '_handleBufferingEvents',
      'timeupdate': '_onTimeUpdate',
      'waiting': '_onWaiting'
    }
  }

  /**
   * Determine if the playback has ended.
   * @property ended
   * @type Boolean
   */
  get ended() {
    return this.el.ended
  }

  /**
   * Determine if the playback is having to buffer in order for
   * playback to be smooth.
   * This is related to the PLAYBACK_BUFFERING and PLAYBACK_BUFFERFULL events
   * @property buffering
   * @type Boolean
   */
  get buffering() {
    return !!this._bufferingState
  }

  constructor(options) {
    super(options)
    this._loadStarted = false
    this._playheadMoving = false
    this._playheadMovingTimer = null
    this._stopped = false
    this._options = options
    this._setupSrc(options.src)
    this.el.loop = options.loop
    if (options.poster) {
      this.$el.attr("poster", options.poster)
    }
    this.el.autoplay = options.autoPlay
    if (Browser.isSafari) {
      this._setupSafari()
    } else {
      this.el.preload = options.preload ? options.preload: 'metadata'
    }
    // TODO should settings be private?
    this.settings = {default: ['seekbar']}
    this.settings.left = ["playpause", "position", "duration"]
    this.settings.right = ["fullscreen", "volume", "hd-indicator"]
  }

  /**
   * Sets the source url on the <video> element, and also the 'src' property.
   * @method setupSrc
   * @private
   * @param {String} srcUrl The source URL.
   */
  _setupSrc(srcUrl) {
    this._src = srcUrl
    this.el.src = srcUrl
  }

  _setupSafari() {
    this.el.preload = 'auto'
  }

  _onLoadedMetadata(e) {
    this._handleBufferingEvents()
    this.trigger(Events.PLAYBACK_LOADEDMETADATA, {duration: e.target.duration, data: e})
    this._updateSettings()
    var autoSeekFromUrl = typeof(this._options.autoSeekFromUrl) === "undefined" || this._options.autoSeekFromUrl
    if (this.getPlaybackType() !== Playback.LIVE && autoSeekFromUrl) {
      this._checkInitialSeek()
    }
  }

  _onDurationChange() {
    this._updateSettings()
    this._onTimeUpdate()
    // onProgress uses the duration
    this._onProgress()
  }

  _updateSettings() {
    // we can't figure out if hls resource is VoD or not until it is being loaded or duration has changed.
    // that's why we check it again and update media control accordingly.
    if (this.getPlaybackType() === Playback.VOD) {
      this.settings.left = ["playpause", "position", "duration"]
    } else {
      this.settings.left = ["playstop"]
    }
    this.settings.seekEnabled = this.isSeekEnabled()
    this.trigger(Events.PLAYBACK_SETTINGSUPDATE)
  }

  isSeekEnabled() {
    return isFinite(this.getDuration())
  }

  getPlaybackType() {
    return [0, undefined, Infinity].indexOf(this.el.duration) >= 0 ? Playback.LIVE : Playback.VOD
  }

  isHighDefinitionInUse() {
    return false
  }

  play() {
    this._stopped = false
    this._handleBufferingEvents()
    this.el.play()
  }

  pause() {
    this.el.pause()
  }

  stop() {
    this.pause()
    this._stopped = true
    this.el.currentTime = 0
    this._stopPlayheadMovingChecks()
    this._handleBufferingEvents()
    this.trigger(Events.PLAYBACK_STOP)
  }

  volume(value) {
    this.el.volume = value / 100
  }

  mute() {
    this.el.volume = 0
  }

  unmute() {
    this.el.volume = 1
  }

  isMuted() {
    return !!this.el.volume
  }

  isPlaying() {
    return !this.el.paused && !this.el.ended
  }

  get isReady() {
    return this._isReadyState
  }

  _startPlayheadMovingChecks() {
    if (this._playheadMovingTimer !== null) {
      return
    }
    this._playheadMovingTimeOnCheck = null
    this._determineIfPlayheadMoving()
    this._playheadMovingTimer = setInterval(this._determineIfPlayheadMoving.bind(this), 500)
  }

  _stopPlayheadMovingChecks() {
    if (this._playheadMovingTimer === null) {
      return
    }
    clearInterval(this._playheadMovingTimer)
    this._playheadMovingTimer = null
    this._playheadMoving = false
  }

  _determineIfPlayheadMoving() {
    var before = this._playheadMovingTimeOnCheck
    var now = this.el.currentTime
    this._playheadMoving = before !== now
    this._playheadMovingTimeOnCheck = now
    this._handleBufferingEvents()
  }

  // this seems to happen when the user is having to wait
  // for something to happen AFTER A USER INTERACTION
  // e.g the player might be buffering, but when `play()` is called
  // only at this point will this be called.
  // Or the user may seek somewhere but the new area requires buffering,
  // so it will fire then as well.
  // On devices where playing is blocked until requested with a user action,
  // buffering may start, but never finish until the user initiates a play,
  // but this only happens when play is actually requested
  _onWaiting() {
    this._loadStarted = true
    this._handleBufferingEvents()
  }

  // called after the first frame has loaded
  // note this doesn't fire on ios before the user has requested play
  // ideally the "loadstart" event would be used instead, but this fires
  // before a user has requested play on iOS, and also this is always fired
  // even if the preload setting is "none". In both these cases this causes
  // infinite buffering until the user does something which isn't great.
  _onLoadedData() {
    this._loadStarted = true
    this._handleBufferingEvents()
  }

  // note this doesn't fire on ios before user has requested play
  _onCanPlay() {
    this._handleBufferingEvents()
  }

  _onPlaying() {
    this._startPlayheadMovingChecks()
    this._handleBufferingEvents()
    this.trigger(Events.PLAYBACK_PLAY)
  }

  _onPause() {
    this._stopPlayheadMovingChecks()
    this._handleBufferingEvents()
    this.trigger(Events.PLAYBACK_PAUSE)
  }

  _onEnded() {
    this._handleBufferingEvents()
    this.trigger(Events.PLAYBACK_ENDED, this.name)
  }

  // The playback should be classed as buffering if the following are true:
  // - the ready state is less then HAVE_FUTURE_DATA or the playhead isn't moving and it should be
  // - the media hasn't "ended",
  // - the media hasn't been stopped
  // - loading has started
  _handleBufferingEvents() {
    var playheadShouldBeMoving = !this.el.ended && !this.el.paused
    var buffering = this._loadStarted && !this.el.ended && !this.stopped && ((playheadShouldBeMoving && !this._playheadMoving) || this.el.readyState < this.el.HAVE_FUTURE_DATA)
    if (this._bufferingState !== buffering) {
      this._bufferingState = buffering
      if (buffering) {
        this.trigger(Events.PLAYBACK_BUFFERING, this.name)
      }
      else {
        this.trigger(Events.PLAYBACK_BUFFERFULL, this.name)
      }
    }
  }

  _onError(event) {
    this.trigger(Events.PLAYBACK_ERROR, this.el.error, this.name)
  }

  destroy() {
    this.$el.remove()
    this.el.src = ''
    this._src = null
  }

  seek(time) {
    this.el.currentTime = time
  }

  seekPercentage(percentage) {
    var time = this.el.duration * (percentage / 100)
    this.seek(time)
  }

  _checkInitialSeek() {
    var seekTime = seekStringToSeconds(window.location.href)
    if (seekTime !== 0) {
      this.seek(seekTime)
    }
  }

  getCurrentTime() {
    return this.el.currentTime
  }

  getDuration() {
    return this.el.duration
  }

  _onTimeUpdate() {
    this._handleBufferingEvents()
    if (this.getPlaybackType() === Playback.LIVE) {
      this.trigger(Events.PLAYBACK_TIMEUPDATE, {current: 1, total: 1}, this.name)
    } else {
      this.trigger(Events.PLAYBACK_TIMEUPDATE, {current: this.el.currentTime, total: this.el.duration}, this.name)
    }
  }

  _onProgress() {
    if (!this.el.buffered.length) {
      return
    }
    var bufferedPos = 0
    for (var i = 0;  i < this.el.buffered.length; i++) {
      if (this.el.currentTime >= this.el.buffered.start(i) && this.el.currentTime <= this.el.buffered.end(i)) {
        bufferedPos = i
        break
      }
    }
    this.trigger(Events.PLAYBACK_PROGRESS, {
      start: this.el.buffered.start(bufferedPos),
      current: this.el.buffered.end(bufferedPos),
      total: this.el.duration
    })
  }

  _typeFor(src) {
    var resourceParts = src.split('?')[0].match(/.*\.(.*)$/) || []
    var isHls = resourceParts.length > 1 && resourceParts[1] === "m3u8"
    return isHls ? 'application/vnd.apple.mpegurl' : 'video/mp4'
  }

  _ready() {
    if (this._isReadyState) {
      return
    }
    this._isReadyState = true
    this.trigger(Events.PLAYBACK_READY, this.name)
  }

  render() {
    var style = Styler.getStyleFor(tagStyle)

    this._src && this.$el.html(this.template({ src: this._src, type: this._typeFor(this._src) }))

    if (this._options.useVideoTagDefaultControls) {
      this.$el.attr('controls', 'controls')
    }

    if (this._options.disableVideoTagContextMenu) {
      this.$el.on("contextmenu", () => {
        return false
      })
    }

    this.$el.append(style)
    this._ready()
    return this
  }
}

HTML5Video._canPlay = function(type, mimeTypesByExtension, resourceUrl, mimeType) {
  var extension = (resourceUrl.split('?')[0].match(/.*\.(.*)$/) || [])[1]
  var mimeTypes = mimeType || mimeTypesByExtension[extension] || []
  mimeTypes = (mimeTypes.constructor === Array) ? mimeTypes : [mimeTypes]

  var media = document.createElement(type)
  return !!find(mimeTypes, (mediaType) => !!media.canPlayType(mediaType).replace(/no/, ''))
}

HTML5Video.canPlay = function(resourceUrl, mimeType) {
  return HTML5Video._canPlay('video', MIMETYPES, resourceUrl, mimeType)
}

module.exports = HTML5Video
<|endoftext|>


<commit_before>var memCache    = require('memory-cache');
var inflection  = require('inflection');
var _           = require('lodash');
var redis       = require('redis');

var t           = {
  seconds:      1000,
  minutes:      60000,
  hours:        3600000,
  days:         3600000 * 24,
  weeks:        3600000 * 24 * 7,
  months:       3600000 * 24 * 30,
  years:        3600000 * 24 * 365,
};

function cacheDriver() {
  var driver;
  var client;

  this.setDriver = function(cacheType) {
    driver = cacheType;

    if(driver === 'redis' && _.isEmpty(client)) {
      client = redis.createClient();
    }
  };

  this.get = function(key, callback) {
    if(driver === 'memcache') {
      var reply = memCache.get(key);
      return callback(null, reply);
    }

    client.get(key, function(err, reply) {
      callback(err, JSON.parse(reply));
    });
  };

  this.set = function(key, response, duration) {
    if(driver === 'memcache') {
      memCache.put(key, response, duration);
      return true;
    }

    return client.setex(key, Math.round(duration / 1000), JSON.stringify(response));
  };

  this.del = function(key) {
    if(driver === 'memcache') {
      memCache.del(key);
      return true;
    }

    client.del(key);
    return true;
  };

  this.clearAll = function() {
    if(driver === 'memcache') {
      memCache.clear();
      return true;
    }

    client.keys('*');
    return true;
  };

  return this;
}

function ApiCache() {
  var globalOptions = {
    debug:            false,
    defaultDuration:  3600000,
    enabled:          true,
    driver:           'memcache'
  };

  var index = null;
  var cacheSystem = cacheDriver();
  cacheSystem.setDriver(globalOptions.driver);

  this.clear = function(target) {
    var group = index.groups[target];

    if (group) {
      if (globalOptions.debug) {
        console.log('[api-cache]: clearing group: ', target);
      }

      _.each(group, function(key) {
        if (globalOptions.debug) {
          console.log('[api-cache]: clearing key: ', key);
        }
        cacheSystem.del(key);
        index.all = _.without(index.all, key);
      });

      delete index.groups[target];
    } else if (target) {
      if (globalOptions.debug) {
        console.log('[api-cache]: clearing key: ', target);
      }
      cacheSystem.del(target);
      index.all = _.without(index.all, target);
      _.each(index.groups, function(group, groupName) {
        index.groups[groupName] = _.without(group, target);
        if (!index.groups[groupName].length) {
          delete index.groups[groupName];
        }
      });
    } else {
      if (globalOptions.debug) {
        console.log('[api-cache]: clearing entire index');
      }
      cacheSystem.clearAll();
      this.resetIndex();
    }

    return this.getIndex();
  };

  this.getIndex = function(group) {
    if (group) {
      return index.groups[group];
    } else {
      return index;
    }
  };

  this.middleware = function cache(duration, middlewareToggle) {
    if (typeof duration === 'string') {
      var split = duration.match(/^(\d+)\s(\w+)$/);

      if (split.length === 3) {
        var len = split[1];
        var unit = inflection.pluralize(split[2]);

        duration = (len || 1) * (t[unit] || 0);
      }
    }

    if (typeof duration !== 'number' || duration === 0) {
      duration = globalOptions.defaultDuration ;
    }
  };

  this.middleware = function cache(duration, middlewareToggle) {

    if (typeof duration === 'string') {
      var split = duration.match(/^(\d+)\s(\w+)$/);

      if (split.length === 3) {
        var len = split[1];
        var unit = inflection.pluralize(split[2]);

        duration = (len || 1) * (t[unit] || 0);
      }
    }

    if (typeof duration !== 'number' || duration === 0) {
      duration = globalOptions.defaultDuration ;
    }
    return function cache(req, res, next) {

      var cached;

      if (!globalOptions.enabled || req.headers['x-apicache-bypass'] || (_.isFunction(middlewareToggle) && !middlewareToggle(req, res))) {
        if (globalOptions.debug && req.headers['x-apicache-bypass']) {
          console.log('[api-cache]: bypass detected, skipping cache.');
        }
        return next();
      }

      cacheSystem.get(req.url, function(err, reply) {

        if (cached = reply) {
          if (globalOptions.debug) {
            console.log('[api-cache]: returning cached version of "' + req.url + '"');
          }

          res.statusCode = cached.status;
          res.set(cached.headers);

          return res.send(cached.body);
        } else {

          if (globalOptions.debug) {
            console.log('[api-cache]: path "' + req.url + '" not found in cache');
          }

          res.realSend = res.send;

          res.send = function(a, b) {
            var responseObj = {
              headers: {
                'Content-Type': 'application/json; charset=utf-8'
              }
            };

            responseObj.status  = !_.isUndefined(b) ? a : (_.isNumber(a) ? a : res.statusCode);
            responseObj.body    = !_.isUndefined(b) ? b : (!_.isNumber(a) ? a : null);

            // last bypass attempt
            if (!cached && !req.headers['x-apicache-bypass']) {
              if (globalOptions.debug) {
                if (req.apicacheGroup) {
                  console.log('[api-cache]: group detected: ' + req.apicacheGroup);
                  index.groups[req.apicacheGroup] = index.groups[req.apicacheGroup] || [];
                  index.groups[req.apicacheGroup].push(req.url);
                }

                index.all.push(req.url);
                console.log('[api-cache]: adding cache entry for "' + req.url + '" @ ' + duration + ' milliseconds');
              }

              _.each(['Cache-Control', 'Expires'], function(h) {
                var header = res.get(h);
                if (!_.isUndefined(header)) {
                  responseObj.headers[h] = header;
                }
              });
              cacheSystem.set(req.url, responseObj, duration);
            }

            return res.realSend(responseObj.body);
          };
          next();
        }
      });

    };
  };

  this.options = function(options) {
    if (options) {
      _.extend(globalOptions, options);

      cacheSystem.setDriver(options.driver);

      return this;
    } else {
      return globalOptions;
    }
  };

  this.resetIndex = function() {
    index = {
      all:    [],
      groups: {}
    };
  };

  // initialize index
  this.resetIndex();

  return this;
}

module.exports = new ApiCache();
<commit_msg>Correcting flush db command<commit_after>var memCache    = require('memory-cache');
var inflection  = require('inflection');
var _           = require('lodash');
var redis       = require('redis');

var t           = {
  seconds:      1000,
  minutes:      60000,
  hours:        3600000,
  days:         3600000 * 24,
  weeks:        3600000 * 24 * 7,
  months:       3600000 * 24 * 30,
  years:        3600000 * 24 * 365,
};

function cacheDriver() {
  var driver;
  var client;

  this.setDriver = function(cacheType) {
    driver = cacheType;

    if(driver === 'redis' && _.isEmpty(client)) {
      client = redis.createClient();
    }
  };

  this.get = function(key, callback) {
    if(driver === 'memcache') {
      var reply = memCache.get(key);
      return callback(null, reply);
    }

    client.get(key, function(err, reply) {
      callback(err, JSON.parse(reply));
    });
  };

  this.set = function(key, response, duration) {
    if(driver === 'memcache') {
      memCache.put(key, response, duration);
      return true;
    }

    return client.setex(key, Math.round(duration / 1000), JSON.stringify(response));
  };

  this.del = function(key) {
    if(driver === 'memcache') {
      memCache.del(key);
      return true;
    }

    client.del(key);
    return true;
  };

  this.clearAll = function() {
    if(driver === 'memcache') {
      memCache.clear();
      return true;
    }

    client.flushdb();
    return true;
  };

  return this;
}

function ApiCache() {
  var globalOptions = {
    debug:            false,
    defaultDuration:  3600000,
    enabled:          true,
    driver:           'memcache'
  };

  var index = null;
  var cacheSystem = cacheDriver();
  cacheSystem.setDriver(globalOptions.driver);

  this.clear = function(target) {
    var group = index.groups[target];

    if (group) {
      if (globalOptions.debug) {
        console.log('[api-cache]: clearing group: ', target);
      }

      _.each(group, function(key) {
        if (globalOptions.debug) {
          console.log('[api-cache]: clearing key: ', key);
        }
        cacheSystem.del(key);
        index.all = _.without(index.all, key);
      });

      delete index.groups[target];
    } else if (target) {
      if (globalOptions.debug) {
        console.log('[api-cache]: clearing key: ', target);
      }
      cacheSystem.del(target);
      index.all = _.without(index.all, target);
      _.each(index.groups, function(group, groupName) {
        index.groups[groupName] = _.without(group, target);
        if (!index.groups[groupName].length) {
          delete index.groups[groupName];
        }
      });
    } else {
      if (globalOptions.debug) {
        console.log('[api-cache]: clearing entire index');
      }
      cacheSystem.clearAll();
      this.resetIndex();
    }

    return this.getIndex();
  };

  this.getIndex = function(group) {
    if (group) {
      return index.groups[group];
    } else {
      return index;
    }
  };

  this.middleware = function cache(duration, middlewareToggle) {
    if (typeof duration === 'string') {
      var split = duration.match(/^(\d+)\s(\w+)$/);

      if (split.length === 3) {
        var len = split[1];
        var unit = inflection.pluralize(split[2]);

        duration = (len || 1) * (t[unit] || 0);
      }
    }

    if (typeof duration !== 'number' || duration === 0) {
      duration = globalOptions.defaultDuration ;
    }
  };

  this.middleware = function cache(duration, middlewareToggle) {

    if (typeof duration === 'string') {
      var split = duration.match(/^(\d+)\s(\w+)$/);

      if (split.length === 3) {
        var len = split[1];
        var unit = inflection.pluralize(split[2]);

        duration = (len || 1) * (t[unit] || 0);
      }
    }

    if (typeof duration !== 'number' || duration === 0) {
      duration = globalOptions.defaultDuration ;
    }
    return function cache(req, res, next) {

      var cached;

      if (!globalOptions.enabled || req.headers['x-apicache-bypass'] || (_.isFunction(middlewareToggle) && !middlewareToggle(req, res))) {
        if (globalOptions.debug && req.headers['x-apicache-bypass']) {
          console.log('[api-cache]: bypass detected, skipping cache.');
        }
        return next();
      }

      cacheSystem.get(req.url, function(err, reply) {

        if (cached = reply) {
          if (globalOptions.debug) {
            console.log('[api-cache]: returning cached version of "' + req.url + '"');
          }

          res.statusCode = cached.status;
          res.set(cached.headers);

          return res.send(cached.body);
        } else {

          if (globalOptions.debug) {
            console.log('[api-cache]: path "' + req.url + '" not found in cache');
          }

          res.realSend = res.send;

          res.send = function(a, b) {
            var responseObj = {
              headers: {
                'Content-Type': 'application/json; charset=utf-8'
              }
            };

            responseObj.status  = !_.isUndefined(b) ? a : (_.isNumber(a) ? a : res.statusCode);
            responseObj.body    = !_.isUndefined(b) ? b : (!_.isNumber(a) ? a : null);

            // last bypass attempt
            if (!cached && !req.headers['x-apicache-bypass']) {
              if (globalOptions.debug) {
                if (req.apicacheGroup) {
                  console.log('[api-cache]: group detected: ' + req.apicacheGroup);
                  index.groups[req.apicacheGroup] = index.groups[req.apicacheGroup] || [];
                  index.groups[req.apicacheGroup].push(req.url);
                }

                index.all.push(req.url);
                console.log('[api-cache]: adding cache entry for "' + req.url + '" @ ' + duration + ' milliseconds');
              }

              _.each(['Cache-Control', 'Expires'], function(h) {
                var header = res.get(h);
                if (!_.isUndefined(header)) {
                  responseObj.headers[h] = header;
                }
              });
              cacheSystem.set(req.url, responseObj, duration);
            }

            return res.realSend(responseObj.body);
          };
          next();
        }
      });

    };
  };

  this.options = function(options) {
    if (options) {
      _.extend(globalOptions, options);

      cacheSystem.setDriver(options.driver);

      return this;
    } else {
      return globalOptions;
    }
  };

  this.resetIndex = function() {
    index = {
      all:    [],
      groups: {}
    };
  };

  // initialize index
  this.resetIndex();

  return this;
}

module.exports = new ApiCache();
<|endoftext|>


<commit_before>// Copyright 2020 The Google Research Authors.
//
// 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 Prediction by Partial Matching (PPM) language model.
 *
 * The original PPM algorithm is described in [1]. This particular
 * implementation has been inspired by the PPM model used by Dasher, an
 * Augmentative and alternative communication (AAC) input method developed by
 * the Inference Group at University of Cambridge. The overview of the system
 * is provided in [2]. The details of this algorithm, which is different from
 * the standard PPM, are outlined in general terms in [3]. Please also see [4]
 * for an excellent overview of various PPM variants.
 *
 * References:
 * -----------
 *   [1] Cleary, John G. and Witten, Ian H. (1984): “Data Compression Using
 *       Adaptive Coding and Partial String Matching”, IEEE Transactions on
 *       Communications, vol. 32, no. 4, pp. 396–402.
 *   [2] Ward, David J. and Blackwell, Alan F. and MacKay, David J. C. (2000):
 *       “Dasher - A Data Entry Interface Using Continuous Gestures and
 *       Language Models”, UIST'00 Proceedings of the 13th annual ACM symposium
 *       on User interface software and technology, pp. 129–137, November, San
 *       Diego, USA.
 *   [3] Cowans, Phil (2005): “Language Modelling In Dasher -- A Tutorial”,
 *       June, Inference Lab, Cambridge University (presentation).
 *   [4] Jin Hu Huang and David Powers (2004): "Adaptive Compression-based
 *       Approach for Chinese Pinyin Input." Proceedings of the Third SIGHAN
 *       Workshop on Chinese Language Processing, pp. 24--27.
 */

const assert = require("assert");

const vocab = require("./vocabulary");

/**
 * Kneser-Ney "-like" smoothing parameters.
 *
 * These hardcoded values are copied from Dasher. Please see the documentation
 * for PPMLanguageModel.getProbs() below for more information.
 */
const knAlpha = 0.49;
const knBeta = 0.77;

/* Epsilon for sanity checks. */
const epsilon = 1E-10;

/**
 * Node in a search tree, which is implemented as a suffix trie that represents
 * every suffix of a sequence used during its construction. Please see
 *   [1] Esko Ukknonen (1995): "On-line construction of suffix trees",
 *       Algorithmica, volume 14, pp. 249--260, Springer, 1995.
 *   [2] Kennington, C. (2011): "Application of Suffix Trees as an
 *       Implementation Technique for Varied-Length N-gram Language Models",
 *       MSc. Thesis, Saarland University.
 *
 * @final
 */
class Node {
  constructor() {
    // Leftmost child node for the current node.
    this.child_ = null;
    // Next node.
    this.next_ = null;
    // Node in the "vine" structure, also known as "suffix link" in Ukkonen's
    // algorithm ([1] above). The vine for the given node points at the node
    // representing the shorter context. For example, if the current node in the
    // trie represents string "AA" (corresponding to the branch "[R] -> [A] ->
    // [*A*]" in the trie, where [R] stands for root), then its vine points at
    // the node "A" (represented by "[R] -> [*A*]"). In this case both nodes are
    // in the same branch but they don't need to be. For example, for the node
    // "B" in the trie path for the string "AB" ("[R] -> [A] -> [*B*]") the vine
    // points at the child node of a different path "[R] -> [*B*]".
    this.vine_ = null;
    // Frequency count for this node. Number of times the suffix symbol stored
    // in this node was observed.
    this.count_ = 1;
    // Symbol that this node stores.
    this.symbol_ = vocab.rootSymbol;
  }

  /**
   * Finds child of the current node with a specified symbol.
   * @param {number} symbol Integer symbol.
   * @return {?Node} Node with the symbol.
   * @final
   */
  findChildWithSymbol(symbol) {
    let current = this.child_;
    while (current != null) {
      if (current.symbol_ == symbol) {
        return current;
      }
      current = current.next_;
    }
    return current;
  }

  /**
   * Total number of observations for all the children of this node. This
   * counts all the events observed in this context.
   * @return {number} Total number of observations under this node.
   * @final
   */
  totalChildrenCounts() {
    let childNode = this.child_;
    let count = 0;
    while (childNode != null) {
      count += childNode.count_;
      childNode = childNode.next_;
    }
    return count;
  }
}

/**
 * Handle encapsulating the search context.
 * @final
 */
class Context {
  /**
   * Constructor.
   * @param {?Node} head Head node of the context.
   * @param {number} order Length of the context.
   */
  constructor(head, order) {
    // Current node.
    this.head_ = head;
    // The order corresponding to length of the context.
    this.order_ = order;
  }
}

/**
 * Prediction by Partial Matching (PPM) Language Model.
 * @final
 */
class PPMLanguageModel {
  /**
   * @param {?Vocabulary} vocab Symbol vocabulary object.
   * @param {number} maxOrder Maximum length of the context.
   */
  constructor(vocab, maxOrder) {
    this.vocab_ = vocab;
    assert(this.vocab_.size() > 1,
           "Expecting at least two symbols in the vocabulary");

    this.maxOrder_ = maxOrder;
    this.root_ = new Node();
    this.rootContext_ = new Context();
    this.rootContext_.head_ = this.root_;
    this.rootContext_.order_ = 0;
    this.numNodes_ = 1;
  }

  /**
   * Adds symbol to the supplied node.
   * @param {?Node} node Tree node which to grow.
   * @param {number} symbol Symbol.
   * @return {?Node} Node with the symbol.
   * @final @private
   */
  addSymbolToNode_(node, symbol) {
    let symbolNode = node.findChildWithSymbol(symbol);
    if (symbolNode != null) {
      // Update the counts for the given node and also for all the vine nodes
      // representing shorter contexts.
      symbolNode.count_++;
      let vineNode = symbolNode.vine_;
      assert(vineNode != null, "Expected valid vine node!");
      while (vineNode != null) {
        assert(vineNode == this.root_ || vineNode.symbol_ == symbol,
               "Expected vine node to be root or to contain " + symbol +
               ". Found " + vineNode.symbol_ + " instead");
        vineNode.count_++;
        vineNode = vineNode.vine_;
      }
    } else {
      // Symbol does not exist under the given node. Create a new child node
      // and update the vine structure for lower contexts.
      symbolNode = new Node();
      symbolNode.symbol_ = symbol;
      symbolNode.next_ = node.child_;
      node.child_ = symbolNode;
      this.numNodes_++;
      if (node == this.root_) {
        // Shortest possible context.
        symbolNode.vine_ = this.root_;
      } else {
        assert(node.vine_ != null, "Expected valid vine node");
        symbolNode.vine_ = this.addSymbolToNode_(node.vine_, symbol);
      }
    }
    return symbolNode;
  }

  /**
   * Creates new context which is initially empty.
   * @return {?Context} Context object.
   * @final
   */
  createContext() {
    return new Context(this.rootContext_.head_, this.rootContext_.order_);
  }

  /**
   * Clones existing context.
   * @param {?Context} context Existing context object.
   * @return {?Context} Cloned context object.
   * @final
   */
  cloneContext(context) {
    return new Context(context.head_, context.order_);
  }

  /**
   * Adds symbol to the supplied context. Does not update the model.
   * @param {?Context} context Context object.
   * @param {number} symbol Integer symbol.
   * @final
   */
  addSymbolToContext(context, symbol) {
    if (symbol <= vocab.rootSymbol) {  // Only add valid symbols.
      return;
    }
    assert(symbol < this.vocab_.size(), "Invalid symbol: " + symbol);
    while (context.head_ != null) {
      if (context.order_ < this.maxOrder_) {
        // Extend the current context.
        let childNode = context.head_.findChildWithSymbol(symbol);
        if (childNode != null) {
          context.head_ = childNode;
          context.order_++;
          return;
        }
      }
      // Try to extend the shorter context.
      context.order_--;
      context.head_ = context.head_.vine_;
    }
    if (context.head_ == null) {
      context.head_ = this.root_;
      context.order_ = 0;
    }
  }

  /**
   * Adds symbol to the supplied context and updates the model.
   * @param {?Context} context Context object.
   * @param {number} symbol Integer symbol.
   * @final
   */
  addSymbolAndUpdate(context, symbol) {
    if (symbol <= vocab.rootSymbol) {  // Only add valid symbols.
      return;
    }
    assert(symbol < this.vocab_.size(), "Invalid symbol: " + symbol);
    let symbolNode = this.addSymbolToNode_(context.head_, symbol);
    assert(symbolNode == context.head_.findChildWithSymbol(symbol));
    context.head_ = symbolNode;
    context.order_++;
    while (context.order_ > this.maxOrder_) {
      context.head_ = context.head_.vine_;
      context.order_--;
    }
  }

  /**
   * Returns probabilities for all the symbols in the vocabulary given the
   * context.
   *
   * Notation:
   * ---------
   *         $x_h$ : Context representing history, $x_{h-1}$ shorter context.
   *   $n(w, x_h)$ : Count of symbol $w$ in context $x_h$.
   *      $T(x_h)$ : Total count in context $x_h$.
   *      $q(x_h)$ : Number of symbols with non-zero counts seen in context
   *                 $x_h$, i.e. |{w' : c(x_h, w') > 0}|. Alternatively, this
   *                 represents the number of distinct extensions of history
   *                 $x_h$ in the training data.
   *
   * Standard Kneser-Ney method (aka Absolute Discounting):
   * ------------------------------------------------------
   * Subtracting \beta (in [0, 1)) from all counts.
   *   P_{kn}(w | x_h) = \over{\max(n(w, x_h) - \beta, 0)}{T(x_h)} +
   *                     \beta * \over{q(x_h)}{T(x_h)} * P_{kn}(w | x_{h-1}),
   * where the second term in summation represents escaping to lower-order
   * context.
   *
   * See: Ney, Reinhard and Kneser, Hermann (1995): “Improved backing-off for
   * M-gram language modeling”, Proc. of Acoustics, Speech, and Signal
   * Processing (ICASSP), May, pp. 181–184.
   *
   * Modified Kneser-Ney method (Dasher version [3]):
   * ------------------------------------------------
   * Introducing \alpha parameter (in [0, 1)) and estimating as
   *   P_{kn}(w | x_h) = \over{\max(n(w, x_h) - \beta, 0)}{T(x_h) + \alpha} +
   *                     \over{\alpha + \beta * q(x_h)}{T(x_h) + \alpha} *
   *                     P_{kn}(w | x_{h-1}) .
   *
   * Additional details on the above version are provided in Section 3 of:
   *   Steinruecken, Christian and Ghahramani, Zoubin and MacKay, David (2016):
   *   "Improving PPM with dynamic parameter updates", In Proc. Data
   *   Compression Conference (DCC-2015), pp. 193--202, April, Snowbird, UT,
   *   USA. IEEE.
   *
   * @param {?Context} context Context symbols.
   * @return {?array} Array of floating point probabilities.
   * @final
   */
  getProbs(context) {
    // Initialize the initial estimates. Note, we don't use uniform
    // distribution here.
    let numSymbols = this.vocab_.size();
    let probs = new Array(numSymbols);
    for (let i = 0; i < numSymbols; ++i) {
      probs[i] = 0.0;
    }

    // Estimate the probabilities for all the symbols in the supplied context.
    // This runs over all the symbols in the context and over all the suffixes
    // (orders) of the context.
    let totalMass = 1.0;
    let node = context.head_;
    let gamma = totalMass;
    while (node != null) {
      const count = node.totalChildrenCounts();
      if (count > 0) {
        let childNode = node.child_;
        while (childNode != null) {
          let p = gamma * (childNode.count_ - knBeta) / (count + knAlpha);
          probs[childNode.symbol_] += p;
          totalMass -= p;
          childNode = childNode.next_;
        }
      }
      // Backoff to lower-order context. The $\gamma$ factor represents the
      // total probability mass after processing the current $n$-th order before
      // backing off to $n-1$-th order. It roughly corresponds to the usual
      // interpolation parameter, as used in the literature, e.g. in
      //   Stanley F. Chen and Joshua Goodman (1999): "An empirical study of
      //   smoothing techniques for language modeling", Computer Speech and
      //   Language, vol. 13, pp. 359-–394.
      node = node.vine_;
      gamma = totalMass;
    }
    assert(totalMass >= 0.0,
           "Invalid remaining probability mass: " + totalMass);

    // Adjust the probability mass for all the symbols.
    let numValidSymbols = numSymbols - 1;
    let remainingMass = totalMass;
    for (let i = 1; i < numSymbols; ++i) {
      // Following is estimated according to a uniform distribution
      // corresponding to the context length of zero.
      let p = remainingMass / numValidSymbols;
      probs[i] += p;
      totalMass -= p;
    }
    let leftSymbols = numValidSymbols;
    let newProbMass = 0.0;
    for (let i = 1; i < numSymbols; ++i) {
      let p = totalMass / leftSymbols;
      probs[i] += p;
      totalMass -= p;
      newProbMass += probs[i];
      --leftSymbols;
    }
    assert(totalMass == 0.0, "Expected remaining probability mass to be zero!");
    assert(Math.abs(1.0 - newProbMass) < epsilon);
    return probs;
  }

  /**
   * Prints the trie to console.
   * @param {?Node} node Current trie node.
   * @param {string} indent Indentation prefix.
   * @final @private
   */
  printToConsole_(node, indent) {
    console.log(indent + "  " + this.vocab_.symbols_[node.symbol_] +
                "(" + node.symbol_ + ") [" + node.count_ + "]");
    indent += "  ";
    let child = node.child_;
    while (child != null) {
      this.printToConsole_(child, indent);
      child = child.next_;
    }
  }

  /**
   * Prints the trie to console.
   * @final
   */
  printToConsole() {
    this.printToConsole_(this.root_, "");
  }
}

/**
 * Exported APIs.
 */
exports.PPMLanguageModel = PPMLanguageModel;
<commit_msg>Cosmetic change: Better naming for the suffix links.<commit_after>// Copyright 2020 The Google Research Authors.
//
// 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 Prediction by Partial Matching (PPM) language model.
 *
 * The original PPM algorithm is described in [1]. This particular
 * implementation has been inspired by the PPM model used by Dasher, an
 * Augmentative and alternative communication (AAC) input method developed by
 * the Inference Group at University of Cambridge. The overview of the system
 * is provided in [2]. The details of this algorithm, which is different from
 * the standard PPM, are outlined in general terms in [3]. Please also see [4]
 * for an excellent overview of various PPM variants.
 *
 * References:
 * -----------
 *   [1] Cleary, John G. and Witten, Ian H. (1984): “Data Compression Using
 *       Adaptive Coding and Partial String Matching”, IEEE Transactions on
 *       Communications, vol. 32, no. 4, pp. 396–402.
 *   [2] Ward, David J. and Blackwell, Alan F. and MacKay, David J. C. (2000):
 *       “Dasher - A Data Entry Interface Using Continuous Gestures and
 *       Language Models”, UIST'00 Proceedings of the 13th annual ACM symposium
 *       on User interface software and technology, pp. 129–137, November, San
 *       Diego, USA.
 *   [3] Cowans, Phil (2005): “Language Modelling In Dasher -- A Tutorial”,
 *       June, Inference Lab, Cambridge University (presentation).
 *   [4] Jin Hu Huang and David Powers (2004): "Adaptive Compression-based
 *       Approach for Chinese Pinyin Input." Proceedings of the Third SIGHAN
 *       Workshop on Chinese Language Processing, pp. 24--27.
 */

const assert = require("assert");

const vocab = require("./vocabulary");

/**
 * Kneser-Ney "-like" smoothing parameters.
 *
 * These hardcoded values are copied from Dasher. Please see the documentation
 * for PPMLanguageModel.getProbs() below for more information.
 */
const knAlpha = 0.49;
const knBeta = 0.77;

/* Epsilon for sanity checks. */
const epsilon = 1E-10;

/**
 * Node in a search tree, which is implemented as a suffix trie that represents
 * every suffix of a sequence used during its construction. Please see
 *   [1] Esko Ukknonen (1995): "On-line construction of suffix trees",
 *       Algorithmica, volume 14, pp. 249--260, Springer, 1995.
 *   [2] Kennington, C. (2011): "Application of Suffix Trees as an
 *       Implementation Technique for Varied-Length N-gram Language Models",
 *       MSc. Thesis, Saarland University.
 *
 * @final
 */
class Node {
  constructor() {
    // Leftmost child node for the current node.
    this.child_ = null;
    // Next node.
    this.next_ = null;
    // Node in the backoff structure, also known as "vine" structure and "suffix
    // link" in Ukkonen's algorithm ([1] above). The backoff for the given node
    // points at the node representing the shorter context. For example, if the
    // current node in the trie represents string "AA" (corresponding to the
    // branch "[R] -> [A] -> [*A*]" in the trie, where [R] stands for root),
    // then its backoff points at the node "A" (represented by "[R] ->
    // [*A*]"). In this case both nodes are in the same branch but they don't
    // need to be. For example, for the node "B" in the trie path for the string
    // "AB" ("[R] -> [A] -> [*B*]") the backoff points at the child node of a
    // different path "[R] -> [*B*]".
    this.backoff_ = null;
    // Frequency count for this node. Number of times the suffix symbol stored
    // in this node was observed.
    this.count_ = 1;
    // Symbol that this node stores.
    this.symbol_ = vocab.rootSymbol;
  }

  /**
   * Finds child of the current node with a specified symbol.
   * @param {number} symbol Integer symbol.
   * @return {?Node} Node with the symbol.
   * @final
   */
  findChildWithSymbol(symbol) {
    let current = this.child_;
    while (current != null) {
      if (current.symbol_ == symbol) {
        return current;
      }
      current = current.next_;
    }
    return current;
  }

  /**
   * Total number of observations for all the children of this node. This
   * counts all the events observed in this context.
   * @return {number} Total number of observations under this node.
   * @final
   */
  totalChildrenCounts() {
    let childNode = this.child_;
    let count = 0;
    while (childNode != null) {
      count += childNode.count_;
      childNode = childNode.next_;
    }
    return count;
  }
}

/**
 * Handle encapsulating the search context.
 * @final
 */
class Context {
  /**
   * Constructor.
   * @param {?Node} head Head node of the context.
   * @param {number} order Length of the context.
   */
  constructor(head, order) {
    // Current node.
    this.head_ = head;
    // The order corresponding to length of the context.
    this.order_ = order;
  }
}

/**
 * Prediction by Partial Matching (PPM) Language Model.
 * @final
 */
class PPMLanguageModel {
  /**
   * @param {?Vocabulary} vocab Symbol vocabulary object.
   * @param {number} maxOrder Maximum length of the context.
   */
  constructor(vocab, maxOrder) {
    this.vocab_ = vocab;
    assert(this.vocab_.size() > 1,
           "Expecting at least two symbols in the vocabulary");

    this.maxOrder_ = maxOrder;
    this.root_ = new Node();
    this.rootContext_ = new Context();
    this.rootContext_.head_ = this.root_;
    this.rootContext_.order_ = 0;
    this.numNodes_ = 1;
  }

  /**
   * Adds symbol to the supplied node.
   * @param {?Node} node Tree node which to grow.
   * @param {number} symbol Symbol.
   * @return {?Node} Node with the symbol.
   * @final @private
   */
  addSymbolToNode_(node, symbol) {
    let symbolNode = node.findChildWithSymbol(symbol);
    if (symbolNode != null) {
      // Update the counts for the given node and also for all the backoff nodes
      // representing shorter contexts.
      symbolNode.count_++;
      let backoffNode = symbolNode.backoff_;
      assert(backoffNode != null, "Expected valid backoff node!");
      while (backoffNode != null) {
        assert(backoffNode == this.root_ || backoffNode.symbol_ == symbol,
               "Expected backoff node to be root or to contain " + symbol +
               ". Found " + backoffNode.symbol_ + " instead");
        backoffNode.count_++;
        backoffNode = backoffNode.backoff_;
      }
    } else {
      // Symbol does not exist under the given node. Create a new child node
      // and update the backoff structure for lower contexts.
      symbolNode = new Node();
      symbolNode.symbol_ = symbol;
      symbolNode.next_ = node.child_;
      node.child_ = symbolNode;
      this.numNodes_++;
      if (node == this.root_) {
        // Shortest possible context.
        symbolNode.backoff_ = this.root_;
      } else {
        assert(node.backoff_ != null, "Expected valid backoff node");
        symbolNode.backoff_ = this.addSymbolToNode_(node.backoff_, symbol);
      }
    }
    return symbolNode;
  }

  /**
   * Creates new context which is initially empty.
   * @return {?Context} Context object.
   * @final
   */
  createContext() {
    return new Context(this.rootContext_.head_, this.rootContext_.order_);
  }

  /**
   * Clones existing context.
   * @param {?Context} context Existing context object.
   * @return {?Context} Cloned context object.
   * @final
   */
  cloneContext(context) {
    return new Context(context.head_, context.order_);
  }

  /**
   * Adds symbol to the supplied context. Does not update the model.
   * @param {?Context} context Context object.
   * @param {number} symbol Integer symbol.
   * @final
   */
  addSymbolToContext(context, symbol) {
    if (symbol <= vocab.rootSymbol) {  // Only add valid symbols.
      return;
    }
    assert(symbol < this.vocab_.size(), "Invalid symbol: " + symbol);
    while (context.head_ != null) {
      if (context.order_ < this.maxOrder_) {
        // Extend the current context.
        let childNode = context.head_.findChildWithSymbol(symbol);
        if (childNode != null) {
          context.head_ = childNode;
          context.order_++;
          return;
        }
      }
      // Try to extend the shorter context.
      context.order_--;
      context.head_ = context.head_.backoff_;
    }
    if (context.head_ == null) {
      context.head_ = this.root_;
      context.order_ = 0;
    }
  }

  /**
   * Adds symbol to the supplied context and updates the model.
   * @param {?Context} context Context object.
   * @param {number} symbol Integer symbol.
   * @final
   */
  addSymbolAndUpdate(context, symbol) {
    if (symbol <= vocab.rootSymbol) {  // Only add valid symbols.
      return;
    }
    assert(symbol < this.vocab_.size(), "Invalid symbol: " + symbol);
    let symbolNode = this.addSymbolToNode_(context.head_, symbol);
    assert(symbolNode == context.head_.findChildWithSymbol(symbol));
    context.head_ = symbolNode;
    context.order_++;
    while (context.order_ > this.maxOrder_) {
      context.head_ = context.head_.backoff_;
      context.order_--;
    }
  }

  /**
   * Returns probabilities for all the symbols in the vocabulary given the
   * context.
   *
   * Notation:
   * ---------
   *         $x_h$ : Context representing history, $x_{h-1}$ shorter context.
   *   $n(w, x_h)$ : Count of symbol $w$ in context $x_h$.
   *      $T(x_h)$ : Total count in context $x_h$.
   *      $q(x_h)$ : Number of symbols with non-zero counts seen in context
   *                 $x_h$, i.e. |{w' : c(x_h, w') > 0}|. Alternatively, this
   *                 represents the number of distinct extensions of history
   *                 $x_h$ in the training data.
   *
   * Standard Kneser-Ney method (aka Absolute Discounting):
   * ------------------------------------------------------
   * Subtracting \beta (in [0, 1)) from all counts.
   *   P_{kn}(w | x_h) = \over{\max(n(w, x_h) - \beta, 0)}{T(x_h)} +
   *                     \beta * \over{q(x_h)}{T(x_h)} * P_{kn}(w | x_{h-1}),
   * where the second term in summation represents escaping to lower-order
   * context.
   *
   * See: Ney, Reinhard and Kneser, Hermann (1995): “Improved backing-off for
   * M-gram language modeling”, Proc. of Acoustics, Speech, and Signal
   * Processing (ICASSP), May, pp. 181–184.
   *
   * Modified Kneser-Ney method (Dasher version [3]):
   * ------------------------------------------------
   * Introducing \alpha parameter (in [0, 1)) and estimating as
   *   P_{kn}(w | x_h) = \over{\max(n(w, x_h) - \beta, 0)}{T(x_h) + \alpha} +
   *                     \over{\alpha + \beta * q(x_h)}{T(x_h) + \alpha} *
   *                     P_{kn}(w | x_{h-1}) .
   *
   * Additional details on the above version are provided in Sections 3 and 4
   * of:
   *   Steinruecken, Christian and Ghahramani, Zoubin and MacKay, David (2016):
   *   "Improving PPM with dynamic parameter updates", In Proc. Data
   *   Compression Conference (DCC-2015), pp. 193--202, April, Snowbird, UT,
   *   USA. IEEE.
   *
   * @param {?Context} context Context symbols.
   * @return {?array} Array of floating point probabilities.
   * @final
   */
  getProbs(context) {
    // Initialize the initial estimates. Note, we don't use uniform
    // distribution here.
    let numSymbols = this.vocab_.size();
    let probs = new Array(numSymbols);
    for (let i = 0; i < numSymbols; ++i) {
      probs[i] = 0.0;
    }

    // Estimate the probabilities for all the symbols in the supplied context.
    // This runs over all the symbols in the context and over all the suffixes
    // (orders) of the context.
    let totalMass = 1.0;
    let node = context.head_;
    let gamma = totalMass;
    while (node != null) {
      const count = node.totalChildrenCounts();
      if (count > 0) {
        let childNode = node.child_;
        while (childNode != null) {
          let p = gamma * (childNode.count_ - knBeta) / (count + knAlpha);
          probs[childNode.symbol_] += p;
          totalMass -= p;
          childNode = childNode.next_;
        }
      }
      // Backoff to lower-order context. The $\gamma$ factor represents the
      // total probability mass after processing the current $n$-th order before
      // backing off to $n-1$-th order. It roughly corresponds to the usual
      // interpolation parameter, as used in the literature, e.g. in
      //   Stanley F. Chen and Joshua Goodman (1999): "An empirical study of
      //   smoothing techniques for language modeling", Computer Speech and
      //   Language, vol. 13, pp. 359-–394.
      node = node.backoff_;
      gamma = totalMass;
    }
    assert(totalMass >= 0.0,
           "Invalid remaining probability mass: " + totalMass);

    // Adjust the probability mass for all the symbols.
    let numValidSymbols = numSymbols - 1;
    let remainingMass = totalMass;
    for (let i = 1; i < numSymbols; ++i) {
      // Following is estimated according to a uniform distribution
      // corresponding to the context length of zero.
      let p = remainingMass / numValidSymbols;
      probs[i] += p;
      totalMass -= p;
    }
    let leftSymbols = numValidSymbols;
    let newProbMass = 0.0;
    for (let i = 1; i < numSymbols; ++i) {
      let p = totalMass / leftSymbols;
      probs[i] += p;
      totalMass -= p;
      newProbMass += probs[i];
      --leftSymbols;
    }
    assert(totalMass == 0.0, "Expected remaining probability mass to be zero!");
    assert(Math.abs(1.0 - newProbMass) < epsilon);
    return probs;
  }

  /**
   * Prints the trie to console.
   * @param {?Node} node Current trie node.
   * @param {string} indent Indentation prefix.
   * @final @private
   */
  printToConsole_(node, indent) {
    console.log(indent + "  " + this.vocab_.symbols_[node.symbol_] +
                "(" + node.symbol_ + ") [" + node.count_ + "]");
    indent += "  ";
    let child = node.child_;
    while (child != null) {
      this.printToConsole_(child, indent);
      child = child.next_;
    }
  }

  /**
   * Prints the trie to console.
   * @final
   */
  printToConsole() {
    this.printToConsole_(this.root_, "");
  }
}

/**
 * Exported APIs.
 */
exports.PPMLanguageModel = PPMLanguageModel;
<|endoftext|>