<commit_before>/**
 * The Users namespace contains operations available for the known users of your bot.
 * @public
 * @namespace Users
 * @example
 * bp.users
 */

import _ from 'lodash'
import helpers from './database/helpers'

module.exports = ({ db }) => {
  /**
   * Returns whether or not a user has a specific tag or not.
   * @param  {String} userId
   * @param  {String} tag    The name of the tag. Case insensitive.
   * @return {Boolean}
   * @async
   * @memberof! Users
   * @example
   * if (await bp.users.hasTag(event.user.id, 'IS_SUBSCRIBED')) {...}
   */
  const hasTag = async (userId, tag) => {
    const knex = await db.get()

    return knex('users_tags')
      .select('userId')
      .where({ userId, tag: _.toUpper(tag) })
      .limit(1)
      .then(ret => ret.length > 0)
  }

  /**
   * Tags a user with a specific tag (or overwrites an existing one) and a given value for that tag (optional).
   * Tags can be used to classify users (no value needed) or to store information about them (with a tag value).
   * Values are useful to store user information like emails, etc.
   * Value is always assumed to be a string.
   * @param  {String}  userId
   * @param  {String}  tag    The name of the tag.
   * Case insensitive. Note that this property will always be upper-cased.
   * @param  {String} [value]  Any string value to store info about this tag
   * @async
   * @memberof! Users
   * @example
   * await bp.users.tag(event.user.id, 'EMAIL', 'sylvain@botpress.io')
   * await bp.users.tag(event.user.id, 'PAYING_USER')
   */
  const tag = async (userId, tag, value = true) => {
    const knex = await db.get()

    tag = _.toUpper(tag)

    if (await hasTag(userId, tag)) {
      await knex('users_tags')
        .where({ userId, tag })
        .update({
          userId,
          tag,
          value,
          tagged_on: helpers(knex).date.now()
        })
        .then()
    } else {
      await knex('users_tags')
        .insert({
          userId,
          tag,
          value,
          tagged_on: helpers(knex).date.now()
        })
        .then()
    }
  }

  /**
   * Removes a tag from a user if it exists.
   * @param  {String} userId
   * @param  {String} tag    Name of the tag. Case-insensitive.
   * @memberof! Users
   */
  const untag = async (userId, tag) => {
    const knex = await db.get()

    await knex('users_tags')
      .where({ userId, tag: _.toUpper(tag) })
      .del()
      .then()
  }

  /**
   * Returns the value of a user tag, if it exists
   * @param  {String} userId [description]
   * @param  {String} tag    [description]
   * @return {?String}        Value of the tag
   * @memberof! Users
   */
  const getTag = async (userId, tag, details = false) => {
    const knex = await db.get()

    return knex('users_tags')
      .select('value', 'tagged_on', 'tag')
      .where({ userId, tag: _.toUpper(tag) })
      .limit(1)
      .then()
      .get(0)
      .then(ret => {
        if (ret && details) {
          return {
            ...ret,
            tagged_on: new Date(ret.tagged_on)
          }
        }

        return ret && ret.value
      })
  }

  /**
   * Returns all the tags for a given user
   * @param  {String} userId [description]
   * @return {Array.<{ tag: String, value: String}>}        An array of all the tags for this user
   * @memberof! Users
   */
  const getTags = async userId => {
    const knex = await db.get()

    return knex('users_tags')
      .where({ userId })
      .select('tag', 'value')
      .then(tags => {
        return _.map(tags, v => {
          return { tag: v.tag, value: v.value }
        })
      })
  }

  // TODO: Fix this, list of tags is always empty
  const list = async (limit = 50, from = 0) => {
    const knex = await db.get()

    const isLite = helpers(knex).isLite()

    const selectTags = isLite ? 'group_concat(tag) as tags' : "string_agg(tag, ',') as tags"

    const subQuery = knex('users_tags')
      .select('userId', knex.raw(selectTags))
      .groupBy('userId')

    return knex('users')
      .leftJoin(knex.raw('(' + subQuery.toString() + ') AS t2'), 'users.id', '=', 't2.userId')
      .select(
        'users.id',
        'users.userId',
        'users.platform',
        'users.gender',
        'users.timezone',
        'users.locale',
        'users.picture_url',
        'users.first_name',
        'users.last_name',
        'users.created_on',
        't2.tags'
      )
      .orderBy('users.created_on', 'asc')
      .offset(from)
      .limit(limit)
      .then(users =>
        users.map(x =>
          Object.assign(x, {
            tags: (x.tags && x.tags.split(',')) || []
          })
        )
      )
  }

  // TODO: Fix this, just doesn't work
  const listWithTags = async (tags, limit = 50, from = 0) => {
    const knex = await db.get()

    tags = _.filter(tags, t => _.isString(t)).map(t => t.toUpperCase())
    const filterByTag = tag =>
      knex('users_tags')
        .select('userId')
        .where('tag', tag)

    const isLite = helpers(knex).isLite()
    const selectTags = isLite ? 'group_concat(tag) as tags' : "string_agg(tag, ',') as tags"

    let query = knex('users')
    let i = 0

    const subQuery = knex('users_tags')
      .select('userId', knex.raw(selectTags))
      .groupBy('userId')

    tags.forEach(tag => {
      const name = 't' + ++i
      query = query.join(
        knex.raw('(' + filterByTag(tag).toString() + ') AS ' + name),
        'users.id',
        '=',
        name + '.userId'
      )
    })

    return query
      .leftJoin(knex.raw('(' + subQuery.toString() + ') AS tt'), 'users.id', '=', 'tt.userId')
      .select(
        'users.userId as userId',
        'users.platform as platform',
        'users.gender as gender',
        'users.timezone as timezone',
        'users.locale as locale',
        'users.picture_url as picture_url',
        'users.first_name as first_name',
        'users.last_name as last_name',
        'users.created_on as created_on',
        'tt.tags as tags'
      )
      .orderBy('users.created_on', 'asc')
      .offset(from)
      .limit(limit)
      .then(users =>
        users.map(x =>
          Object.assign(x, {
            tags: (x.tags && x.tags.split(',')) || []
          })
        )
      )
  }

  const count = async () => {
    const knex = await db.get()

    return knex('users')
      .count('* as count')
      .then()
      .get(0)
      .then(ret => parseInt(ret && ret.count))
  }

  return { tag, untag, hasTag, getTag, getTags, list, count, listWithTags }
}
<commit_msg>fix(core): fix fetching users' tags (#1023)<commit_after>/**
 * The Users namespace contains operations available for the known users of your bot.
 * @public
 * @namespace Users
 * @example
 * bp.users
 */

import _ from 'lodash'
import helpers from './database/helpers'

module.exports = ({ db }) => {
  /**
   * Returns whether or not a user has a specific tag or not.
   * @param  {String} userId
   * @param  {String} tag    The name of the tag. Case insensitive.
   * @return {Boolean}
   * @async
   * @memberof! Users
   * @example
   * if (await bp.users.hasTag(event.user.id, 'IS_SUBSCRIBED')) {...}
   */
  const hasTag = async (userId, tag) => {
    const knex = await db.get()

    return knex('users_tags')
      .select('userId')
      .where({ userId, tag: _.toUpper(tag) })
      .limit(1)
      .then(ret => ret.length > 0)
  }

  /**
   * Tags a user with a specific tag (or overwrites an existing one) and a given value for that tag (optional).
   * Tags can be used to classify users (no value needed) or to store information about them (with a tag value).
   * Values are useful to store user information like emails, etc.
   * Value is always assumed to be a string.
   * @param  {String}  userId
   * @param  {String}  tag    The name of the tag.
   * Case insensitive. Note that this property will always be upper-cased.
   * @param  {String} [value]  Any string value to store info about this tag
   * @async
   * @memberof! Users
   * @example
   * await bp.users.tag(event.user.id, 'EMAIL', 'sylvain@botpress.io')
   * await bp.users.tag(event.user.id, 'PAYING_USER')
   */
  const tag = async (userId, tag, value = true) => {
    const knex = await db.get()

    tag = _.toUpper(tag)

    if (await hasTag(userId, tag)) {
      await knex('users_tags')
        .where({ userId, tag })
        .update({
          userId,
          tag,
          value,
          tagged_on: helpers(knex).date.now()
        })
        .then()
    } else {
      await knex('users_tags')
        .insert({
          userId,
          tag,
          value,
          tagged_on: helpers(knex).date.now()
        })
        .then()
    }
  }

  /**
   * Removes a tag from a user if it exists.
   * @param  {String} userId
   * @param  {String} tag    Name of the tag. Case-insensitive.
   * @memberof! Users
   */
  const untag = async (userId, tag) => {
    const knex = await db.get()

    await knex('users_tags')
      .where({ userId, tag: _.toUpper(tag) })
      .del()
      .then()
  }

  /**
   * Returns the value of a user tag, if it exists
   * @param  {String} userId [description]
   * @param  {String} tag    [description]
   * @return {?String}        Value of the tag
   * @memberof! Users
   */
  const getTag = async (userId, tag, details = false) => {
    const knex = await db.get()

    return knex('users_tags')
      .select('value', 'tagged_on', 'tag')
      .where({ userId, tag: _.toUpper(tag) })
      .limit(1)
      .then()
      .get(0)
      .then(ret => {
        if (ret && details) {
          return {
            ...ret,
            tagged_on: new Date(ret.tagged_on)
          }
        }

        return ret && ret.value
      })
  }

  /**
   * Returns all the tags for a given user
   * @param  {String} userId [description]
   * @return {Array.<{ tag: String, value: String}>}        An array of all the tags for this user
   * @memberof! Users
   */
  const getTags = async userId => {
    const knex = await db.get()

    return knex('users_tags')
      .where({ userId })
      .select('tag', 'value')
      .then(tags => {
        return _.map(tags, v => {
          return { tag: v.tag, value: v.value }
        })
      })
  }

  const list = async (limit = 50, from = 0) => {
    const knex = await db.get()

    const isLite = helpers(knex).isLite()

    const TAG_VALUE_SEPARATOR = '::::'
    const tagWithValue = `tag || '${TAG_VALUE_SEPARATOR}' || value`
    const parseTagValues = tagAndValue => {
      const [tag, value] = tagAndValue.split(TAG_VALUE_SEPARATOR)
      return { tag, value }
    }

    const selectTags = isLite ? `group_concat(${tagWithValue}) as tags` : `string_agg(${tagWithValue}, ',') as tags`

    const subQuery = knex('users_tags')
      .select('userId', knex.raw(selectTags))
      .groupBy('userId')

    return knex('users')
      .leftJoin(knex.raw('(' + subQuery.toString() + ') AS t2'), 'users.id', '=', 't2.userId')
      .select(
        'users.id',
        'users.userId',
        'users.platform',
        'users.gender',
        'users.timezone',
        'users.locale',
        'users.picture_url',
        'users.first_name',
        'users.last_name',
        'users.created_on',
        't2.tags'
      )
      .orderBy('users.created_on', 'asc')
      .offset(from)
      .limit(limit)
      .then(users =>
        users.map(x => ({
          ...x,
          tags: (x.tags && x.tags.split(',').map(parseTagValues)) || []
        }))
      )
  }

  // TODO: Fix this, just doesn't work
  const listWithTags = async (tags, limit = 50, from = 0) => {
    const knex = await db.get()

    tags = _.filter(tags, t => _.isString(t)).map(t => t.toUpperCase())
    const filterByTag = tag =>
      knex('users_tags')
        .select('userId')
        .where('tag', tag)

    const isLite = helpers(knex).isLite()
    const selectTags = isLite ? 'group_concat(tag) as tags' : "string_agg(tag, ',') as tags"

    let query = knex('users')
    let i = 0

    const subQuery = knex('users_tags')
      .select('userId', knex.raw(selectTags))
      .groupBy('userId')

    tags.forEach(tag => {
      const name = 't' + ++i
      query = query.join(
        knex.raw('(' + filterByTag(tag).toString() + ') AS ' + name),
        'users.id',
        '=',
        name + '.userId'
      )
    })

    return query
      .leftJoin(knex.raw('(' + subQuery.toString() + ') AS tt'), 'users.id', '=', 'tt.userId')
      .select(
        'users.userId as userId',
        'users.platform as platform',
        'users.gender as gender',
        'users.timezone as timezone',
        'users.locale as locale',
        'users.picture_url as picture_url',
        'users.first_name as first_name',
        'users.last_name as last_name',
        'users.created_on as created_on',
        'tt.tags as tags'
      )
      .orderBy('users.created_on', 'asc')
      .offset(from)
      .limit(limit)
      .then(users =>
        users.map(x =>
          Object.assign(x, {
            tags: (x.tags && x.tags.split(',')) || []
          })
        )
      )
  }

  const count = async () => {
    const knex = await db.get()

    return knex('users')
      .count('* as count')
      .then()
      .get(0)
      .then(ret => parseInt(ret && ret.count))
  }

  return { tag, untag, hasTag, getTag, getTags, list, count, listWithTags }
}
<|endoftext|>


<commit_before>/* global cordova:false */
/* globals window, document, navigator */

/*!
 * Module dependencies.
 */

var exec = cordova.require('cordova/exec');

/**
 * PushNotification constructor.
 *
 * @param {Object} options to initiate Push Notifications.
 * @return {PushNotification} instance that can be monitored and cancelled.
 */
var serviceWorker, subscription;
var PushNotification = function(options) {
    this._handlers = {
        'registration': [],
        'notification': [],
        'error': []
    };

    // require options parameter
    if (typeof options === 'undefined') {
        throw new Error('The options argument is required.');
    }

    // store the options to this object instance
    this.options = options;

    // triggered on registration and notification
    var that = this;

    // Add manifest.json to main HTML file
    var linkElement = document.createElement('link');
    linkElement.rel = 'manifest';
    linkElement.href = 'manifest.json';
    document.getElementsByTagName('head')[0].appendChild(linkElement);

    if ('serviceWorker' in navigator && 'MessageChannel' in window) {
        var result;
        var channel = new MessageChannel();
        channel.port1.onmessage = function(event) {
            that.emit('notification', event.data);
        };

        navigator.serviceWorker.register('ServiceWorker.js').then(function() {
            return navigator.serviceWorker.ready;
        })
        .then(function(reg) {
            serviceWorker = reg;
            reg.pushManager.subscribe({userVisibleOnly: true}).then(function(sub) {
                subscription = sub;
                result = { 'registrationId': sub.endpoint.substring(sub.endpoint.lastIndexOf('/') + 1) };
                that.emit('registration', result);

                // send encryption keys to push server
                var xmlHttp = new XMLHttpRequest();
                var xmlURL = (options.browser.pushServiceURL || 'http://push.api.phonegap.com/v1/push') + '/keys';
                xmlHttp.open('POST', xmlURL, true);

                var formData = new FormData();
                formData.append('subscription', JSON.stringify(sub));

                xmlHttp.send(formData);

                navigator.serviceWorker.controller.postMessage(result, [channel.port2]);
            }).catch(function(error) {
                if (navigator.serviceWorker.controller === null) {
                    // When you first register a SW, need a page reload to handle network operations
                    window.location.reload();
                    return;
                }

                throw new Error('Error subscribing for Push notifications.');
            });
        }).catch(function(error) {
            console.log(error);
            throw new Error('Error registering Service Worker');
        });
    } else {
        throw new Error('Service Workers are not supported on your browser.');
    }
};

/**
 * Unregister from push notifications
 */

PushNotification.prototype.unregister = function(successCallback, errorCallback, options) {
    if (!errorCallback) { errorCallback = function() {}; }

    if (typeof errorCallback !== 'function')  {
        console.log('PushNotification.unregister failure: failure parameter not a function');
        return;
    }

    if (typeof successCallback !== 'function') {
        console.log('PushNotification.unregister failure: success callback parameter must be a function');
        return;
    }

    var that = this;
    if (!options) {
        that._handlers = {
            'registration': [],
            'notification': [],
            'error': []
        };
    }

    if (serviceWorker) {
        serviceWorker.unregister().then(function(isSuccess) {
            if (isSuccess) {
                var deviceID = subscription.endpoint.substring(subscription.endpoint.lastIndexOf('/') + 1);
                var xmlHttp = new XMLHttpRequest();
                var xmlURL = (that.options.browser.pushServiceURL || 'http://push.api.phonegap.com/v1/push')
                    + '/keys/' + deviceID;
                xmlHttp.open('DELETE', xmlURL, true);
                xmlHttp.send();

                successCallback();
            } else {
                errorCallback();
            }
        });
    }
};

/**
 * Call this to set the application icon badge
 */

PushNotification.prototype.setApplicationIconBadgeNumber = function(successCallback, errorCallback, badge) {
    if (!errorCallback) { errorCallback = function() {}; }

    if (typeof errorCallback !== 'function')  {
        console.log('PushNotification.setApplicationIconBadgeNumber failure: failure parameter not a function');
        return;
    }

    if (typeof successCallback !== 'function') {
        console.log('PushNotification.setApplicationIconBadgeNumber failure: success callback parameter must be a function');
        return;
    }

    successCallback();
};

/**
 * Get the application icon badge
 */

PushNotification.prototype.getApplicationIconBadgeNumber = function(successCallback, errorCallback) {
    if (!errorCallback) { errorCallback = function() {}; }

    if (typeof errorCallback !== 'function')  {
        console.log('PushNotification.getApplicationIconBadgeNumber failure: failure parameter not a function');
        return;
    }

    if (typeof successCallback !== 'function') {
        console.log('PushNotification.getApplicationIconBadgeNumber failure: success callback parameter must be a function');
        return;
    }

    successCallback();
};

/**
 * Get the application icon badge
 */

PushNotification.prototype.clearAllNotifications = function(successCallback, errorCallback) {
    if (!errorCallback) { errorCallback = function() {}; }

    if (typeof errorCallback !== 'function')  {
        console.log('PushNotification.clearAllNotifications failure: failure parameter not a function');
        return;
    }

    if (typeof successCallback !== 'function') {
        console.log('PushNotification.clearAllNotifications failure: success callback parameter must be a function');
        return;
    }

    successCallback();
};

/**
 * Listen for an event.
 *
 * The following events are supported:
 *
 *   - registration
 *   - notification
 *   - error
 *
 * @param {String} eventName to subscribe to.
 * @param {Function} callback triggered on the event.
 */

PushNotification.prototype.on = function(eventName, callback) {
    if (this._handlers.hasOwnProperty(eventName)) {
        this._handlers[eventName].push(callback);
    }
};

/**
 * Remove event listener.
 *
 * @param {String} eventName to match subscription.
 * @param {Function} handle function associated with event.
 */

PushNotification.prototype.off = function (eventName, handle) {
    if (this._handlers.hasOwnProperty(eventName)) {
        var handleIndex = this._handlers[eventName].indexOf(handle);
        if (handleIndex >= 0) {
            this._handlers[eventName].splice(handleIndex, 1);
        }
    }
};

/**
 * Emit an event.
 *
 * This is intended for internal use only.
 *
 * @param {String} eventName is the event to trigger.
 * @param {*} all arguments are passed to the event listeners.
 *
 * @return {Boolean} is true when the event is triggered otherwise false.
 */

PushNotification.prototype.emit = function() {
    var args = Array.prototype.slice.call(arguments);
    var eventName = args.shift();

    if (!this._handlers.hasOwnProperty(eventName)) {
        return false;
    }

    for (var i = 0, length = this._handlers[eventName].length; i < length; i++) {
        var callback = this._handlers[eventName][i];
        if (typeof callback === 'function') {
            callback.apply(undefined,args);
        } else {
            console.log('event handler: ' + eventName + ' must be a function');
        }
    }

    return true;
};

PushNotification.prototype.finish = function(successCallback, errorCallback, id) {
    if (!successCallback) { successCallback = function() {}; }
    if (!errorCallback) { errorCallback = function() {}; }
    if (!id) { id = 'handler'; }

    if (typeof successCallback !== 'function') {
        console.log('finish failure: success callback parameter must be a function');
        return;
    }

    if (typeof errorCallback !== 'function')  {
        console.log('finish failure: failure parameter not a function');
        return;
    }

    successCallback();
};

/*!
 * Push Notification Plugin.
 */

module.exports = {
    /**
     * Register for Push Notifications.
     *
     * This method will instantiate a new copy of the PushNotification object
     * and start the registration process.
     *
     * @param {Object} options
     * @return {PushNotification} instance
     */

    init: function(options) {
        return new PushNotification(options);
    },

    hasPermission: function(successCallback, errorCallback) {
        successCallback(true);
    },

    unregister: function(successCallback, errorCallback, options) {
        PushNotification.unregister(successCallback, errorCallback, options);
    },

    /**
     * PushNotification Object.
     *
     * Expose the PushNotification object for direct use
     * and testing. Typically, you should use the
     * .init helper method.
     */

    PushNotification: PushNotification
};
<commit_msg>:bug: Issue #1412: push.subscribe 'not a function' error<commit_after>/* global cordova:false */
/* globals window, document, navigator */

/*!
 * Module dependencies.
 */

var exec = cordova.require('cordova/exec');

/**
 * PushNotification constructor.
 *
 * @param {Object} options to initiate Push Notifications.
 * @return {PushNotification} instance that can be monitored and cancelled.
 */
var serviceWorker, subscription;
var PushNotification = function(options) {
    this._handlers = {
        'registration': [],
        'notification': [],
        'error': []
    };

    // require options parameter
    if (typeof options === 'undefined') {
        throw new Error('The options argument is required.');
    }

    // store the options to this object instance
    this.options = options;

    // triggered on registration and notification
    var that = this;

    // Add manifest.json to main HTML file
    var linkElement = document.createElement('link');
    linkElement.rel = 'manifest';
    linkElement.href = 'manifest.json';
    document.getElementsByTagName('head')[0].appendChild(linkElement);

    if ('serviceWorker' in navigator && 'MessageChannel' in window) {
        var result;
        var channel = new MessageChannel();
        channel.port1.onmessage = function(event) {
            that.emit('notification', event.data);
        };

        navigator.serviceWorker.register('ServiceWorker.js').then(function() {
            return navigator.serviceWorker.ready;
        })
        .then(function(reg) {
            serviceWorker = reg;
            reg.pushManager.subscribe({userVisibleOnly: true}).then(function(sub) {
                subscription = sub;
                result = { 'registrationId': sub.endpoint.substring(sub.endpoint.lastIndexOf('/') + 1) };
                that.emit('registration', result);

                // send encryption keys to push server
                var xmlHttp = new XMLHttpRequest();
                var xmlURL = (options.browser.pushServiceURL || 'http://push.api.phonegap.com/v1/push') + '/keys';
                xmlHttp.open('POST', xmlURL, true);

                var formData = new FormData();
                formData.append('subscription', JSON.stringify(sub));

                xmlHttp.send(formData);

                navigator.serviceWorker.controller.postMessage(result, [channel.port2]);
            }).catch(function(error) {
                if (navigator.serviceWorker.controller === null) {
                    // When you first register a SW, need a page reload to handle network operations
                    window.location.reload();
                    return;
                }

                throw new Error('Error subscribing for Push notifications.');
            });
        }).catch(function(error) {
            console.log(error);
            throw new Error('Error registering Service Worker');
        });
    } else {
        throw new Error('Service Workers are not supported on your browser.');
    }
};

/**
 * Unregister from push notifications
 */

PushNotification.prototype.unregister = function(successCallback, errorCallback, options) {
    if (!errorCallback) { errorCallback = function() {}; }

    if (typeof errorCallback !== 'function')  {
        console.log('PushNotification.unregister failure: failure parameter not a function');
        return;
    }

    if (typeof successCallback !== 'function') {
        console.log('PushNotification.unregister failure: success callback parameter must be a function');
        return;
    }

    var that = this;
    if (!options) {
        that._handlers = {
            'registration': [],
            'notification': [],
            'error': []
        };
    }

    if (serviceWorker) {
        serviceWorker.unregister().then(function(isSuccess) {
            if (isSuccess) {
                var deviceID = subscription.endpoint.substring(subscription.endpoint.lastIndexOf('/') + 1);
                var xmlHttp = new XMLHttpRequest();
                var xmlURL = (that.options.browser.pushServiceURL || 'http://push.api.phonegap.com/v1/push')
                    + '/keys/' + deviceID;
                xmlHttp.open('DELETE', xmlURL, true);
                xmlHttp.send();

                successCallback();
            } else {
                errorCallback();
            }
        });
    }
};

/**
 * subscribe to a topic
 * @param   {String}      topic               topic to subscribe
 * @param   {Function}    successCallback     success callback
 * @param   {Function}    errorCallback       error callback
 * @return  {void}
 */
PushNotification.prototype.subscribe = function(topic, successCallback, errorCallback) {
    if (!errorCallback) { errorCallback = function() {}; }

    if (typeof errorCallback !== 'function')  {
        console.log('PushNotification.subscribe failure: failure parameter not a function');
        return;
    }

    if (typeof successCallback !== 'function') {
        console.log('PushNotification.subscribe failure: success callback parameter must be a function');
        return;
    }

    successCallback();
};

/**
 * unsubscribe to a topic
 * @param   {String}      topic               topic to unsubscribe
 * @param   {Function}    successCallback     success callback
 * @param   {Function}    errorCallback       error callback
 * @return  {void}
 */
PushNotification.prototype.unsubscribe = function(topic, successCallback, errorCallback) {
    if (!errorCallback) { errorCallback = function() {}; }

    if (typeof errorCallback !== 'function')  {
        console.log('PushNotification.unsubscribe failure: failure parameter not a function');
        return;
    }

    if (typeof successCallback !== 'function') {
        console.log('PushNotification.unsubscribe failure: success callback parameter must be a function');
        return;
    }

    successCallback();
};

/**
 * Call this to set the application icon badge
 */

PushNotification.prototype.setApplicationIconBadgeNumber = function(successCallback, errorCallback, badge) {
    if (!errorCallback) { errorCallback = function() {}; }

    if (typeof errorCallback !== 'function')  {
        console.log('PushNotification.setApplicationIconBadgeNumber failure: failure parameter not a function');
        return;
    }

    if (typeof successCallback !== 'function') {
        console.log('PushNotification.setApplicationIconBadgeNumber failure: success callback parameter must be a function');
        return;
    }

    successCallback();
};

/**
 * Get the application icon badge
 */

PushNotification.prototype.getApplicationIconBadgeNumber = function(successCallback, errorCallback) {
    if (!errorCallback) { errorCallback = function() {}; }

    if (typeof errorCallback !== 'function')  {
        console.log('PushNotification.getApplicationIconBadgeNumber failure: failure parameter not a function');
        return;
    }

    if (typeof successCallback !== 'function') {
        console.log('PushNotification.getApplicationIconBadgeNumber failure: success callback parameter must be a function');
        return;
    }

    successCallback();
};

/**
 * Get the application icon badge
 */

PushNotification.prototype.clearAllNotifications = function(successCallback, errorCallback) {
    if (!errorCallback) { errorCallback = function() {}; }

    if (typeof errorCallback !== 'function')  {
        console.log('PushNotification.clearAllNotifications failure: failure parameter not a function');
        return;
    }

    if (typeof successCallback !== 'function') {
        console.log('PushNotification.clearAllNotifications failure: success callback parameter must be a function');
        return;
    }

    successCallback();
};

/**
 * Listen for an event.
 *
 * The following events are supported:
 *
 *   - registration
 *   - notification
 *   - error
 *
 * @param {String} eventName to subscribe to.
 * @param {Function} callback triggered on the event.
 */

PushNotification.prototype.on = function(eventName, callback) {
    if (this._handlers.hasOwnProperty(eventName)) {
        this._handlers[eventName].push(callback);
    }
};

/**
 * Remove event listener.
 *
 * @param {String} eventName to match subscription.
 * @param {Function} handle function associated with event.
 */

PushNotification.prototype.off = function (eventName, handle) {
    if (this._handlers.hasOwnProperty(eventName)) {
        var handleIndex = this._handlers[eventName].indexOf(handle);
        if (handleIndex >= 0) {
            this._handlers[eventName].splice(handleIndex, 1);
        }
    }
};

/**
 * Emit an event.
 *
 * This is intended for internal use only.
 *
 * @param {String} eventName is the event to trigger.
 * @param {*} all arguments are passed to the event listeners.
 *
 * @return {Boolean} is true when the event is triggered otherwise false.
 */

PushNotification.prototype.emit = function() {
    var args = Array.prototype.slice.call(arguments);
    var eventName = args.shift();

    if (!this._handlers.hasOwnProperty(eventName)) {
        return false;
    }

    for (var i = 0, length = this._handlers[eventName].length; i < length; i++) {
        var callback = this._handlers[eventName][i];
        if (typeof callback === 'function') {
            callback.apply(undefined,args);
        } else {
            console.log('event handler: ' + eventName + ' must be a function');
        }
    }

    return true;
};

PushNotification.prototype.finish = function(successCallback, errorCallback, id) {
    if (!successCallback) { successCallback = function() {}; }
    if (!errorCallback) { errorCallback = function() {}; }
    if (!id) { id = 'handler'; }

    if (typeof successCallback !== 'function') {
        console.log('finish failure: success callback parameter must be a function');
        return;
    }

    if (typeof errorCallback !== 'function')  {
        console.log('finish failure: failure parameter not a function');
        return;
    }

    successCallback();
};

/*!
 * Push Notification Plugin.
 */

module.exports = {
    /**
     * Register for Push Notifications.
     *
     * This method will instantiate a new copy of the PushNotification object
     * and start the registration process.
     *
     * @param {Object} options
     * @return {PushNotification} instance
     */

    init: function(options) {
        return new PushNotification(options);
    },

    hasPermission: function(successCallback, errorCallback) {
        successCallback(true);
    },

    unregister: function(successCallback, errorCallback, options) {
        PushNotification.unregister(successCallback, errorCallback, options);
    },

    /**
     * PushNotification Object.
     *
     * Expose the PushNotification object for direct use
     * and testing. Typically, you should use the
     * .init helper method.
     */

    PushNotification: PushNotification
};
<|endoftext|>


<commit_before>export Icon from './components/base/icons/Icon';

export Notification from './components/containers/notification/Notification';
export LightNotification from './components/containers/notification/LightNotification';
export MessageNotification from './components/containers/notification/MessageNotification';
export {
  InlineMessage,
  Message
} from './components/containers/notification/Message';
export Drawer from './components/containers/drawer/Drawer';
export Fieldset from './components/containers/fieldset/Fieldset';
export Tooltip from './components/containers/tooltip/Tooltip';
export Popover from './components/containers/popover/Popover';
export Modal from './components/containers/modal/Modal';
export ModalButtonContainer from './components/containers/modal/ModalButtonContainer';
export Menu from './components/containers/menu/Menu';
export TabPanel from './components/containers/tabPanels/TabPanel';
export StripedContainer from './components/containers/striped-container/StripedContainer';
export Heading from './components/containers/heading/Heading';
export Table from './components/containers/table/Table';

export Control from './components/controls/Control';
export AdditionalHelp from './components/controls/AdditionalHelp';
export Label from './components/controls/label/Label';
export Textbox from './components/controls/textbox/Textbox';
export MaskedTextbox from './components/controls/textbox/MaskedTextbox';
export Button from './components/controls/buttons/Button';
export ToggleButton from './components/controls/buttons/ToggleButton';
export DropdownButton from './components/controls/buttons/DropdownButton';
export OutlineButton from './components/controls/buttons/OutlineButton';
export LinkButton from './components/controls/buttons/LinkButton';
export PopoverLink from './components/controls/buttons/PopoverLink';
export AnswerButton from './components/controls/answer-group/AnswerButton';
export AnswerGroup from './components/controls/answer-group/AnswerGroup';
export RadioGroup from './components/controls/radio-buttons/RadioGroup';
export RadioButton from './components/controls/radio-buttons/RadioButton';
export Dropdown from './components/controls/dropdown/Dropdown';
export Checkbox from './components/controls/checkbox/Checkbox';

export SideNav from './components/navigation/sidenav/SideNav';
export HorizontalNav from './components/navigation/horizontalnav/HorizontalNav';

export DatePicker from './components/patterns/datepicker/DatePicker';
export Incrementer from './components/patterns/incrementer/Incrementer';
export screenReaderOnly from './components/patterns/screenReaderOnly/screenReaderOnly';
export CallToAction from './components/patterns/callToAction/CallToAction';
export LightCallToAction from './components/patterns/callToAction/LightCallToAction';
export Action from './components/patterns/callToAction/Action';

export Spinner from './components/base/spinner/Spinner';

export generateAlphaName from './components/util/generateAlphaName';
export { useWindowWidth } from './components/util/useWindowWidth';
export withWindowSize from './components/util/withWindowSize';
export StyleReset from './components/util/StyleReset';
export { useTheme } from './components/util/useTheme';
<commit_msg>New: expose Fade component<commit_after>export Icon from './components/base/icons/Icon';

export Notification from './components/containers/notification/Notification';
export LightNotification from './components/containers/notification/LightNotification';
export MessageNotification from './components/containers/notification/MessageNotification';
export {
  InlineMessage,
  Message
} from './components/containers/notification/Message';
export Drawer from './components/containers/drawer/Drawer';
export Fieldset from './components/containers/fieldset/Fieldset';
export Tooltip from './components/containers/tooltip/Tooltip';
export Popover from './components/containers/popover/Popover';
export Modal from './components/containers/modal/Modal';
export ModalButtonContainer from './components/containers/modal/ModalButtonContainer';
export Menu from './components/containers/menu/Menu';
export TabPanel from './components/containers/tabPanels/TabPanel';
export StripedContainer from './components/containers/striped-container/StripedContainer';
export Heading from './components/containers/heading/Heading';
export Table from './components/containers/table/Table';

export Control from './components/controls/Control';
export AdditionalHelp from './components/controls/AdditionalHelp';
export Label from './components/controls/label/Label';
export Textbox from './components/controls/textbox/Textbox';
export MaskedTextbox from './components/controls/textbox/MaskedTextbox';
export Button from './components/controls/buttons/Button';
export ToggleButton from './components/controls/buttons/ToggleButton';
export DropdownButton from './components/controls/buttons/DropdownButton';
export OutlineButton from './components/controls/buttons/OutlineButton';
export LinkButton from './components/controls/buttons/LinkButton';
export PopoverLink from './components/controls/buttons/PopoverLink';
export AnswerButton from './components/controls/answer-group/AnswerButton';
export AnswerGroup from './components/controls/answer-group/AnswerGroup';
export RadioGroup from './components/controls/radio-buttons/RadioGroup';
export RadioButton from './components/controls/radio-buttons/RadioButton';
export Dropdown from './components/controls/dropdown/Dropdown';
export Checkbox from './components/controls/checkbox/Checkbox';

export SideNav from './components/navigation/sidenav/SideNav';
export HorizontalNav from './components/navigation/horizontalnav/HorizontalNav';

export DatePicker from './components/patterns/datepicker/DatePicker';
export Incrementer from './components/patterns/incrementer/Incrementer';
export screenReaderOnly from './components/patterns/screenReaderOnly/screenReaderOnly';
export CallToAction from './components/patterns/callToAction/CallToAction';
export LightCallToAction from './components/patterns/callToAction/LightCallToAction';
export Action from './components/patterns/callToAction/Action';

export Spinner from './components/base/spinner/Spinner';

export Fade from './components/util/Fade';
export generateAlphaName from './components/util/generateAlphaName';
export { useWindowWidth } from './components/util/useWindowWidth';
export withWindowSize from './components/util/withWindowSize';
export StyleReset from './components/util/StyleReset';
export { useTheme } from './components/util/useTheme';
<|endoftext|>


<commit_before>/**
 * List of new rules vetted by NCR D3
 * This is a separate file for now to keep the new rules separate
 * This may get refactored at some point if we decide to go a
 * different direction with our ruleset
 */
module.exports = {
  rules: {
    // Return values in promise exectutors don't do anything
    // so this enforces empty return to prevent possible bugs
    'no-promise-executor-return': 'error',

    // Catch instances of loops that probably don't need to be loops
    'no-unreachable-loop': 'error',

    // Currently not enforcing strong variable names
    'id-denylist': 'off'
  }
};
<commit_msg>feat(linting): disallow bracketless if, else if, else, for, while, do (#14)<commit_after>/**
 * List of new rules vetted by NCR D3
 * This is a separate file for now to keep the new rules separate
 * This may get refactored at some point if we decide to go a
 * different direction with our ruleset
 */
module.exports = {
  rules: {
    // Return values in promise exectutors don't do anything
    // so this enforces empty return to prevent possible bugs
    'no-promise-executor-return': 'error',

    // Catch instances of loops that probably don't need to be loops
    'no-unreachable-loop': 'error',

    // Currently not enforcing strong variable names
    'id-denylist': 'off',

    // Disallow bracket-less if, else if, else, for, while, do
    curly: 'error'
  }
};
<|endoftext|>


<commit_before>const canvas = document.getElementById("gamecanvas");
const context = canvas.getContext("2d");

var keys = [];
document.body.addEventListener("keydown", function (e) {
    keys[e.keyCode] = true;
});
document.body.addEventListener("keyup", function (e) {
    keys[e.keyCode] = false;
});

lastTime = (new Date()).getTime();
currentTime = 0;
deltaTime = 0;

const game = {
    demoMode: null,
    score: null,
    multiplier: null,
    lives: null,
    level: null,
    blocks: null,
    reset: function() {
        this.demoMode = true;
        this.score = 0;
        this.multiplier = 1;
        this.lives = 3;
        this.level = 1;
        this.blocks = generateLevel(this.level);
    },
}

const ball = {
    pos: {x: null, y: null},
    velocity: {x: null, y: null },
    width: null,
    height: null,
    speed: null,
    draw: function() {
        context.fillStyle = "#F8F8F2";
        context.fillRect(this.pos.x, this.pos.y, this.width, this.height);
    },
    reset: function() {
        this.pos.x = canvas.width / 2;
        this.pos.y = canvas.height - 64;
        this.speed = -300;
        this.velocity.x = (Math.random() + 0.5) * Math.pow(-1, Math.floor(Math.random() * 2)) * 60;
        if (game.demoMode) this.velocity.x += 2;
        this.velocity.y = this.speed;
        this.width = this.height = 12;
    },
};

const player = {
    pos: { x: null, y: null},
    velocity: null,
    width: null,
    height: null,
    speed: null,
    friction: null,
    draw: function() {
        context.fillStyle = "#F92672";
        context.fillRect(this.pos.x, this.pos.y, this.width, this.height);
    },
    moveLeft: function() {
        this.velocity -= this.speed;
    },
    moveRight: function() {
        this.velocity += this.speed;
    },
    updatePosition: function() {
        this.pos.x += this.velocity;
        this.velocity *= this.friction;

        if (this.pos.x+this.width/2 < 0) {
            this.pos.x = -this.width/2;
            this.velocity = 0;
        } else if (this.pos.x+this.width/2 > canvas.width) {
            this.pos.x = canvas.width-this.width/2;
            this.velocity = 0;
        }
    },
    reset: function() {
        this.width = 120;
        this.height = 16;
        this.pos.x = canvas.width / 2 - this.width / 2;
        this.pos.y = canvas.height - 32;
        this.velocity = 0;
        this.speed = 2;
        this.friction = 0.9;
    },
};

function Block(x, y, width, height, color="#FFE792") {
    this.pos = { x: x, y: y};
    this.width = width;
    this.height = height;
    this.color = color;
    this.draw = function() {
        context.fillStyle = this.color;
        context.fillRect(this.pos.x, this.pos.y, this.width, this.height);
    };
}

function collisionDetection(a, b) {
    if (a.pos.x < b.pos.x + b.width &&
        a.pos.x + a.width > b.pos.x &&
        a.pos.y < b.pos.y + b.height &&
        a.height + a.pos.y > b.pos.y) {
        return true;
    }
    return false;
}

// returns side on which the ball made contact
function collisionSide(ball, object) {
    // distance between upper side of ball and lower side of object
    const up = Math.abs(ball.pos.y - object.pos.y+object.height);
    // distance between lower side of ball and upper side of object
    const down = Math.abs(ball.pos.y+ball.height - object.pos.y);
    // distance between left side of ball and right side of object
    const left = Math.abs(ball.pos.x - object.pos.x+object.width);
    // distance between right side of ball and left side of object
    const right = Math.abs(ball.pos.x+ball.width - object.pos.x);

    const min = Math.min(up, down, left, right);
    if (min == up) return "up";
    if (min == down) return "down";
    if (min == left) return "left";
    return "right";
}

function generateLevel(level) {
    const blocks = [];
    const colors = ["#F92672", "#66D9EF", "#A6E22E", "#FD971F", "#AE81FF", "#FFE792", "#FFE792"];
    if (level == 1) {
        for (row = 0; row < 7; row++) {
            for (col = 0; col < 8; col++) {
                blocks.push(new Block(2+col*60, 60+row*20, 58, 16, colors[row]));
            }
        }
    }
    else {
        // simple random level
        for (row = 0; row < 7; row++) {
            for (col = 0; col < 8; col++) {
                if (Math.random() >= 0.5) continue;
                blocks.push(new Block(2+col*60, 60+row*20, 58, 16, colors[row]));
            }
        }
    }
    return blocks;
}

function sound(src) {
    this.sound = document.createElement("audio");
    this.sound.src = src;
    this.sound.setAttribute("preload", "auto");
    this.sound.setAttribute("controls", "none");
    this.sound.style.display = "none";
    document.body.appendChild(this.sound);
    this.play = function(){
        this.sound.play();
    }
    this.stop = function(){
        this.sound.pause();
    }
}

function loop() {
    if (keys[37] && !game.demoMode) player.moveLeft();
    else if (keys[39] && !game.demoMode) player.moveRight();
    if (keys[13] && game.demoMode) {
        game.reset();
        game.demoMode = false;
        ball.reset();
        player.reset();
    }

    currentTime = (new Date()).getTime();
    deltaTime = (currentTime - lastTime) / 1000;
    lastTime = currentTime;

    player.updatePosition();

    if (ball.pos.y-ball.height/2 > canvas.height) {
        // player missed the ball
        soundLifeLost.play();
        ball.reset();
        player.reset();
        game.multiplier = 1;
        if (--game.lives == 0) game.reset();
    } else if (ball.pos.y-ball.height/2 <= 0) {
        // ball touched ceiling
        soundWallHit.play();
        ball.velocity.y = -ball.velocity.y;
    } else if (ball.pos.x-ball.width/2 <= 0 || ball.pos.x+ball.width/2 >= canvas.width) {
        // ball touched left or right wall
        soundWallHit.play();
        ball.velocity.x = -ball.velocity.x;
    }
    ball.pos.x += ball.velocity.x * deltaTime;
    ball.pos.y += ball.velocity.y * deltaTime;

    // detect ball collision with player
    if (collisionDetection(ball, player)) {
        soundPlayerHit.play();
        ball.velocity.x = ((ball.pos.x+ball.width/2) - (player.pos.x+player.width/2)) * 6;
        ball.velocity.y = -ball.velocity.y;
        game.multiplier = 1;
    }

    // detect ball collision with blocks
    game.blocks.forEach(function(block) {
        if (collisionDetection(ball, block)) {
            soundBlockHit.play();
            var i = game.blocks.indexOf(block);
            game.blocks.splice(i, 1);
            game.score += game.multiplier++;

            var side = collisionSide(ball, block);
            if (side == "up" || side == "down") {
                ball.velocity.y = -ball.velocity.y;
            } else {
                ball.velocity.x = -ball.velocity.x;
            }
        }
    });

    // check if level cleared
    if (game.blocks.length == 0) {
        game.blocks = generateLevel(++game.level);
        ball.reset();
        player.reset();
    }

    // draw background
    context.fillStyle = "#272822";
	context.fillRect(0, 0, canvas.width, canvas.height);

    // draw objects
    ball.draw();
    player.draw();
    game.blocks.forEach(function(block) {
        block.draw();
    });

    // draw text
    context.fillStyle = "rgba(255, 255, 255, 0.75)";
    context.font = "20px Georgia";
    context.textAlign = "left";
    context.fillText("Score: " + game.score, 10, 20);
    context.textAlign = "center";
    context.fillText("Level: " + game.level, canvas.width/2, 20);
    context.textAlign = "right";
    context.fillText("Lives: " + game.lives, canvas.width-10, 20);
    context.globalAlpha = 1;

    if (game.demoMode) {
        // AI
        if (ball.pos.y > canvas.height*0.3 && ball.pos.y < canvas.height*0.9) {
            if ((player.pos.x + player.width/2) - (ball.pos.x + ball.width/2) < 50) {
                player.moveRight();
            } else if ((player.pos.x + player.width/2) - (ball.pos.x + ball.width/2) > -50) {
                player.moveLeft();
            }
        }

        context.fillStyle = "white";
        context.textAlign = "center";
        context.fillText("Press <enter> to start playing", canvas.width / 2, canvas.height / 2);
    }
    window.requestAnimationFrame(loop);
}

game.reset();
ball.reset();
player.reset();
soundPlayerHit = new sound("sounds/playerhit.wav");
soundBlockHit = new sound("sounds/blockhit.mp3");
soundWallHit = new sound("sounds/wallhit.wav");
soundLifeLost = new sound("sounds/lifelost.mp3");
loop();<commit_msg>Better detection which side the ball made contact<commit_after>const canvas = document.getElementById("gamecanvas");
const context = canvas.getContext("2d");

var keys = [];
document.body.addEventListener("keydown", function (e) {
    keys[e.keyCode] = true;
});
document.body.addEventListener("keyup", function (e) {
    keys[e.keyCode] = false;
});

lastTime = (new Date()).getTime();
currentTime = 0;
deltaTime = 0;

const game = {
    demoMode: null,
    score: null,
    multiplier: null,
    lives: null,
    level: null,
    blocks: null,
    reset: function() {
        this.demoMode = true;
        this.score = 0;
        this.multiplier = 1;
        this.lives = 3;
        this.level = 1;
        this.blocks = generateLevel(this.level);
    },
}

const ball = {
    pos: {x: null, y: null},
    velocity: {x: null, y: null },
    width: null,
    height: null,
    speed: null,
    draw: function() {
        context.fillStyle = "#F8F8F2";
        context.fillRect(this.pos.x, this.pos.y, this.width, this.height);
    },
    reset: function() {
        this.pos.x = canvas.width / 2;
        this.pos.y = canvas.height - 64;
        this.speed = -300;
        this.velocity.x = (Math.random() + 0.5) * Math.pow(-1, Math.floor(Math.random() * 2)) * 60;
        if (game.demoMode) this.velocity.x += 2;
        this.velocity.y = this.speed;
        this.width = this.height = 12;
    },
};

const player = {
    pos: { x: null, y: null},
    velocity: null,
    width: null,
    height: null,
    speed: null,
    friction: null,
    draw: function() {
        context.fillStyle = "#F92672";
        context.fillRect(this.pos.x, this.pos.y, this.width, this.height);
    },
    moveLeft: function() {
        this.velocity -= this.speed;
    },
    moveRight: function() {
        this.velocity += this.speed;
    },
    updatePosition: function() {
        this.pos.x += this.velocity;
        this.velocity *= this.friction;

        if (this.pos.x+this.width/2 < 0) {
            this.pos.x = -this.width/2;
            this.velocity = 0;
        } else if (this.pos.x+this.width/2 > canvas.width) {
            this.pos.x = canvas.width-this.width/2;
            this.velocity = 0;
        }
    },
    reset: function() {
        this.width = 120;
        this.height = 16;
        this.pos.x = canvas.width / 2 - this.width / 2;
        this.pos.y = canvas.height - 32;
        this.velocity = 0;
        this.speed = 2;
        this.friction = 0.9;
    },
};

function Block(x, y, width, height, color="#FFE792") {
    this.pos = { x: x, y: y};
    this.width = width;
    this.height = height;
    this.color = color;
    this.draw = function() {
        context.fillStyle = this.color;
        context.fillRect(this.pos.x, this.pos.y, this.width, this.height);
    };
}

function collisionDetection(a, b) {
    if (a.pos.x < b.pos.x + b.width &&
        a.pos.x + a.width > b.pos.x &&
        a.pos.y < b.pos.y + b.height &&
        a.height + a.pos.y > b.pos.y) {
        return true;
    }
    return false;
}

// returns side on which the ball made contact
function collisionSide(ball, object) {
    // top/bottom or left/right
    if (ball.pos.x+ball.width >= object.pos.x && ball.pos.x <= object.pos.x+object.width) {
        console.log("top/bottom");
        return "top/bottom";
    } else if (ball.pos.y >= object.pos.y-object.height && object.pos.y+object.height <= object.pos.y) {
        console.log("left/right");
        return "left/right";
    }
    return "";
}

function generateLevel(level) {
    const blocks = [];
    const colors = ["#F92672", "#66D9EF", "#A6E22E", "#FD971F", "#AE81FF", "#FFE792", "#FFE792"];
    if (level == 1) {
        for (row = 0; row < 7; row++) {
            for (col = 0; col < 8; col++) {
                blocks.push(new Block(2+col*60, 60+row*20, 58, 16, colors[row]));
            }
        }
    }
    else {
        // simple random level
        for (row = 0; row < 7; row++) {
            for (col = 0; col < 8; col++) {
                if (Math.random() >= 0.5) continue;
                blocks.push(new Block(2+col*60, 60+row*20, 58, 16, colors[row]));
            }
        }
    }
    return blocks;
}

function sound(src) {
    this.sound = document.createElement("audio");
    this.sound.src = src;
    this.sound.setAttribute("preload", "auto");
    this.sound.setAttribute("controls", "none");
    this.sound.style.display = "none";
    document.body.appendChild(this.sound);
    this.play = function(){
        this.sound.play();
    }
    this.stop = function(){
        this.sound.pause();
    }
}

function loop() {
    if (keys[37] && !game.demoMode) player.moveLeft();
    else if (keys[39] && !game.demoMode) player.moveRight();
    if (keys[13] && game.demoMode) {
        game.reset();
        game.demoMode = false;
        ball.reset();
        player.reset();
    }

    currentTime = (new Date()).getTime();
    deltaTime = (currentTime - lastTime) / 1000;
    lastTime = currentTime;

    player.updatePosition();

    if (ball.pos.y-ball.height/2 > canvas.height) {
        // player missed the ball
        soundLifeLost.play();
        ball.reset();
        player.reset();
        game.multiplier = 1;
        if (--game.lives == 0) game.reset();
    } else if (ball.pos.y-ball.height/2 <= 0) {
        // ball touched ceiling
        soundWallHit.play();
        ball.velocity.y = -ball.velocity.y;
    } else if (ball.pos.x-ball.width/2 <= 0 || ball.pos.x+ball.width/2 >= canvas.width) {
        // ball touched left or right wall
        soundWallHit.play();
        ball.velocity.x = -ball.velocity.x;
    }
    ball.pos.x += ball.velocity.x * deltaTime;
    ball.pos.y += ball.velocity.y * deltaTime;

    // detect ball collision with player
    if (collisionDetection(ball, player)) {
        soundPlayerHit.play();
        ball.velocity.x = ((ball.pos.x+ball.width/2) - (player.pos.x+player.width/2)) * 6;
        ball.velocity.y = -ball.velocity.y;
        game.multiplier = 1;
    }

    // detect ball collision with blocks
    game.blocks.forEach(function(block) {
        if (collisionDetection(ball, block)) {
            soundBlockHit.play();
            var i = game.blocks.indexOf(block);
            game.blocks.splice(i, 1);
            game.score += game.multiplier++;

            var side = collisionSide(ball, block);
            if (side == "top/bottom") {
                ball.velocity.y = -ball.velocity.y;
            } else {
                ball.velocity.x = -ball.velocity.x;
            }
        }
    });

    // check if level cleared
    if (game.blocks.length == 0) {
        game.blocks = generateLevel(++game.level);
        ball.reset();
        player.reset();
    }

    // draw background
    context.fillStyle = "#272822";
	context.fillRect(0, 0, canvas.width, canvas.height);

    // draw objects
    ball.draw();
    player.draw();
    game.blocks.forEach(function(block) {
        block.draw();
    });

    // draw text
    context.fillStyle = "rgba(255, 255, 255, 0.75)";
    context.font = "20px Georgia";
    context.textAlign = "left";
    context.fillText("Score: " + game.score, 10, 20);
    context.textAlign = "center";
    context.fillText("Level: " + game.level, canvas.width/2, 20);
    context.textAlign = "right";
    context.fillText("Lives: " + game.lives, canvas.width-10, 20);
    context.globalAlpha = 1;

    if (game.demoMode) {
        // AI
        if (ball.pos.y > canvas.height*0.3 && ball.pos.y < canvas.height*0.9) {
            if ((player.pos.x + player.width/2) - (ball.pos.x + ball.width/2) < 50) {
                player.moveRight();
            } else if ((player.pos.x + player.width/2) - (ball.pos.x + ball.width/2) > -50) {
                player.moveLeft();
            }
        }

        context.fillStyle = "white";
        context.textAlign = "center";
        context.fillText("Press <enter> to start playing", canvas.width / 2, canvas.height / 2);
    }
    window.requestAnimationFrame(loop);
}

game.reset();
ball.reset();
player.reset();
soundPlayerHit = new sound("sounds/playerhit.wav");
soundBlockHit = new sound("sounds/blockhit.mp3");
soundWallHit = new sound("sounds/wallhit.wav");
soundLifeLost = new sound("sounds/lifelost.mp3");
loop();<|endoftext|>


<commit_before>
import _ from 'lodash';
import async from 'async';
import config from 'config';

import deb from 'debug';
const debug = deb('ldstats:server');

import {Author, Entry} from 'lib/models';
import scraper from 'lib/scraper';

//const tJam = _.clone(config.jam).reverse();
//const tCompo = _.clone(config.compo).reverse();

function fireErr(res, err){
  if (err) {
    debug('ERROR at /authors/:author ', err);
    return res.status(500).end();
  }
}

export const findAuthor = (req, res, next) => {
  let name = req.params.name;

  Author.findOne({ ldUser: name }, (err, author) => {
    if (err) return fireErr(res, err);
    req.author = author;
    next();
  });
};

export const fetchAuthor = (req, res, next) => {
  let name = req.params.name;

  if (req.author && req.author.ludumFetch >= config.lastLudum){
    // Author data is up to date so go on
    debug('Using existing Author ' + req.author.ldUser);
    return next();
  }

  function updateAndGoOn(err, author){
    if (err) return fireErr(res, err);
    req.author = author;
    next();
  }

  // Scrap Author
  scraper.fetchAuthor(name, (err, _author) => {
    if (err) return fireErr(res, err);

    if (!_author){
      return res.status(404).send({ error: "Author Not Found"});
    }

    if (req.author) {
      _author.updated_at = Date.now();
      return req.author.save(_author, updateAndGoOn);
    }

    return Author.create(_author, updateAndGoOn);
  });

};

export const findEntries = (req, res, next) => {
  if (req.entries){
    return next();
  }

  debug('Filling entries for Author ' + req.author.ldUser);

  Entry.find({ author: req.author._id }).exec((err, entries) => {
    if (err) return fireErr(res, err);
    req.entries = entries;
    next();
  });

};

export const fetchEntries = (req, res, next) => {

  let missingLudums = _.filter(req.author.ludums, ld => {
    return !_.find(req.entries, entry => entry.ludum === ld);
  });

  if (!missingLudums.length){
    return next();
  }

  scraper.fetchEntries(req.author, missingLudums, (err, entries) => {
    if (err) return fireErr(res, err);
    Entry.create(entries, (err) => {
      if (err) return fireErr(res, err);
      req.entries = null; // so it re-finds them
      next();
    });

  });

};

export const sendAuthor = (req, res) => {
  req.author = _.omit(req.author.toJSON(), ['_id', 'created_at', 'updated_at', '__v', 'ludumFetch']);

  req.author.entries = _.map(req.entries, entry => {
    return _.omit(entry.toJSON(), ['_id', 'author', '__v', 'created_at', 'updated_at']);
  });

  res.send(req.author);
};
<commit_msg>fixed re-fetch for new author entries.<commit_after>
import _ from 'lodash';
import async from 'async';
import config from 'config';

import deb from 'debug';
const debug = deb('ldstats:server');

import {Author, Entry} from 'lib/models';
import scraper from 'lib/scraper';

function fireErr(res, err){
  if (err) {
    debug('ERROR at /authors/:author ', err);
    return res.status(500).end();
  }
}

export const findAuthor = (req, res, next) => {
  let name = req.params.name;

  Author.findOne({ ldUser: name }, (err, author) => {
    if (err) return fireErr(res, err);
    req.author = author;
    next();
  });
};

export const fetchAuthor = (req, res, next) => {
  let name = req.params.name;

  if (req.author && req.author.ludumFetch >= config.lastLudum){
    // Author data is up to date so go on
    debug('Using existing Author ' + req.author.ldUser);
    return next();
  }

  function updateAndGoOn(err, author){
    if (err) return fireErr(res, err);
    req.author = author;
    next();
  }

  // Scrap Author
  scraper.fetchAuthor(name, (err, _author) => {
    if (err) return fireErr(res, err);

    if (!_author){
      return res.status(404).send({ error: "Author Not Found"});
    }

    if (req.author) {
      req.author.updated_at = Date.now();
      req.author.link = _author.link;
      req.author.ldUser = _author.ldUser;
      req.author.ldUserId = _author.ldUserId;
      req.author.ludumFetch = _author.ludumFetch;
      req.author.ludums = _author.ludums;

      req.author.markModified('ludums');
      return req.author.save(updateAndGoOn);
    }

    return Author.create(_author, updateAndGoOn);
  });

};

export const findEntries = (req, res, next) => {
  if (req.entries){
    return next();
  }

  debug('Filling entries for Author ' + req.author.ldUser);

  Entry.find({ author: req.author._id }).exec((err, entries) => {
    if (err) return fireErr(res, err);
    req.entries = entries;
    next();
  });

};

export const fetchEntries = (req, res, next) => {

  let missingLudums = _.filter(req.author.ludums, ld => {
    return !_.find(req.entries, entry => entry.ludum === ld);
  });

  if (!missingLudums.length){
    return next();
  }

  scraper.fetchEntries(req.author, missingLudums, (err, entries) => {
    if (err) return fireErr(res, err);
    Entry.create(entries, (err) => {
      if (err) return fireErr(res, err);
      req.entries = null; // so it re-finds them
      next();
    });

  });

};

export const sendAuthor = (req, res) => {
  req.author = _.omit(req.author.toJSON(), ['_id', 'created_at', 'updated_at', '__v', 'ludumFetch']);

  req.author.entries = _.map(req.entries, entry => {
    return _.omit(entry.toJSON(), ['_id', 'author', '__v', 'created_at', 'updated_at']);
  });

  res.send(req.author);
};
<|endoftext|>


<commit_before>'use strict';

(function () {

  var _ = require('underscore');
  var request = require('superagent');
  var cheerio = require('cheerio');
  var wordCount = require('word-count');
  var yaml = require('js-yaml');

  var utils = require('./utils');

  var isSameDomain = utils.isSameDomain;
  var resolveUrl = utils.resolveUrl;
  var isValidBaseUrl = utils.isValidBaseUrl;

  var PROGRESS_LINES = 9;
  var PADDING = '                ';

  function padLeft (value) {
    value = PADDING + value;
    return value.substring(value.length - PADDING.length);
  }

  function clearLines (lines) {
    for (var i = lines; i > 0; i -= 1) {
      process.stdout.cursorTo(0, i); // eslint-disable-line no-undef
      process.stdout.clearLine(); // eslint-disable-line no-undef
    }
  }

  function createProgressBar (scope) {
    var barLength = 50;
    var currentDepth = scope.farthestDepth;
    var totalDepth = scope.depth;
    var progress = barLength / totalDepth * currentDepth;

    var bar = '';

    for (var i = 0; i < barLength; i += 1) {
      bar += progress > i ? '#' : '-';
    }

    return '[' + bar + '] ' + (progress * 2).toFixed(2) + '%';
  }

  function createProgressMessage (scope, currentTask) {
    return (
      '--------------------- Progress ---------------------\n\n' +
      'Depth:  {farthestDepth} / {depth}\n' +
      'Urls:   {currentUrls} / {totalUrls}\n' +
      'Failed: {failed}\n\n' +
      createProgressBar(scope) + '\n\n' +
      currentTask
    )
    .replace('{farthestDepth}', padLeft(scope.farthestDepth))
    .replace('{depth}', scope.depth)
    .replace('{currentUrls}', padLeft(scope.urlsCrawled.length))
    .replace('{totalUrls}', scope.urlsToCrawl.length)
    .replace('{failed}', padLeft(scope.failed));
  }

  function updateProgress (scope, currentTask) {
    if (typeof process === 'object') {
      clearLines(PROGRESS_LINES);
      process.stdout.write(createProgressMessage(scope, currentTask)); // eslint-disable-line no-undef
    }
  }

  function markHrefsAsFailed (scope) {
    _.each(scope.json.urls, function (crawledUrl, index) {
      if (crawledUrl.hrefs) {
        _.each(crawledUrl.hrefs.internal, function (href, hrefIndex) {
          if (href.resolved in scope.json) {
            scope.json.urls[index].hrefs.internal[hrefIndex].crawled = true;
            scope.json.urls[index].hrefs.internal[hrefIndex].failed = scope.json[href.resolved].failed;
            scope.json.urls[index].hrefs.internal[hrefIndex].type = scope.json[href.resolved].type;
            scope.json.urls[index].hrefs.internal[hrefIndex].status = scope.json[href.resolved].status;
          } else {
            scope.json.urls[index].hrefs.internal[hrefIndex].crawled = false;
          }
        });
      }
    });
  }

  function getFailedUrls (scope) {
    return _.chain(scope.json.urls)
    .filter(function (crawledUrl) {
      return crawledUrl.failed;
    })
    .map(function (crawledUrl) {
      return {
        url: crawledUrl.url,
        resolved: crawledUrl.resolved,
        linkedFrom: _.chain(scope.json.urls)
        .filter(function (possiblyLinkedFrom) {
          return possiblyLinkedFrom.hrefs && _.any(possiblyLinkedFrom.hrefs.internal, function (href) {
            return href.url === crawledUrl.url;
          });
        })
        .map(function (linkedFrom) {
          return linkedFrom.resolved;
        })
      };
    })
    .value();
  }

  function getHrefs (scope, url, $) {
    return _.chain($('a[href]').toArray().map(function (el) {
      var element = $(el);

      return element.attr('href');
    }))
    .groupBy(_.identity)
    .map(function (values, key) {
      return {
        url: key,
        resolved: resolveUrl(key, url.resolved),
        references: values.length
      };
    })
    .sortBy('url')
    .groupBy(function (href) {
      if (href.url.indexOf('#') === 0) {
        return 'samePage';
      }

      if (href.url.indexOf('tel:') === 0) {
        return 'phone';
      }

      if (href.url.indexOf('mailto:') === 0) {
        return 'email';
      }

      return isSameDomain(href.url, scope.baseUrl.resolved) ? 'internal' : 'external';
    })
    .value();
  }

  function getAttribs ($, selector) {
    return _.chain($(selector).toArray())
    .map(function (link) {
      return link.attribs;
    })
    .value();
  }

  function getData (scope, res, url) {
    var $ = cheerio.load(res.text);

    var hrefs = getHrefs(scope, url, $);

    return {
      url: url.url,
      resolved: url.resolved,
      failed: false,
      type: res.type,
      status: res.status,
      title: $('title').text() || null,
      wordCount: wordCount($('body').text()),
      charset: $('meta[charset]').attr('charset') || null,
      meta: getAttribs($, 'meta[name],meta[property]'),
      links: getAttribs($, 'link[rel]'),
      scripts: getAttribs($, 'script[src]'),
      h1: $('h1').first().text() || null,
      h2: $('h2').first().text() || null,
      h3: $('h3').first().text() || null,
      p: $('p').first().text() || null,
      hrefs: {
        internal: hrefs.internal || [],
        external: hrefs.external || [],
        samePage: hrefs.samePage || [],
        email: hrefs.email || [],
        phone: hrefs.phone || []
      }
    };
  }

  function getOutput (scope) {
    switch (scope.format) {
      case 'yaml':
        return yaml.safeDump(scope.json, {indent: 2});
      case 'json':
      default:
        return JSON.stringify(scope.json, null, 2) + '\n';
    }
  }

  function complete (scope) {
    markHrefsAsFailed(scope);

    scope.json.failedUrls = getFailedUrls(scope);;
    scope.json.totalUrlsCrawled = _.size(scope.json.urls);
    scope.json.totalFailedUrls = _.size(scope.json.failedUrls);;

    if (typeof process === 'object') {
      clearLines(PROGRESS_LINES);
      process.stdout.write(getOutput(scope)); // eslint-disable-line no-undef
    } else if (typeof scope.callback === 'function') {
      scope.callback(getOutput(scope));
    }
  }

  function continueCrawl (scope, url, currentDepth, then) {
    if (currentDepth > scope.farthestDepth) {
      scope.farthestDepth = currentDepth;
    }

    updateProgress(scope, 'Crawling: ' + url.resolved);

    request
    .get(url.resolved)
    .accept('text/html')
    .send()
    .end(function (err, res) {
      scope.urlsCrawled.push(url.resolved);
      updateProgress(scope, 'Crawled: ' + url.resolved);

      if (err) {
        scope.failed += 1;

        scope.json.urls.push({
          url: url.url,
          resolved: url.resolved,
          failed: true,
          type: err.type || null,
          status: err.status || null
        });
      } else if (res.type === 'text/html' && res.text) {
        scope.json.urls.push(getData(scope, res, url));

        var index = scope.json.urls.length - 1;

        if (currentDepth < scope.depth) {
          _.each(scope.json.urls[index].hrefs.internal, function (href) {
            if (scope.urlsToCrawl.indexOf(href.resolved) < 0) {
              scope.urlsToCrawl.push(href.resolved);
              scope.queue.push(continueCrawl.bind(null, scope, href, currentDepth + 1));
            }
          });
        }
      } else {
        scope.json.urls.push({
          url: url.url,
          resolved: url.resolved,
          failed: false,
          type: res.type,
          status: res.status
        });
      }

      if (typeof then === 'function') {
        then();
      }

      if (scope.urlsToCrawl.length === scope.urlsCrawled.length) {
        complete(scope);
      }
    });
  }

  function runCrawl (scope) {
    if (scope.parallel) {
      while (scope.queue.length) {
        scope.queue.shift()(runCrawl.bind(null, scope));
      }
    } if (scope.queue.length) {
      scope.queue.shift()(runCrawl.bind(null, scope));
    }
  }

  function crawl (options) {
    isValidBaseUrl(options.url);

    var resolvedUrl = resolveUrl(options.url);

    var scope = {
      json: {
        urls: []
      },
      baseUrl: {
        url: options.url,
        resolved: resolvedUrl
      },
      urlsToCrawl: [resolvedUrl],
      urlsCrawled: [],
      queue: [],
      farthestDepth: 0,
      failed: 0,
      depth: options.depth,
      format: options.format,
      callback: options.callback,
      parallel: options.parallel
    };

    scope.queue.push(continueCrawl.bind(null, scope, scope.baseUrl, 0));

    runCrawl(scope);

  }

  module.exports = {
    crawl: crawl
  };

})();
<commit_msg>Add newline after progress<commit_after>'use strict';

(function () {

  var _ = require('underscore');
  var request = require('superagent');
  var cheerio = require('cheerio');
  var wordCount = require('word-count');
  var yaml = require('js-yaml');

  var utils = require('./utils');

  var isSameDomain = utils.isSameDomain;
  var resolveUrl = utils.resolveUrl;
  var isValidBaseUrl = utils.isValidBaseUrl;

  var PROGRESS_LINES = 10;
  var PADDING = '                ';

  function padLeft (value) {
    value = PADDING + value;
    return value.substring(value.length - PADDING.length);
  }

  function clearLines (lines) {
    for (var i = lines; i > 0; i -= 1) {
      process.stdout.cursorTo(0, i); // eslint-disable-line no-undef
      process.stdout.clearLine(); // eslint-disable-line no-undef
    }
  }

  function createProgressBar (scope) {
    var barLength = 50;
    var currentDepth = scope.farthestDepth;
    var totalDepth = scope.depth;
    var progress = barLength / totalDepth * currentDepth;

    var bar = '';

    for (var i = 0; i < barLength; i += 1) {
      bar += progress > i ? '#' : '-';
    }

    return '[' + bar + '] ' + (progress * 2).toFixed(2) + '%';
  }

  function createProgressMessage (scope, currentTask) {
    return (
      '--------------------- Progress ---------------------\n\n' +
      'Depth:  {farthestDepth} / {depth}\n' +
      'Urls:   {currentUrls} / {totalUrls}\n' +
      'Failed: {failed}\n\n' +
      createProgressBar(scope) + '\n\n' +
      currentTask + '\n'
    )
    .replace('{farthestDepth}', padLeft(scope.farthestDepth))
    .replace('{depth}', scope.depth)
    .replace('{currentUrls}', padLeft(scope.urlsCrawled.length))
    .replace('{totalUrls}', scope.urlsToCrawl.length)
    .replace('{failed}', padLeft(scope.failed));
  }

  function updateProgress (scope, currentTask) {
    if (typeof process === 'object') {
      clearLines(PROGRESS_LINES);
      process.stdout.write(createProgressMessage(scope, currentTask)); // eslint-disable-line no-undef
    }
  }

  function markHrefsAsFailed (scope) {
    _.each(scope.json.urls, function (crawledUrl, index) {
      if (crawledUrl.hrefs) {
        _.each(crawledUrl.hrefs.internal, function (href, hrefIndex) {
          if (href.resolved in scope.json) {
            scope.json.urls[index].hrefs.internal[hrefIndex].crawled = true;
            scope.json.urls[index].hrefs.internal[hrefIndex].failed = scope.json[href.resolved].failed;
            scope.json.urls[index].hrefs.internal[hrefIndex].type = scope.json[href.resolved].type;
            scope.json.urls[index].hrefs.internal[hrefIndex].status = scope.json[href.resolved].status;
          } else {
            scope.json.urls[index].hrefs.internal[hrefIndex].crawled = false;
          }
        });
      }
    });
  }

  function getFailedUrls (scope) {
    return _.chain(scope.json.urls)
    .filter(function (crawledUrl) {
      return crawledUrl.failed;
    })
    .map(function (crawledUrl) {
      return {
        url: crawledUrl.url,
        resolved: crawledUrl.resolved,
        linkedFrom: _.chain(scope.json.urls)
        .filter(function (possiblyLinkedFrom) {
          return possiblyLinkedFrom.hrefs && _.any(possiblyLinkedFrom.hrefs.internal, function (href) {
            return href.url === crawledUrl.url;
          });
        })
        .map(function (linkedFrom) {
          return linkedFrom.resolved;
        })
      };
    })
    .value();
  }

  function getHrefs (scope, url, $) {
    return _.chain($('a[href]').toArray().map(function (el) {
      var element = $(el);

      return element.attr('href');
    }))
    .groupBy(_.identity)
    .map(function (values, key) {
      return {
        url: key,
        resolved: resolveUrl(key, url.resolved),
        references: values.length
      };
    })
    .sortBy('url')
    .groupBy(function (href) {
      if (href.url.indexOf('#') === 0) {
        return 'samePage';
      }

      if (href.url.indexOf('tel:') === 0) {
        return 'phone';
      }

      if (href.url.indexOf('mailto:') === 0) {
        return 'email';
      }

      return isSameDomain(href.url, scope.baseUrl.resolved) ? 'internal' : 'external';
    })
    .value();
  }

  function getAttribs ($, selector) {
    return _.chain($(selector).toArray())
    .map(function (link) {
      return link.attribs;
    })
    .value();
  }

  function getData (scope, res, url) {
    var $ = cheerio.load(res.text);

    var hrefs = getHrefs(scope, url, $);

    return {
      url: url.url,
      resolved: url.resolved,
      failed: false,
      type: res.type,
      status: res.status,
      title: $('title').text() || null,
      wordCount: wordCount($('body').text()),
      charset: $('meta[charset]').attr('charset') || null,
      meta: getAttribs($, 'meta[name],meta[property]'),
      links: getAttribs($, 'link[rel]'),
      scripts: getAttribs($, 'script[src]'),
      h1: $('h1').first().text() || null,
      h2: $('h2').first().text() || null,
      h3: $('h3').first().text() || null,
      p: $('p').first().text() || null,
      hrefs: {
        internal: hrefs.internal || [],
        external: hrefs.external || [],
        samePage: hrefs.samePage || [],
        email: hrefs.email || [],
        phone: hrefs.phone || []
      }
    };
  }

  function getOutput (scope) {
    switch (scope.format) {
      case 'yaml':
        return yaml.safeDump(scope.json, {indent: 2});
      case 'json':
      default:
        return JSON.stringify(scope.json, null, 2) + '\n';
    }
  }

  function complete (scope) {
    markHrefsAsFailed(scope);

    scope.json.failedUrls = getFailedUrls(scope);;
    scope.json.totalUrlsCrawled = _.size(scope.json.urls);
    scope.json.totalFailedUrls = _.size(scope.json.failedUrls);;

    if (typeof process === 'object') {
      clearLines(PROGRESS_LINES);
      process.stdout.write(getOutput(scope)); // eslint-disable-line no-undef
    } else if (typeof scope.callback === 'function') {
      scope.callback(getOutput(scope));
    }
  }

  function continueCrawl (scope, url, currentDepth, then) {
    if (currentDepth > scope.farthestDepth) {
      scope.farthestDepth = currentDepth;
    }

    updateProgress(scope, 'Crawling: ' + url.resolved);

    request
    .get(url.resolved)
    .accept('text/html')
    .send()
    .end(function (err, res) {
      scope.urlsCrawled.push(url.resolved);
      updateProgress(scope, 'Crawled: ' + url.resolved);

      if (err) {
        scope.failed += 1;

        scope.json.urls.push({
          url: url.url,
          resolved: url.resolved,
          failed: true,
          type: err.type || null,
          status: err.status || null
        });
      } else if (res.type === 'text/html' && res.text) {
        scope.json.urls.push(getData(scope, res, url));

        var index = scope.json.urls.length - 1;

        if (currentDepth < scope.depth) {
          _.each(scope.json.urls[index].hrefs.internal, function (href) {
            if (scope.urlsToCrawl.indexOf(href.resolved) < 0) {
              scope.urlsToCrawl.push(href.resolved);
              scope.queue.push(continueCrawl.bind(null, scope, href, currentDepth + 1));
            }
          });
        }
      } else {
        scope.json.urls.push({
          url: url.url,
          resolved: url.resolved,
          failed: false,
          type: res.type,
          status: res.status
        });
      }

      if (typeof then === 'function') {
        then();
      }

      if (scope.urlsToCrawl.length === scope.urlsCrawled.length) {
        complete(scope);
      }
    });
  }

  function runCrawl (scope) {
    if (scope.parallel) {
      while (scope.queue.length) {
        scope.queue.shift()(runCrawl.bind(null, scope));
      }
    } if (scope.queue.length) {
      scope.queue.shift()(runCrawl.bind(null, scope));
    }
  }

  function crawl (options) {
    isValidBaseUrl(options.url);

    var resolvedUrl = resolveUrl(options.url);

    var scope = {
      json: {
        urls: []
      },
      baseUrl: {
        url: options.url,
        resolved: resolvedUrl
      },
      urlsToCrawl: [resolvedUrl],
      urlsCrawled: [],
      queue: [],
      farthestDepth: 0,
      failed: 0,
      depth: options.depth,
      format: options.format,
      callback: options.callback,
      parallel: options.parallel
    };

    scope.queue.push(continueCrawl.bind(null, scope, scope.baseUrl, 0));

    runCrawl(scope);

  }

  module.exports = {
    crawl: crawl
  };

})();
<|endoftext|>


<commit_before>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

var App = require('app');

App.MainServiceManageConfigGroupView = Em.View.extend({

  templateName: require('templates/main/service/manage_configuration_groups_popup'),

  /**
   * Select Config Group
   * @type {App.ConfigGroup}
   */
  selectedConfigGroup: null,

  /**
   * Determines if "Delete Config Group" button is disabled
   * @type {boolean}
   */
  isRemoveButtonDisabled: true,

  /**
   * Determines if "Rename Config Group" button is disabled
   * @type {boolean}
   */
  isRenameButtonDisabled: true,

  /**
   * Determines if "Duplicate Config Group" button is disabled
   * @type {boolean}
   */
  isDuplicateButtonDisabled: true,

  /**
   * Tooltip for "Add Config Group" button
   * @type {string}
   */
  addButtonTooltip: Em.I18n.t('services.service.config_groups_popup.addButton'),

  /**
   * Tooltip for "Remove Config Group" button
   * @type {string}
   */
  removeButtonTooltip: Em.I18n.t('services.service.config_groups_popup.removeButton'),

  /**
   * Tooltip for "Rename Config Group" button
   * @type {string}
   */
  renameButtonTooltip: Em.I18n.t('services.service.config_groups_popup.renameButton'),

  /**
   * Tooltip for "Duplicate Config Group" button
   * @type {string}
   */
  duplicateButtonTooltip: Em.I18n.t('services.service.config_groups_popup.duplicateButton'),

  /**
   * Tooltip for "Remove Host From Config Group" button
   * @type {string}
   */
  removeHostTooltip: Em.I18n.t('services.service.config_groups_popup.removeHost'),

  /**
   * Tooltip for "Add Host To Config Group" button
   * @type {string}
   */
  addHostTooltip: function () {
    var selectedConfigGroup = this.get('controller.selectedConfigGroup');
    if (!selectedConfigGroup.get('isDefault') && selectedConfigGroup.get('isAddHostsDisabled')) {
      return Em.I18n.t('services.service.config_groups_popup.addHostDisabled');
    } else {
      return  Em.I18n.t('services.service.config_groups_popup.addHost');
    }
  }.property('controller.selectedConfigGroup.isDefault', 'controller.selectedConfigGroup.isAddHostsDisabled'),

  willInsertElement: function() {
    this.get('controller').loadHosts();
  },

  didInsertElement: function () {
    this.selectDefaultGroup();
    App.tooltip($('.properties-link'));
    App.tooltip($("[rel='button-info']"));
    App.tooltip($("[rel='button-info-dropdown']"), {placement: 'left'});
  },

  willDestroyElement: function () {
    this.get('controller.configGroups').clear();
    this.get('controller.originalConfigGroups').clear();
  },

  /**
   * Disable actions remove and rename for Default config group
   * @method buttonObserver
   */
  buttonObserver: function () {
    var selectedConfigGroup = this.get('controller.selectedConfigGroup');
    this.set('isRemoveButtonDisabled', selectedConfigGroup.get('isDefault'));
    this.set('isRenameButtonDisabled', selectedConfigGroup.get('isDefault'));
    this.set('isDuplicateButtonDisabled', false);
  }.observes('controller.selectedConfigGroup'),

  /**
   * Prevent user to select more than 1 config group
   * Select last one of "selected"
   * Clean up controller.selectedHosts
   * @method onGroupSelect
   */
  onGroupSelect: function () {
    var selectedConfigGroup = this.get('selectedConfigGroup');
    // to unable user select more than one config group at a time
    if (selectedConfigGroup.length) {
      this.set('controller.selectedConfigGroup', selectedConfigGroup[selectedConfigGroup.length - 1]);
    }
    if (selectedConfigGroup.length > 1) {
      this.set('selectedConfigGroup', selectedConfigGroup[selectedConfigGroup.length - 1]);
    }
    this.set('controller.selectedHosts', []);
  }.observes('selectedConfigGroup'),

  /**
   * Select default config group after all config groups are loaded
   * @method selectDefaultGroup
   */
  selectDefaultGroup: function () {
    if (this.get('controller.isLoaded')) {
      this.set('selectedConfigGroup', [this.get('controller.configGroups').findProperty('isDefault')]);
    }
  }.observes('controller.isLoaded', 'controller.groupDeleteTrigger')

});
<commit_msg>AMBARI-15948. Fix bugs with config groups restoring after next/back actions (akovalenko)<commit_after>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

var App = require('app');

App.MainServiceManageConfigGroupView = Em.View.extend({

  templateName: require('templates/main/service/manage_configuration_groups_popup'),

  /**
   * Select Config Group
   * @type {App.ConfigGroup}
   */
  selectedConfigGroup: null,

  /**
   * Determines if "Delete Config Group" button is disabled
   * @type {boolean}
   */
  isRemoveButtonDisabled: true,

  /**
   * Determines if "Rename Config Group" button is disabled
   * @type {boolean}
   */
  isRenameButtonDisabled: true,

  /**
   * Determines if "Duplicate Config Group" button is disabled
   * @type {boolean}
   */
  isDuplicateButtonDisabled: true,

  /**
   * Tooltip for "Add Config Group" button
   * @type {string}
   */
  addButtonTooltip: Em.I18n.t('services.service.config_groups_popup.addButton'),

  /**
   * Tooltip for "Remove Config Group" button
   * @type {string}
   */
  removeButtonTooltip: Em.I18n.t('services.service.config_groups_popup.removeButton'),

  /**
   * Tooltip for "Rename Config Group" button
   * @type {string}
   */
  renameButtonTooltip: Em.I18n.t('services.service.config_groups_popup.renameButton'),

  /**
   * Tooltip for "Duplicate Config Group" button
   * @type {string}
   */
  duplicateButtonTooltip: Em.I18n.t('services.service.config_groups_popup.duplicateButton'),

  /**
   * Tooltip for "Remove Host From Config Group" button
   * @type {string}
   */
  removeHostTooltip: Em.I18n.t('services.service.config_groups_popup.removeHost'),

  /**
   * Tooltip for "Add Host To Config Group" button
   * @type {string}
   */
  addHostTooltip: function () {
    var selectedConfigGroup = this.get('controller.selectedConfigGroup');
    if (!selectedConfigGroup.get('isDefault') && selectedConfigGroup.get('isAddHostsDisabled')) {
      return Em.I18n.t('services.service.config_groups_popup.addHostDisabled');
    } else {
      return  Em.I18n.t('services.service.config_groups_popup.addHost');
    }
  }.property('controller.selectedConfigGroup.isDefault', 'controller.selectedConfigGroup.isAddHostsDisabled'),

  willInsertElement: function() {
    this.get('controller').loadHosts();
  },

  willDestroyElement: function () {
    this.get('controller.configGroups').clear();
    this.get('controller.originalConfigGroups').clear();
  },

  showTooltip: function () {
    if (!this.get('controller.isLoaded')) return false;
    Em.run.next(function(){
      App.tooltip($('.properties-link'));
      App.tooltip($("[rel='button-info']"));
      App.tooltip($("[rel='button-info-dropdown']"), {placement: 'left'});
    });
  }.observes('controller.isLoaded'),

  /**
   * Disable actions remove and rename for Default config group
   * @method buttonObserver
   */
  buttonObserver: function () {
    var selectedConfigGroup = this.get('controller.selectedConfigGroup');
    this.set('isRemoveButtonDisabled', selectedConfigGroup.get('isDefault'));
    this.set('isRenameButtonDisabled', selectedConfigGroup.get('isDefault'));
    this.set('isDuplicateButtonDisabled', false);
  }.observes('controller.selectedConfigGroup'),

  /**
   * Prevent user to select more than 1 config group
   * Select last one of "selected"
   * Clean up controller.selectedHosts
   * @method onGroupSelect
   */
  onGroupSelect: function () {
    var selectedConfigGroup = this.get('selectedConfigGroup');
    // to unable user select more than one config group at a time
    if (selectedConfigGroup.length) {
      this.set('controller.selectedConfigGroup', selectedConfigGroup[selectedConfigGroup.length - 1]);
    }
    if (selectedConfigGroup.length > 1) {
      this.set('selectedConfigGroup', selectedConfigGroup[selectedConfigGroup.length - 1]);
    }
    this.set('controller.selectedHosts', []);
  }.observes('selectedConfigGroup'),

  /**
   * Select default config group after all config groups are loaded
   * @method selectDefaultGroup
   */
  selectDefaultGroup: function () {
    if (this.get('controller.isLoaded')) {
      this.set('selectedConfigGroup', [this.get('controller.configGroups').findProperty('isDefault')]);
    }
  }.observes('controller.isLoaded', 'controller.groupDeleteTrigger')

});
<|endoftext|>


<commit_before>// Convert straight quotation marks to typographic ones
//
'use strict';

var quoteReg = /"|'/g;
var punctReg = /[-\s()\[\]]/;
var apostrophe = '’';

// This function returns true if the character at `pos`
// could be inside a word.
function isLetter(str, pos) {
  if (pos < 0 || pos >= str.length) { return false; }
  return !punctReg.test(str[pos]);
}

function addQuote(obj, tokenId, posId, str) {
  if (!obj[tokenId]) { obj[tokenId] = {}; }
  obj[tokenId][posId] = str;
}

module.exports = function smartquotes(typographer, state) {
  var i, token, text, t, pos, max, thisLevel, lastSpace, nextSpace, item, canOpen, canClose, j, isSingle, fn, chars,
      options = typographer.options,
      replace = {},
      tokens = state.tokens,
      stack = [];

  for (i = 0; i < tokens.length; i++) {
    token = tokens[i];
    thisLevel = tokens[i].level;

    for (j = stack.length - 1; j >= 0; j--) {
      if (stack[j].level <= thisLevel) { break; }
    }
    stack.length = j + 1;

    if (token.type === 'text') {
      text = token.content;
      pos = 0;
      max = text.length;

      while (pos < max) {
        quoteReg.lastIndex = pos;
        t = quoteReg.exec(text);
        if (!t) { break; }

        lastSpace = !isLetter(text, t.index - 1);
        pos = t.index + t[0].length;
        isSingle = t[0] === "'";
        nextSpace = !isLetter(text, pos);

        if (!nextSpace && !lastSpace) {
          // middle word
          if (isSingle) {
            addQuote(replace, i, t.index, apostrophe);
          }
          continue;
        }

        canOpen = !nextSpace;
        canClose = !lastSpace;

        if (canClose) {
          // this could be a closing quote, rewind the stack to get a match
          for (j = stack.length - 1; j >= 0; j--) {
            item = stack[j];
            if (stack[j].level < thisLevel) { break; }
            if (item.single === isSingle && stack[j].level === thisLevel) {
              item = stack[j];
              chars = isSingle ? options.singleQuotes : options.doubleQuotes;
              if (chars) {
                addQuote(replace, item.token, item.start, chars[0]);
                addQuote(replace, i, t.index, chars[1]);
              }
              stack.length = j;
              canOpen = false; // should be "continue OUTER;", but eslint refuses labels :(
              break;
            }
          }
        }

        if (canOpen) {
          stack.push({
            token: i,
            start: t.index,
            end: pos,
            single: isSingle,
            level: thisLevel
          });
        } else if (canClose && isSingle) {
          addQuote(replace, i, t.index, apostrophe);
        }
      }
    }
  }

  fn = function(str, pos) {
    if (!replace[i][pos]) { return str; }
    return replace[i][pos];
  };

  for (i = 0; i < tokens.length; i++) {
    if (!replace[i]) { continue; }
    quoteReg.lastIndex = 0;
    tokens[i].content = tokens[i].content.replace(quoteReg, fn);
  }
};
<commit_msg>Removed eslint warning<commit_after>// Convert straight quotation marks to typographic ones
//
'use strict';

var quoteReg = /"|'/g;
var punctReg = /[-\s()\[\]]/;
var apostrophe = '’';

// This function returns true if the character at `pos`
// could be inside a word.
function isLetter(str, pos) {
  if (pos < 0 || pos >= str.length) { return false; }
  return !punctReg.test(str[pos]);
}

function addQuote(obj, tokenId, posId, str) {
  if (!obj[tokenId]) { obj[tokenId] = {}; }
  obj[tokenId][posId] = str;
}

module.exports = function smartquotes(typographer, state) {
  /*eslint max-depth:0*/
  var i, token, text, t, pos, max, thisLevel, lastSpace, nextSpace, item, canOpen, canClose, j, isSingle, fn, chars,
      options = typographer.options,
      replace = {},
      tokens = state.tokens,
      stack = [];

  for (i = 0; i < tokens.length; i++) {
    token = tokens[i];
    thisLevel = tokens[i].level;

    for (j = stack.length - 1; j >= 0; j--) {
      if (stack[j].level <= thisLevel) { break; }
    }
    stack.length = j + 1;

    if (token.type === 'text') {
      text = token.content;
      pos = 0;
      max = text.length;

      while (pos < max) {
        quoteReg.lastIndex = pos;
        t = quoteReg.exec(text);
        if (!t) { break; }

        lastSpace = !isLetter(text, t.index - 1);
        pos = t.index + t[0].length;
        isSingle = t[0] === "'";
        nextSpace = !isLetter(text, pos);

        if (!nextSpace && !lastSpace) {
          // middle word
          if (isSingle) {
            addQuote(replace, i, t.index, apostrophe);
          }
          continue;
        }

        canOpen = !nextSpace;
        canClose = !lastSpace;

        if (canClose) {
          // this could be a closing quote, rewind the stack to get a match
          for (j = stack.length - 1; j >= 0; j--) {
            item = stack[j];
            if (stack[j].level < thisLevel) { break; }
            if (item.single === isSingle && stack[j].level === thisLevel) {
              item = stack[j];
              chars = isSingle ? options.singleQuotes : options.doubleQuotes;
              if (chars) {
                addQuote(replace, item.token, item.start, chars[0]);
                addQuote(replace, i, t.index, chars[1]);
              }
              stack.length = j;
              canOpen = false; // should be "continue OUTER;", but eslint refuses labels :(
              break;
            }
          }
        }

        if (canOpen) {
          stack.push({
            token: i,
            start: t.index,
            end: pos,
            single: isSingle,
            level: thisLevel
          });
        } else if (canClose && isSingle) {
          addQuote(replace, i, t.index, apostrophe);
        }
      }
    }
  }

  fn = function(str, pos) {
    if (!replace[i][pos]) { return str; }
    return replace[i][pos];
  };

  for (i = 0; i < tokens.length; i++) {
    if (!replace[i]) { continue; }
    quoteReg.lastIndex = 0;
    tokens[i].content = tokens[i].content.replace(quoteReg, fn);
  }
};
<|endoftext|>