<commit_before>import assert from 'assert';
import makeDebug from 'debug';
import fp from 'mostly-func';
import { Service as BaseService } from './base';
import { splitHead, repeatDoubleStar } from './helpers';

const debug = makeDebug('mostly:feathers-mongoose:service');

const defaultOptions = {
  lean: true,
  paginate: {
    default: 10,
    max: 50
  }
};

const defaultMethods = ['find', 'get', 'create', 'update', 'patch', 'remove'];

// prevent accidental multiple operations
const assertMultiple = function (id, params, message) {
  if (!id) {
    if (params && params.query && (params.$multi || params.query.$multi)) {
      delete params.query.$multi;
    } else {
      throw new Error(message);
    }
  }
};

const unsetOptions = fp.pipe(
  fp.dissoc('Model'),
  fp.dissoc('ModelName')
);

const unset_id = function (obj) {
  if (obj && obj._id) {
    return fp.pipe(
      fp.assoc('id', String(obj.id || obj._id)),
      fp.dissoc('_id'),
      fp.dissoc('__v')
    )(obj);
  } else {
    return obj;
  }
};

const unsetObj = function (obj) {
  if (Array.isArray(obj)) {
    return fp.map(unset_id, obj);
  } else {
    return unset_id(obj);
  }
};

const normalizeSelect = function (select) {
  if (select) {
    // convert string $select to array
    if (fp.is(String, select)) {
      select = fp.map(fp.trim, fp.split(',', select));
    }
    // repeat ** as recursive fields
    select = repeatDoubleStar(select);
  }
  return select;
};

const filterSelect = function (params) {
  // select by * and field.* or field.**
  if (params && params.query && params.query.$select) {
    // normalize the $select (mutate on purpose)
    params.query.$select = normalizeSelect(params.query.$select);

    const select = fp.map(splitHead, params.query.$select);
    if (fp.contains('*', select)) {
      return fp.dissocPath(['query', '$select'], params);
    } else {
      return fp.assocPath(['query', '$select'], select, params);
    }
  }
  return params;
};

// transform the results
const transform = function (results) {
  if (results) {
    if (results.data) {
      results.data = unsetObj(results.data);
    } else {
      results = unsetObj(results);
    }
  }
  return results;
};

export class Service extends BaseService {
  constructor (options) {
    options = Object.assign({}, defaultOptions, options);
    super(options);

    this.options = unsetOptions(options);
    this.name = options.name || 'mongoose-service';
  }

  setup(app) {
    this.app = app;
  }

  find (params) {
    // filter $select
    params = filterSelect(params);

    params = fp.assign({ query: {} }, params);

    if (params.query) {
      // fix id query inconsistent with service.id
      if (this.id === '_id' && params.query.id) {
        params.query._id = params.query.id;
        delete params.query.id;
      }
      if (this.id === 'id' && params.query._id) {
        params.query.id = params.query._id;
        delete params.query._id;
      }
      // filter destroyed item by default
      if (!params.query.destroyedAt) {
        params.query.destroyedAt = null;
      }
      // default sort
      if (!params.query.$sort) {
        params.query.$sort = { createdAt: -1 };
      }
    }

    // search by regex
    Object.keys(params.query || []).forEach(field => {
      if (params.query[field] && params.query[field].$like !== undefined && field.indexOf('$') === -1) {
        params.query[field] = { $regex: new RegExp(params.query[field].$like), $options: 'i' };
      }
    });

    const action = params.__action || (params.query && params.query.$action);

    if (!action || action === 'find') {
      debug('service %s find %j', this.name, params.query);
      return super.find(params).then(transform);
    }

    // TODO secure action call by find
    return this._action('find', action, null, null, params);
  }

  _idOrAction (id, params) {
    if (id === 'null') id = null;
    let action = params.__action || (params.query && params.query.$action);
    // check if id is action for find
    if (id && !action) {
      if (this['_' + id] && defaultMethods.indexOf(id) < 0) {
        return [null, id];
      }
    }
    return [id, action];
  }

  get (id, params) {
    // filter $select
    params = filterSelect(params);

    params = fp.assign({ query: {} }, params);

    let action = null;
    [id, action] = this._idOrAction(id, params);

    if (!action || action === 'get') {
      debug('service %s get %j', this.name, id, params);
      return super.get(id, params).then(transform);
    }

    // TODO secure action call by get
    return this._action('get', action, id, null, params);
  }

  create (data, params) {
    params = fp.assign({ query: {} }, params);

    // add support to create multiple objects
    if (Array.isArray(data)) {
      return Promise.all(data.map(current => this.create(current, params)));
    }

    const action = params.__action || (params.query && params.query.$action);
    if (!action || action === 'create') {
      debug('service %s create %j', this.name, data);
      return super.create(data, params).then(transform);
    }

    // TODO secure action call by get
    return this._action('create', action, null, data, params);
  }

  update (id, data, params) {
    params = fp.assign({}, params);
    assertMultiple(id, params, "Found null id, update must be called with $multi.");

    let action = null;
    [id, action] = this._idOrAction(id, params);

    if (!action || action === 'update') {
      debug('service %s update %j', this.name, id, data);
      return super.update(id, data, params).then(transform);
    }

    // TODO secure action call by get
    return this._action('update', action, id, data, params);
  }

  patch (id, data, params) {
    params = fp.assign({}, params);
    assertMultiple(id, params, "Found null id, patch must be called with $multi.");

    let action = null;
    [id, action] = this._idOrAction(id, params);

    if (!action || action === 'patch') {
      return super.patch(id, data, params).then(transform);
    }

    // TODO secure action call by get
    return this._action('patch', action, id, data, params);
  }

  remove (id, params) {
    params = fp.assign({}, params);
    assertMultiple(id, params, "Found null id, remove must be called with $multi.");

    let action;
    [id, action] = this._idOrAction(id, params);

    if (!action || action === 'remove') {
      if (params.query && params.query.$soft) {
        debug('service %s remove soft %j', this.name, id);
        params = fp.dissocPath(['query', '$soft'], params);
        return super.patch(id, { destroyedAt: new Date() }, params).then(transform);
      } else {
        debug('service %s remove %j', this.name, id);
        return super.remove(id, params).then(transform);
      }
    }

    // TODO secure action call by get
    this._action('remove', action, id, null, params);
  }

  /**
   * proxy to action method
   * syntax sugar for calling from other services, do not call them by super
   */
  action (action) {
    return {
      find: (params = {}) => {
        params.__action = action;
        return this.find(params);
      },

      get: (id, params = {}) => {
        params.__action = action;
        return this.get(id, params);
      },

      create: (data, params = {}) => {
        params.__action = action;
        return this.create(data, params);
      },

      update: (id, data, params = {}) => {
        params.__action = action;
        return this.update(id, data, params);
      },

      patch: (id, data, params = {}) => {
        params.__action = action;
        return this.patch(id, data, params);
      },

      remove: (id, params = {}) => {
        params.__action = action;
        return this.remove(id, params);
      }
    };
  }

  /**
   * private actions, aciton method are pseudo private by underscore
   */

  _action (method, action, id, data, params) {
    if (this['_' + action] === undefined || defaultMethods.indexOf(action) >= 0) {
      throw new Error(`No such **${method}** action: ${action}`);
    }
    if (params.__action) {
      params = fp.dissoc('__action', params);
    }
    if (params.query && params.query.$action) {
      params.query = fp.dissoc('$action', params.query);
    }
    debug('service %s %s action %s id %j => %j', this.name, method, action, id, data);

    // get target item with params.query (without provider)
    let query = id
      ? this.get(id, { query: params.query || {} })
      : Promise.resolve(null);
    return query.then(origin => {
      if (id && !origin) {
        throw new Error('Not found record ' + id + ' in ' + this.Model.modelName);
      }
      return this['_' + action].call(this, id, data, params, origin);
    });
  }

  _upsert (id, data, params) {
    params = fp.assign({}, params);
    if (fp.isNil(params.query) || fp.isEmpty(params.query)) {
      params.query = fp.assign({}, data);  // default find by input data
    }
    params.mongoose = fp.assign({}, params.mongoose, { upsert: true });

    // upsert do not set default value in schema, so ...
    const schemas = this.Model.schema && this.Model.schema.paths;
    if (schemas) {
      for (const key in schemas) {
        const val = fp.path(key.split('.'), data);
        if (!key.startsWith('_') && val === undefined && schemas[key].defaultValue !== undefined) {
          data[key] = typeof schemas[key].defaultValue === 'function'
            ? schemas[key].defaultValue() : schemas[key].defaultValue;
        }
      }
    }
    return super.patch(null, data, params).then(fp.head).then(transform);
  }

  _count (id, data, params) {
    params = fp.assign({ query: {} }, params || id);

    params.query.$limit = 0;
    return super.find(params).then(result => result.total);
  }

  _first (id, data, params) {
    // filter $select
    params = filterSelect(params || id);

    params = fp.assign({ query: {} }, params);

    params.query.$limit = 1;
    params.paginate = false; // disable paginate

    return super.find(params).then(results => {
      return results && results.length > 0? results[0] : null;
    }).then(transform);
  }

  _last (id, data, params) {
    // filter $select
    params = filterSelect(params || id);

    params = fp.assign({ query: {} }, params);

    return this._count(id, data, params).then(total => {
      params.query.$limit = 1;
      params.query.$skip = total - 1;
      params.paginate = false;
      return super.find(params).then(results => {
        return results && results.length > 0? results[0] : null;
      }).then(transform);
    });
  }

  _restore (id, data, params) {
    return super.patch(id, { destroyedAt: null }, params).then(transform);
  }
}

export default function init (options) {
  return new Service(options);
}

init.Service = Service;
init.transform = transform;
<commit_msg>default find service sort options<commit_after>import assert from 'assert';
import makeDebug from 'debug';
import fp from 'mostly-func';
import { Service as BaseService } from './base';
import { splitHead, repeatDoubleStar } from './helpers';

const debug = makeDebug('mostly:feathers-mongoose:service');

const defaultOptions = {
  lean: true,
  sort: {
    createdAt: -1
  },
  paginate: {
    default: 10,
    max: 50
  }
};

const defaultMethods = ['find', 'get', 'create', 'update', 'patch', 'remove'];

// prevent accidental multiple operations
const assertMultiple = function (id, params, message) {
  if (!id) {
    if (params && params.query && (params.$multi || params.query.$multi)) {
      delete params.query.$multi;
    } else {
      throw new Error(message);
    }
  }
};

const unsetOptions = fp.pipe(
  fp.dissoc('Model'),
  fp.dissoc('ModelName')
);

const unset_id = function (obj) {
  if (obj && obj._id) {
    return fp.pipe(
      fp.assoc('id', String(obj.id || obj._id)),
      fp.dissoc('_id'),
      fp.dissoc('__v')
    )(obj);
  } else {
    return obj;
  }
};

const unsetObj = function (obj) {
  if (Array.isArray(obj)) {
    return fp.map(unset_id, obj);
  } else {
    return unset_id(obj);
  }
};

const normalizeSelect = function (select) {
  if (select) {
    // convert string $select to array
    if (fp.is(String, select)) {
      select = fp.map(fp.trim, fp.split(',', select));
    }
    // repeat ** as recursive fields
    select = repeatDoubleStar(select);
  }
  return select;
};

const filterSelect = function (params) {
  // select by * and field.* or field.**
  if (params && params.query && params.query.$select) {
    // normalize the $select (mutate on purpose)
    params.query.$select = normalizeSelect(params.query.$select);

    const select = fp.map(splitHead, params.query.$select);
    if (fp.contains('*', select)) {
      return fp.dissocPath(['query', '$select'], params);
    } else {
      return fp.assocPath(['query', '$select'], select, params);
    }
  }
  return params;
};

// transform the results
const transform = function (results) {
  if (results) {
    if (results.data) {
      results.data = unsetObj(results.data);
    } else {
      results = unsetObj(results);
    }
  }
  return results;
};

export class Service extends BaseService {
  constructor (options) {
    options = Object.assign({}, defaultOptions, options);
    super(options);

    this.options = unsetOptions(options);
    this.name = options.name || 'mongoose-service';
  }

  setup(app) {
    this.app = app;
  }

  find (params) {
    // filter $select
    params = filterSelect(params);

    params = fp.assign({ query: {} }, params);

    if (params.query) {
      // fix id query inconsistent with service.id
      if (this.id === '_id' && params.query.id) {
        params.query._id = params.query.id;
        delete params.query.id;
      }
      if (this.id === 'id' && params.query._id) {
        params.query.id = params.query._id;
        delete params.query._id;
      }
      // filter destroyed item by default
      if (!params.query.destroyedAt) {
        params.query.destroyedAt = null;
      }
      // default sort
      if (!params.query.$sort) {
        params.query.$sort = this.options.sort;
      }
    }

    // search by regex
    Object.keys(params.query || []).forEach(field => {
      if (params.query[field] && params.query[field].$like !== undefined && field.indexOf('$') === -1) {
        params.query[field] = { $regex: new RegExp(params.query[field].$like), $options: 'i' };
      }
    });

    const action = params.__action || (params.query && params.query.$action);

    if (!action || action === 'find') {
      debug('service %s find %j', this.name, params.query);
      return super.find(params).then(transform);
    }

    // TODO secure action call by find
    return this._action('find', action, null, null, params);
  }

  _idOrAction (id, params) {
    if (id === 'null') id = null;
    let action = params.__action || (params.query && params.query.$action);
    // check if id is action for find
    if (id && !action) {
      if (this['_' + id] && defaultMethods.indexOf(id) < 0) {
        return [null, id];
      }
    }
    return [id, action];
  }

  get (id, params) {
    // filter $select
    params = filterSelect(params);

    params = fp.assign({ query: {} }, params);

    let action = null;
    [id, action] = this._idOrAction(id, params);

    if (!action || action === 'get') {
      debug('service %s get %j', this.name, id, params);
      return super.get(id, params).then(transform);
    }

    // TODO secure action call by get
    return this._action('get', action, id, null, params);
  }

  create (data, params) {
    params = fp.assign({ query: {} }, params);

    // add support to create multiple objects
    if (Array.isArray(data)) {
      return Promise.all(data.map(current => this.create(current, params)));
    }

    const action = params.__action || (params.query && params.query.$action);
    if (!action || action === 'create') {
      debug('service %s create %j', this.name, data);
      return super.create(data, params).then(transform);
    }

    // TODO secure action call by get
    return this._action('create', action, null, data, params);
  }

  update (id, data, params) {
    params = fp.assign({}, params);
    assertMultiple(id, params, "Found null id, update must be called with $multi.");

    let action = null;
    [id, action] = this._idOrAction(id, params);

    if (!action || action === 'update') {
      debug('service %s update %j', this.name, id, data);
      return super.update(id, data, params).then(transform);
    }

    // TODO secure action call by get
    return this._action('update', action, id, data, params);
  }

  patch (id, data, params) {
    params = fp.assign({}, params);
    assertMultiple(id, params, "Found null id, patch must be called with $multi.");

    let action = null;
    [id, action] = this._idOrAction(id, params);

    if (!action || action === 'patch') {
      return super.patch(id, data, params).then(transform);
    }

    // TODO secure action call by get
    return this._action('patch', action, id, data, params);
  }

  remove (id, params) {
    params = fp.assign({}, params);
    assertMultiple(id, params, "Found null id, remove must be called with $multi.");

    let action;
    [id, action] = this._idOrAction(id, params);

    if (!action || action === 'remove') {
      if (params.query && params.query.$soft) {
        debug('service %s remove soft %j', this.name, id);
        params = fp.dissocPath(['query', '$soft'], params);
        return super.patch(id, { destroyedAt: new Date() }, params).then(transform);
      } else {
        debug('service %s remove %j', this.name, id);
        return super.remove(id, params).then(transform);
      }
    }

    // TODO secure action call by get
    this._action('remove', action, id, null, params);
  }

  /**
   * proxy to action method
   * syntax sugar for calling from other services, do not call them by super
   */
  action (action) {
    return {
      find: (params = {}) => {
        params.__action = action;
        return this.find(params);
      },

      get: (id, params = {}) => {
        params.__action = action;
        return this.get(id, params);
      },

      create: (data, params = {}) => {
        params.__action = action;
        return this.create(data, params);
      },

      update: (id, data, params = {}) => {
        params.__action = action;
        return this.update(id, data, params);
      },

      patch: (id, data, params = {}) => {
        params.__action = action;
        return this.patch(id, data, params);
      },

      remove: (id, params = {}) => {
        params.__action = action;
        return this.remove(id, params);
      }
    };
  }

  /**
   * private actions, aciton method are pseudo private by underscore
   */

  _action (method, action, id, data, params) {
    if (this['_' + action] === undefined || defaultMethods.indexOf(action) >= 0) {
      throw new Error(`No such **${method}** action: ${action}`);
    }
    if (params.__action) {
      params = fp.dissoc('__action', params);
    }
    if (params.query && params.query.$action) {
      params.query = fp.dissoc('$action', params.query);
    }
    debug('service %s %s action %s id %j => %j', this.name, method, action, id, data);

    // get target item with params.query (without provider)
    let query = id
      ? this.get(id, { query: params.query || {} })
      : Promise.resolve(null);
    return query.then(origin => {
      if (id && !origin) {
        throw new Error('Not found record ' + id + ' in ' + this.Model.modelName);
      }
      return this['_' + action].call(this, id, data, params, origin);
    });
  }

  _upsert (id, data, params) {
    params = fp.assign({}, params);
    if (fp.isNil(params.query) || fp.isEmpty(params.query)) {
      params.query = fp.assign({}, data);  // default find by input data
    }
    params.mongoose = fp.assign({}, params.mongoose, { upsert: true });

    // upsert do not set default value in schema, so ...
    const schemas = this.Model.schema && this.Model.schema.paths;
    if (schemas) {
      for (const key in schemas) {
        const val = fp.path(key.split('.'), data);
        if (!key.startsWith('_') && val === undefined && schemas[key].defaultValue !== undefined) {
          data[key] = typeof schemas[key].defaultValue === 'function'
            ? schemas[key].defaultValue() : schemas[key].defaultValue;
        }
      }
    }
    return super.patch(null, data, params).then(fp.head).then(transform);
  }

  _count (id, data, params) {
    params = fp.assign({ query: {} }, params || id);

    params.query.$limit = 0;
    return super.find(params).then(result => result.total);
  }

  _first (id, data, params) {
    // filter $select
    params = filterSelect(params || id);

    params = fp.assign({ query: {} }, params);

    params.query.$limit = 1;
    params.paginate = false; // disable paginate

    return super.find(params).then(results => {
      return results && results.length > 0? results[0] : null;
    }).then(transform);
  }

  _last (id, data, params) {
    // filter $select
    params = filterSelect(params || id);

    params = fp.assign({ query: {} }, params);

    return this._count(id, data, params).then(total => {
      params.query.$limit = 1;
      params.query.$skip = total - 1;
      params.paginate = false;
      return super.find(params).then(results => {
        return results && results.length > 0? results[0] : null;
      }).then(transform);
    });
  }

  _restore (id, data, params) {
    return super.patch(id, { destroyedAt: null }, params).then(transform);
  }
}

export default function init (options) {
  return new Service(options);
}

init.Service = Service;
init.transform = transform;
<|endoftext|>


<commit_before>/*global angular */
(function (ng) {
    'use strict';

    function throwNoKeyException() {
        throw new Error('You need to set the "key" attribute to your public reCaptcha key. If you don\'t have a key, please get one from https://www.google.com/recaptcha/admin/create');
    }

    var app = ng.module('vcRecaptcha');

    /**
     * An angular service to wrap the reCaptcha API
     */
    app.provider('vcRecaptchaService', function(){
        var provider = this;
        var config = {};
        provider.onLoadFunctionName = 'vcRecaptchaApiLoaded';

        /**
         * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param defaults  object which overrides the current defaults object.
         */
        provider.setDefaults = function(defaults){
            ng.copy(defaults, config);
        };

        /**
         * Sets the reCaptcha key which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param siteKey  the reCaptcha public key (refer to the README file if you don't know what this is).
         */
        provider.setSiteKey = function(siteKey){
            config.key = siteKey;
        };

        /**
         * Sets the reCaptcha theme which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param theme  The reCaptcha theme.
         */
        provider.setTheme = function(theme){
            config.theme = theme;
        };

        /**
         * Sets the reCaptcha stoken which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param stoken  The reCaptcha stoken.
         */
        provider.setStoken = function(stoken){
            config.stoken = stoken;
        };

        /**
         * Sets the reCaptcha size which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param size  The reCaptcha size.
         */
        provider.setSize = function(size){
            config.size = size;
        };

        /**
         * Sets the reCaptcha type which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param type  The reCaptcha type.
         */
        provider.setType = function(type){
            config.type = type;
        };

        /**
         * Sets the reCaptcha language which will be used by default is not specified in a specific directive instance.
         *
         * @param lang  The reCaptcha language.
         */
        provider.setLang = function(lang){
            config.lang = lang;
        };

        /**
         * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param onLoadFunctionName  string name which overrides the name of the onload function. Should match what is in the recaptcha script querystring onload value.
         */
        provider.setOnLoadFunctionName = function(onLoadFunctionName){
            provider.onLoadFunctionName = onLoadFunctionName;
        };

        provider.$get = ['$rootScope','$window', '$q', function ($rootScope, $window, $q) {
            var deferred = $q.defer(), promise = deferred.promise, instances = {}, recaptcha;

            $window.vcRecaptchaApiLoadedCallback = $window.vcRecaptchaApiLoadedCallback || [];

            var callback = function () {
                recaptcha = $window.grecaptcha;

                deferred.resolve(recaptcha);
            };

            $window.vcRecaptchaApiLoadedCallback.push(callback);

            $window[provider.onLoadFunctionName] = function () {
                $window.vcRecaptchaApiLoadedCallback.forEach(function(callback) {
                    callback();
                });
            };

            function getRecaptcha() {
                if (!!recaptcha) {
                    return $q.when(recaptcha);
                }

                return promise;
            }

            function validateRecaptchaInstance() {
                if (!recaptcha) {
                    throw new Error('reCaptcha has not been loaded yet.');
                }
            }

            // Check if grecaptcha is not defined already.
            if (ng.isDefined($window.grecaptcha)) {
                callback();
            }

            return {

                /**
                 * Creates a new reCaptcha object
                 *
                 * @param elm  the DOM element where to put the captcha
                 * @param conf the captcha object configuration
                 * @throws NoKeyException    if no key is provided in the provider config or the directive instance (via attribute)
                 */
                create: function (elm, conf) {

                    conf.sitekey = conf.key || config.key;
                    conf.theme = conf.theme || config.theme;
                    conf.stoken = conf.stoken || config.stoken;
                    conf.size = conf.size || config.size;
                    conf.type = conf.type || config.type;
                    conf.hl = conf.lang || config.lang;

                    if (!conf.sitekey || conf.sitekey.length !== 40) {
                        throwNoKeyException();
                    }
                    return getRecaptcha().then(function (recaptcha) {
                        var widgetId = recaptcha.render(elm, conf);
                        instances[widgetId] = elm;
                        return widgetId;
                    });
                },

                /**
                 * Reloads the reCaptcha
                 */
                reload: function (widgetId) {
                    validateRecaptchaInstance();

                    recaptcha.reset(widgetId);

                    // Let everyone know this widget has been reset.
                    $rootScope.$broadcast('reCaptchaReset', widgetId);
                },

                /**
                 * Get/Set reCaptcha language
                 */
                useLang: function (widgetId, lang) {
                    var instance = instances[widgetId];

                    if (instance) {
                        var iframe = instance.querySelector('iframe');
                        if (lang) {
                            // Setter
                            if (iframe && iframe.src) {
                                var s = iframe.src;
                                if (/[?&]hl=/.test(s)) {
                                    s = s.replace(/([?&]hl=)\w+/, '$1' + lang);
                                } else {
                                    s += ((s.indexOf('?') === -1) ? '?' : '&') + 'hl=' + lang;
                                }

                                iframe.src = s;
                            }
                        } else {
                            // Getter
                            if (iframe && iframe.src && /[?&]hl=\w+/.test(iframe.src)) {
                                return iframe.src.replace(/.+[?&]hl=(\w+)([^\w].+)?/, '$1');
                            } else {
                                return null;
                            }
                        }
                    } else {
                        throw new Error('reCaptcha Widget ID not exists', widgetId);
                    }
                },

                /**
                 * Gets the response from the reCaptcha widget.
                 *
                 * @see https://developers.google.com/recaptcha/docs/display#js_api
                 *
                 * @returns {String}
                 */
                getResponse: function (widgetId) {
                    validateRecaptchaInstance();

                    return recaptcha.getResponse(widgetId);
                },

                /**
                 * Gets reCaptcha instance and configuration
                 */
                getInstance: function (widgetId) {
                    return instances[widgetId];
                },

                /**
                 * Destroy reCaptcha instance.
                 */
                destroy: function (widgetId) {
                    delete instances[widgetId];
                }
            };

        }];
    });

}(angular));
<commit_msg>Improvement: Generate recaptcha script on demand.<commit_after>/*global angular */
(function (ng) {
    'use strict';

    function throwNoKeyException() {
        throw new Error('You need to set the "key" attribute to your public reCaptcha key. If you don\'t have a key, please get one from https://www.google.com/recaptcha/admin/create');
    }

    var app = ng.module('vcRecaptcha');

    /**
     * An angular service to wrap the reCaptcha API
     */
    app.provider('vcRecaptchaService', function(){
        var provider = this;
        var config = {};
        provider.onLoadFunctionName = 'vcRecaptchaApiLoaded';

        /**
         * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param defaults  object which overrides the current defaults object.
         */
        provider.setDefaults = function(defaults){
            ng.copy(defaults, config);
        };

        /**
         * Sets the reCaptcha key which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param siteKey  the reCaptcha public key (refer to the README file if you don't know what this is).
         */
        provider.setSiteKey = function(siteKey){
            config.key = siteKey;
        };

        /**
         * Sets the reCaptcha theme which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param theme  The reCaptcha theme.
         */
        provider.setTheme = function(theme){
            config.theme = theme;
        };

        /**
         * Sets the reCaptcha stoken which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param stoken  The reCaptcha stoken.
         */
        provider.setStoken = function(stoken){
            config.stoken = stoken;
        };

        /**
         * Sets the reCaptcha size which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param size  The reCaptcha size.
         */
        provider.setSize = function(size){
            config.size = size;
        };

        /**
         * Sets the reCaptcha type which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param type  The reCaptcha type.
         */
        provider.setType = function(type){
            config.type = type;
        };

        /**
         * Sets the reCaptcha language which will be used by default is not specified in a specific directive instance.
         *
         * @param lang  The reCaptcha language.
         */
        provider.setLang = function(lang){
            config.lang = lang;
        };

        /**
         * Sets the reCaptcha configuration values which will be used by default is not specified in a specific directive instance.
         *
         * @since 2.5.0
         * @param onLoadFunctionName  string name which overrides the name of the onload function. Should match what is in the recaptcha script querystring onload value.
         */
        provider.setOnLoadFunctionName = function(onLoadFunctionName){
            provider.onLoadFunctionName = onLoadFunctionName;
        };

        provider.$get = ['$rootScope','$window', '$q', '$document', function ($rootScope, $window, $q, $document) {
            var deferred = $q.defer(), promise = deferred.promise, instances = {}, recaptcha;

            $window.vcRecaptchaApiLoadedCallback = $window.vcRecaptchaApiLoadedCallback || [];

            var callback = function () {
                recaptcha = $window.grecaptcha;

                deferred.resolve(recaptcha);
            };

            $window.vcRecaptchaApiLoadedCallback.push(callback);

            $window[provider.onLoadFunctionName] = function () {
                $window.vcRecaptchaApiLoadedCallback.forEach(function(callback) {
                    callback();
                });
            };

            function getRecaptcha() {
                if (!!recaptcha) {
                    return $q.when(recaptcha);
                }

                return promise;
            }

            function validateRecaptchaInstance() {
                if (!recaptcha) {
                    throw new Error('reCaptcha has not been loaded yet.');
                }
            }

            // Check if grecaptcha is not defined already.
            if (ng.isDefined($window.grecaptcha)) {
                callback();
            } else {
                // Generate link on demand
                var script = $document.get(0).createElement('script');
                script.async = true;
                script.defer = true;
                script.src = 'https://www.google.com/recaptcha/api.js?onload='+provider.onLoadFunctionName+'&render=explicit';
                $document.get(0).body.appendChild(script);
            }

            return {

                /**
                 * Creates a new reCaptcha object
                 *
                 * @param elm  the DOM element where to put the captcha
                 * @param conf the captcha object configuration
                 * @throws NoKeyException    if no key is provided in the provider config or the directive instance (via attribute)
                 */
                create: function (elm, conf) {

                    conf.sitekey = conf.key || config.key;
                    conf.theme = conf.theme || config.theme;
                    conf.stoken = conf.stoken || config.stoken;
                    conf.size = conf.size || config.size;
                    conf.type = conf.type || config.type;
                    conf.hl = conf.lang || config.lang;

                    if (!conf.sitekey || conf.sitekey.length !== 40) {
                        throwNoKeyException();
                    }
                    return getRecaptcha().then(function (recaptcha) {
                        var widgetId = recaptcha.render(elm, conf);
                        instances[widgetId] = elm;
                        return widgetId;
                    });
                },

                /**
                 * Reloads the reCaptcha
                 */
                reload: function (widgetId) {
                    validateRecaptchaInstance();

                    recaptcha.reset(widgetId);

                    // Let everyone know this widget has been reset.
                    $rootScope.$broadcast('reCaptchaReset', widgetId);
                },

                /**
                 * Get/Set reCaptcha language
                 */
                useLang: function (widgetId, lang) {
                    var instance = instances[widgetId];

                    if (instance) {
                        var iframe = instance.querySelector('iframe');
                        if (lang) {
                            // Setter
                            if (iframe && iframe.src) {
                                var s = iframe.src;
                                if (/[?&]hl=/.test(s)) {
                                    s = s.replace(/([?&]hl=)\w+/, '$1' + lang);
                                } else {
                                    s += ((s.indexOf('?') === -1) ? '?' : '&') + 'hl=' + lang;
                                }

                                iframe.src = s;
                            }
                        } else {
                            // Getter
                            if (iframe && iframe.src && /[?&]hl=\w+/.test(iframe.src)) {
                                return iframe.src.replace(/.+[?&]hl=(\w+)([^\w].+)?/, '$1');
                            } else {
                                return null;
                            }
                        }
                    } else {
                        throw new Error('reCaptcha Widget ID not exists', widgetId);
                    }
                },

                /**
                 * Gets the response from the reCaptcha widget.
                 *
                 * @see https://developers.google.com/recaptcha/docs/display#js_api
                 *
                 * @returns {String}
                 */
                getResponse: function (widgetId) {
                    validateRecaptchaInstance();

                    return recaptcha.getResponse(widgetId);
                },

                /**
                 * Gets reCaptcha instance and configuration
                 */
                getInstance: function (widgetId) {
                    return instances[widgetId];
                },

                /**
                 * Destroy reCaptcha instance.
                 */
                destroy: function (widgetId) {
                    delete instances[widgetId];
                }
            };

        }];
    });

}(angular));
<|endoftext|>


<commit_before>"use strict";

var path = require('path');
var fs = require('fs-extra');
var of = require('./object_factory');
var pa = require('./pattern_assembler');
var pattern_assembler = new pa();
var eol = require('os').EOL;

// PRIVATE FUNCTIONS

function addToPatternPaths(patternlab, patternTypeName, pattern) {
  //this is messy, could use a refactor.
  patternlab.patternPaths[patternTypeName][pattern.patternName] = pattern.subdir.replace(/\\/g, '/') + "/" + pattern.fileName;
}

//todo: refactor this as a method on the pattern object itself once we merge dev with pattern-engines branch
function isPatternExcluded(pattern) {
  // returns whether or not the first character of the pattern filename is an underscore, or excluded
  return pattern.fileName.charAt(0) === '_';
}

// Returns the array of patterns to be rendered in the styleguide view and
// linked to in the pattern navigation. Checks if patterns are excluded.
function assembleStyleguidePatterns(patternlab) {
  var styleguideExcludes = patternlab.config.styleGuideExcludes;
  var styleguidePatterns = [];

  if (styleguideExcludes && styleguideExcludes.length) {
    for (var i = 0; i < patternlab.patterns.length; i++) {

      // skip underscore-prefixed files
      if (isPatternExcluded(patternlab.patterns[i])) {
        if (patternlab.config.debug) {
          console.log('Omitting ' + patternlab.patterns[i].patternPartial + " from styleguide pattern exclusion.");
        }
        continue;
      }

      var partial = patternlab.patterns[i].patternPartial;
      var partialType = partial.substring(0, partial.indexOf('-'));
      var isExcluded = (styleguideExcludes.indexOf(partialType) > -1);
      if (!isExcluded) {
        styleguidePatterns.push(patternlab.patterns[i]);
      }
    }
  } else {
    for (i = 0; i < patternlab.patterns.length; i++) {
      // skip underscore-prefixed files
      if (isPatternExcluded(patternlab.patterns[i])) {
        if (patternlab.config.debug) {
          console.log('Omitting ' + patternlab.patterns[i].patternPartial + " from styleguide pattern exclusion.");
        }
        continue;
      }
      styleguidePatterns.push(patternlab.patterns[i]);
    }
  }

  return styleguidePatterns;
}

function buildNavigation(patternlab) {
  for (var i = 0; i < patternlab.patterns.length; i++) {

    var pattern = patternlab.patterns[i];

    //todo: check if this is already available
    var patternTypeName = pattern.name.replace(/\\/g, '-').split('-')[1];

    // skip underscore-prefixed files. don't create a patternType on account of an underscored pattern
    if (isPatternExcluded(pattern)) {
      continue;
    }

    var patternSubTypeName;
    var patternSubTypeItemName;
    var flatPatternItem;
    var patternSubType;
    var patternSubTypeItem;
    var viewAllPatternSubTypeItem;

    //get the patternSubType.
    //if there is one or more slashes in the subdir, get everything after
    //the last slash. if no slash, get the whole subdir string and strip
    //any numeric + hyphen prefix
    patternSubTypeName = pattern.subdir.split('/').pop().replace(/^\d*\-/, '');

    //get the patternSubTypeItem
    patternSubTypeItemName = pattern.patternName.replace(/-/g, ' ');

    //assume the patternSubTypeItem does not exist.
    patternSubTypeItem = new of.oPatternSubTypeItem(patternSubTypeItemName);
    patternSubTypeItem.patternPath = pattern.patternLink;
    patternSubTypeItem.patternPartial = pattern.patternPartial;

    //check if the patternType already exists
    var patternTypeIndex = patternlab.patternTypeIndex.indexOf(patternTypeName);
    if (patternTypeIndex === -1) {
      //add the patternType
      var patternType = new of.oPatternType(patternTypeName);

      //add patternPath and viewAllPath
      patternlab.patternPaths[patternTypeName] = {};
      patternlab.viewAllPaths[patternTypeName] = {};

      //test whether the pattern structure is flat or not - usually due to a template or page
      flatPatternItem = patternSubTypeName === patternTypeName;

      //assume the patternSubType does not exist.
      patternSubType = new of.oPatternSubType(patternSubTypeName);

      //add the patternState if it exists
      if (pattern.patternState) {
        patternSubTypeItem.patternState = pattern.patternState;
      }

      //if it is flat - we should not add the pattern to patternPaths
      if (flatPatternItem) {

        patternType.patternItems.push(patternSubTypeItem);

        //add to patternPaths
        addToPatternPaths(patternlab, patternTypeName, pattern);

      } else {

        patternType.patternTypeItems.push(patternSubType);
        patternType.patternTypeItemsIndex.push(patternSubTypeName);
        patternSubType.patternSubtypeItems.push(patternSubTypeItem);
        patternSubType.patternSubtypeItemsIndex.push(patternSubTypeItemName);

        //add to patternPaths
        addToPatternPaths(patternlab, patternTypeName, pattern);

        //add the view all PatternSubTypeItem
        viewAllPatternSubTypeItem = new of.oPatternSubTypeItem("View All");
        viewAllPatternSubTypeItem.patternPath = pattern.subdir.slice(0, pattern.subdir.indexOf(pattern.patternGroup) + pattern.patternGroup.length) + "/index.html";
        viewAllPatternSubTypeItem.patternPartial = "viewall-" + pattern.patternGroup;

        patternType.patternItems.push(viewAllPatternSubTypeItem);
        patternlab.viewAllPaths[patternTypeName].viewall = pattern.subdir.slice(0, pattern.subdir.indexOf(pattern.patternGroup) + pattern.patternGroup.length);

      }

      //add the patternType.
      patternlab.patternTypes.push(patternType);
      patternlab.patternTypeIndex.push(patternTypeName);

      //done

    } else {
      //find the patternType
      patternType = patternlab.patternTypes[patternTypeIndex];

      //add the patternState if it exists
      if (pattern.patternState) {
        patternSubTypeItem.patternState = pattern.patternState;
      }

      //test whether the pattern structure is flat or not - usually due to a template or page
      flatPatternItem = patternSubTypeName === patternTypeName;

      //if it is flat - we should not add the pattern to patternPaths
      if (flatPatternItem) {
        //add the patternSubType to patternItems
        patternType.patternItems.push(patternSubTypeItem);

        //add to patternPaths
        addToPatternPaths(patternlab, patternTypeName, pattern);

      } else {

        // only do this if pattern is included
        //check to see if patternSubType exists
        var patternTypeItemsIndex = patternType.patternTypeItemsIndex.indexOf(patternSubTypeName);
        if (patternTypeItemsIndex === -1) {
          patternSubType = new of.oPatternSubType(patternSubTypeName);

          //add the patternSubType and patternSubTypeItem
          patternSubType.patternSubtypeItems.push(patternSubTypeItem);
          patternSubType.patternSubtypeItemsIndex.push(patternSubTypeItemName);
          patternType.patternTypeItems.push(patternSubType);
          patternType.patternTypeItemsIndex.push(patternSubTypeName);

        } else {
          //add the patternSubTypeItem
          patternSubType = patternType.patternTypeItems[patternTypeItemsIndex];
          patternSubType.patternSubtypeItems.push(patternSubTypeItem);
          patternSubType.patternSubtypeItemsIndex.push(patternSubTypeItemName);
        }

        //check if we are moving to a new subgroup in the next loop
        if (!patternlab.patterns[i + 1] || pattern.patternSubGroup !== patternlab.patterns[i + 1].patternSubGroup) {

          //add the viewall SubTypeItem
          var viewAllPatternSubTypeItem = new of.oPatternSubTypeItem("View All");
          viewAllPatternSubTypeItem.patternPath = pattern.flatPatternPath + "/index.html";
          viewAllPatternSubTypeItem.patternPartial = "viewall-" + pattern.patternGroup + "-" + pattern.patternSubGroup;

          patternSubType.patternSubtypeItems.push(viewAllPatternSubTypeItem);
          patternSubType.patternSubtypeItemsIndex.push("View All");
        }

        // just add to patternPaths
        addToPatternPaths(patternlab, patternTypeName, pattern);
      }
    }

    patternlab.viewAllPaths[patternTypeName][pattern.patternSubGroup] = pattern.flatPatternPath;
  }
  return patternTypeIndex;
}

function buildFooterHTML(patternlab, patternPartial) {
  //set the pattern-specific footer by compiling the general-footer with data, and then adding it to the meta footer
  var footerPartial = pattern_assembler.renderPattern(patternlab.footer, {
    patternData: JSON.stringify({
      patternPartial: patternPartial,
    }),
    cacheBuster: patternlab.cacheBuster
  });
  var footerHTML = pattern_assembler.renderPattern(patternlab.userFoot, {
    patternLabFoot : footerPartial
  });
  return footerHTML;
}

function buildViewAllHTML(patternlab, patterns, patternPartial) {
  var viewAllHTML = pattern_assembler.renderPattern(patternlab.viewAll,
    {
      partials: patterns,
      patternPartial: patternPartial,
      cacheBuster: patternlab.cacheBuster
    }, {
      patternSection: patternlab.patternSection,
      patternSectionSubType: patternlab.patternSectionSubType
    });
  return viewAllHTML;
}

function buildViewAllPages(mainPageHeadHtml, patternlab) {
  var paths = patternlab.config.paths;
  var prevSubdir = '';
  var prevGroup = '';
  var i;

  for (i = 0; i < patternlab.patterns.length; i++) {
    // skip underscore-prefixed files
    if (isPatternExcluded(patternlab.patterns[i])) {
      if (patternlab.config.debug) {
        console.log('Omitting ' + patternlab.patterns[i].patternPartial + " from view all rendering.");
      }
      continue;
    }

    var pattern = patternlab.patterns[i];

    //create the view all for the section
    // check if the current section is different from the previous one
    if (pattern.patternGroup !== prevGroup) {
      prevGroup = pattern.patternGroup;

      var viewAllPatterns = [];
      var patternPartial = "viewall-" + pattern.patternGroup;
      var j;

      for (j = 0; j < patternlab.patterns.length; j++) {
        if (patternlab.patterns[j].patternGroup === pattern.patternGroup) {
          //again, skip any sibling patterns to the current one that may have underscores
          if (isPatternExcluded(patternlab.patterns[j])) {
            if (patternlab.config.debug) {
              console.log('Omitting ' + patternlab.patterns[j].patternPartial + " from view all sibling rendering.");
            }
            continue;
          }

          viewAllPatterns.push(patternlab.patterns[j]);
        }
      }

      //render the footer needed for the viewall template
      var footerHTML = buildFooterHTML(patternlab, patternPartial);

      //render the viewall template
      var viewAllHTML = buildViewAllHTML(patternlab, viewAllPatterns, patternPartial);

      fs.outputFileSync(paths.public.patterns + pattern.subdir.slice(0, pattern.subdir.indexOf(pattern.patternGroup) + pattern.patternGroup.length) + '/index.html', mainPageHeadHtml + viewAllHTML + footerHTML);
    }

    //create the view all for the subsection
    // check if the current sub section is different from the previous one
    if (pattern.subdir !== prevSubdir) {
      prevSubdir = pattern.subdir;

      viewAllPatterns = [];
      patternPartial = "viewall-" + pattern.patternGroup + "-" + pattern.patternSubGroup;

      for (j = 0; j < patternlab.patterns.length; j++) {
        if (patternlab.patterns[j].subdir === pattern.subdir) {
          //again, skip any sibling patterns to the current one that may have underscores
          if (isPatternExcluded(patternlab.patterns[j])) {
            if (patternlab.config.debug) {
              console.log('Omitting ' + patternlab.patterns[j].patternPartial + " from view all sibling rendering.");
            }
            continue;
          }

          viewAllPatterns.push(patternlab.patterns[j]);
        }
      }

      //render the footer needed for the viewall template
      var footerHTML = buildFooterHTML(patternlab, patternPartial);

      //render the viewall template
      var viewAllHTML = buildViewAllHTML(patternlab, viewAllPatterns, patternPartial);

      fs.outputFileSync(paths.public.patterns + pattern.flatPatternPath + '/index.html', mainPageHeadHtml + viewAllHTML + footerHTML);
    }
  }
}

function sortPatterns(patternsArray) {
  return patternsArray.sort(function (a, b) {
    if (a.name > b.name) {
      return 1;
    }
    if (a.name < b.name) {
      return -1;
    }

    // a must be equal to b
    return 0;
  });
}

// MAIN BUILDER FUNCTION

function buildFrontEnd(patternlab) {
  var mh = require('./media_hunter');
  var media_hunter = new mh();
  var ae = require('./annotation_exporter');
  var annotation_exporter = new ae(patternlab);
  var styleguidePatterns = [];
  var paths = patternlab.config.paths;

  patternlab.patternTypes = [];
  patternlab.patternTypeIndex = [];
  patternlab.patternPaths = {};
  patternlab.viewAllPaths = {};

  //sort all patterns explicitly.
  patternlab.patterns = sortPatterns(patternlab.patterns);

  //find mediaQueries
  media_hunter.find_media_queries('./source/css', patternlab);

  // check if patterns are excluded, if not add them to styleguidePatterns
  styleguidePatterns = assembleStyleguidePatterns(patternlab);

  //set the pattern-specific header by compiling the general-header with data, and then adding it to the meta header
  var headerPartial = pattern_assembler.renderPattern(patternlab.header, {
    cacheBuster: patternlab.cacheBuster
  });
  var headerHTML = pattern_assembler.renderPattern(patternlab.userHead, {
    patternLabHead : headerPartial
  });

  //set the pattern-specific footer by compiling the general-footer with data, and then adding it to the meta footer
  var footerPartial = pattern_assembler.renderPattern(patternlab.footer, {
    patternData: '{}',
    cacheBuster: patternlab.cacheBuster
  });
  var footerHTML = pattern_assembler.renderPattern(patternlab.userFoot, {
    patternLabFoot : footerPartial
  });

  //build the styleguide
  var styleguideTemplate = fs.readFileSync(path.resolve(paths.source.patternlabFiles, 'viewall.mustache'), 'utf8');
  var styleguideHtml = pattern_assembler.renderPattern(styleguideTemplate,
    {
      partials: styleguidePatterns,
      cacheBuster: patternlab.cacheBuster
    }, {
      patternSection: patternlab.patternSection,
      patternSectionSubType: patternlab.patternSectionSubType
    });

  fs.outputFileSync(path.resolve(paths.public.styleguide, 'html/styleguide.html'), headerHTML + styleguideHtml + footerHTML);

  //build the viewall pages
  buildViewAllPages(headerHTML, patternlab);

  //build the patternlab website
  buildNavigation(patternlab);

  //move the index file from its asset location into public root
  var patternlabSiteHtml;
  try {
    patternlabSiteHtml = fs.readFileSync(path.resolve(paths.source.styleguide, 'index.html'), 'utf8');
  } catch (error) {
    console.log(error);
    console.log("\nERROR: Could not load one or more styleguidekit assets from", paths.source.styleguide, '\n');
    process.exit(1);
  }
  fs.outputFileSync(path.resolve(paths.public.root, 'index.html'), patternlabSiteHtml);

  //write out the data
  var output = '';

  //config
  output += 'var config = ' + JSON.stringify(patternlab.config) + ';\n';

  //ishControls
  output += 'var ishControls = {"ishControlsHide":' + JSON.stringify(patternlab.config.ishControlsHide) + '};' + eol;

  //todo add media queries to this
  //navItems
  output += 'var navItems = {"patternTypes": ' + JSON.stringify(patternlab.patternTypes) + '};' + eol;

  //patternPaths
  output += 'var patternPaths = ' + JSON.stringify(patternlab.patternPaths) + ';' + eol;

  //viewAllPaths
  output += 'var viewAllPaths = ' + JSON.stringify(patternlab.viewAllPaths) + ';' + eol;

  //plugins someday
  output += 'var plugins = [];' + eol;
  fs.outputFileSync(path.resolve(paths.public.data, 'patternlab-data.js'), output);

  //annotations
  var annotationsJSON = annotation_exporter.gather();
  var annotations = 'var comments = ' + JSON.stringify(annotationsJSON);
  fs.outputFileSync(path.resolve(paths.public.annotations, 'annotations.js'), annotations);

}

module.exports = buildFrontEnd;
<commit_msg>attempt to fix hardcoded path in media_hunter call<commit_after>"use strict";

var path = require('path');
var fs = require('fs-extra');
var of = require('./object_factory');
var pa = require('./pattern_assembler');
var pattern_assembler = new pa();
var eol = require('os').EOL;

// PRIVATE FUNCTIONS

function addToPatternPaths(patternlab, patternTypeName, pattern) {
  //this is messy, could use a refactor.
  patternlab.patternPaths[patternTypeName][pattern.patternName] = pattern.subdir.replace(/\\/g, '/') + "/" + pattern.fileName;
}

//todo: refactor this as a method on the pattern object itself once we merge dev with pattern-engines branch
function isPatternExcluded(pattern) {
  // returns whether or not the first character of the pattern filename is an underscore, or excluded
  return pattern.fileName.charAt(0) === '_';
}

// Returns the array of patterns to be rendered in the styleguide view and
// linked to in the pattern navigation. Checks if patterns are excluded.
function assembleStyleguidePatterns(patternlab) {
  var styleguideExcludes = patternlab.config.styleGuideExcludes;
  var styleguidePatterns = [];

  if (styleguideExcludes && styleguideExcludes.length) {
    for (var i = 0; i < patternlab.patterns.length; i++) {

      // skip underscore-prefixed files
      if (isPatternExcluded(patternlab.patterns[i])) {
        if (patternlab.config.debug) {
          console.log('Omitting ' + patternlab.patterns[i].patternPartial + " from styleguide pattern exclusion.");
        }
        continue;
      }

      var partial = patternlab.patterns[i].patternPartial;
      var partialType = partial.substring(0, partial.indexOf('-'));
      var isExcluded = (styleguideExcludes.indexOf(partialType) > -1);
      if (!isExcluded) {
        styleguidePatterns.push(patternlab.patterns[i]);
      }
    }
  } else {
    for (i = 0; i < patternlab.patterns.length; i++) {
      // skip underscore-prefixed files
      if (isPatternExcluded(patternlab.patterns[i])) {
        if (patternlab.config.debug) {
          console.log('Omitting ' + patternlab.patterns[i].patternPartial + " from styleguide pattern exclusion.");
        }
        continue;
      }
      styleguidePatterns.push(patternlab.patterns[i]);
    }
  }

  return styleguidePatterns;
}

function buildNavigation(patternlab) {
  for (var i = 0; i < patternlab.patterns.length; i++) {

    var pattern = patternlab.patterns[i];

    //todo: check if this is already available
    var patternTypeName = pattern.name.replace(/\\/g, '-').split('-')[1];

    // skip underscore-prefixed files. don't create a patternType on account of an underscored pattern
    if (isPatternExcluded(pattern)) {
      continue;
    }

    var patternSubTypeName;
    var patternSubTypeItemName;
    var flatPatternItem;
    var patternSubType;
    var patternSubTypeItem;
    var viewAllPatternSubTypeItem;

    //get the patternSubType.
    //if there is one or more slashes in the subdir, get everything after
    //the last slash. if no slash, get the whole subdir string and strip
    //any numeric + hyphen prefix
    patternSubTypeName = pattern.subdir.split('/').pop().replace(/^\d*\-/, '');

    //get the patternSubTypeItem
    patternSubTypeItemName = pattern.patternName.replace(/-/g, ' ');

    //assume the patternSubTypeItem does not exist.
    patternSubTypeItem = new of.oPatternSubTypeItem(patternSubTypeItemName);
    patternSubTypeItem.patternPath = pattern.patternLink;
    patternSubTypeItem.patternPartial = pattern.patternPartial;

    //check if the patternType already exists
    var patternTypeIndex = patternlab.patternTypeIndex.indexOf(patternTypeName);
    if (patternTypeIndex === -1) {
      //add the patternType
      var patternType = new of.oPatternType(patternTypeName);

      //add patternPath and viewAllPath
      patternlab.patternPaths[patternTypeName] = {};
      patternlab.viewAllPaths[patternTypeName] = {};

      //test whether the pattern structure is flat or not - usually due to a template or page
      flatPatternItem = patternSubTypeName === patternTypeName;

      //assume the patternSubType does not exist.
      patternSubType = new of.oPatternSubType(patternSubTypeName);

      //add the patternState if it exists
      if (pattern.patternState) {
        patternSubTypeItem.patternState = pattern.patternState;
      }

      //if it is flat - we should not add the pattern to patternPaths
      if (flatPatternItem) {

        patternType.patternItems.push(patternSubTypeItem);

        //add to patternPaths
        addToPatternPaths(patternlab, patternTypeName, pattern);

      } else {

        patternType.patternTypeItems.push(patternSubType);
        patternType.patternTypeItemsIndex.push(patternSubTypeName);
        patternSubType.patternSubtypeItems.push(patternSubTypeItem);
        patternSubType.patternSubtypeItemsIndex.push(patternSubTypeItemName);

        //add to patternPaths
        addToPatternPaths(patternlab, patternTypeName, pattern);

        //add the view all PatternSubTypeItem
        viewAllPatternSubTypeItem = new of.oPatternSubTypeItem("View All");
        viewAllPatternSubTypeItem.patternPath = pattern.subdir.slice(0, pattern.subdir.indexOf(pattern.patternGroup) + pattern.patternGroup.length) + "/index.html";
        viewAllPatternSubTypeItem.patternPartial = "viewall-" + pattern.patternGroup;

        patternType.patternItems.push(viewAllPatternSubTypeItem);
        patternlab.viewAllPaths[patternTypeName].viewall = pattern.subdir.slice(0, pattern.subdir.indexOf(pattern.patternGroup) + pattern.patternGroup.length);

      }

      //add the patternType.
      patternlab.patternTypes.push(patternType);
      patternlab.patternTypeIndex.push(patternTypeName);

      //done

    } else {
      //find the patternType
      patternType = patternlab.patternTypes[patternTypeIndex];

      //add the patternState if it exists
      if (pattern.patternState) {
        patternSubTypeItem.patternState = pattern.patternState;
      }

      //test whether the pattern structure is flat or not - usually due to a template or page
      flatPatternItem = patternSubTypeName === patternTypeName;

      //if it is flat - we should not add the pattern to patternPaths
      if (flatPatternItem) {
        //add the patternSubType to patternItems
        patternType.patternItems.push(patternSubTypeItem);

        //add to patternPaths
        addToPatternPaths(patternlab, patternTypeName, pattern);

      } else {

        // only do this if pattern is included
        //check to see if patternSubType exists
        var patternTypeItemsIndex = patternType.patternTypeItemsIndex.indexOf(patternSubTypeName);
        if (patternTypeItemsIndex === -1) {
          patternSubType = new of.oPatternSubType(patternSubTypeName);

          //add the patternSubType and patternSubTypeItem
          patternSubType.patternSubtypeItems.push(patternSubTypeItem);
          patternSubType.patternSubtypeItemsIndex.push(patternSubTypeItemName);
          patternType.patternTypeItems.push(patternSubType);
          patternType.patternTypeItemsIndex.push(patternSubTypeName);

        } else {
          //add the patternSubTypeItem
          patternSubType = patternType.patternTypeItems[patternTypeItemsIndex];
          patternSubType.patternSubtypeItems.push(patternSubTypeItem);
          patternSubType.patternSubtypeItemsIndex.push(patternSubTypeItemName);
        }

        //check if we are moving to a new subgroup in the next loop
        if (!patternlab.patterns[i + 1] || pattern.patternSubGroup !== patternlab.patterns[i + 1].patternSubGroup) {

          //add the viewall SubTypeItem
          var viewAllPatternSubTypeItem = new of.oPatternSubTypeItem("View All");
          viewAllPatternSubTypeItem.patternPath = pattern.flatPatternPath + "/index.html";
          viewAllPatternSubTypeItem.patternPartial = "viewall-" + pattern.patternGroup + "-" + pattern.patternSubGroup;

          patternSubType.patternSubtypeItems.push(viewAllPatternSubTypeItem);
          patternSubType.patternSubtypeItemsIndex.push("View All");
        }

        // just add to patternPaths
        addToPatternPaths(patternlab, patternTypeName, pattern);
      }
    }

    patternlab.viewAllPaths[patternTypeName][pattern.patternSubGroup] = pattern.flatPatternPath;
  }
  return patternTypeIndex;
}

function buildFooterHTML(patternlab, patternPartial) {
  //set the pattern-specific footer by compiling the general-footer with data, and then adding it to the meta footer
  var footerPartial = pattern_assembler.renderPattern(patternlab.footer, {
    patternData: JSON.stringify({
      patternPartial: patternPartial,
    }),
    cacheBuster: patternlab.cacheBuster
  });
  var footerHTML = pattern_assembler.renderPattern(patternlab.userFoot, {
    patternLabFoot : footerPartial
  });
  return footerHTML;
}

function buildViewAllHTML(patternlab, patterns, patternPartial) {
  var viewAllHTML = pattern_assembler.renderPattern(patternlab.viewAll,
    {
      partials: patterns,
      patternPartial: patternPartial,
      cacheBuster: patternlab.cacheBuster
    }, {
      patternSection: patternlab.patternSection,
      patternSectionSubType: patternlab.patternSectionSubType
    });
  return viewAllHTML;
}

function buildViewAllPages(mainPageHeadHtml, patternlab) {
  var paths = patternlab.config.paths;
  var prevSubdir = '';
  var prevGroup = '';
  var i;

  for (i = 0; i < patternlab.patterns.length; i++) {
    // skip underscore-prefixed files
    if (isPatternExcluded(patternlab.patterns[i])) {
      if (patternlab.config.debug) {
        console.log('Omitting ' + patternlab.patterns[i].patternPartial + " from view all rendering.");
      }
      continue;
    }

    var pattern = patternlab.patterns[i];

    //create the view all for the section
    // check if the current section is different from the previous one
    if (pattern.patternGroup !== prevGroup) {
      prevGroup = pattern.patternGroup;

      var viewAllPatterns = [];
      var patternPartial = "viewall-" + pattern.patternGroup;
      var j;

      for (j = 0; j < patternlab.patterns.length; j++) {
        if (patternlab.patterns[j].patternGroup === pattern.patternGroup) {
          //again, skip any sibling patterns to the current one that may have underscores
          if (isPatternExcluded(patternlab.patterns[j])) {
            if (patternlab.config.debug) {
              console.log('Omitting ' + patternlab.patterns[j].patternPartial + " from view all sibling rendering.");
            }
            continue;
          }

          viewAllPatterns.push(patternlab.patterns[j]);
        }
      }

      //render the footer needed for the viewall template
      var footerHTML = buildFooterHTML(patternlab, patternPartial);

      //render the viewall template
      var viewAllHTML = buildViewAllHTML(patternlab, viewAllPatterns, patternPartial);

      fs.outputFileSync(paths.public.patterns + pattern.subdir.slice(0, pattern.subdir.indexOf(pattern.patternGroup) + pattern.patternGroup.length) + '/index.html', mainPageHeadHtml + viewAllHTML + footerHTML);
    }

    //create the view all for the subsection
    // check if the current sub section is different from the previous one
    if (pattern.subdir !== prevSubdir) {
      prevSubdir = pattern.subdir;

      viewAllPatterns = [];
      patternPartial = "viewall-" + pattern.patternGroup + "-" + pattern.patternSubGroup;

      for (j = 0; j < patternlab.patterns.length; j++) {
        if (patternlab.patterns[j].subdir === pattern.subdir) {
          //again, skip any sibling patterns to the current one that may have underscores
          if (isPatternExcluded(patternlab.patterns[j])) {
            if (patternlab.config.debug) {
              console.log('Omitting ' + patternlab.patterns[j].patternPartial + " from view all sibling rendering.");
            }
            continue;
          }

          viewAllPatterns.push(patternlab.patterns[j]);
        }
      }

      //render the footer needed for the viewall template
      var footerHTML = buildFooterHTML(patternlab, patternPartial);

      //render the viewall template
      var viewAllHTML = buildViewAllHTML(patternlab, viewAllPatterns, patternPartial);

      fs.outputFileSync(paths.public.patterns + pattern.flatPatternPath + '/index.html', mainPageHeadHtml + viewAllHTML + footerHTML);
    }
  }
}

function sortPatterns(patternsArray) {
  return patternsArray.sort(function (a, b) {
    if (a.name > b.name) {
      return 1;
    }
    if (a.name < b.name) {
      return -1;
    }

    // a must be equal to b
    return 0;
  });
}

// MAIN BUILDER FUNCTION

function buildFrontEnd(patternlab) {
  var mh = require('./media_hunter');
  var media_hunter = new mh();
  var ae = require('./annotation_exporter');
  var annotation_exporter = new ae(patternlab);
  var styleguidePatterns = [];
  var paths = patternlab.config.paths;

  patternlab.patternTypes = [];
  patternlab.patternTypeIndex = [];
  patternlab.patternPaths = {};
  patternlab.viewAllPaths = {};

  //sort all patterns explicitly.
  patternlab.patterns = sortPatterns(patternlab.patterns);

  //find mediaQueries
  media_hunter.find_media_queries(path.resolve(paths.source.css), patternlab);

  // check if patterns are excluded, if not add them to styleguidePatterns
  styleguidePatterns = assembleStyleguidePatterns(patternlab);

  //set the pattern-specific header by compiling the general-header with data, and then adding it to the meta header
  var headerPartial = pattern_assembler.renderPattern(patternlab.header, {
    cacheBuster: patternlab.cacheBuster
  });
  var headerHTML = pattern_assembler.renderPattern(patternlab.userHead, {
    patternLabHead : headerPartial
  });

  //set the pattern-specific footer by compiling the general-footer with data, and then adding it to the meta footer
  var footerPartial = pattern_assembler.renderPattern(patternlab.footer, {
    patternData: '{}',
    cacheBuster: patternlab.cacheBuster
  });
  var footerHTML = pattern_assembler.renderPattern(patternlab.userFoot, {
    patternLabFoot : footerPartial
  });

  //build the styleguide
  var styleguideTemplate = fs.readFileSync(path.resolve(paths.source.patternlabFiles, 'viewall.mustache'), 'utf8');
  var styleguideHtml = pattern_assembler.renderPattern(styleguideTemplate,
    {
      partials: styleguidePatterns,
      cacheBuster: patternlab.cacheBuster
    }, {
      patternSection: patternlab.patternSection,
      patternSectionSubType: patternlab.patternSectionSubType
    });

  fs.outputFileSync(path.resolve(paths.public.styleguide, 'html/styleguide.html'), headerHTML + styleguideHtml + footerHTML);

  //build the viewall pages
  buildViewAllPages(headerHTML, patternlab);

  //build the patternlab website
  buildNavigation(patternlab);

  //move the index file from its asset location into public root
  var patternlabSiteHtml;
  try {
    patternlabSiteHtml = fs.readFileSync(path.resolve(paths.source.styleguide, 'index.html'), 'utf8');
  } catch (error) {
    console.log(error);
    console.log("\nERROR: Could not load one or more styleguidekit assets from", paths.source.styleguide, '\n');
    process.exit(1);
  }
  fs.outputFileSync(path.resolve(paths.public.root, 'index.html'), patternlabSiteHtml);

  //write out the data
  var output = '';

  //config
  output += 'var config = ' + JSON.stringify(patternlab.config) + ';\n';

  //ishControls
  output += 'var ishControls = {"ishControlsHide":' + JSON.stringify(patternlab.config.ishControlsHide) + '};' + eol;

  //todo add media queries to this
  //navItems
  output += 'var navItems = {"patternTypes": ' + JSON.stringify(patternlab.patternTypes) + '};' + eol;

  //patternPaths
  output += 'var patternPaths = ' + JSON.stringify(patternlab.patternPaths) + ';' + eol;

  //viewAllPaths
  output += 'var viewAllPaths = ' + JSON.stringify(patternlab.viewAllPaths) + ';' + eol;

  //plugins someday
  output += 'var plugins = [];' + eol;
  fs.outputFileSync(path.resolve(paths.public.data, 'patternlab-data.js'), output);

  //annotations
  var annotationsJSON = annotation_exporter.gather();
  var annotations = 'var comments = ' + JSON.stringify(annotationsJSON);
  fs.outputFileSync(path.resolve(paths.public.annotations, 'annotations.js'), annotations);

}

module.exports = buildFrontEnd;
<|endoftext|>


<commit_before>// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (c) 2012 Giovanni Campagna <scampa.giovanni@gmail.com>
// Copyright (C) 2015  Daiki Ueno <dueno@src.gnome.org>
//
// Gnome Weather is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by the
// Free Software Foundation; either version 2 of the License, or (at your
// option) any later version.
//
// Gnome Weather is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License along
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

pkg.initGettext();
pkg.initFormat();
pkg.require({ 'Gio': '2.0',
              'GLib': '2.0',
              'GObject': '2.0',
              'Gtk': '3.0' });

const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;

const Util = imports.util;
const SearchProvider = imports.searchProvider;

function initEnvironment() {
    window.getApp = function() {
        return Gio.Application.get_default();
    };
}

const BackgroundService = new Lang.Class({
    Name: 'CharactersBackgroundService',
    // This needs to be a Gtk.Application instead of Gio.Application,
    // to get Gtk.Clipboard working.
    Extends: Gtk.Application,

    _init: function() {
        this.parent({ application_id: pkg.name,
                      flags: Gio.ApplicationFlags.IS_SERVICE,
                      inactivity_timeout: 30000 });
        GLib.set_application_name(_("Characters"));

        this._searchProvider = new SearchProvider.SearchProvider(this);
    },

    _onQuit: function() {
        this.quit();
    },

    vfunc_dbus_register: function(connection, path) {
        this.parent(connection, path);

        this._searchProvider.export(connection, path);
        return true;
    },

/*
  Can't do until GApplication is fixed.

    vfunc_dbus_unregister: function(connection, path) {
        this._searchProvider.unexport(connection);

        this.parent(connection, path);
    },
*/

    vfunc_startup: function() {
        this.parent();

        Util.initActions(this,
                         [{ name: 'quit',
                            activate: this._onQuit }]);
    },

    vfunc_activate: function() {
        // do nothing, this is a background service
    },
});

function main(argv) {
    initEnvironment();

    return (new BackgroundService()).run(argv);
}
<commit_msg>Use ES6 classes<commit_after>// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (c) 2012 Giovanni Campagna <scampa.giovanni@gmail.com>
// Copyright (C) 2015  Daiki Ueno <dueno@src.gnome.org>
//
// Gnome Weather is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by the
// Free Software Foundation; either version 2 of the License, or (at your
// option) any later version.
//
// Gnome Weather is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
// or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License along
// with Gnome Weather; if not, write to the Free Software Foundation,
// Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

pkg.initGettext();
pkg.initFormat();
pkg.require({ 'Gio': '2.0',
              'GLib': '2.0',
              'GObject': '2.0',
              'Gtk': '3.0' });

const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const GObject = imports.gi.GObject;

const Util = imports.util;
const SearchProvider = imports.searchProvider;

function initEnvironment() {
    window.getApp = function() {
        return Gio.Application.get_default();
    };
}

const BackgroundService = GObject.registerClass({
    // This needs to be a Gtk.Application instead of Gio.Application,
    // to get Gtk.Clipboard working.
}, class BackgroundService extends Gtk.Application {
    _init() {
        super._init({ application_id: pkg.name,
                      flags: Gio.ApplicationFlags.IS_SERVICE,
                      inactivity_timeout: 30000 });
        GLib.set_application_name(_("Characters"));

        this._searchProvider = new SearchProvider.SearchProvider(this);
    }

    _onQuit() {
        this.quit();
    }

    vfunc_dbus_register(connection, path) {
        super.vfunc_dbus_register(connection, path);

        this._searchProvider.export(connection, path);
        return true;
    }

/*
  Can't do until GApplication is fixed.

    vfunc_dbus_unregister(connection, path) {
        this._searchProvider.unexport(connection);

        super.vfunc_dbus_unregister(connection, path);
    },
*/

    vfunc_startup() {
        super.vfunc_startup();

        Util.initActions(this,
                         [{ name: 'quit',
                            activate: this._onQuit }]);
    }

    vfunc_activate() {
        // do nothing, this is a background service
    }
});

function main(argv) {
    initEnvironment();

    return (new BackgroundService()).run(argv);
}
<|endoftext|>


<commit_before>/* @flow */

import type { LocalEvent } from './event'
import type {
  LocalChange,
  LocalDirAddition,
  LocalDirDeletion,
  LocalDirMove,
  LocalFileAddition,
  LocalFileDeletion,
  LocalFileMove
} from './change'

const path = require('path')

const { getInode } = require('./event')
const localChange = require('./change')
const logger = require('../logger')
const measureTime = require('../perftools')

const log = logger({
  component: 'local/analysis'
})
log.chokidar = log.child({
  component: 'chokidar'
})

module.exports = function analysis (events: LocalEvent[], pendingChanges: LocalChange[]): LocalChange[] {
  const changes: LocalChange[] = analyseEvents(events, pendingChanges)
  sortBeforeSquash(changes)
  squashMoves(changes)
  finalSort(changes)
  return separatePendingChanges(changes, pendingChanges)
}

const panic = (context, description) => {
  log.error(context, description)
  throw new Error(description)
}

function analyseEvents (events: LocalEvent[], pendingChanges: LocalChange[]): LocalChange[] {
  const stopMeasure = measureTime('LocalWatcher#analyseEvents')
  // OPTIMIZE: new Array(events.length)
  const changes: LocalChange[] = []
  const changesByInode:Map<number, LocalChange> = new Map()
  const changesByPath:Map<string, LocalChange> = new Map()
  const getChangeByInode = (e) => {
    const ino = getInode(e)
    if (ino) return changesByInode.get(ino)
    else return null
  }
  const getChangeByPath = (e) => {
    return changesByPath.get(e.path)
  }
  const changeFound = (c: LocalChange) => {
    changesByPath.set(c.path, c)
    if (typeof c.ino === 'number') changesByInode.set(c.ino, c)
    else changes.push(c)
  }

  if (pendingChanges.length > 0) {
    log.warn({changes: pendingChanges}, `Prepend ${pendingChanges.length} pending change(s)`)
    for (const a of pendingChanges) { changeFound(a) }
    pendingChanges.length = 0
  }

  log.trace('Analyze events...')

  for (let e: LocalEvent of events) {
    try {
      // chokidar make mistakes
      if (e.type === 'unlinkDir' && e.old && e.old.docType === 'file') {
        log.warn({event: e, old: e.old}, 'chokidar miscategorized event (was file, event unlinkDir)')
        // $FlowFixMe
        e.type = 'unlink'
      }

      if (e.type === 'unlink' && e.old && e.old.docType === 'folder') {
        log.warn({event: e, old: e.old}, 'chokidar miscategorized event (was folder, event unlink)')
        // $FlowFixMe
        e.type = 'unlinkDir'
      }

      switch (e.type) {
        case 'add':
          {
            const moveChange: ?LocalFileMove = localChange.maybeMoveFile(getChangeByInode(e))
            if (moveChange) {
              localChange.includeAddEventInFileMove(moveChange, e)
              break
            }

            const unlinkChange: ?LocalFileDeletion = localChange.maybeDeleteFile(getChangeByInode(e))
            if (unlinkChange) {
              changeFound(localChange.fileMoveFromUnlinkAdd(unlinkChange, e))
            } else {
              changeFound(localChange.fromEvent(e))
            }
          }
          break
        case 'addDir':
          {
            const moveChange: ?LocalDirMove = localChange.maybeMoveFolder(getChangeByInode(e))
            if (moveChange) {
              localChange.includeAddDirEventInDirMove(moveChange, e)
            }

            const unlinkChange: ?LocalDirDeletion = localChange.maybeDeleteFolder(getChangeByInode(e))
            if (unlinkChange) {
              changeFound(localChange.dirMoveFromUnlinkAdd(unlinkChange, e))
            } else {
              changeFound(localChange.fromEvent(e))
            }
          }
          break
        case 'change':
          const moveChange: ?LocalFileMove = localChange.maybeMoveFile(getChangeByInode(e))
          if (moveChange) {
            localChange.includeChangeEventIntoFileMove(moveChange, e)
          } else {
            changeFound(localChange.fromEvent(e))
          }
          break
        case 'unlink':
          {
            const moveChange: ?LocalFileMove = localChange.maybeMoveFile(getChangeByInode(e))
            /* istanbul ignore next */
            if (moveChange) {
              // TODO: Pending move
              panic({path: e.path, moveChange, event: e},
                'We should not have both move and unlink changes since ' +
                'checksumless adds and inode-less unlink events are dropped')
            }

            const addChange: ?LocalFileAddition = localChange.maybeAddFile(getChangeByInode(e))
            if (addChange) {
              // TODO: pending move
              changeFound(localChange.fileMoveFromAddUnlink(addChange, e))
              break
            } else if (getInode(e)) {
              changeFound(localChange.fromEvent(e))
              break
            }
            const change: ?LocalFileMove = localChange.maybeMoveFile(getChangeByPath(e))
            if (change && change.md5sum == null) { // FIXME: if change && change.wip?
              localChange.convertFileMoveToDeletion(change)
            }
            // Otherwise, skip unlink event by multiple moves
          }
          break
        case 'unlinkDir':
          {
            const moveChange: ?LocalDirMove = localChange.maybeMoveFolder(getChangeByInode(e))
            /* istanbul ignore next */
            if (moveChange) {
              // TODO: pending move
              panic({path: e.path, moveChange, event: e},
                'We should not have both move and unlinkDir changes since ' +
                'non-existing addDir and inode-less unlinkDir events are dropped')
            }

            const addChange: ?LocalDirAddition = localChange.maybePutFolder(getChangeByInode(e))
            if (addChange) {
              changeFound(localChange.dirMoveFromAddUnlink(addChange, e))
            } else if (getInode(e)) {
              changeFound(localChange.fromEvent(e))
            } else {
              const addChangeSamePath: ?LocalDirAddition = localChange.maybePutFolder(getChangeByPath(e))
              if (addChangeSamePath && addChangeSamePath.wip) {
                log.debug({path: addChangeSamePath.path, ino: addChangeSamePath.ino},
                  'Folder was added then deleted. Ignoring add.')
                // $FlowFixMe
                addChangeSamePath.type = 'Ignored'
              }

              const moveChangeSamePath: ?LocalDirMove = localChange.maybeMoveFolder(getChangeByPath(e))
              if (moveChangeSamePath && moveChangeSamePath.wip) {
                localChange.convertDirMoveToDeletion(moveChangeSamePath)
              }
            }
          }
          break
        default:
          throw new TypeError(`Unknown event type: ${e.type}`)
      }
    } catch (err) {
      log.error({err, path: e.path})
      throw err
    }
    if (process.env.DEBUG) log.trace({currentEvent: e, changes})
  }

  log.trace('Flatten changes map...')
  for (let a of changesByInode.values()) changes.push(a)

  stopMeasure()
  return changes
}

// TODO: Rename according to the sort logic
function sortBeforeSquash (changes: LocalChange[]) {
  log.trace('Sort changes before squash...')
  const stopMeasure = measureTime('LocalWatcher#sortBeforeSquash')
  changes.sort((a, b) => {
    if (a.type === 'DirMove' || a.type === 'FileMove') {
      if (b.type === 'DirMove' || b.type === 'FileMove') {
        if (a.path < b.path) return -1
        else if (a.path > b.path) return 1
        else return 0
      } else return -1
    } else if (b.type === 'DirMove' || b.type === 'FileMove') {
      return 1
    } else {
      return 0
    }
  })
  stopMeasure()
}

function squashMoves (changes: LocalChange[]) {
  log.trace('Squash moves...')
  const stopMeasure = measureTime('LocalWatcher#squashMoves')

  for (let i = 0; i < changes.length; i++) {
    let a = changes[i]

    if (a.type !== 'DirMove' && a.type !== 'FileMove') break
    for (let j = i + 1; j < changes.length; j++) {
      let b = changes[j]
      if (b.type !== 'DirMove' && b.type !== 'FileMove') break

      // inline of LocalChange.isChildMove
      if (a.type === 'DirMove' &&
      b.path.indexOf(a.path + path.sep) === 0 &&
      a.old && b.old &&
      b.old.path.indexOf(a.old.path + path.sep) === 0) {
        log.debug({oldpath: b.old.path, path: b.path}, 'descendant move')
        a.wip = a.wip || b.wip
        if (b.path.substr(a.path.length) === b.old.path.substr(a.old.path.length)) {
          changes.splice(j--, 1)
        } else {
          // move inside move
          b.old.path = b.old.path.replace(a.old.path, a.path)
          b.needRefetch = true
        }
      }
    }
  }

  stopMeasure()
}

function separatePendingChanges (changes: LocalChange[], pendingChanges: LocalChange[]): LocalChange[] {
  log.trace('Reserve changes in progress for next flush...')

  for (let i = 0; i < changes.length; i++) {
    const change = changes[i]
    if (change.wip) {
      if (change.type === 'DirMove' || change.type === 'FileMove') {
        log.debug({
          change: change.type,
          oldpath: change.old.path,
          path: change.path,
          ino: change.ino
        }, 'incomplete change')
      } else {
        log.debug({change: change.type, path: change.path}, 'incomplete change')
      }
      pendingChanges.push(changes[i])
    } else {
      log.debug(`Identified ${changes.length} change(s).`)
      log.debug(`${pendingChanges.length} of them are still pending.`)
      return changes.slice(i)
    }
  }
  // All actions are WIP
  return []
}

// TODO: Rename according to the sort logic
const finalSorter = (a: LocalChange, b: LocalChange) => {
  if (a.wip && !b.wip) return -1
  if (b.wip && !a.wip) return 1

  if (localChange.childOf(localChange.addPath(a), localChange.delPath(b))) return -1
  if (localChange.childOf(localChange.addPath(b), localChange.delPath(a))) return 1

  // if one change is a child of another, it takes priority
  if (localChange.isChildAdd(a, b)) return -1
  if (localChange.isChildDelete(b, a)) return -1
  if (localChange.isChildAdd(b, a)) return 1
  if (localChange.isChildDelete(a, b)) return 1

  // otherwise, order by add path
  if (localChange.lower(localChange.addPath(a), localChange.addPath(b))) return -1
  if (localChange.lower(localChange.addPath(b), localChange.addPath(a))) return 1

  // if there isnt 2 add paths, sort by del path
  if (localChange.lower(localChange.delPath(b), localChange.delPath(a))) return -1

  return 1
}

// TODO: Rename according to the sort logic
function finalSort (changes: LocalChange[]) {
  log.trace('Final sort...')
  const stopMeasure = measureTime('LocalWatcher#finalSort')
  changes.sort(finalSorter)
  stopMeasure()
}
<commit_msg>core/local/analysis: Log currentEvent's path (debug)<commit_after>/* @flow */

import type { LocalEvent } from './event'
import type {
  LocalChange,
  LocalDirAddition,
  LocalDirDeletion,
  LocalDirMove,
  LocalFileAddition,
  LocalFileDeletion,
  LocalFileMove
} from './change'

const path = require('path')

const { getInode } = require('./event')
const localChange = require('./change')
const logger = require('../logger')
const measureTime = require('../perftools')

const log = logger({
  component: 'local/analysis'
})
log.chokidar = log.child({
  component: 'chokidar'
})

module.exports = function analysis (events: LocalEvent[], pendingChanges: LocalChange[]): LocalChange[] {
  const changes: LocalChange[] = analyseEvents(events, pendingChanges)
  sortBeforeSquash(changes)
  squashMoves(changes)
  finalSort(changes)
  return separatePendingChanges(changes, pendingChanges)
}

const panic = (context, description) => {
  log.error(context, description)
  throw new Error(description)
}

function analyseEvents (events: LocalEvent[], pendingChanges: LocalChange[]): LocalChange[] {
  const stopMeasure = measureTime('LocalWatcher#analyseEvents')
  // OPTIMIZE: new Array(events.length)
  const changes: LocalChange[] = []
  const changesByInode:Map<number, LocalChange> = new Map()
  const changesByPath:Map<string, LocalChange> = new Map()
  const getChangeByInode = (e) => {
    const ino = getInode(e)
    if (ino) return changesByInode.get(ino)
    else return null
  }
  const getChangeByPath = (e) => {
    return changesByPath.get(e.path)
  }
  const changeFound = (c: LocalChange) => {
    changesByPath.set(c.path, c)
    if (typeof c.ino === 'number') changesByInode.set(c.ino, c)
    else changes.push(c)
  }

  if (pendingChanges.length > 0) {
    log.warn({changes: pendingChanges}, `Prepend ${pendingChanges.length} pending change(s)`)
    for (const a of pendingChanges) { changeFound(a) }
    pendingChanges.length = 0
  }

  log.trace('Analyze events...')

  for (let e: LocalEvent of events) {
    try {
      // chokidar make mistakes
      if (e.type === 'unlinkDir' && e.old && e.old.docType === 'file') {
        log.warn({event: e, old: e.old}, 'chokidar miscategorized event (was file, event unlinkDir)')
        // $FlowFixMe
        e.type = 'unlink'
      }

      if (e.type === 'unlink' && e.old && e.old.docType === 'folder') {
        log.warn({event: e, old: e.old}, 'chokidar miscategorized event (was folder, event unlink)')
        // $FlowFixMe
        e.type = 'unlinkDir'
      }

      switch (e.type) {
        case 'add':
          {
            const moveChange: ?LocalFileMove = localChange.maybeMoveFile(getChangeByInode(e))
            if (moveChange) {
              localChange.includeAddEventInFileMove(moveChange, e)
              break
            }

            const unlinkChange: ?LocalFileDeletion = localChange.maybeDeleteFile(getChangeByInode(e))
            if (unlinkChange) {
              changeFound(localChange.fileMoveFromUnlinkAdd(unlinkChange, e))
            } else {
              changeFound(localChange.fromEvent(e))
            }
          }
          break
        case 'addDir':
          {
            const moveChange: ?LocalDirMove = localChange.maybeMoveFolder(getChangeByInode(e))
            if (moveChange) {
              localChange.includeAddDirEventInDirMove(moveChange, e)
            }

            const unlinkChange: ?LocalDirDeletion = localChange.maybeDeleteFolder(getChangeByInode(e))
            if (unlinkChange) {
              changeFound(localChange.dirMoveFromUnlinkAdd(unlinkChange, e))
            } else {
              changeFound(localChange.fromEvent(e))
            }
          }
          break
        case 'change':
          const moveChange: ?LocalFileMove = localChange.maybeMoveFile(getChangeByInode(e))
          if (moveChange) {
            localChange.includeChangeEventIntoFileMove(moveChange, e)
          } else {
            changeFound(localChange.fromEvent(e))
          }
          break
        case 'unlink':
          {
            const moveChange: ?LocalFileMove = localChange.maybeMoveFile(getChangeByInode(e))
            /* istanbul ignore next */
            if (moveChange) {
              // TODO: Pending move
              panic({path: e.path, moveChange, event: e},
                'We should not have both move and unlink changes since ' +
                'checksumless adds and inode-less unlink events are dropped')
            }

            const addChange: ?LocalFileAddition = localChange.maybeAddFile(getChangeByInode(e))
            if (addChange) {
              // TODO: pending move
              changeFound(localChange.fileMoveFromAddUnlink(addChange, e))
              break
            } else if (getInode(e)) {
              changeFound(localChange.fromEvent(e))
              break
            }
            const change: ?LocalFileMove = localChange.maybeMoveFile(getChangeByPath(e))
            if (change && change.md5sum == null) { // FIXME: if change && change.wip?
              localChange.convertFileMoveToDeletion(change)
            }
            // Otherwise, skip unlink event by multiple moves
          }
          break
        case 'unlinkDir':
          {
            const moveChange: ?LocalDirMove = localChange.maybeMoveFolder(getChangeByInode(e))
            /* istanbul ignore next */
            if (moveChange) {
              // TODO: pending move
              panic({path: e.path, moveChange, event: e},
                'We should not have both move and unlinkDir changes since ' +
                'non-existing addDir and inode-less unlinkDir events are dropped')
            }

            const addChange: ?LocalDirAddition = localChange.maybePutFolder(getChangeByInode(e))
            if (addChange) {
              changeFound(localChange.dirMoveFromAddUnlink(addChange, e))
            } else if (getInode(e)) {
              changeFound(localChange.fromEvent(e))
            } else {
              const addChangeSamePath: ?LocalDirAddition = localChange.maybePutFolder(getChangeByPath(e))
              if (addChangeSamePath && addChangeSamePath.wip) {
                log.debug({path: addChangeSamePath.path, ino: addChangeSamePath.ino},
                  'Folder was added then deleted. Ignoring add.')
                // $FlowFixMe
                addChangeSamePath.type = 'Ignored'
              }

              const moveChangeSamePath: ?LocalDirMove = localChange.maybeMoveFolder(getChangeByPath(e))
              if (moveChangeSamePath && moveChangeSamePath.wip) {
                localChange.convertDirMoveToDeletion(moveChangeSamePath)
              }
            }
          }
          break
        default:
          throw new TypeError(`Unknown event type: ${e.type}`)
      }
    } catch (err) {
      log.error({err, path: e.path})
      throw err
    }
    if (process.env.DEBUG) log.trace({currentEvent: e, path: e.path})
  }

  log.trace('Flatten changes map...')
  for (let a of changesByInode.values()) changes.push(a)

  stopMeasure()
  return changes
}

// TODO: Rename according to the sort logic
function sortBeforeSquash (changes: LocalChange[]) {
  log.trace('Sort changes before squash...')
  const stopMeasure = measureTime('LocalWatcher#sortBeforeSquash')
  changes.sort((a, b) => {
    if (a.type === 'DirMove' || a.type === 'FileMove') {
      if (b.type === 'DirMove' || b.type === 'FileMove') {
        if (a.path < b.path) return -1
        else if (a.path > b.path) return 1
        else return 0
      } else return -1
    } else if (b.type === 'DirMove' || b.type === 'FileMove') {
      return 1
    } else {
      return 0
    }
  })
  stopMeasure()
}

function squashMoves (changes: LocalChange[]) {
  log.trace('Squash moves...')
  const stopMeasure = measureTime('LocalWatcher#squashMoves')

  for (let i = 0; i < changes.length; i++) {
    let a = changes[i]

    if (a.type !== 'DirMove' && a.type !== 'FileMove') break
    for (let j = i + 1; j < changes.length; j++) {
      let b = changes[j]
      if (b.type !== 'DirMove' && b.type !== 'FileMove') break

      // inline of LocalChange.isChildMove
      if (a.type === 'DirMove' &&
      b.path.indexOf(a.path + path.sep) === 0 &&
      a.old && b.old &&
      b.old.path.indexOf(a.old.path + path.sep) === 0) {
        log.debug({oldpath: b.old.path, path: b.path}, 'descendant move')
        a.wip = a.wip || b.wip
        if (b.path.substr(a.path.length) === b.old.path.substr(a.old.path.length)) {
          changes.splice(j--, 1)
        } else {
          // move inside move
          b.old.path = b.old.path.replace(a.old.path, a.path)
          b.needRefetch = true
        }
      }
    }
  }

  stopMeasure()
}

function separatePendingChanges (changes: LocalChange[], pendingChanges: LocalChange[]): LocalChange[] {
  log.trace('Reserve changes in progress for next flush...')

  for (let i = 0; i < changes.length; i++) {
    const change = changes[i]
    if (change.wip) {
      if (change.type === 'DirMove' || change.type === 'FileMove') {
        log.debug({
          change: change.type,
          oldpath: change.old.path,
          path: change.path,
          ino: change.ino
        }, 'incomplete change')
      } else {
        log.debug({change: change.type, path: change.path}, 'incomplete change')
      }
      pendingChanges.push(changes[i])
    } else {
      log.debug(`Identified ${changes.length} change(s).`)
      log.debug(`${pendingChanges.length} of them are still pending.`)
      return changes.slice(i)
    }
  }
  // All actions are WIP
  return []
}

// TODO: Rename according to the sort logic
const finalSorter = (a: LocalChange, b: LocalChange) => {
  if (a.wip && !b.wip) return -1
  if (b.wip && !a.wip) return 1

  if (localChange.childOf(localChange.addPath(a), localChange.delPath(b))) return -1
  if (localChange.childOf(localChange.addPath(b), localChange.delPath(a))) return 1

  // if one change is a child of another, it takes priority
  if (localChange.isChildAdd(a, b)) return -1
  if (localChange.isChildDelete(b, a)) return -1
  if (localChange.isChildAdd(b, a)) return 1
  if (localChange.isChildDelete(a, b)) return 1

  // otherwise, order by add path
  if (localChange.lower(localChange.addPath(a), localChange.addPath(b))) return -1
  if (localChange.lower(localChange.addPath(b), localChange.addPath(a))) return 1

  // if there isnt 2 add paths, sort by del path
  if (localChange.lower(localChange.delPath(b), localChange.delPath(a))) return -1

  return 1
}

// TODO: Rename according to the sort logic
function finalSort (changes: LocalChange[]) {
  log.trace('Final sort...')
  const stopMeasure = measureTime('LocalWatcher#finalSort')
  changes.sort(finalSorter)
  stopMeasure()
}
<|endoftext|>