<commit_before>const _ = require('underscore');
const AVRequest = require('./request').request;

const getUser = (options = {}) => AV.User.currentAsync()
  .then(currUser => currUser
    ? currUser
    : AV.User._fetchUserBySessionToken(options.sessionToken));

const getUserPointer = options => getUser(options)
  .then(currUser => AV.Object.createWithoutData('_User', currUser.id)._toPointer());

module.exports = function(AV) {
  /**
   * Contains functions to deal with Status in LeanCloud.
   * @class
   */
  AV.Status = function(imageUrl, message) {
    this.data = {};
    this.inboxType = 'default';
    this.query = null;
    if(imageUrl && typeof imageUrl === 'object') {
        this.data = imageUrl;
    } else {
      if(imageUrl){
        this.data.image = imageUrl;
      }
      if(message){
        this.data.message = message;
      }
    }
    return this;
  };

  AV.Status.prototype = {
    /**
     * Gets the value of an attribute in status data.
     * @param {String} attr The string name of an attribute.
     */
    get: function(attr){
      return this.data[attr];
    },
    /**
     * Sets a hash of model attributes on the status data.
     * @param {String} key The key to set.
     * @param {} value The value to give it.
     */
    set: function(key, value){
      this.data[key] = value;
      return this;
    },
    /**
     * Destroy this status,then it will not be avaiable in other user's inboxes.
     * @param {AuthOptions} options
     * @return {Promise} A promise that is fulfilled when the destroy
     *     completes.
     */
    destroy: function(options){
      if(!this.id)
        return AV.Promise.reject(new Error('The status id is not exists.'));
      var request = AVRequest('statuses', null, this.id, 'DELETE', options && options.sessionToken);
      return request;
    },
    /**
      * Cast the AV.Status object to an AV.Object pointer.
      * @return {AV.Object} A AV.Object pointer.
      */
    toObject: function(){
      if(!this.id)
          return null;
      return AV.Object.createWithoutData('_Status', this.id);
    },
    _getDataJSON: function() {
      var json = _.clone(this.data);
      return AV._encode(json);
    },
   /**
    * Send a status by a AV.Query object.
    * @since 0.3.0
    * @param {AuthOptions} options
    * @return {Promise} A promise that is fulfilled when the send
    *     completes.
    * @example
    *     // send a status to male users
    *     var status = new AVStatus('image url', 'a message');
    *     status.query = new AV.Query('_User');
    *     status.query.equalTo('gender', 'male');
    *     status.send().then(function(){
    *              //send status successfully.
    *      }, function(err){
    *             //an error threw.
    *             console.dir(err);
    *      });
    */
    send: function(options = {}){
      if(!options.sessionToken && !AV.User.current()) {
        throw new Error('Please signin an user.');
      }
      if(!this.query){
        return AV.Status.sendStatusToFollowers(this, options);
      }

      return getUserPointer(options).then(currUser => {
        var query = this.query.toJSON();
        query.className = this.query.className;
        var data = {};
        data.query = query;
        this.data = this.data || {};
        this.data.source =  this.data.source || currUser;
        data.data = this._getDataJSON();
        data.inboxType = this.inboxType || 'default';

        return AVRequest('statuses', null, null, 'POST', data, options.sessionToken);
      })
      .then((response) => {
        this.id = response.objectId;
        this.createdAt = AV._parseDate(response.createdAt);
        return this;
      });
    },

    _finishFetch: function(serverData){
        this.id = serverData.objectId;
        this.createdAt = AV._parseDate(serverData.createdAt);
        this.updatedAt = AV._parseDate(serverData.updatedAt);
        this.messageId = serverData.messageId;
        delete serverData.messageId;
        delete serverData.objectId;
        delete serverData.createdAt;
        delete serverData.updatedAt;
        this.data = AV._decode(undefined, serverData);
    }
  };

  /**
   * Send a status to current signined user's followers.
   * @since 0.3.0
   * @param {AV.Status} status  A status object to be send to followers.
   * @param {AuthOptions} options
   * @return {Promise} A promise that is fulfilled when the send
   *     completes.
   * @example
   *     var status = new AVStatus('image url', 'a message');
   *     AV.Status.sendStatusToFollowers(status).then(function(){
   *              //send status successfully.
   *      }, function(err){
   *             //an error threw.
   *             console.dir(err);
   *      });
   */
  AV.Status.sendStatusToFollowers = function(status, options = {}) {
    if(!options.sessionToken && !AV.User.current()){
      throw new Error('Please signin an user.');
    }
    return getUserPointer(options).then(currUser => {
      var query = {};
      query.className = '_Follower';
      query.keys = 'follower';
      query.where = {user: currUser};
      var data = {};
      data.query = query;
      status.data = status.data || {};
      status.data.source =  status.data.source || currUser;
      data.data = status._getDataJSON();
      data.inboxType = status.inboxType || 'default';

      var request = AVRequest('statuses', null, null, 'POST', data, options.sessionToken);
      return request.then(function(response){
        status.id = response.objectId;
        status.createdAt = AV._parseDate(response.createdAt);
        return status;
      });
    });
  };

  /**
   *
Send  a status from current signined user to other user's private status inbox.

   * @since 0.3.0
   * @param {AV.Status} status  A status object to be send to followers.
   * @param {String} target The target user or user's objectId.
   * @param {AuthOptions} options
   * @return {Promise} A promise that is fulfilled when the send
   *     completes.
   * @example
   *     // send a private status to user '52e84e47e4b0f8de283b079b'
   *     var status = new AVStatus('image url', 'a message');
   *     AV.Status.sendPrivateStatus(status, '52e84e47e4b0f8de283b079b').then(function(){
   *              //send status successfully.
   *      }, function(err){
   *             //an error threw.
   *             console.dir(err);
   *      });
   */
  AV.Status.sendPrivateStatus = function(status, target, options = {}) {
    if(!options.sessionToken && !AV.User.current()){
      throw new Error('Please signin an user.');
    }
    if(!target){
      throw new Error("Invalid target user.");
    }
    var userObjectId = _.isString(target) ? target: target.id;
    if(!userObjectId){
      throw new Error("Invalid target user.");
    }
    return getUserPointer(options).then(currUser => {
      var query = {};
      query.className = '_User';
      query.where = {objectId: userObjectId};
      var data = {};
      data.query = query;
      status.data = status.data || {};
      status.data.source =  status.data.source || currUser;
      data.data = status._getDataJSON();
      data.inboxType = 'private';
      status.inboxType = 'private';

      var request = AVRequest('statuses', null, null, 'POST', data, options.sessionToken);
      return request.then(function(response){
        status.id = response.objectId;
        status.createdAt = AV._parseDate(response.createdAt);
        return status;
      });
    });
  };

  /**
   * Count unread statuses in someone's inbox.
   * @since 0.3.0
   * @param {Object} source The status source.
   * @param {String} inboxType The inbox type,'default' by default.
   * @param {AuthOptions} options
   * @return {Promise} A promise that is fulfilled when the count
   *     completes.
   * @example
   *  AV.Status.countUnreadStatuses(AV.User.current()).then(function(response){
   *    console.log(response.unread); //unread statuses number.
   *    console.log(response.total);  //total statuses number.
   *  });
   */
  AV.Status.countUnreadStatuses = function(owner){
    var options = (!_.isString(arguments[1]) ? arguments[1] : arguments[2]) || {};
    var inboxType =  !_.isString(arguments[1]) ? 'default' : arguments[1];
    if(!options.sessionToken && owner == null && !AV.User.current()){
      throw new Error('Please signin an user or pass the owner objectId.');
    }
    return getUser(options).then(owner => {
      var params = {};
      params.inboxType = AV._encode(inboxType);
      params.owner = AV._encode(owner);
      return AVRequest('subscribe/statuses/count', null, null, 'GET', params, options.sessionToken);
    });
  };

  /**
   * Create a status query to find someone's published statuses.
   * @since 0.3.0
   * @param {Object} source The status source.
   * @return {AV.Query} The query object for status.
   * @example
   *   //Find current user's published statuses.
   *   var query = AV.Status.statusQuery(AV.User.current());
   *   query.find().then(function(statuses){
   *      //process statuses
   *   });
   */
  AV.Status.statusQuery = function(source){
    var query = new AV.Query('_Status');
    if(source){
      query.equalTo('source', source);
    }
    return query;
  };

   /**
    *
AV.InboxQuery defines a query that is used to fetch somebody's inbox statuses.

    * @class
    */
   AV.InboxQuery = AV.Query._extend(/** @lends AV.InboxQuery.prototype */{
     _objectClass: AV.Status,
     _sinceId: 0,
     _maxId:  0,
     _inboxType: 'default',
     _owner: null,
     _newObject: function(){
      return new AV.Status();
    },
    _createRequest: function(params, options){
      return AVRequest('subscribe/statuses', null, null, 'GET',
                                   params || this.toJSON(), options && options.sessionToken);
    },

    /**
     * Sets the messageId of results to skip before returning any results.
     * This is useful for pagination.
     * Default is zero.
     * @param {Number} n the mesage id.
     * @return {AV.InboxQuery} Returns the query, so you can chain this call.
     */
    sinceId: function(id){
      this._sinceId = id;
      return this;
    },
    /**
     * Sets the maximal messageId of results。
     * This is useful for pagination.
     * Default is zero that is no limition.
     * @param {Number} n the mesage id.
     * @return {AV.InboxQuery} Returns the query, so you can chain this call.
     */
    maxId: function(id){
      this._maxId = id;
      return this;
    },
    /**
     * Sets the owner of the querying inbox.
     * @param {Object} owner The inbox owner.
     * @return {AV.InboxQuery} Returns the query, so you can chain this call.
     */
    owner: function(owner){
      this._owner = owner;
      return this;
    },
    /**
     * Sets the querying inbox type.default is 'default'.
     * @param {Object} owner The inbox type.
     * @return {AV.InboxQuery} Returns the query, so you can chain this call.
     */
    inboxType: function(type){
      this._inboxType = type;
      return this;
    },
    toJSON: function(){
      var params = AV.InboxQuery.__super__.toJSON.call(this);
      params.owner = AV._encode(this._owner);
      params.inboxType = AV._encode(this._inboxType);
      params.sinceId = AV._encode(this._sinceId);
      params.maxId = AV._encode(this._maxId);
      return params;
    }
   });

  /**
   * Create a inbox status query to find someone's inbox statuses.
   * @since 0.3.0
   * @param {Object} owner The inbox's owner
   * @param {String} inboxType The inbox type,'default' by default.
   * @return {AV.InboxQuery} The inbox query object.
   * @see AV.InboxQuery
   * @example
   *   //Find current user's default inbox statuses.
   *   var query = AV.Status.inboxQuery(AV.User.current());
   *   //find the statuses after the last message id
   *   query.sinceId(lastMessageId);
   *   query.find().then(function(statuses){
   *      //process statuses
   *   });
   */
  AV.Status.inboxQuery = function(owner, inboxType){
    var query = new AV.InboxQuery(AV.Status);
    if(owner){
      query._owner = owner;
    }
    if(inboxType){
      query._inboxType = inboxType;
    }
    return query;
  };

};
<commit_msg>fix(status): fix undefined AV error (#403)<commit_after>const _ = require('underscore');
const AVRequest = require('./request').request;

module.exports = function(AV) {
  const getUser = (options = {}) => AV.User.currentAsync()
    .then(currUser => currUser || AV.User._fetchUserBySessionToken(options.sessionToken));

  const getUserPointer = options => getUser(options)
    .then(currUser => AV.Object.createWithoutData('_User', currUser.id)._toPointer());

  /**
   * Contains functions to deal with Status in LeanCloud.
   * @class
   */
  AV.Status = function(imageUrl, message) {
    this.data = {};
    this.inboxType = 'default';
    this.query = null;
    if(imageUrl && typeof imageUrl === 'object') {
        this.data = imageUrl;
    } else {
      if(imageUrl){
        this.data.image = imageUrl;
      }
      if(message){
        this.data.message = message;
      }
    }
    return this;
  };

  AV.Status.prototype = {
    /**
     * Gets the value of an attribute in status data.
     * @param {String} attr The string name of an attribute.
     */
    get: function(attr){
      return this.data[attr];
    },
    /**
     * Sets a hash of model attributes on the status data.
     * @param {String} key The key to set.
     * @param {} value The value to give it.
     */
    set: function(key, value){
      this.data[key] = value;
      return this;
    },
    /**
     * Destroy this status,then it will not be avaiable in other user's inboxes.
     * @param {AuthOptions} options
     * @return {Promise} A promise that is fulfilled when the destroy
     *     completes.
     */
    destroy: function(options){
      if(!this.id)
        return AV.Promise.reject(new Error('The status id is not exists.'));
      var request = AVRequest('statuses', null, this.id, 'DELETE', options && options.sessionToken);
      return request;
    },
    /**
      * Cast the AV.Status object to an AV.Object pointer.
      * @return {AV.Object} A AV.Object pointer.
      */
    toObject: function(){
      if(!this.id)
          return null;
      return AV.Object.createWithoutData('_Status', this.id);
    },
    _getDataJSON: function() {
      var json = _.clone(this.data);
      return AV._encode(json);
    },
   /**
    * Send a status by a AV.Query object.
    * @since 0.3.0
    * @param {AuthOptions} options
    * @return {Promise} A promise that is fulfilled when the send
    *     completes.
    * @example
    *     // send a status to male users
    *     var status = new AVStatus('image url', 'a message');
    *     status.query = new AV.Query('_User');
    *     status.query.equalTo('gender', 'male');
    *     status.send().then(function(){
    *              //send status successfully.
    *      }, function(err){
    *             //an error threw.
    *             console.dir(err);
    *      });
    */
    send: function(options = {}){
      if(!options.sessionToken && !AV.User.current()) {
        throw new Error('Please signin an user.');
      }
      if(!this.query){
        return AV.Status.sendStatusToFollowers(this, options);
      }

      return getUserPointer(options).then(currUser => {
        var query = this.query.toJSON();
        query.className = this.query.className;
        var data = {};
        data.query = query;
        this.data = this.data || {};
        this.data.source =  this.data.source || currUser;
        data.data = this._getDataJSON();
        data.inboxType = this.inboxType || 'default';

        return AVRequest('statuses', null, null, 'POST', data, options.sessionToken);
      })
      .then((response) => {
        this.id = response.objectId;
        this.createdAt = AV._parseDate(response.createdAt);
        return this;
      });
    },

    _finishFetch: function(serverData){
        this.id = serverData.objectId;
        this.createdAt = AV._parseDate(serverData.createdAt);
        this.updatedAt = AV._parseDate(serverData.updatedAt);
        this.messageId = serverData.messageId;
        delete serverData.messageId;
        delete serverData.objectId;
        delete serverData.createdAt;
        delete serverData.updatedAt;
        this.data = AV._decode(undefined, serverData);
    }
  };

  /**
   * Send a status to current signined user's followers.
   * @since 0.3.0
   * @param {AV.Status} status  A status object to be send to followers.
   * @param {AuthOptions} options
   * @return {Promise} A promise that is fulfilled when the send
   *     completes.
   * @example
   *     var status = new AVStatus('image url', 'a message');
   *     AV.Status.sendStatusToFollowers(status).then(function(){
   *              //send status successfully.
   *      }, function(err){
   *             //an error threw.
   *             console.dir(err);
   *      });
   */
  AV.Status.sendStatusToFollowers = function(status, options = {}) {
    if(!options.sessionToken && !AV.User.current()){
      throw new Error('Please signin an user.');
    }
    return getUserPointer(options).then(currUser => {
      var query = {};
      query.className = '_Follower';
      query.keys = 'follower';
      query.where = {user: currUser};
      var data = {};
      data.query = query;
      status.data = status.data || {};
      status.data.source =  status.data.source || currUser;
      data.data = status._getDataJSON();
      data.inboxType = status.inboxType || 'default';

      var request = AVRequest('statuses', null, null, 'POST', data, options.sessionToken);
      return request.then(function(response){
        status.id = response.objectId;
        status.createdAt = AV._parseDate(response.createdAt);
        return status;
      });
    });
  };

  /**
   *
Send  a status from current signined user to other user's private status inbox.

   * @since 0.3.0
   * @param {AV.Status} status  A status object to be send to followers.
   * @param {String} target The target user or user's objectId.
   * @param {AuthOptions} options
   * @return {Promise} A promise that is fulfilled when the send
   *     completes.
   * @example
   *     // send a private status to user '52e84e47e4b0f8de283b079b'
   *     var status = new AVStatus('image url', 'a message');
   *     AV.Status.sendPrivateStatus(status, '52e84e47e4b0f8de283b079b').then(function(){
   *              //send status successfully.
   *      }, function(err){
   *             //an error threw.
   *             console.dir(err);
   *      });
   */
  AV.Status.sendPrivateStatus = function(status, target, options = {}) {
    if(!options.sessionToken && !AV.User.current()){
      throw new Error('Please signin an user.');
    }
    if(!target){
      throw new Error("Invalid target user.");
    }
    var userObjectId = _.isString(target) ? target: target.id;
    if(!userObjectId){
      throw new Error("Invalid target user.");
    }
    return getUserPointer(options).then(currUser => {
      var query = {};
      query.className = '_User';
      query.where = {objectId: userObjectId};
      var data = {};
      data.query = query;
      status.data = status.data || {};
      status.data.source =  status.data.source || currUser;
      data.data = status._getDataJSON();
      data.inboxType = 'private';
      status.inboxType = 'private';

      var request = AVRequest('statuses', null, null, 'POST', data, options.sessionToken);
      return request.then(function(response){
        status.id = response.objectId;
        status.createdAt = AV._parseDate(response.createdAt);
        return status;
      });
    });
  };

  /**
   * Count unread statuses in someone's inbox.
   * @since 0.3.0
   * @param {Object} source The status source.
   * @param {String} inboxType The inbox type,'default' by default.
   * @param {AuthOptions} options
   * @return {Promise} A promise that is fulfilled when the count
   *     completes.
   * @example
   *  AV.Status.countUnreadStatuses(AV.User.current()).then(function(response){
   *    console.log(response.unread); //unread statuses number.
   *    console.log(response.total);  //total statuses number.
   *  });
   */
  AV.Status.countUnreadStatuses = function(owner){
    var options = (!_.isString(arguments[1]) ? arguments[1] : arguments[2]) || {};
    var inboxType =  !_.isString(arguments[1]) ? 'default' : arguments[1];
    if(!options.sessionToken && owner == null && !AV.User.current()){
      throw new Error('Please signin an user or pass the owner objectId.');
    }
    return getUser(options).then(owner => {
      var params = {};
      params.inboxType = AV._encode(inboxType);
      params.owner = AV._encode(owner);
      return AVRequest('subscribe/statuses/count', null, null, 'GET', params, options.sessionToken);
    });
  };

  /**
   * Create a status query to find someone's published statuses.
   * @since 0.3.0
   * @param {Object} source The status source.
   * @return {AV.Query} The query object for status.
   * @example
   *   //Find current user's published statuses.
   *   var query = AV.Status.statusQuery(AV.User.current());
   *   query.find().then(function(statuses){
   *      //process statuses
   *   });
   */
  AV.Status.statusQuery = function(source){
    var query = new AV.Query('_Status');
    if(source){
      query.equalTo('source', source);
    }
    return query;
  };

   /**
    *
AV.InboxQuery defines a query that is used to fetch somebody's inbox statuses.

    * @class
    */
   AV.InboxQuery = AV.Query._extend(/** @lends AV.InboxQuery.prototype */{
     _objectClass: AV.Status,
     _sinceId: 0,
     _maxId:  0,
     _inboxType: 'default',
     _owner: null,
     _newObject: function(){
      return new AV.Status();
    },
    _createRequest: function(params, options){
      return AVRequest('subscribe/statuses', null, null, 'GET',
                                   params || this.toJSON(), options && options.sessionToken);
    },

    /**
     * Sets the messageId of results to skip before returning any results.
     * This is useful for pagination.
     * Default is zero.
     * @param {Number} n the mesage id.
     * @return {AV.InboxQuery} Returns the query, so you can chain this call.
     */
    sinceId: function(id){
      this._sinceId = id;
      return this;
    },
    /**
     * Sets the maximal messageId of results。
     * This is useful for pagination.
     * Default is zero that is no limition.
     * @param {Number} n the mesage id.
     * @return {AV.InboxQuery} Returns the query, so you can chain this call.
     */
    maxId: function(id){
      this._maxId = id;
      return this;
    },
    /**
     * Sets the owner of the querying inbox.
     * @param {Object} owner The inbox owner.
     * @return {AV.InboxQuery} Returns the query, so you can chain this call.
     */
    owner: function(owner){
      this._owner = owner;
      return this;
    },
    /**
     * Sets the querying inbox type.default is 'default'.
     * @param {Object} owner The inbox type.
     * @return {AV.InboxQuery} Returns the query, so you can chain this call.
     */
    inboxType: function(type){
      this._inboxType = type;
      return this;
    },
    toJSON: function(){
      var params = AV.InboxQuery.__super__.toJSON.call(this);
      params.owner = AV._encode(this._owner);
      params.inboxType = AV._encode(this._inboxType);
      params.sinceId = AV._encode(this._sinceId);
      params.maxId = AV._encode(this._maxId);
      return params;
    }
   });

  /**
   * Create a inbox status query to find someone's inbox statuses.
   * @since 0.3.0
   * @param {Object} owner The inbox's owner
   * @param {String} inboxType The inbox type,'default' by default.
   * @return {AV.InboxQuery} The inbox query object.
   * @see AV.InboxQuery
   * @example
   *   //Find current user's default inbox statuses.
   *   var query = AV.Status.inboxQuery(AV.User.current());
   *   //find the statuses after the last message id
   *   query.sinceId(lastMessageId);
   *   query.find().then(function(statuses){
   *      //process statuses
   *   });
   */
  AV.Status.inboxQuery = function(owner, inboxType){
    var query = new AV.InboxQuery(AV.Status);
    if(owner){
      query._owner = owner;
    }
    if(inboxType){
      query._inboxType = inboxType;
    }
    return query;
  };

};
<|endoftext|>


<commit_before>/**
 * @file Streams helpers
 */
/*#ifndef(UMD)*/
"use strict";
/*global _gpfErrorDeclare*/ // Declare new gpf.Error names
/*exported _GPF_STREAM_DEFAULT_READ_SIZE*/ // Global default for stream read size
/*exported _gpfStreamSecureRead*/ // Generate a wrapper to secure multiple calls to stream#read
/*exported _gpfStreamSecureWrite*/ // Generates a wrapper to secure multiple calls to stream#write
/*#endif*/

_gpfErrorDeclare("stream", {
    readInProgress:
        "A read operation is already in progress",
    writeInProgress:
        "A write operation is already in progress"
});

var _GPF_STREAM_DEFAULT_READ_SIZE = 4096;

/**
 * @namespace gpf.stream
 * @description Root namespace for GPF streams
 */
gpf.stream = {};

/**
 * Generate a wrapper to secure multiple calls to stream#read
 *
 * @param {Function} read Read function
 * @return {Function} Function exposing {@see gpf.interfaces.IReadableStream#read}
 * @gpf:closure
 */
function _gpfStreamSecureRead (read) {
    var inProgress = false;
    return function (output) {
        if (inProgress) {
            gpf.error.readInProgress();
        }
        inProgress = true;
        return read(output)
            .then(function (result) {
                inProgress = false;
                return Promise.resolve(result);
            }, function (reason) {
                inProgress = false;
                return Promise.reject(reason);
            });
    };
}

/**
 * Generate a wrapper to secure multiple calls to stream#write
 *
 * @param {Function} write Write function
 * @return {Function} Function exposing {@see gpf.interfaces.IWritableStream#write}
 * @gpf:closure
 */
function _gpfStreamSecureWrite (write) {
    var inProgress = false;
    return function (buffer) {
        if (inProgress) {
            gpf.error.readInProgress();
        }
        inProgress = true;
        return write(buffer)
            .then(function (result) {
                inProgress = false;
                return Promise.resolve(result);
            }, function (reason) {
                inProgress = false;
                return Promise.reject(reason);
            });
    };
}
<commit_msg>Documentation<commit_after>/**
 * @file Streams helpers
 */
/*#ifndef(UMD)*/
"use strict";
/*global _gpfErrorDeclare*/ // Declare new gpf.Error names
/*exported _GPF_STREAM_DEFAULT_READ_SIZE*/ // Global default for stream read size
/*exported _gpfStreamSecureRead*/ // Generate a wrapper to secure multiple calls to stream#read
/*exported _gpfStreamSecureWrite*/ // Generates a wrapper to secure multiple calls to stream#write
/*#endif*/

_gpfErrorDeclare("stream", {

    /**
     * ### Summary
     *
     * A read operation is already in progress
     *
     * ### Description
     *
     * This error is triggered if two reads are made simultaneously on the stream
     */
    readInProgress:
        "A read operation is already in progress",

    /**
     * ### Summary
     *
     * A write operation is already in progress
     *
     * ### Description
     *
     * This error is triggered if two writes are made simultaneously on the stream
     */
    writeInProgress:
        "A write operation is already in progress",

    /**
     * ### Summary
     *
     * Stream is in an invalid state
     *
     * ### Description
     *
     * If an error occurred while using the stream, no additional operations can be made
     */
    invalidStreamState:
        "Stream is in an invalid state"
});

var _GPF_STREAM_DEFAULT_READ_SIZE = 4096;

/**
 * @namespace gpf.stream
 * @description Root namespace for GPF streams
 */
gpf.stream = {};

/**
 * Generate a wrapper to secure multiple calls to stream#read
 *
 * @param {Function} read Read function
 * @return {Function} Function exposing {@see gpf.interfaces.IReadableStream#read}
 * @gpf:closure
 */
function _gpfStreamSecureRead (read) {
    var inProgress = false;
    return function (output) {
        if (inProgress) {
            gpf.error.readInProgress();
        }
        inProgress = true;
        return read(output)
            .then(function (result) {
                inProgress = false;
                return Promise.resolve(result);
            }, function (reason) {
                inProgress = false;
                return Promise.reject(reason);
            });
    };
}

/**
 * Generate a wrapper to secure multiple calls to stream#write
 *
 * @param {Function} write Write function
 * @return {Function} Function exposing {@see gpf.interfaces.IWritableStream#write}
 * @gpf:closure
 */
function _gpfStreamSecureWrite (write) {
    var inProgress = false;
    return function (buffer) {
        if (inProgress) {
            gpf.error.readInProgress();
        }
        inProgress = true;
        return write(buffer)
            .then(function (result) {
                inProgress = false;
                return Promise.resolve(result);
            }, function (reason) {
                inProgress = false;
                return Promise.reject(reason);
            });
    };
}
<|endoftext|>


<commit_before>/*
 * Copyright 2007 Matthew Eernisse (mde@fleegix.org)
 *
 * 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.
 *
*/
if (typeof fleegix == 'undefined') { var fleegix = {}; }
fleegix.string = new function () {
  var ltr = /^\s+/; var rtr = /\s+$/; var tr = /^\s+|\s+$/g;
  this.toArray = function (str) {
    var arr = [];
    for (var i = 0; i < str.length; i++) {
      arr[i] = str.substr(i, 1);
    }
    return arr;
  };
  this.reverse = function (str) {
    return this.toArray(str).reverse().join('');
  };
  this.ltrim = function (str, chr) {
    var pat = chr ? new RegExp('^' + chr + '+') : ltr;
    return str.replace(pat, '');
  };
  this.rtrim = function (str, chr) {
    var pat = chr ? new RegExp(chr + '+$') : rtr;
    return str.replace(pat, '');
  };
  this.trim = function (str, chr) {
    var pat = chr ? new RegExp('^' + chr + '+|' + chr + '+$', 'g') : tr;
    return str.replace(pat, '');
  };
};

<commit_msg>Added two utility methods for converting strings back and forth between (JavaScript) camelCase naming convention and (Ruby/Python) lowercase_with_underscores.<commit_after>/*
 * Copyright 2007 Matthew Eernisse (mde@fleegix.org)
 *
 * 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.
 *
*/
if (typeof fleegix == 'undefined') { var fleegix = {}; }
fleegix.string = new function () {
  var ltr = /^\s+/; var rtr = /\s+$/; var tr = /^\s+|\s+$/g;
  this.toArray = function (str) {
    var arr = [];
    for (var i = 0; i < str.length; i++) {
      arr[i] = str.substr(i, 1);
    }
    return arr;
  };
  this.reverse = function (str) {
    return this.toArray(str).reverse().join('');
  };
  this.ltrim = function (str, chr) {
    var pat = chr ? new RegExp('^' + chr + '+') : ltr;
    return str.replace(pat, '');
  };
  this.rtrim = function (str, chr) {
    var pat = chr ? new RegExp(chr + '+$') : rtr;
    return str.replace(pat, '');
  };
  this.trim = function (str, chr) {
    var pat = chr ? new RegExp('^' + chr + '+|' + chr + '+$', 'g') : tr;
    return str.replace(pat, '');
  };
  // Converts someVariableName to some_variable_name
  this.toLowerCaseWithUnderscores = function (s) {
    return s.replace(/([A-Z]+)/g, '_$1').toLowerCase().
      replace(/^_/, '');
  };
  // Converts some_variable_name to someVariableName
  this.toCamelCase = function (s) {
    return s.replace(/_[a-z]{1}/g, function (s)
      { return s.replace('_', '').toUpperCase() });
  };
};

<|endoftext|>


<commit_before>"use strict";

/**
 * Tests if the string contains vowels
 *
 * @returns {Boolean}
 */
Object.defineProperty(String.prototype, "hasVowels", {
    value () {

        return /[aeiou]/ig.test(this);

    }
});

/**
 * Returns an upper case only version of the string
 *
 * @returns {String}
 */
Object.defineProperty(String.prototype, "toUpper", {
    value () {

        return this.replace(/[a-z]/g, (match, index) => {

            return String.fromCharCode(this.charCodeAt(index) - 32);

        });

    }
});

/**
 * Converts the string to solely lower case
 *
 * @returns {String}
 */
Object.defineProperty(String.prototype, "toLower", {
    value () {

        return this.replace(/[A-Z]/g, (match, index) => {

            return String.fromCharCode(this.charCodeAt(index) + 32);

        });

    }
});

/**
 * Makes the first character of the string upper case if it isn't
 *
 * @returns {String}
 */
Object.defineProperty(String.prototype, "ucFirst", {
    value () {

        return this.replace(/^[a-z]/,(match) => {

            return match.toUpper();

        });

    }
});

/**
 * Checks if the string contains a question
 *
 * @returns {Boolean}
 */
Object.defineProperty(String.prototype, "isQuestion", {
    value () {

        return /\?$/.test(this);

    }
});

/**
 * Gets the words contained in the string
 *
 * @returns {Array}
 */
Object.defineProperty(String.prototype, "words", {
    value () {

        return this.match(/(\w+)/g);

    }
});

/**
 * Gets the number of words contained in the string
 *
 * @returns {Number}
 */
Object.defineProperty(String.prototype, "wordCount", {
    value () {

        return this.words().length;

    }
});

/**
 * Formats the given number as currency
 *
 * @returns {String}
 */
Object.defineProperty(String.prototype, "toCurrency", {
    value () {

        return this.replace(/(\d)(?=(\d{3})+(?!\d))/g,"$1,");

    }
});

/**
 * Formats the given currency string as numerical
 *
 * @returns {String}
 */
Object.defineProperty(String.prototype, "fromCurrency", {
    value () {

        return Number(this.replace(/[,]/g,""));

    }
});

/**
 * Returns a version of the string with the cases inverted
 *
 * @returns {String}
 */
Object.defineProperty(String.prototype, "inverseCase", {
    value () {

        return this.replace(/[A-Za-z]/g,(matched) => {

            return /[A-Z]/g.test(matched) ? matched.toLower() : matched.toUpper();

        });

    }
});

/**
 * Returns a version of the string where the cases alternate per character
 *
 * @returns {String}
 */
Object.defineProperty(String.prototype, "alternatingCase", {
    value () {

        return this.replace(/[A-Za-z]/g,(matched,index) => {

            return index % 2 ? matched.toUpper() : matched.toLower();

        });

    }
});

/**
 * Returns the middle character(s) of the string
 *
 * @returns {String}
 */
Object.defineProperty(String.prototype, "getMiddle", {
    value () {

        //if string is just one character return that character
        if (this.length === 1) return this[0];

        //split string into two
        const regex = new RegExp(`.{${ ~~(this.length / 2) }}`,"g"),
            matches = this.match(regex);

        /** if of even length return last letter of first match and first letter of second
         * else return just the first letter of the second match
         */
        return this.length % 2 ? matches[1][0] : matches[0][matches[0].length - 1] + matches[1][0];

    }
});

/**
 * Returns a version of the given numerical string
 * where the numbers are expressed as their text
 * versions
 *
 * @returns {String}
 */
Object.defineProperty(String.prototype, "numberWords", {
    value () {

        const codex = ["zero","one","two","three","four",
            "five", "six", "seven", "eight","nine"];

        return this.replace(/[0-9]/g,(matched) => {

            return `${codex[matched]} `;

        }).trim();

    }
});

/**
 * Checks if the string is a single digit
 * @returns {Boolean}
 */
Object.defineProperty(String.prototype, "isDigit", {
    value () {

        if (! /\d/.test(this) ) return false;
        return this.match(/\d/g).length === 1;

    }
});

/**
 * Checks if the string contains two characters repeated
 * next to each other
 *
 * @returns {String}
 */
Object.defineProperty(String.prototype, "doubleCheck", {
    value () {

        return /(\S)\1/g.test(this);

    }
});
<commit_msg>fix issues raised by @g-ramos in code review<commit_after>"use strict";

/**
 * Tests if the string contains vowels
 *
 * @returns {Boolean}
 */
String.prototype.hasVowels = function () {
    return /[aeiou]/i.test(this);
}

/**
 * Returns an upper case only version of the string
 *
 * @returns {String}
 */
String.prototype.toUpper = function () {
    return this.replace(/[a-z]/g, (match, index) => {
        return String.fromCharCode(this.charCodeAt(index) - 32);
    });
};

/**
 * Converts the string to solely lower case
 *
 * @returns {String}
 */
String.prototype.toLower = function (){
    return this.replace(/[A-Z]/g, (match, index) => {
        return String.fromCharCode(this.charCodeAt(index) + 32);
    });
};

/**
 * Makes the first character of the string upper case if it isn't
 *
 * @returns {String}
 */
String.prototype.ucFirst = function () {
    return this.replace(/^[a-z]/,(match) => {
        return match.toUpper();
    });
};

/**
 * Checks if the string contains a question
 *
 * @returns {Boolean}
 */
String.prototype.isQuestion = function () {
    return /\?$/.test(this);
};

/**
 * Gets the words contained in the string
 *
 * @returns {Array}
 */
String.prototype.words = function () {
    return this.match(/(\w+)/g);
};

/**
 * Gets the number of words contained in the string
 *
 * @returns {Number}
 */
String.prototype.wordCount = function () {
    return this.words().length;
};

/**
 * Formats the given number as currency
 *
 * @returns {String}
 */
String.prototype.toCurrency = function () {
    return this.replace(/(\d)(?=(\d{3})+(?!\d))/g,"$1,");
};

/**
 * Formats the given currency string as numerical
 *
 * @returns {String}
 */
String.prototype.fromCurrency = function () {
    return Number(this.replace(/[,]/g,""));
};

/**
 * Returns a version of the string with the cases inverted
 *
 * @returns {String}
 */
String.prototype.inverseCase = function () {
    return this.replace(/[A-Za-z]/g, (matched) => {
        return /[A-Z]/g.test(matched) ? matched.toLower() : matched.toUpper();
    });
};

/**
 * Returns a version of the string where the cases alternate per character
 *
 * @returns {String}
 */
String.prototype.alternatingCase = function () {
    return this.replace(/[A-Za-z]/g,(matched,index) => {
        return index % 2 ? matched.toUpper() : matched.toLower();
    });
};

/**
 * Returns the middle character(s) of the string
 *
 * @returns {String}
 */
String.prototype.getMiddle = function () {
    //if string is just one character return that character
    if (this.length === 1) return this[0];
    //split string into two
    const regex = new RegExp(`.{${ ~~(this.length / 2) }}`,"g"),
        matches = this.match(regex);
    /** if of even length return last letter of first match and first letter of second
     * else return just the first letter of the second match
    */
    return this.length % 2 ? matches[1][0] : matches[0][matches[0].length - 1] + matches[1][0];
};

/**
 * Returns a string representation of a number *
 * @returns {String}
 */
String.prototype.numberWords = function () {
    const codex = ["zero","one","two","three","four",
        "five", "six", "seven", "eight","nine"];
    return this.replace(/[0-9]/g,(matched) => {
        return `${codex[matched]} `;
    }).trim();
};

/**
 * Checks if the string is a single digit
 * @returns {Boolean}
 */
String.prototype.isDigit = function () {
    return /\d/.test(this) ? this.match(/\d/g).length === 1 : false;
};

/**
 * Checks if the string contains two characters repeated
 * next to each other
 *
 * @returns {String}
 */
String.prototype.doubleCheck = function () {
    return /(\S)\1/g.test(this);
};
<|endoftext|>


<commit_before>// =================================================================================================
// Core.js | String Functions
// (c) 2014 Mathigon / Philipp Legner
// =================================================================================================

(function() {

    M.extend(String.prototype, {

        endsWith: function(search) {
            var end = this.length;
            var start = end - search.length;
            return (this.substring(start, end) === search);
        },

        strip: function() {
            return this.replace(/^\s+/, '').replace(/\s+$/, '');
        },

        collapse: function() {
            return this.trim().replace(/\s+/g, ' ');
        },

        toTitleCase: function() {
            return this.replace(/\S+/g, function(a){
                return a.charAt(0).toUpperCase() + a.slice(1);
            });
        },

        words: function() {
            return this.strip().split(/\s+/);
        }

    }, true);

    if ( !String.prototype.contains ) {
        M.extend(String.prototype, {

            contains: function() {
                return String.prototype.indexOf.apply( this, arguments ) !== -1;
            }
        }, true);
    }

})();
<commit_msg>Fix String endsWith prototype shadowing<commit_after>// =================================================================================================
// Core.js | String Functions
// (c) 2014 Mathigon / Philipp Legner
// =================================================================================================

(function() {

    M.extend(String.prototype, {

        strip: function() {
            return this.replace(/^\s+/, '').replace(/\s+$/, '');
        },

        collapse: function() {
            return this.trim().replace(/\s+/g, ' ');
        },

        toTitleCase: function() {
            return this.replace(/\S+/g, function(a){
                return a.charAt(0).toUpperCase() + a.slice(1);
            });
        },

        words: function() {
            return this.strip().split(/\s+/);
        }

    }, true);

    if (!String.prototype.endsWith) {
        M.extend(String.prototype, {
            endsWith: function(search) {
                var end = this.length;
                var start = end - search.length;
                return (this.substring(start, end) === search);
            }
        }, true);
    }

    if (!String.prototype.contains) {
        M.extend(String.prototype, {
            contains: function() {
                return String.prototype.indexOf.apply( this, arguments ) !== -1;
            }
        }, true);
    }

})();
<|endoftext|>


<commit_before>import Promise from 'bluebird';
import debug from 'debug';
import EventEmitter from 'events';
import moment from 'moment';
import path from 'path';
import streamBuffers from 'stream-buffers';
import ArchieConverter from './archie-converter';
import SpreadsheetConverter from './spreadsheet-converter';
import State from './state';
import url from 'url';

const log = debug('google-drive-sync:syncer');

const mimeTypes = [
    'application/vnd.google-apps.spreadsheet',
    'application/vnd.google-apps.document',
];

const IGNORED_ERRORS = [429];

export default class Syncer {
    constructor(opts) {
        this.client = opts.client;
        this.fs = opts.fs;

        this.outputDirectories = opts.outputDirectory.split(',');

        if (!opts.state) {
            throw new Error(`must specify "state" option`);
        }

        this.state = new State(this.fs, opts.state);
        this.events = new EventEmitter();
    }

    sync() {
        this.events.emit('sync');

        return Promise.join(
            this.state.read(),
            this.client.authorize(), // TODO: re-use JWT tokens but handle expiry
            () => {
                return this.ensurePageTokenInState().then(this.fetchChanges);
            }
        );
    }

    fetchChanges = () => {
        return this.client
            .getChanges(this.state.getPageToken())
            .then(this.handleChanges)
            .then(this.fetchSpecialFiles)
            .catch(err => this.events.emit('error', err));
    };

    ensurePageTokenInState() {
        if (this.state.getPageToken()) {
            return Promise.resolve();
        } else {
            return this.client
                .getStartPageToken()
                .then(res => this.state.save({ pageToken: res.startPageToken - 1 }));
        }
    }

    on(...args) {
        this.events.on(...args);
    }

    handleChanges = list => {
        log('found changes', list.changes);

        const changes = list.changes.filter(
            i => !i.removed && mimeTypes.indexOf(i.file.mimeType) !== -1
        );

        log(`fetching ${changes.length}`);

        return Promise.map(changes, this.processChange, { concurrency: 3 })
            .then(() =>
                this.state.save({
                    pageToken: list.nextPageToken || list.newStartPageToken,
                    lastCheck: moment().format(),
                })
            )
            .then(state => this.events.emit('synced', state.data));
    };

    fetchSpecialFiles = () => {
        const ids = process.env.GDRIVE_ALWAYS_FETCH
            ? process.env.GDRIVE_ALWAYS_FETCH.split(',')
            : [];

        return Promise.map(
            ids,
            id => this.client.getFile(id).then(this.downloadFile),
            { concurrency: 1 }
        );
    };

    processChange = change => {
        return this.client
            .getFile(change.fileId)
            .then(file =>
                this.downloadFile(file).then(() =>
                    this.state.setFile(change.fileId, { change, file })
                )
            );
    };

    downloadFile = file => {
        let promise = null;

        switch (file.mimeType) {
            case 'application/vnd.google-apps.spreadsheet':
            case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
                promise = this.convertSpreadsheet(file).then(data => [
                    {
                        fileName: `${file.id}.json`,
                        data,
                        mimeType: 'application/json',
                    },
                ]);
                break;
            case 'application/vnd.google-apps.document':
                promise = this.convertDocument(file).then(results => [
                    {
                        fileName: `${file.id}.json`,
                        data: results.plain,
                        mimeType: 'application/json',
                    },
                    {
                        fileName: `${file.id}.styled.json`,
                        data: results.styled,
                        mimeType: 'application/json',
                    },
                    {
                        fileName: `${file.id}.aml`,
                        data: results.aml,
                        mimeType: 'text/plain',
                    },
                    {
                        fileName: `${file.id}.html`,
                        data: results.html,
                        mimeType: 'text/html',
                    },
                ]);

                break;
            default:
                throw new Error(`unknown mime type: ${file.mimeType}`);
        }

        const modification = this.getModification(file);

        return promise
            .then(results => {
                return Promise.each(results, ({ fileName, data, mimeType }) =>
                    this.save(fileName, {
                        title: file.name,
                        modification,
                        data,
                        mimeType,
                    })
                );
            })
            .catch(err => {
                console.error(file.webViewLink, err.message);
                this.events.emit('error', err);

                let isIgnored =
                    err.httpResponse &&
                    IGNORED_ERRORS.includes(err.httpResponse.statusCode);
                isIgnored = isIgnored || !!err.isArchieMLError;

                const preThrow = err.aml
                    ? this.save(`${file.id}.aml`, { data: err.aml })
                    : Promise.resolve();

                if (isIgnored) {
                    log('error ignored');
                    return preThrow;
                } else {
                    return preThrow.then(() => Promise.reject(err));
                }
            });
    };

    convertSpreadsheet(doc) {
        return new Promise((resolve, reject) => {
            this.client
                .getExportFileStream(
                    doc.id,
                    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
                )
                .catch(reject)
                .then(stream => {
                    const writable = new streamBuffers.WritableStreamBuffer();

                    stream
                        .on('end', () => {
                            log('end stream');
                            const buf = writable.getContents();
                            writable.end();
                            resolve(buf);
                        })
                        .on('error', reject)
                        .pipe(writable);
                });
        }).then(buf => SpreadsheetConverter.convert(buf.toString('binary')));
    }

    convertDocument(doc) {
        return this.client
            .exportFile(doc.id, 'text/html')
            .then(html =>
                Promise.props({
                    html,
                    plain: ArchieConverter.convert(html),
                    styled: ArchieConverter.convert(html, {
                        preserve_styles: ['bold', 'italic', 'underline'],
                    }),
                })
            )
            .then(({ plain, styled, html }) => ({
                plain: plain.result,
                styled: styled.result,
                aml: plain.aml,
                html,
            }));
    }

    getModification(doc) {
        const now = moment();
        const modified = moment(doc.modifiedTime);
        const lag = moment.duration(now.diff(modified));

        const user = doc.lastModifyingUser || doc.sharingUser || {};

        const result = {
            user: {
                name: user.displayName,
                email: user.emailAddress,
            },
            date: modified.format(),
            version: doc.version,
            downloadTime: now.format(),
            downloadLag: {
                ms: lag.valueOf(),
                human: lag.humanize(),
            },
        };

        const previous = this.state.getFile(doc.id);

        if (previous && previous.file && previous.file.version) {
            result.lastUpdate = {
                version: previous.file.version,
                changeTime: previous.change
                    ? moment(previous.change.time).format()
                    : null,
            };

            if (+previous.file.version && +doc.version) {
                result.versionDiff = +doc.version - +previous.file.version;
            }
        }

        return result;
    }

    save(fileName, data) {
        if (data === null) {
            // probably an operational error - don't write the file
            return Promise.resolve();
        }

        const body = fileName.endsWith('.json') ? JSON.stringify(data) : data.data;

        return Promise.map(this.outputDirectories, dir => {
            const filePath = path.join(dir, fileName);

            return this.fs
                .write(filePath, body, { mimeType })
                .then(() => this.events.emit('saved', fileName, data));
        });
    }
}
<commit_msg>fix content-type<commit_after>import Promise from 'bluebird';
import debug from 'debug';
import EventEmitter from 'events';
import moment from 'moment';
import path from 'path';
import streamBuffers from 'stream-buffers';
import ArchieConverter from './archie-converter';
import SpreadsheetConverter from './spreadsheet-converter';
import State from './state';
import url from 'url';

const log = debug('google-drive-sync:syncer');

const mimeTypes = [
    'application/vnd.google-apps.spreadsheet',
    'application/vnd.google-apps.document',
];

const IGNORED_ERRORS = [429];

export default class Syncer {
    constructor(opts) {
        this.client = opts.client;
        this.fs = opts.fs;

        this.outputDirectories = opts.outputDirectory.split(',');

        if (!opts.state) {
            throw new Error(`must specify "state" option`);
        }

        this.state = new State(this.fs, opts.state);
        this.events = new EventEmitter();
    }

    sync() {
        this.events.emit('sync');

        return Promise.join(
            this.state.read(),
            this.client.authorize(), // TODO: re-use JWT tokens but handle expiry
            () => {
                return this.ensurePageTokenInState().then(this.fetchChanges);
            }
        );
    }

    fetchChanges = () => {
        return this.client
            .getChanges(this.state.getPageToken())
            .then(this.handleChanges)
            .then(this.fetchSpecialFiles)
            .catch(err => this.events.emit('error', err));
    };

    ensurePageTokenInState() {
        if (this.state.getPageToken()) {
            return Promise.resolve();
        } else {
            return this.client
                .getStartPageToken()
                .then(res => this.state.save({ pageToken: res.startPageToken - 1 }));
        }
    }

    on(...args) {
        this.events.on(...args);
    }

    handleChanges = list => {
        log('found changes', list.changes);

        const changes = list.changes.filter(
            i => !i.removed && mimeTypes.indexOf(i.file.mimeType) !== -1
        );

        log(`fetching ${changes.length}`);

        return Promise.map(changes, this.processChange, { concurrency: 3 })
            .then(() =>
                this.state.save({
                    pageToken: list.nextPageToken || list.newStartPageToken,
                    lastCheck: moment().format(),
                })
            )
            .then(state => this.events.emit('synced', state.data));
    };

    fetchSpecialFiles = () => {
        const ids = process.env.GDRIVE_ALWAYS_FETCH
            ? process.env.GDRIVE_ALWAYS_FETCH.split(',')
            : [];

        return Promise.map(
            ids,
            id => this.client.getFile(id).then(this.downloadFile),
            { concurrency: 1 }
        );
    };

    processChange = change => {
        return this.client
            .getFile(change.fileId)
            .then(file =>
                this.downloadFile(file).then(() =>
                    this.state.setFile(change.fileId, { change, file })
                )
            );
    };

    downloadFile = file => {
        let promise = null;

        switch (file.mimeType) {
            case 'application/vnd.google-apps.spreadsheet':
            case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
                promise = this.convertSpreadsheet(file).then(data => [
                    {
                        fileName: `${file.id}.json`,
                        data,
                        mimeType: 'application/json',
                    },
                ]);
                break;
            case 'application/vnd.google-apps.document':
                promise = this.convertDocument(file).then(results => [
                    {
                        fileName: `${file.id}.json`,
                        data: results.plain,
                        mimeType: 'application/json',
                    },
                    {
                        fileName: `${file.id}.styled.json`,
                        data: results.styled,
                        mimeType: 'application/json',
                    },
                    {
                        fileName: `${file.id}.aml`,
                        data: results.aml,
                        mimeType: 'text/plain',
                    },
                    {
                        fileName: `${file.id}.html`,
                        data: results.html,
                        mimeType: 'text/html',
                    },
                ]);

                break;
            default:
                throw new Error(`unknown mime type: ${file.mimeType}`);
        }

        const modification = this.getModification(file);

        return promise
            .then(results => {
                return Promise.each(results, ({ fileName, data, mimeType }) =>
                    this.save(fileName, {
                        title: file.name,
                        modification,
                        data,
                        mimeType,
                    })
                );
            })
            .catch(err => {
                console.error(file.webViewLink, err.message);
                this.events.emit('error', err);

                let isIgnored =
                    err.httpResponse &&
                    IGNORED_ERRORS.includes(err.httpResponse.statusCode);
                isIgnored = isIgnored || !!err.isArchieMLError;

                const preThrow = err.aml
                    ? this.save(`${file.id}.aml`, { data: err.aml })
                    : Promise.resolve();

                if (isIgnored) {
                    log('error ignored');
                    return preThrow;
                } else {
                    return preThrow.then(() => Promise.reject(err));
                }
            });
    };

    convertSpreadsheet(doc) {
        return new Promise((resolve, reject) => {
            this.client
                .getExportFileStream(
                    doc.id,
                    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
                )
                .catch(reject)
                .then(stream => {
                    const writable = new streamBuffers.WritableStreamBuffer();

                    stream
                        .on('end', () => {
                            log('end stream');
                            const buf = writable.getContents();
                            writable.end();
                            resolve(buf);
                        })
                        .on('error', reject)
                        .pipe(writable);
                });
        }).then(buf => SpreadsheetConverter.convert(buf.toString('binary')));
    }

    convertDocument(doc) {
        return this.client
            .exportFile(doc.id, 'text/html')
            .then(html =>
                Promise.props({
                    html,
                    plain: ArchieConverter.convert(html),
                    styled: ArchieConverter.convert(html, {
                        preserve_styles: ['bold', 'italic', 'underline'],
                    }),
                })
            )
            .then(({ plain, styled, html }) => ({
                plain: plain.result,
                styled: styled.result,
                aml: plain.aml,
                html,
            }));
    }

    getModification(doc) {
        const now = moment();
        const modified = moment(doc.modifiedTime);
        const lag = moment.duration(now.diff(modified));

        const user = doc.lastModifyingUser || doc.sharingUser || {};

        const result = {
            user: {
                name: user.displayName,
                email: user.emailAddress,
            },
            date: modified.format(),
            version: doc.version,
            downloadTime: now.format(),
            downloadLag: {
                ms: lag.valueOf(),
                human: lag.humanize(),
            },
        };

        const previous = this.state.getFile(doc.id);

        if (previous && previous.file && previous.file.version) {
            result.lastUpdate = {
                version: previous.file.version,
                changeTime: previous.change
                    ? moment(previous.change.time).format()
                    : null,
            };

            if (+previous.file.version && +doc.version) {
                result.versionDiff = +doc.version - +previous.file.version;
            }
        }

        return result;
    }

    save(fileName, data) {
        if (data === null) {
            // probably an operational error - don't write the file
            return Promise.resolve();
        }

        const body = fileName.endsWith('.json') ? JSON.stringify(data) : data.data;

        return Promise.map(this.outputDirectories, dir => {
            const filePath = path.join(dir, fileName);

            return this.fs
                .write(filePath, body, { mimeType: data.mimeType })
                .then(() => this.events.emit('saved', fileName, data));
        });
    }
}
<|endoftext|>


<commit_before>var tipBox = {
	tip:(function(element,text,options){

	if(typeof element == 'string'){
		element = '#' + element;

		if(document.querySelector(element)==null)
			throw new Exception('Element ' + element + " does not exist!");
	}

	 var _target = (typeof element == 'string') ? document.querySelector(element): element,
	 	 _webkit = navigator.userAgent.match(/WebKit\/([\d.]+)/) ? true : false,
	 	 _gecko2 = navigator.userAgent.match(/(rv:2)(.*)(Gecko\/([\d.]+))/) ? true : false,
         _text = text,
         _tipBox,
         _showTimer,
         _hideTimer,
         _visible = false,
         _addEvent = function(el,event,callback){
		 	if(el.addEventListener)
		 		el.addEventListener(event,callback,false);
		 	else
		 		el.attachEvent(event,callback);
	 	 },
	 	 _removeEvent = function(el,event,callback){
	 		 if(el.removeEventListener)
	 			 el.removeEventListener(event,callback,false);
	 		 else
	 			 el.detachEvent(event,callback);
	 	 },
         _getObjKeys = function(obj){
	   			if(typeof obj != 'Object')
	   				return [];

	   			var keys = [];
	   			for(var prop in obj)
	   				keys.push(prop);

	   			return keys;
	   		},
	   	_getObjVars = function(obj){
	   			if(typeof obj != 'Object')
	   				return [];

	   			var vars = [];
	   			for(var prop in obj)
	   				vars.push(obj[prop]);

	   			return vars;
	   		},
         _mergeOptions = function(destination,source){
	   			var property,prop;
	   			for (property in source){
	   				if(_getObjVars(destination[property]).length>0 && this._getObjVars(source[property]).length>0){
	   					for(prop in source[property])
	   	 			 		destination[property][prop] = source[property][prop];
	   	 			 } else {
	   	 			 	destination[property] = source[property];
	   	 			 }
	   	 		}
	   			return destination;
	   		},
        _parseTxt = function(){
              if(typeof _text == 'string')
                   return _options.template.replace('[text]',_text);
              var i,
                  template = _options.template;
              for(var prop in _text)
                   template=template.replace('[' + prop + ']',_text[prop]);

              return template;

         },
         _options = _mergeOptions({
        	 				snapTo:'element',
        	 				position: 'top-right',
                            showOn: 'mouseover',
                            showDelay:1,
                            hideOn: 'mouseout',
                            hideDelay:1,
                            template: "
[text]
",
                            animation:"none",
                            duration:0.5,
                            easing:''
                            },options),
         _position = function(e){
        	 var snapTo = {},
        	 	docElement = document.documentElement,
        	 	body = document.body || { scrollLeft: 0 };

        	if(_options.snapTo=='mouse')
        		snapTo = {
    					'top':e.pageY || (e.clientY + (docElement.scrollTop || body.scrollTop) - (docElement.clientTop || 0)),
    					'left': e.pageX || (e.clientX + (docElement.scrollLeft || body.scrollLeft) - (docElement.clientLeft || 0)),
    					'width':0,
    					'height':0
    				};
           	else
        		snapTo = {
    					'top':_target.offsetTop,
    					'left': _target.offsetLeft,
    					'width':_target.offsetWidth,
    					'height':_target.offsetHeight
    				};

        		//get y position
        		if(_options.position.indexOf('top')!=-1)
        			_tipBox.style.top = snapTo.top - _tipBox.offsetHeight + "px";
        		else if(_options.position.indexOf('bottom')!=-1)
        			_tipBox.style.top = snapTo.top + snapTo.height + "px";
        		else
        			_tipBox.style.top = snapTo.top - ((_tipBox.offsetHeight-snapTo.height)/2) + "px";

        		//get x position
        		if(_options.position.indexOf('left')!=-1)
        			_tipBox.style.left = snapTo.left - _tipBox.offsetWidth + "px";
        		else if(_options.position.indexOf('right')!=-1)
        			_tipBox.style.left = snapTo.left + snapTo.width + "px";
        		else
        			_tipBox.style.left = snapTo.left - ((_tipBox.offsetWidth-snapTo.width)/2) + "px";

        	if(_options.snapTo=='mouse'){
        		_addEvent(_target,'mousemove',_position);
        		_addEvent(_tipBox,'mousemove',_position);
        	}
         },
         _animate = function(action,callback){
         	switch(_options.animation){
         		case 'fade':
         			if(_webkit){
         				_tipBox.style.setProperty('-webkit-transition','opacity ' + _options.duration + "s " + _options.easing,'');
         				_tipBox.style.opacity = (action=='show') ? 1 : 0;
         				if(callback !== undefined)
         					_addEvent(_tipBox,'transitionend',callback);
         			} else if(_gecko2){
         				_tipBox.style.setProperty('-moz-transition','opacity ' + _options.duration + "s " + _options.easing,'');
         				_tipBox.style.opacity = 1;
         				_tipBox.style.opacity = (action=='show') ? 1 : 0;
         				if(callback !== undefined)
         					_addEvent(_tipBox,'transitionend',callback);
         			} else {
         				var start = (new Date).getTime(),
         					duration = _options.duration*1000,
         					finish = start + duration,
         					orig = (action=='hide') ? 1 : 0,
         					target = (action=='show') ? 1 : 0,
         					interval = setInterval(function(){
         						var time = (new Date).getTime(),
         							pos = time > finish ? 1 : (time-start)/duration;
         							_tipBox.style.opacity = orig + ((target-orig) * pos);

         						if(time>finish)
         							clearInterval(interval);
         					},10);

         			}
         	}
         },
         _draw = function(e){
        	_tipBox = document.createElement('div');
          	_tipBox.innerHTML = _parseTxt();
          	_tipBox.style.position = 'absolute';
          	if(_options.animation=="fade")
         		_tipBox.style.opacity = 0;

            _target.parentNode.insertBefore(_tipBox,_target.nextSibling);
            _position(e);
            if(_options.animation!="none")
            	_animate('show');

            if(_options.hideOn=='mouseout'){
            	_addEvent(_tipBox,'mouseover',function(e){window.clearTimeout(_hideTimer)});
               	_addEvent(_tipBox,'mouseout',hide);
            }
            _visible = true;
         },
         _remove = function(){
         	if(_options.animation != 'none'){
         		_animate('hide',function(e){_target.parentNode.removeChild(_tipBox);_tipBox = null})
         	} else {
        	 	_target.parentNode.removeChild(_tipBox);
        	 	_tipBox = null;
        	 }
        	 if(_options.snapTo=='mouse')
        		 _removeEvent(_target,'mousemove',_position);

        	 _visible = false;
         },
         show = function(e){
        	if(!_visible)
               	_showTimer = setTimeout(_draw,_options.showDelay*1000,e);
        	else
        		window.clearTimeout(_hideTimer);
         },
         hide = function(e){
        	 if(_visible)
        		 _hideTimer = setTimeout(_remove,_options.hideDelay*1000);
        	 else
        		 window.clearTimeout(_showTimer);
         };

         if(_options.snapTo=='mouse')
        	 _options.hideDelay=0;

         _addEvent(_target,_options.showOn,show);
         _addEvent(_target,_options.hideOn,hide);

         return {
        	 show:show,
        	 hide:hide
         }
	}),
	scan: function(className,options){
		//auto magical toolbox generation
		var tips = [],
			triggers = document.querySelectorAll('.' + className),
			tipOptions = options || {};
		for(var i= 0; i<triggers.length;i++)
			tips.push(new tipBox.tip(triggers[i],triggers[i].title,tipOptions));
	}
}

<commit_msg>enabled callbacks on js animation<commit_after>var tipBox = {
	tip:(function(element,text,options){

	if(typeof element == 'string'){
		element = '#' + element;

		if(document.querySelector(element)==null)
			throw new Exception('Element ' + element + " does not exist!");
	}

	 var _target = (typeof element == 'string') ? document.querySelector(element): element,
	 	 _webkit = navigator.userAgent.match(/WebKit\/([\d.]+)/) ? true : false,
	 	 _gecko2 = navigator.userAgent.match(/(rv:2)(.*)(Gecko\/([\d.]+))/) ? true : false,
         _text = text,
         _tipBox,
         _showTimer,
         _hideTimer,
         _visible = false,
         _addEvent = function(el,event,callback){
		 	if(el.addEventListener)
		 		el.addEventListener(event,callback,false);
		 	else
		 		el.attachEvent(event,callback);
	 	 },
	 	 _removeEvent = function(el,event,callback){
	 		 if(el.removeEventListener)
	 			 el.removeEventListener(event,callback,false);
	 		 else
	 			 el.detachEvent(event,callback);
	 	 },
         _getObjKeys = function(obj){
	   			if(typeof obj != 'Object')
	   				return [];

	   			var keys = [];
	   			for(var prop in obj)
	   				keys.push(prop);

	   			return keys;
	   		},
	   	_getObjVars = function(obj){
	   			if(typeof obj != 'Object')
	   				return [];

	   			var vars = [];
	   			for(var prop in obj)
	   				vars.push(obj[prop]);

	   			return vars;
	   		},
         _mergeOptions = function(destination,source){
	   			var property,prop;
	   			for (property in source){
	   				if(_getObjVars(destination[property]).length>0 && this._getObjVars(source[property]).length>0){
	   					for(prop in source[property])
	   	 			 		destination[property][prop] = source[property][prop];
	   	 			 } else {
	   	 			 	destination[property] = source[property];
	   	 			 }
	   	 		}
	   			return destination;
	   		},
        _parseTxt = function(){
              if(typeof _text == 'string')
                   return _options.template.replace('[text]',_text);
              var i,
                  template = _options.template;
              for(var prop in _text)
                   template=template.replace('[' + prop + ']',_text[prop]);

              return template;

         },
         _options = _mergeOptions({
        	 				snapTo:'element',
        	 				position: 'top-right',
                            showOn: 'mouseover',
                            showDelay:1,
                            hideOn: 'mouseout',
                            hideDelay:1,
                            template: "
[text]
",
                            animation:"none",
                            duration:0.5,
                            easing:''
                            },options),
         _position = function(e){
        	 var snapTo = {},
        	 	docElement = document.documentElement,
        	 	body = document.body || { scrollLeft: 0 };

        	if(_options.snapTo=='mouse')
        		snapTo = {
    					'top':e.pageY || (e.clientY + (docElement.scrollTop || body.scrollTop) - (docElement.clientTop || 0)),
    					'left': e.pageX || (e.clientX + (docElement.scrollLeft || body.scrollLeft) - (docElement.clientLeft || 0)),
    					'width':0,
    					'height':0
    				};
           	else
        		snapTo = {
    					'top':_target.offsetTop,
    					'left': _target.offsetLeft,
    					'width':_target.offsetWidth,
    					'height':_target.offsetHeight
    				};

        		//get y position
        		if(_options.position.indexOf('top')!=-1)
        			_tipBox.style.top = snapTo.top - _tipBox.offsetHeight + "px";
        		else if(_options.position.indexOf('bottom')!=-1)
        			_tipBox.style.top = snapTo.top + snapTo.height + "px";
        		else
        			_tipBox.style.top = snapTo.top - ((_tipBox.offsetHeight-snapTo.height)/2) + "px";

        		//get x position
        		if(_options.position.indexOf('left')!=-1)
        			_tipBox.style.left = snapTo.left - _tipBox.offsetWidth + "px";
        		else if(_options.position.indexOf('right')!=-1)
        			_tipBox.style.left = snapTo.left + snapTo.width + "px";
        		else
        			_tipBox.style.left = snapTo.left - ((_tipBox.offsetWidth-snapTo.width)/2) + "px";

        	if(_options.snapTo=='mouse'){
        		_addEvent(_target,'mousemove',_position);
        		_addEvent(_tipBox,'mousemove',_position);
        	}
         },
         _animate = function(action,callback){
         	switch(_options.animation){
         		case 'fade':
         			if(_webkit){
         				_tipBox.style.setProperty('-webkit-transition','opacity ' + _options.duration + "s " + _options.easing,'');
         				_tipBox.style.opacity = (action=='show') ? 1 : 0;
         				if(callback !== undefined)
         					_addEvent(_tipBox,'transitionend',callback);
         			} else if(_gecko2){
         				_tipBox.style.setProperty('-moz-transition','opacity ' + _options.duration + "s " + _options.easing,'');
         				_tipBox.style.opacity = 1;
         				_tipBox.style.opacity = (action=='show') ? 1 : 0;
         				if(callback !== undefined)
         					_addEvent(_tipBox,'transitionend',callback);
         			} else {
         				var start = (new Date).getTime(),
         					duration = _options.duration*1000,
         					finish = start + duration,
         					orig = (action=='hide') ? 1 : 0,
         					target = (action=='show') ? 1 : 0,
         					interval = setInterval(function(){
         						var time = (new Date).getTime(),
         							pos = time > finish ? 1 : (time-start)/duration;
         							_tipBox.style.opacity = orig + ((target-orig) * pos);

         						if(time>finish){
         							clearInterval(interval);
         							if(callback !== undefined)
         								callback.call();
         						}
         					},10);

         			}
         	}
         },
         _draw = function(e){
        	_tipBox = document.createElement('div');
          	_tipBox.innerHTML = _parseTxt();
          	_tipBox.style.position = 'absolute';
          	if(_options.animation=="fade")
         		_tipBox.style.opacity = 0;

            _target.parentNode.insertBefore(_tipBox,_target.nextSibling);
            _position(e);
            if(_options.animation!="none")
            	_animate('show');

            if(_options.hideOn=='mouseout'){
            	_addEvent(_tipBox,'mouseover',function(e){window.clearTimeout(_hideTimer)});
               	_addEvent(_tipBox,'mouseout',hide);
            }
            _visible = true;
         },
         _remove = function(){
         	if(_options.animation != 'none'){
         		_animate('hide',function(){_target.parentNode.removeChild(_tipBox);_tipBox = null})
         	} else {
        	 	_target.parentNode.removeChild(_tipBox);
        	 	_tipBox = null;
        	 }
        	 if(_options.snapTo=='mouse')
        		 _removeEvent(_target,'mousemove',_position);

        	 _visible = false;
         },
         show = function(e){
        	if(!_visible)
               	_showTimer = setTimeout(_draw,_options.showDelay*1000,e);
        	else
        		window.clearTimeout(_hideTimer);
         },
         hide = function(e){
        	 if(_visible)
        		 _hideTimer = setTimeout(_remove,_options.hideDelay*1000);
        	 else
        		 window.clearTimeout(_showTimer);
         };

         if(_options.snapTo=='mouse')
        	 _options.hideDelay=0;

         _addEvent(_target,_options.showOn,show);
         _addEvent(_target,_options.hideOn,hide);

         return {
        	 show:show,
        	 hide:hide
         }
	}),
	scan: function(className,options){
		//auto magical toolbox generation
		var tips = [],
			triggers = document.querySelectorAll('.' + className),
			tipOptions = options || {};
		for(var i= 0; i<triggers.length;i++)
			tips.push(new tipBox.tip(triggers[i],triggers[i].title,tipOptions));
	}
}

<|endoftext|>


<commit_before>var fs = require('fs.extra');
var path = require('path');
var check = require('check-types');
var parseCode = require('./parser').parseCode;
var parseUnitTestCode = require('./parserUnitTest').parseUnitTestCode;
var reformat = require('./code').reformat;
var moment = require('moment');
var sampleDiv = require('./sample');
var exampleDiv = require('./example');
var rethrow = require('./errors').rethrow;

var html = require('pithy');
var pretty = require('html/lib/html').prettyPrint;
var prettyOptions = { indent_size: 2 };
var apiComments = null;

function copyAndIncludeScript(filename, destinationFolder) {
	check.verifyString(filename, 'missing script filename');
	check.verifyString(destinationFolder, 'missing destination folder');

	var basename = path.basename(filename);
	check.verifyString(basename, 'could not get base name from ' + filename);
	fs.copy(path.join(__dirname, filename),
		path.join(destinationFolder, basename),
		rethrow);
	var script = html.script({
		src: basename
	});
	return script;
}

module.exports = function (apiJson, options) {
	check.verifyArray(apiJson, 'missing api array');
	check.verifyObject(options, 'missing options');

	check.verifyString(options.outputFolder, 'missing output folder in ' + JSON.stringify(options));

	var htmlFilename = path.join(options.outputFolder, "index.html");
	check.verifyString(htmlFilename, 'missing output filename');

	console.log('generating docs', options.outputFolder);
	var title = options.title || 'API';
	check.verifyString(title, 'missing title ' + title);

	var o = '<!DOCTYPE HTML>\n';
	o += '\n';

	var titleElement = html.title(null, title);

	/* disable IE shim for now, need to figure out how to include this in pithy */
	/*
	o += '\n';
	*/

	fs.copy(path.join(__dirname, 'api.css'),
		path.join(options.outputFolder, 'api.css'),
		rethrow);

	var apiCss = html.link({
    	rel: 'stylesheet',
    	href: 'api.css'
	});

	var jqueryJs = html.script({
		src: 'http://code.jquery.com/jquery-1.9.1.min.js'
	});
	var codePrettifyJs = html.script({
		src: 'https://google-code-prettify.googlecode.com/svn/loader/run_prettify.js?skin=desert'
	});

	var toggleJs = copyAndIncludeScript('toggle.js', options.outputFolder);

	var headElement = html.head(null, [
		titleElement,
		apiCss,
		jqueryJs,
		codePrettifyJs,
		toggleJs
		]);
	o += pretty(headElement.toString(), prettyOptions) + '\n';

	var apiVersion = options.apiVersion || '';

	var prevFilename = null;
	var rootModule = {};
	var currentModule = rootModule;

	apiComments = apiJson;
	apiComments.forEach(function (apiComment) {
		check.verifyString(apiComment.filename, 'missing filename');
		if (apiComment.filename !== prevFilename) {
			prevFilename = apiComment.filename;
			currentModule = rootModule;
		}
		if (isModule(apiComment)) {
			var name = getModuleName(apiComment);
			check.verifyString(name, 'invalid module name');
			currentModule = setupModule(name, rootModule);
		}
		check.verifyObject(currentModule, 'invalid current module');
		if (typeof currentModule.methodDocs === 'undefined') {
			currentModule.methodDocs = [];
		}

		// console.log('checking comment', apiComment);
		if (!isMethod(apiComment)) {
			return;
		}
		// console.log('found method comment');
		var info = methodDiv(apiComment);
		check.verifyObject(info.name, 'did not get method name');
		check.verifyObject(info.docs, 'did not get method docs');
		currentModule.methodDocs.push(info);
	});

	var doc = {
		index: [],
		docs: []
	};
	docModule(rootModule, doc);

	var indexElement = html.div({
		id: 'index'
	}, [
		html.h1({
			id: 'mainTitle'
		}, [title,
			html.sub(null, [apiVersion])
		])
	].concat(doc.index));

	var repoUrl = 'https://github.com/bahmutov/xplain';
	var repoHref = html.a({
		href: repoUrl
	}, 'xplained');
	var signature = html.span({	class: "timestamp" }, [
		repoHref,
		' on ' + moment().local().format('dddd, MMMM Do YYYY, h:mm:ss a')
	]);

	var docsElement = html.div({
		id: 'docs'
	}, doc.docs.concat(signature));

	var contentElement = html.div({
		class: 'content'
	}, [indexElement, docsElement]);

	var toggleStart = copyAndIncludeScript('toggleStart.js', options.outputFolder);

	var body = html.body(null, [contentElement, toggleStart]);
	o += body.toString() + '\n';
	o += '';
	fs.writeFileSync(htmlFilename, o, 'utf-8');
};

function docModule(aModule, doc) {
	check.verifyObject(aModule, 'missing module');
	check.verifyArray(doc.index, 'missing index array');
	check.verifyArray(doc.docs, 'missing docs array');

	if (Array.isArray(aModule.methodDocs)) {
		if (aModule.name) {
			check.verifyString(aModule.name, 'missing module name');
			doc.index.push(html.div({
				class: "moduleName"
			}, [aModule.name]));
		}
		aModule.methodDocs.forEach(function (info) {
			doc.index.push(info.name);
			doc.docs.push(info.docs);
		});
	}

	Object.keys(aModule).forEach(function (key) {
		if (key === 'methodDocs') {
			return;
		}
		if (key === 'name') {
			return;
		}
		var value = aModule[key];
		docModule(value, doc);
	});
}

function fileContents(name) {
	check.verifyString(name, 'missing file name');
	var cssFilename = path.join(__dirname, name);
	var cssText = fs.readFileSync(cssFilename, 'utf8');
	return cssText;
}

function getModuleName(apiComment)
{
	check.verifyObject(apiComment, 'invalid api comment');
	var name = null;
	apiComment.tags.some(function (tag) {
		if (tag.type === 'module') {
			name = tag.string;
			return true;
		}
	});
	return name;
}

function setupModule(name, rootModule)
{
	check.verifyString(name, 'invalid module name');
	check.verifyObject(rootModule, 'invalid root module');
	console.log('setup module', name);
	var parts = name.split('/');
	var currentModule = rootModule;
	parts.forEach(function (part) {
		if (typeof currentModule[part] === 'undefined') {
			currentModule[part] = {};
		}
		currentModule = currentModule[part];
	});
	currentModule.name = name;
	return currentModule;
}

function isModule(apiComment) {
	if (!Array.isArray(apiComment.tags)) {
		return false;
	}
	return apiComment.tags.some(function (tag) {
		return tag.type === 'module';
	});
}

function isMethod(apiComment) {
	if (!Array.isArray(apiComment.tags)) {
		return false;
	}
	return apiComment.tags.some(function (tag) {
		return tag.type === 'method';
	});
}

function isExampleFor(apiComment, name) {
	if (!Array.isArray(apiComment.tags)) {
		return false;
	}
	return apiComment.tags.some(function (tag) {
		return (tag.type === 'exampleFor' || tag.type === 'example')
			&& tag.string === name;
	});
}

function isSampleFor(apiComment, name) {
	if (!Array.isArray(apiComment.tags)) {
		return false;
	}
	return apiComment.tags.some(function (tag) {
		return (tag.type === 'sampleFor' || tag.type === 'sample')
			&& tag.string === name;
	});
}

function examplesFor(name) {
	check.verifyString(name, 'missing name');
	check.verifyArray(apiComments, 'missing api comments');
	var apiExamples = apiComments.filter(function (apiComment) {
		return isExampleFor(apiComment, name);
	});
	console.log('have', apiExamples.length, 'examples for', name);
	// console.log(apiExamples);

	var examples = apiExamples.map(function (example) {
		return exampleDiv(name, example);
	});
	return examples;
}

function samplesFor(name) {
	check.verifyString(name, 'missing name');
	check.verifyArray(apiComments, 'missing api comments');
	var apiSamples = apiComments.filter(function (apiComment) {
		return isSampleFor(apiComment, name);
	});
	console.log('have', apiSamples.length, 'samples for', name);
	var samples = apiSamples.map(sampleDiv);
	return samples;
}

function codeDiv(id, apiComment) {
	check.verifyString(id, 'missing code id');
	check.verifyString(apiComment.code, 'missing code');

	var prettyCode = reformat(apiComment.code, true);
	check.verifyString(prettyCode, 'could not make code pretty for\n', apiComment.code);
	var name = apiComment.ctx.type + ' ' + apiComment.ctx.name;

	var codeElement = html.div({
		id: id + 'd',
		class: "methodCode namedCode"
	}, [
		html.span({
			class: "sampleName"
		}, name),
		html.pre({
			class: "prettyprint linenums"
		}, prettyCode)
	]);

	return codeElement;
}

function methodDiv(apiComment) {
	check.verifyObject(apiComment, 'missing api comment object');
	console.assert(apiComment.ctx, 'missing ctx property');
	console.assert(apiComment.ctx.type === 'function', 'ctx is not function');
	check.verifyString(apiComment.ctx.name, 'missing function name');
	var name = apiComment.ctx.name;

	var samples = samplesFor(name);
	var toggles = [];
	var exampleElements = [];

	var examples = examplesFor(name);
	check.verifyArray(examples, 'could not get examples tags');
	examples.forEach(function (example) {
		toggles.push(example.toggle);
		exampleElements.push(example.code);
	});

	var id = name + '_code_toggle';
	var sourceToggle = html.input({
		class: "toggle",
		type: "button",
		value: "source",
		id: id
	});
	toggles.push(sourceToggle);

	var togglesElement = html.div({
		class: "toggles"
	}, toggles);

	var codeElement = codeDiv(id, apiComment);

	console.log(apiComment.description.summary);
	var methodElement = html.div({
		id: name,
		class: "method"
	}, [html.h3(null, name), new html.SafeString(apiComment.description.summary)]
		.concat(samples)
		.concat(togglesElement)
		.concat(exampleElements)
		.concat(codeElement)
	);

	var nameElement = html.div(null, [
		html.a({ href: '#' + name }, [name])
	]);
	return {
		name: nameElement,
		docs: methodElement
	};
}<commit_msg>main body generated using pithy<commit_after>var fs = require('fs.extra');
var path = require('path');
var check = require('check-types');
var parseCode = require('./parser').parseCode;
var parseUnitTestCode = require('./parserUnitTest').parseUnitTestCode;
var reformat = require('./code').reformat;
var moment = require('moment');
var sampleDiv = require('./sample');
var exampleDiv = require('./example');
var rethrow = require('./errors').rethrow;

var html = require('pithy');
var pretty = require('html/lib/html').prettyPrint;
var prettyOptions = { indent_size: 2 };
var apiComments = null;

function copyAndIncludeScript(filename, destinationFolder) {
	check.verifyString(filename, 'missing script filename');
	check.verifyString(destinationFolder, 'missing destination folder');

	var basename = path.basename(filename);
	check.verifyString(basename, 'could not get base name from ' + filename);
	fs.copy(path.join(__dirname, filename),
		path.join(destinationFolder, basename),
		rethrow);
	var script = html.script({
		src: basename
	});
	return script;
}

module.exports = function (apiJson, options) {
	check.verifyArray(apiJson, 'missing api array');
	check.verifyObject(options, 'missing options');

	check.verifyString(options.outputFolder, 'missing output folder in ' + JSON.stringify(options));

	var htmlFilename = path.join(options.outputFolder, "index.html");
	check.verifyString(htmlFilename, 'missing output filename');

	console.log('generating docs', options.outputFolder);
	var title = options.title || 'API';
	check.verifyString(title, 'missing title ' + title);

	var o = '<!DOCTYPE HTML>\n';
	// o += '\n';

	var titleElement = html.title(null, title);

	/* disable IE shim for now, need to figure out how to include this in pithy */
	/*
	o += '\n';
	*/

	fs.copy(path.join(__dirname, 'api.css'),
		path.join(options.outputFolder, 'api.css'),
		rethrow);

	var apiCss = html.link({
    	rel: 'stylesheet',
    	href: 'api.css'
	});

	var jqueryJs = html.script({
		src: 'http://code.jquery.com/jquery-1.9.1.min.js'
	});
	var codePrettifyJs = html.script({
		src: 'https://google-code-prettify.googlecode.com/svn/loader/run_prettify.js?skin=desert'
	});

	var toggleJs = copyAndIncludeScript('toggle.js', options.outputFolder);

	var headElement = html.head(null, [
		titleElement,
		apiCss,
		jqueryJs,
		codePrettifyJs,
		toggleJs
		]);
	// o += pretty(headElement.toString(), prettyOptions) + '\n';

	var apiVersion = options.apiVersion || '';

	var prevFilename = null;
	var rootModule = {};
	var currentModule = rootModule;

	apiComments = apiJson;
	apiComments.forEach(function (apiComment) {
		check.verifyString(apiComment.filename, 'missing filename');
		if (apiComment.filename !== prevFilename) {
			prevFilename = apiComment.filename;
			currentModule = rootModule;
		}
		if (isModule(apiComment)) {
			var name = getModuleName(apiComment);
			check.verifyString(name, 'invalid module name');
			currentModule = setupModule(name, rootModule);
		}
		check.verifyObject(currentModule, 'invalid current module');
		if (typeof currentModule.methodDocs === 'undefined') {
			currentModule.methodDocs = [];
		}

		// console.log('checking comment', apiComment);
		if (!isMethod(apiComment)) {
			return;
		}
		// console.log('found method comment');
		var info = methodDiv(apiComment);
		check.verifyObject(info.name, 'did not get method name');
		check.verifyObject(info.docs, 'did not get method docs');
		currentModule.methodDocs.push(info);
	});

	var doc = {
		index: [],
		docs: []
	};
	docModule(rootModule, doc);

	var indexElement = html.div({
		id: 'index'
	}, [
		html.h1({
			id: 'mainTitle'
		}, [title,
			html.sub(null, [apiVersion])
		])
	].concat(doc.index));

	var repoUrl = 'https://github.com/bahmutov/xplain';
	var repoHref = html.a({
		href: repoUrl
	}, 'xplained');
	var signature = html.span({	class: "timestamp" }, [
		repoHref,
		' on ' + moment().local().format('dddd, MMMM Do YYYY, h:mm:ss a')
	]);

	var docsElement = html.div({
		id: 'docs'
	}, doc.docs.concat(signature));

	var contentElement = html.div({
		class: 'content'
	}, [indexElement, docsElement]);

	var toggleStart = copyAndIncludeScript('toggleStart.js', options.outputFolder);

	var body = html.body(null, [contentElement, toggleStart]);
	var htmlElement = html.html(null, [headElement, body]);
	// o += body.toString() + '\n';
	// o += '';
	o += htmlElement.toString();
	fs.writeFileSync(htmlFilename, o, 'utf-8');
};

function docModule(aModule, doc) {
	check.verifyObject(aModule, 'missing module');
	check.verifyArray(doc.index, 'missing index array');
	check.verifyArray(doc.docs, 'missing docs array');

	if (Array.isArray(aModule.methodDocs)) {
		if (aModule.name) {
			check.verifyString(aModule.name, 'missing module name');
			doc.index.push(html.div({
				class: "moduleName"
			}, [aModule.name]));
		}
		aModule.methodDocs.forEach(function (info) {
			doc.index.push(info.name);
			doc.docs.push(info.docs);
		});
	}

	Object.keys(aModule).forEach(function (key) {
		if (key === 'methodDocs') {
			return;
		}
		if (key === 'name') {
			return;
		}
		var value = aModule[key];
		docModule(value, doc);
	});
}

function fileContents(name) {
	check.verifyString(name, 'missing file name');
	var cssFilename = path.join(__dirname, name);
	var cssText = fs.readFileSync(cssFilename, 'utf8');
	return cssText;
}

function getModuleName(apiComment)
{
	check.verifyObject(apiComment, 'invalid api comment');
	var name = null;
	apiComment.tags.some(function (tag) {
		if (tag.type === 'module') {
			name = tag.string;
			return true;
		}
	});
	return name;
}

function setupModule(name, rootModule)
{
	check.verifyString(name, 'invalid module name');
	check.verifyObject(rootModule, 'invalid root module');
	console.log('setup module', name);
	var parts = name.split('/');
	var currentModule = rootModule;
	parts.forEach(function (part) {
		if (typeof currentModule[part] === 'undefined') {
			currentModule[part] = {};
		}
		currentModule = currentModule[part];
	});
	currentModule.name = name;
	return currentModule;
}

function isModule(apiComment) {
	if (!Array.isArray(apiComment.tags)) {
		return false;
	}
	return apiComment.tags.some(function (tag) {
		return tag.type === 'module';
	});
}

function isMethod(apiComment) {
	if (!Array.isArray(apiComment.tags)) {
		return false;
	}
	return apiComment.tags.some(function (tag) {
		return tag.type === 'method';
	});
}

function isExampleFor(apiComment, name) {
	if (!Array.isArray(apiComment.tags)) {
		return false;
	}
	return apiComment.tags.some(function (tag) {
		return (tag.type === 'exampleFor' || tag.type === 'example')
			&& tag.string === name;
	});
}

function isSampleFor(apiComment, name) {
	if (!Array.isArray(apiComment.tags)) {
		return false;
	}
	return apiComment.tags.some(function (tag) {
		return (tag.type === 'sampleFor' || tag.type === 'sample')
			&& tag.string === name;
	});
}

function examplesFor(name) {
	check.verifyString(name, 'missing name');
	check.verifyArray(apiComments, 'missing api comments');
	var apiExamples = apiComments.filter(function (apiComment) {
		return isExampleFor(apiComment, name);
	});
	console.log('have', apiExamples.length, 'examples for', name);
	// console.log(apiExamples);

	var examples = apiExamples.map(function (example) {
		return exampleDiv(name, example);
	});
	return examples;
}

function samplesFor(name) {
	check.verifyString(name, 'missing name');
	check.verifyArray(apiComments, 'missing api comments');
	var apiSamples = apiComments.filter(function (apiComment) {
		return isSampleFor(apiComment, name);
	});
	console.log('have', apiSamples.length, 'samples for', name);
	var samples = apiSamples.map(sampleDiv);
	return samples;
}

function codeDiv(id, apiComment) {
	check.verifyString(id, 'missing code id');
	check.verifyString(apiComment.code, 'missing code');

	var prettyCode = reformat(apiComment.code, true);
	check.verifyString(prettyCode, 'could not make code pretty for\n', apiComment.code);
	var name = apiComment.ctx.type + ' ' + apiComment.ctx.name;

	var codeElement = html.div({
		id: id + 'd',
		class: "methodCode namedCode"
	}, [
		html.span({
			class: "sampleName"
		}, name),
		html.pre({
			class: "prettyprint linenums"
		}, prettyCode)
	]);

	return codeElement;
}

function methodDiv(apiComment) {
	check.verifyObject(apiComment, 'missing api comment object');
	console.assert(apiComment.ctx, 'missing ctx property');
	console.assert(apiComment.ctx.type === 'function', 'ctx is not function');
	check.verifyString(apiComment.ctx.name, 'missing function name');
	var name = apiComment.ctx.name;

	var samples = samplesFor(name);
	var toggles = [];
	var exampleElements = [];

	var examples = examplesFor(name);
	check.verifyArray(examples, 'could not get examples tags');
	examples.forEach(function (example) {
		toggles.push(example.toggle);
		exampleElements.push(example.code);
	});

	var id = name + '_code_toggle';
	var sourceToggle = html.input({
		class: "toggle",
		type: "button",
		value: "source",
		id: id
	});
	toggles.push(sourceToggle);

	var togglesElement = html.div({
		class: "toggles"
	}, toggles);

	var codeElement = codeDiv(id, apiComment);

	console.log(apiComment.description.summary);
	var methodElement = html.div({
		id: name,
		class: "method"
	}, [html.h3(null, name), new html.SafeString(apiComment.description.summary)]
		.concat(samples)
		.concat(togglesElement)
		.concat(exampleElements)
		.concat(codeElement)
	);

	var nameElement = html.div(null, [
		html.a({ href: '#' + name }, [name])
	]);
	return {
		name: nameElement,
		docs: methodElement
	};
}<|endoftext|>