<commit_before>'use strict';
////////////////////////////////////////////////////////////////////////////////
/// @brief Foxx Router
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2015 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Alan Plum
/// @author Copyright 2015, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

const joi = require('joi');

const DEFAULT_BODY_SCHEMA = joi.object().optional().meta({allowInvalid: true});
const DEFAULT_PARAM_SCHEMA = joi.string().required();

const ALL_METHODS = require('@arangodb/actions').ALL_METHODS;
const $_WILDCARD = Symbol.for('@@wildcard'); // catch-all suffix
const $_TERMINAL = Symbol.for('@@terminal'); // terminal -- routes be here
const $_PARAM = Symbol.for('@@parameter'); // named parameter (no routes here, like static part)
const $_ROUTES = Symbol.for('@@routes'); // routes and child routers
const $_MIDDLEWARE = Symbol.for('@@middleware'); // middleware (not including router.all)

function tokenize(path, ctx) {
  if (path === '/') {
    return [$_TERMINAL];
  }
  let tokens = path.slice(1).split('/').map(function (name) {
    if (name === '*') {
      return $_WILDCARD;
    }
    if (name.charAt(0) !== ':') {
      return name;
    }
    name = name.slice(1);
    ctx._pathParamNames.push(name);
    ctx._pathParams.set(name, {type: DEFAULT_PARAM_SCHEMA});
    return $_PARAM;
  });
  if (tokens[tokens.length - 1] !== $_WILDCARD) {
    tokens.push($_TERMINAL);
  }
  return tokens;
}

class SwaggerContext {
  constructor(path) {
    {
      let n = path.length - 1;
      if (path.charAt(n) === '/') {
        path = path.slice(0, n);
      }
      if (path.charAt(0) !== '/') {
        path = `/${path}`;
      }
    }
    this._headers = new Map();
    this._queryParams = new Map();
    this._bodyParam = null;
    this._response = null;
    this._summary = null;
    this._description = null;
    this._deprecated = false;
    this.path = path;
    this._pathParams = new Map();
    this._pathParamNames = [];
    this._pathTokens = tokenize(path, this);
  }
  header(name, type, description) {
    this._headers.set(name, {type: type, description: description});
    return this;
  }
  pathParam(name, type, description) {
    this._pathParams.set(name, {type: type, description: description});
    return this;
  }
  queryParam(name, type, description) {
    this._queryParams.set(name, {type: type, description: description});
    return this;
  }
  body(type, description) {
    this._bodyParam = {type: type, description: description};
    return this;
  }
  response(type, description) {
    this._response = {type: type, description: description};
    return this;
  }
  summary(text) {
    this._summary = text;
    return this;
  }
  description(text) {
    this._description = text;
    return this;
  }
  deprecated(flag) {
    this._deprecated = typeof flag === 'boolean' ? flag : true;
    return this;
  }
}

class Route extends SwaggerContext {
  constructor(methods, path, handler, name) {
    if (typeof path !== 'string') {
      name = handler;
      handler = path;
      path = '/';
    }
    super(path);
    this.methods = methods;
    this.handler = handler;
    this.name = name;
    if (['POST', 'PUT', 'PATCH'].some(function (method) {
      return methods.indexOf(method) !== -1;
    })) {
      this._bodyParam = {type: DEFAULT_BODY_SCHEMA};
    }
  }
}

class Middleware extends SwaggerContext {
  constructor(path, fn) {
    if (typeof path !== 'string') {
      fn = path;
      path = '/';
    }
    if (path.charAt(path.length - 1) !== '*') {
      if (path.charAt(path.length - 1) !== '/') {
        path += '/';
      }
      path += '*';
    }
    super(path);
    this.middleware = fn;
    this.handler = fn;
    if (typeof fn.register === 'function') {
      this.handler = fn.register(this) || fn;
    }
  }
}

class Router extends SwaggerContext {
  constructor() {
    super('');
    this._routes = [];
    this._middleware = [];
  }
  use(path, fn) {
    if (typeof path !== 'string') {
      fn = path;
      path = '/*';
    }
    if (fn instanceof Router) {
      if (path.charAt(path.length - 1) !== '*') {
        if (path.charAt(path.length - 1) !== '/') {
          path += '/';
        }
        path += '*';
      }
      let child = {
        path: path,
        _pathParams: new Map(),
        _pathParamNames: [],
        router: fn
      };
      child._pathTokens = tokenize(path, child);
      this._routes.push(child);
      return fn;
    }
    const middleware = new Middleware(path, fn);
    this._middleware.push(middleware);
    return middleware;
  }
  all(path, handler, name) {
    const route = new Route(ALL_METHODS, path, handler, name);
    this._routes.push(route);
    return route;
  }
  get(path, handler, name) {
    const route = new Route(['GET'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  post(path, handler, name) {
    const route = new Route(['POST'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  put(path, handler, name) {
    const route = new Route(['PUT'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  patch(path, handler, name) {
    const route = new Route(['PATCH'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  delete(path, handler, name) {
    const route = new Route(['DELETE'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  head(path, handler, name) {
    const route = new Route(['HEAD'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  _buildRouteTree() {
    const root = new Map();
    this._tree = root;

    // middleware always implicitly ends in a wildcard
    // child routers always explicitly end in wildcards
    // routes may explicitly end in a wildcard
    // static names have precedence over params
    // params have precedence over wildcards
    // middleware is executed in order of precedence
    // implicit 404 in middleware does not fail the routing
    // router.all ONLY affects routes of THAT router (and its children)
    // * router.all does NOT affect routes of sibling routers *

    // should child routers have precedence over routes?
    // ideally they should respect each others precedence
    // and be treated equally

    for (let middleware of this._middleware) {
      let node = root;
      for (let token of middleware._pathTokens) {
        if (!node.has(token)) {
          node.set(token, new Map());
        }
        node = node.get(token);
      }
      if (!node.has($_MIDDLEWARE)) {
        node.set($_MIDDLEWARE, []);
      }
      node.get($_MIDDLEWARE).push(middleware);
    }

    for (let route of this._routes) {
      let node = root;
      for (let token of route._pathTokens) {
        if (!node.has(token)) {
          node.set(token, new Map());
        }
        node = node.get(token);
      }
      if (!node.has($_ROUTES)) {
        node.set($_ROUTES, []);
      }
      node.get($_ROUTES).push(route);
      if (route.router) {
        route.router._buildRouteTree();
      }
    }
  }
  * _findRoutes(suffix, result, pathParts) {
    if (!result) {
      result = [];
    }
    if (!pathParts) {
      pathParts = [];
    }
    result = result.concat({router: this, path: pathParts, suffix: suffix});
    for (let route of findRoutes(this._tree, result, suffix, [])) {
      yield route;
    }
  }
}

function* findRoutes(node, result, suffix, pathParts) {
  let wildcardNode = node.get($_WILDCARD);
  let nodeMiddleware = [];
  if (wildcardNode && wildcardNode.has($_MIDDLEWARE)) {
    nodeMiddleware = wildcardNode.get($_MIDDLEWARE);
    result = result.concat(nodeMiddleware.map(function (mw) {
      return {middleware: mw, path: pathParts, suffix: suffix};
    }));
  }
  if (!suffix.length) {
    let terminalNode = node.get($_TERMINAL);
    let terminalRoutes = terminalNode && terminalNode.get($_ROUTES) || [];
    for (let endpoint of terminalRoutes) {
      yield result.concat({endpoint: endpoint, path: pathParts, suffix: suffix});
    }
  } else {
    let part = suffix[0];
    let head = pathParts.concat(part);
    let tail = suffix.slice(1);
    for (let childNode of [node.get(part), node.get($_PARAM)]) {
      if (childNode) {
        for (let route of findRoutes(childNode, result, tail, head)) {
          yield route;
        }
      }
    }
  }
  let wildcardRoutes = wildcardNode && wildcardNode.get($_ROUTES) || [];
  for (let endpoint of wildcardRoutes) {
    if (endpoint.router) {
      for (let route of endpoint.router._findRoutes(suffix, result, pathParts)) {
        yield route;
      }
    } else {
      yield result.concat({endpoint: endpoint, path: pathParts, suffix: suffix});
    }
  }
}

module.exports = function createRouter() {
  return new Router();
};
<commit_msg>Make more route context attributes private<commit_after>'use strict';
////////////////////////////////////////////////////////////////////////////////
/// @brief Foxx Router
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2015 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Alan Plum
/// @author Copyright 2015, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

const joi = require('joi');

const DEFAULT_BODY_SCHEMA = joi.object().optional().meta({allowInvalid: true});
const DEFAULT_PARAM_SCHEMA = joi.string().required();

const ALL_METHODS = require('@arangodb/actions').ALL_METHODS;
const $_WILDCARD = Symbol.for('@@wildcard'); // catch-all suffix
const $_TERMINAL = Symbol.for('@@terminal'); // terminal -- routes be here
const $_PARAM = Symbol.for('@@parameter'); // named parameter (no routes here, like static part)
const $_ROUTES = Symbol.for('@@routes'); // routes and child routers
const $_MIDDLEWARE = Symbol.for('@@middleware'); // middleware (not including router.all)

function tokenize(path, ctx) {
  if (path === '/') {
    return [$_TERMINAL];
  }
  let tokens = path.slice(1).split('/').map(function (name) {
    if (name === '*') {
      return $_WILDCARD;
    }
    if (name.charAt(0) !== ':') {
      return name;
    }
    name = name.slice(1);
    ctx._pathParamNames.push(name);
    ctx._pathParams.set(name, {type: DEFAULT_PARAM_SCHEMA});
    return $_PARAM;
  });
  if (tokens[tokens.length - 1] !== $_WILDCARD) {
    tokens.push($_TERMINAL);
  }
  return tokens;
}

class SwaggerContext {
  constructor(path) {
    {
      let n = path.length - 1;
      if (path.charAt(n) === '/') {
        path = path.slice(0, n);
      }
      if (path.charAt(0) !== '/') {
        path = `/${path}`;
      }
    }
    this._headers = new Map();
    this._queryParams = new Map();
    this._bodyParam = null;
    this._response = null;
    this._summary = null;
    this._description = null;
    this._deprecated = false;
    this.path = path;
    this._pathParams = new Map();
    this._pathParamNames = [];
    this._pathTokens = tokenize(path, this);
  }
  header(name, type, description) {
    this._headers.set(name, {type: type, description: description});
    return this;
  }
  pathParam(name, type, description) {
    this._pathParams.set(name, {type: type, description: description});
    return this;
  }
  queryParam(name, type, description) {
    this._queryParams.set(name, {type: type, description: description});
    return this;
  }
  body(type, description) {
    this._bodyParam = {type: type, description: description};
    return this;
  }
  response(type, description) {
    this._response = {type: type, description: description};
    return this;
  }
  summary(text) {
    this._summary = text;
    return this;
  }
  description(text) {
    this._description = text;
    return this;
  }
  deprecated(flag) {
    this._deprecated = typeof flag === 'boolean' ? flag : true;
    return this;
  }
}

class Route extends SwaggerContext {
  constructor(methods, path, handler, name) {
    if (typeof path !== 'string') {
      name = handler;
      handler = path;
      path = '/';
    }
    super(path);
    this._methods = methods;
    this._handler = handler;
    this.name = name;
    if (['POST', 'PUT', 'PATCH'].some(function (method) {
      return methods.indexOf(method) !== -1;
    })) {
      this._bodyParam = {type: DEFAULT_BODY_SCHEMA};
    }
  }
}

class Middleware extends SwaggerContext {
  constructor(path, fn) {
    if (typeof path !== 'string') {
      fn = path;
      path = '/';
    }
    if (path.charAt(path.length - 1) !== '*') {
      if (path.charAt(path.length - 1) !== '/') {
        path += '/';
      }
      path += '*';
    }
    super(path);
    this._middleware = fn;
    this._handler = fn;
    if (typeof fn.register === 'function') {
      this._handler = fn.register(this) || fn;
    }
  }
}

class Router extends SwaggerContext {
  constructor() {
    super('');
    this._routes = [];
    this._middleware = [];
  }
  use(path, fn) {
    if (typeof path !== 'string') {
      fn = path;
      path = '/*';
    }
    if (fn instanceof Router) {
      if (path.charAt(path.length - 1) !== '*') {
        if (path.charAt(path.length - 1) !== '/') {
          path += '/';
        }
        path += '*';
      }
      let child = {
        path: path,
        _pathParams: new Map(),
        _pathParamNames: [],
        router: fn
      };
      child._pathTokens = tokenize(path, child);
      this._routes.push(child);
      return fn;
    }
    const middleware = new Middleware(path, fn);
    this._middleware.push(middleware);
    return middleware;
  }
  all(path, handler, name) {
    const route = new Route(ALL_METHODS, path, handler, name);
    this._routes.push(route);
    return route;
  }
  get(path, handler, name) {
    const route = new Route(['GET'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  post(path, handler, name) {
    const route = new Route(['POST'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  put(path, handler, name) {
    const route = new Route(['PUT'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  patch(path, handler, name) {
    const route = new Route(['PATCH'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  delete(path, handler, name) {
    const route = new Route(['DELETE'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  head(path, handler, name) {
    const route = new Route(['HEAD'], path, handler, name);
    this._routes.push(route);
    return route;
  }
  _buildRouteTree() {
    const root = new Map();
    this._tree = root;

    // middleware always implicitly ends in a wildcard
    // child routers always explicitly end in wildcards
    // routes may explicitly end in a wildcard
    // static names have precedence over params
    // params have precedence over wildcards
    // middleware is executed in order of precedence
    // implicit 404 in middleware does not fail the routing
    // router.all ONLY affects routes of THAT router (and its children)
    // * router.all does NOT affect routes of sibling routers *

    // should child routers have precedence over routes?
    // ideally they should respect each others precedence
    // and be treated equally

    for (let middleware of this._middleware) {
      let node = root;
      for (let token of middleware._pathTokens) {
        if (!node.has(token)) {
          node.set(token, new Map());
        }
        node = node.get(token);
      }
      if (!node.has($_MIDDLEWARE)) {
        node.set($_MIDDLEWARE, []);
      }
      node.get($_MIDDLEWARE).push(middleware);
    }

    for (let route of this._routes) {
      let node = root;
      for (let token of route._pathTokens) {
        if (!node.has(token)) {
          node.set(token, new Map());
        }
        node = node.get(token);
      }
      if (!node.has($_ROUTES)) {
        node.set($_ROUTES, []);
      }
      node.get($_ROUTES).push(route);
      if (route.router) {
        route.router._buildRouteTree();
      }
    }
  }
  * _findRoutes(suffix, result, pathParts) {
    if (!result) {
      result = [];
    }
    if (!pathParts) {
      pathParts = [];
    }
    result = result.concat({router: this, path: pathParts, suffix: suffix});
    for (let route of findRoutes(this._tree, result, suffix, [])) {
      yield route;
    }
  }
}

function* findRoutes(node, result, suffix, pathParts) {
  let wildcardNode = node.get($_WILDCARD);
  let nodeMiddleware = [];
  if (wildcardNode && wildcardNode.has($_MIDDLEWARE)) {
    nodeMiddleware = wildcardNode.get($_MIDDLEWARE);
    result = result.concat(nodeMiddleware.map(function (mw) {
      return {middleware: mw, path: pathParts, suffix: suffix};
    }));
  }
  if (!suffix.length) {
    let terminalNode = node.get($_TERMINAL);
    let terminalRoutes = terminalNode && terminalNode.get($_ROUTES) || [];
    for (let endpoint of terminalRoutes) {
      yield result.concat({endpoint: endpoint, path: pathParts, suffix: suffix});
    }
  } else {
    let part = suffix[0];
    let head = pathParts.concat(part);
    let tail = suffix.slice(1);
    for (let childNode of [node.get(part), node.get($_PARAM)]) {
      if (childNode) {
        for (let route of findRoutes(childNode, result, tail, head)) {
          yield route;
        }
      }
    }
  }
  let wildcardRoutes = wildcardNode && wildcardNode.get($_ROUTES) || [];
  for (let endpoint of wildcardRoutes) {
    if (endpoint.router) {
      for (let route of endpoint.router._findRoutes(suffix, result, pathParts)) {
        yield route;
      }
    } else {
      yield result.concat({endpoint: endpoint, path: pathParts, suffix: suffix});
    }
  }
}

module.exports = function createRouter() {
  return new Router();
};
<|endoftext|>


<commit_before>import { push, go } from 'react-router-redux'
import { STORE } from './reducer'

export default function (params = {}) {
  if (params.back) {
    const amount = params.back === true ? -1 : -(Math.abs(params.back))
    return go(amount)
  }

  if (params.forward) {
    const amount = params.forward === true ? 1 : Math.abs(params.forward)
    return go(amount)
  }

  const state = STORE.getState()

  if (params.replace) {
    const newParams = { ...params }
    delete newParams.replace

    return STORE.dispatch(push(newParams))
  }

  const location = state.routing.locationBeforeTransitions
  const newParams = Object.assign({}, {
    hash: location.hash,
    pathname: location.pathname
  }, params)

  newParams.query = Object.assign({}, location.query, newParams.query)
  return STORE.dispatch(push(newParams))
}
<commit_msg>Back and Forward Goto props now fire through the store dispatcher<commit_after>import { push, go } from 'react-router-redux'
import { STORE } from './reducer'

export default function (params = {}) {
  if (params.back) {
    const amount = params.back === true ? -1 : -(Math.abs(params.back))
    return STORE.dispatch(go(amount))
  }

  if (params.forward) {
    const amount = params.forward === true ? 1 : Math.abs(params.forward)
    return STORE.dispatch(go(amount))
  }

  if (params.replace) {
    const newParams = { ...params }
    delete newParams.replace

    return STORE.dispatch(push(newParams))
  }

  const state = STORE.getState()
  const location = state.routing.locationBeforeTransitions

  const newParams = Object.assign({}, {
    hash: location.hash,
    pathname: location.pathname
  }, params)

  newParams.query = Object.assign({}, location.query, newParams.query)
  return STORE.dispatch(push(newParams))
}
<|endoftext|>


<commit_before>/**
 * Webpack dev-server block.
 *
 * @see https://github.com/webpack/webpack-dev-server
 */

module.exports = devServer

devServer.proxy = proxy
devServer.reactHot = reactHot

/**
 * @param {object} [options]    See https://webpack.github.io/docs/configuration.html#devserver
 * @param {bool}   [options.historyApiFallback]
 * @param {bool}   [options.hot]
 * @param {bool}   [options.inline]
 * @param {string|string[]} [entry]
 * @return {Function}
 */
function devServer (options = {}, entry = []) {
  if (options && (typeof options === 'string' || Array.isArray(options))) {
    entry = options
    options = {}
  }

  if (!Array.isArray(entry)) {
    entry = entry ? [ entry ] : []
  }

  const setter = context => prevConfig => {
    context.devServer = context.devServer || { entry: [], options: {} }
    context.devServer.entry = context.devServer.entry.concat(entry)
    context.devServer.options = Object.assign({}, context.devServer.options, options)

    return prevConfig
  }

  return Object.assign(setter, { post: postConfig })
}

function postConfig (context, util) {
  const entryPointsToAdd = context.devServer.entry

  return prevConfig => {
    return util.merge({
      devServer: Object.assign({
        hot: true,
        hotOnly: true,
        historyApiFallback: true,
        inline: true
      }, context.devServer.options),
      entry: addDevEntryToAll(prevConfig.entry || {}, entryPointsToAdd),
      plugins: [
        new context.webpack.HotModuleReplacementPlugin()
      ]
    })(prevConfig)
  }
}

function addDevEntryToAll (presentEntryPoints, devServerEntry) {
  const newEntryPoints = {}

  Object.keys(presentEntryPoints).forEach((chunkName) => {
    // It's fine to just set the `devServerEntry`, instead of concat()-ing the present ones.
    // Will be concat()-ed by webpack-merge (see `createConfig()`)
    newEntryPoints[ chunkName ] = devServerEntry
  })

  return newEntryPoints
}

/**
 * @param {object} proxyRoutes    { <path>: <target URL> }
 * @return {Function}
 * @see http://webpack.github.io/docs/webpack-dev-server.html#proxy
 */
function proxy (proxyRoutes) {
  return (context, util) => util.merge({
    devServer: {
      proxy: proxyRoutes
    }
  })
}

/**
 * For adding the react-hot-loader to the JS loaders. Only when using
 * react-hot-loader before version 3.
 * @param {object} [options]
 * @param {RegExp, Function, string}  [options.exclude]   Directories to exclude.
 * @return {Function}
 */
function reactHot (options = {}) {
  const exclude = options.exclude || /\/node_modules\//

  return (context, util) => util.addLoader({
    test: context.fileType('application/javascript'),
    exclude: Array.isArray(exclude) ? exclude : [ exclude ],
    use: [ 'react-hot' ]
  })
}
<commit_msg>Port @bkonkle's PR #129 to 1.0: Add NamedModulesPlugin to dev server<commit_after>/**
 * Webpack dev-server block.
 *
 * @see https://github.com/webpack/webpack-dev-server
 */

module.exports = devServer

devServer.proxy = proxy
devServer.reactHot = reactHot

/**
 * @param {object} [options]    See https://webpack.github.io/docs/configuration.html#devserver
 * @param {bool}   [options.historyApiFallback]
 * @param {bool}   [options.hot]
 * @param {bool}   [options.inline]
 * @param {string|string[]} [entry]
 * @return {Function}
 */
function devServer (options = {}, entry = []) {
  if (options && (typeof options === 'string' || Array.isArray(options))) {
    entry = options
    options = {}
  }

  if (!Array.isArray(entry)) {
    entry = entry ? [ entry ] : []
  }

  const setter = context => prevConfig => {
    context.devServer = context.devServer || { entry: [], options: {} }
    context.devServer.entry = context.devServer.entry.concat(entry)
    context.devServer.options = Object.assign({}, context.devServer.options, options)

    return prevConfig
  }

  return Object.assign(setter, { post: postConfig })
}

function postConfig (context, util) {
  const entryPointsToAdd = context.devServer.entry

  return prevConfig => {
    return util.merge({
      devServer: Object.assign({
        hot: true,
        hotOnly: true,
        historyApiFallback: true,
        inline: true
      }, context.devServer.options),
      entry: addDevEntryToAll(prevConfig.entry || {}, entryPointsToAdd),
      plugins: [
        new context.webpack.HotModuleReplacementPlugin(),
        new context.webpack.NamedModulesPlugin()
      ]
    })(prevConfig)
  }
}

function addDevEntryToAll (presentEntryPoints, devServerEntry) {
  const newEntryPoints = {}

  Object.keys(presentEntryPoints).forEach((chunkName) => {
    // It's fine to just set the `devServerEntry`, instead of concat()-ing the present ones.
    // Will be concat()-ed by webpack-merge (see `createConfig()`)
    newEntryPoints[ chunkName ] = devServerEntry
  })

  return newEntryPoints
}

/**
 * @param {object} proxyRoutes    { <path>: <target URL> }
 * @return {Function}
 * @see http://webpack.github.io/docs/webpack-dev-server.html#proxy
 */
function proxy (proxyRoutes) {
  return (context, util) => util.merge({
    devServer: {
      proxy: proxyRoutes
    }
  })
}

/**
 * For adding the react-hot-loader to the JS loaders. Only when using
 * react-hot-loader before version 3.
 * @param {object} [options]
 * @param {RegExp, Function, string}  [options.exclude]   Directories to exclude.
 * @return {Function}
 */
function reactHot (options = {}) {
  const exclude = options.exclude || /\/node_modules\//

  return (context, util) => util.addLoader({
    test: context.fileType('application/javascript'),
    exclude: Array.isArray(exclude) ? exclude : [ exclude ],
    use: [ 'react-hot' ]
  })
}
<|endoftext|>


<commit_before>import fs from 'fs'
import { execFile } from 'child_process';

import { promisify, promisifyAll } from 'bluebird'
import gm from 'gm'
import meta from 'musicmetadata'
import mime from 'mime-types'
import mmm from 'mmmagic'
import readChunk from 'read-chunk'
import fileType from 'file-type'
import _ from 'lodash'
import mv from 'mv';
import gifsicle from 'gifsicle';
import probe from 'probe-image-size';

import { getS3 } from '../support/s3';
import { load as configLoader } from '../../config/config'

const config = configLoader()
promisifyAll(fs)
const mvAsync = promisify(mv);

const mimeMagic = new mmm.Magic(mmm.MAGIC_MIME_TYPE)
const detectMime = promisify(mimeMagic.detectFile, { context: mimeMagic })

const magic = new mmm.Magic()
const detectFile = promisify(magic.detectFile, { context: magic })

async function mimeTypeDetect(fileName, filePath) {
  const file = `${filePath}/${fileName}`;
  // The file type is detected by checking the magic number of the buffer.
  // It only needs the first 4100 bytes.
  const buffer = await readChunk(filePath, 0, 4100)
  const info = fileType(buffer)

  if (info && info.mime && info.mime !== 'application/octet-stream') {
    return info.mime;
  }

  // legacy mmmagic based detection
  let mimeType = 'application/octet-stream';
  try {
    mimeType = await detectMime(filePath)

    if (mimeType === 'application/octet-stream') {
      const fileType = await detectFile(filePath)

      if (fileType.startsWith('Audio file with ID3')) {
        mimeType = 'audio/mpeg'
      }
    }
  } catch (e) {
    if (_.isEmpty(mimeType)) {
      throw e
    }
  }

  // otherwise, we'll use the fallback to content-type detected with a file extension provided by the user
  if (mimeType === 'application/octet-stream') {
    mimeType = mime.lookup(file) || mimeType
  }

  return mimeType
}

const execFileAsync = promisify(execFile);

export function addModel(dbAdapter) {
  /**
   * @constructor
   */
  const Attachment = function (params) {
    this.id = params.id
    this.file = params.file // FormData File object
    this.fileName = params.fileName // original file name, e.g. 'cute-little-kitten.jpg'
    this.fileSize = params.fileSize // file size in bytes
    this.mimeType = params.mimeType // used as a fallback, in case we can't detect proper one
    this.fileExtension = params.fileExtension // jpg|png|gif etc.
    this.mediaType = params.mediaType // image | audio | general

    this.noThumbnail = params.noThumbnail // if true, image thumbnail URL == original URL
    this.imageSizes = params.imageSizes || {} // pixel sizes of thumbnail(s) and original image, e.g. {t: {w: 200, h: 175}, o: {w: 600, h: 525}}

    this.artist = params.artist  // filled only for audio
    this.title = params.title   // filled only for audio

    this.userId = params.userId
    this.postId = params.postId

    if (parseInt(params.createdAt, 10)) {
      this.createdAt = params.createdAt;
    }

    if (parseInt(params.updatedAt, 10)) {
      this.updatedAt = params.updatedAt;
    }
  }

  Reflect.defineProperty(Attachment.prototype, 'imageSizes', {
    get: function () { return this.imageSizes_ },
    set: function (newValue) {
      if (_.isString(newValue)) {
        newValue = JSON.parse(newValue)
      }

      this.imageSizes_ = newValue
    }
  })

  Attachment.prototype.validate = function () {
    const valid = this.file
               && Object.keys(this.file).length > 0
               && this.file.path
               && this.file.path.length > 0
               && this.userId
               && this.userId.length > 0

    if (!valid) {
      throw new Error('Invalid');
    }
  }

  Attachment.prototype.create = async function () {
    this.createdAt = new Date().getTime()
    this.updatedAt = new Date().getTime()
    this.postId = this.postId || ''

    await this.validate()

    this.id = await dbAdapter.createAttachment({
      postId:    this.postId,
      createdAt: this.createdAt.toString(),
      updatedAt: this.updatedAt.toString()
    })

    this.fileName = this.file.name
    this.fileSize = this.file.size
    this.mimeType = this.file.type

    // Determine initial file extension
    // (it might be overridden later when we know MIME type from its contents)
    // TODO: extract to config
    const supportedExtensions = /\.(jpe?g|png|gif|mp3|m4a|ogg|wav|txt|pdf|docx?|pptx?|xlsx?)$/i

    if (this.fileName && this.fileName.match(supportedExtensions) !== null) {
      this.fileExtension = this.fileName.match(supportedExtensions)[1].toLowerCase()
    } else {
      this.fileExtension = ''
    }

    await this.handleMedia()

    // Save record to DB
    const params = {
      fileName:      this.fileName,
      fileSize:      this.fileSize,
      mimeType:      this.mimeType,
      mediaType:     this.mediaType,
      fileExtension: this.fileExtension,
      noThumbnail:   this.noThumbnail,
      imageSizes:    JSON.stringify(this.imageSizes),
      userId:        this.userId,
      postId:        this.postId,
      createdAt:     this.createdAt.toString(),
      updatedAt:     this.updatedAt.toString()
    }

    if (this.mediaType === 'audio') {
      params.artist = this.artist
      params.title = this.title
    }

    await dbAdapter.updateAttachment(this.id, params)

    return this
  }

  Reflect.defineProperty(Attachment.prototype, 'url', {
    get: function () {
      return config.attachments.url + config.attachments.path + this.getFilename()
    }
  })

  Reflect.defineProperty(Attachment.prototype, 'thumbnailUrl', {
    get: function () {
      if (this.noThumbnail === '1') {
        return this.url
      }
      return this.getResizedImageUrl('t')
    }
  })

  // Get user who created the attachment (via Promise, for serializer)
  Attachment.prototype.getCreatedBy = function () {
    return dbAdapter.getUserById(this.userId)
  }

  // Get public URL of attachment (via Promise, for serializer)
  Attachment.prototype.getUrl = function () {
    return config.attachments.url + config.attachments.path + this.getFilename()
  }

  // Get public URL of attachment's thumbnail (via Promise, for serializer)
  Attachment.prototype.getThumbnailUrl = function () {
    if (this.noThumbnail === '1') {
      return this.getUrl()
    }

    return this.getResizedImageUrl('t')
  }

  // Get public URL of resized image attachment
  Attachment.prototype.getResizedImageUrl = function (sizeId) {
    return config.attachments.url + config.attachments.imageSizes[sizeId].path + this.getFilename()
  }

  // Get local filesystem path for original file
  Attachment.prototype.getPath = function () {
    return config.attachments.storage.rootDir + config.attachments.path + this.getFilename()
  }

  // Get local filesystem path for resized image file
  Attachment.prototype.getResizedImagePath = function (sizeId) {
    return config.attachments.storage.rootDir + config.attachments.imageSizes[sizeId].path + this.getFilename()
  }

  // Get file name
  Attachment.prototype.getFilename = function () {
    if (this.fileExtension) {
      return `${this.id}.${this.fileExtension}`
    }

    return this.id
  }

  // Store the file and process its thumbnail, if necessary
  Attachment.prototype.handleMedia = async function () {
    const tmpAttachmentFile = this.file.path
    const tmpAttachmentFileName = this.file.name

    const supportedImageTypes = {
      'image/jpeg':    'jpg',
      'image/png':     'png',
      'image/gif':     'gif',
      'image/svg+xml': 'svg'
    }
    const supportedAudioTypes = {
      'audio/mpeg':  'mp3',
      'audio/x-m4a': 'm4a',
      'audio/mp4':   'm4a',
      'audio/ogg':   'ogg',
      'audio/x-wav': 'wav'
    }

    this.mimeType = await mimeTypeDetect(tmpAttachmentFileName, tmpAttachmentFile)

    if (supportedImageTypes[this.mimeType]) {
      // Set media properties for 'image' type
      this.mediaType = 'image';
      this.fileExtension = supportedImageTypes[this.mimeType];
      this.noThumbnail = '1';  // this may be overriden below
      await this.handleImage(tmpAttachmentFile);
    } else if (supportedAudioTypes[this.mimeType]) {
      // Set media properties for 'audio' type
      this.mediaType = 'audio'
      this.fileExtension = supportedAudioTypes[this.mimeType]
      this.noThumbnail = '1'

      // Analyze metadata to get Artist & Title
      const readStream = fs.createReadStream(tmpAttachmentFile)
      const asyncMeta = promisify(meta)
      const metadata = await asyncMeta(readStream)

      this.title = metadata.title

      if (_.isArray(metadata.artist)) {
        [this.artist] = metadata.artist;
      } else {
        this.artist = metadata.artist;
      }
    } else {
      // Set media properties for 'general' type
      this.mediaType = 'general'
      this.noThumbnail = '1'
    }

    // Store an original attachment
    if (config.attachments.storage.type === 's3') {
      await this.uploadToS3(tmpAttachmentFile, config.attachments.path)
      await fs.unlinkAsync(tmpAttachmentFile)
    } else {
      await mvAsync(tmpAttachmentFile, this.getPath(), {})
    }
  }

  const fitIntoBounds = (size, bounds) => {
    let width, height;
    if (size.width * bounds.height > size.height * bounds.width) {
      width  = bounds.width;  // eslint-disable-line prefer-destructuring
      height = Math.max(1, Math.round(size.height * bounds.width / size.width));
    } else {
      width  = Math.max(1, Math.round(size.width * bounds.height / size.height));
      height = bounds.height;  // eslint-disable-line prefer-destructuring
    }
    return { width, height };
  }

  /**
   * @param {string} originalFile
   */
  Attachment.prototype.handleImage = async function (originalFile) {
    const tmpResizedFile = (sizeId) => `${this.file.path}.resized.${sizeId}`;

    // Store original image size
    let originalSize = await getImageSize(originalFile);
    this.imageSizes.o = {
      w:   originalSize.width,
      h:   originalSize.height,
      url: await this.getUrl(),
    }

    if (this.mimeType === 'image/svg+xml') {
      return;
    }

    // Reserved for GM-style object
    let originalImage = null;

    // Fix EXIF orientation for original image, if JPEG
    if (this.mimeType === 'image/jpeg') {
      originalImage = promisifyAll(gm(originalFile));
      // orientation() returns a string. Possible values are:
      // unknown, Unknown, TopLeft, TopRight, BottomRight, BottomLeft, LeftTop, RightTop, RightBottom, LeftBottom
      // The first three options are fine, the rest should be fixed.
      const orientation = await originalImage.orientationAsync();
      if (!['unknown', 'Unknown', 'TopLeft'].includes(orientation)) {
        const img = originalImage
          .profile(`${__dirname}/../../lib/assets/sRGB.icm`)
          .autoOrient()
          .quality(95);
        await img.writeAsync(originalFile);
        originalImage = promisifyAll(gm(originalFile));
        originalSize = await getImageSize(originalFile);
        this.imageSizes.o.w = originalSize.width;
        this.imageSizes.o.h = originalSize.height;
      }
    }

    const thumbIds = [];
    for (const sizeId of Object.keys(config.attachments.imageSizes)) {
      const { bounds } = config.attachments.imageSizes[sizeId];
      if (originalSize.width <= bounds.width && originalSize.height <= bounds.height) {
        continue;
      }
      const size = fitIntoBounds(originalSize, bounds);
      this.imageSizes[sizeId] = {
        w:   size.width,
        h:   size.height,
        url: this.getResizedImageUrl(sizeId),
      }
      thumbIds.push(sizeId);
    }

    if (thumbIds.length === 0) {
      // No thumbnails
      return;
    }
    this.noThumbnail = '0';

    if (this.mimeType === 'image/gif') {
      // Resize gif using gifsicle
      await Promise.all(thumbIds.map(async (sizeId) => {
        const { w, h } = this.imageSizes[sizeId];
        await execFileAsync(gifsicle, [
          '--resize', `${w}x${h}`,
          '--resize-colors', '128',
          '--no-background',
          '-o', tmpResizedFile(sizeId),
          originalFile,
        ]);
      }));
    } else {
      // Iterate over image sizes old-fashioned (and very synchronous) way
      // because gm is acting up weirdly when writing files in parallel mode
      if (originalImage === null) {
        originalImage = promisifyAll(gm(originalFile));
      }
      for (const sizeId of thumbIds) {
        const { w, h } = this.imageSizes[sizeId];
        await originalImage  // eslint-disable-line no-await-in-loop
          .resizeExact(w, h)
          .profile(`${__dirname}/../../lib/assets/sRGB.icm`)
          .autoOrient()
          .quality(95)
          .writeAsync(tmpResizedFile(sizeId));
      }
    }

    // Save image (permanently)
    if (config.attachments.storage.type === 's3') {
      await Promise.all(thumbIds.map(async (sizeId) => {
        const { path } = config.attachments.imageSizes[sizeId];
        const file = tmpResizedFile(sizeId);
        await this.uploadToS3(file, path);
        await fs.unlinkAsync(file);
      }));
    } else {
      await Promise.all(thumbIds.map(async (sizeId) => {
        const file = tmpResizedFile(sizeId);
        await mvAsync(file, this.getResizedImagePath(sizeId), {})
      }));
    }
  }

  // Upload original attachment or its thumbnail to the S3 bucket
  Attachment.prototype.uploadToS3 = async function (sourceFile, destPath) {
    const s3 = getS3(config.attachments.storage);
    await s3.upload({
      ACL:                'public-read',
      Bucket:             config.attachments.storage.bucket,
      Key:                destPath + this.getFilename(),
      Body:               fs.createReadStream(sourceFile),
      ContentType:        this.mimeType,
      ContentDisposition: this.getContentDisposition()
    }).promise();
  };

  // Get cross-browser Content-Disposition header for attachment
  Attachment.prototype.getContentDisposition = function () {
    // Old browsers (IE8) need ASCII-only fallback filenames
    const fileNameAscii = this.fileName.replace(/[^\x00-\x7F]/g, '_');

    // Modern browsers support UTF-8 filenames
    const fileNameUtf8 = encodeURIComponent(this.fileName)

    // Inline version of 'attfnboth' method (http://greenbytes.de/tech/tc2231/#attfnboth)
    return `inline; filename="${fileNameAscii}"; filename*=utf-8''${fileNameUtf8}`
  }

  return Attachment
}

async function getImageSize(fileName) {
  const input = fs.createReadStream(fileName);
  try {
    const { width, height } = await probe(input);
    return { width, height };
  } finally {
    input.destroy();
  }
}
<commit_msg>Remove incorrect filename composition in Attachment model<commit_after>import fs from 'fs'
import { execFile } from 'child_process';

import { promisify, promisifyAll } from 'bluebird'
import gm from 'gm'
import meta from 'musicmetadata'
import mime from 'mime-types'
import mmm from 'mmmagic'
import readChunk from 'read-chunk'
import fileType from 'file-type'
import _ from 'lodash'
import mv from 'mv';
import gifsicle from 'gifsicle';
import probe from 'probe-image-size';

import { getS3 } from '../support/s3';
import { load as configLoader } from '../../config/config'

const config = configLoader()
promisifyAll(fs)
const mvAsync = promisify(mv);

const mimeMagic = new mmm.Magic(mmm.MAGIC_MIME_TYPE)
const detectMime = promisify(mimeMagic.detectFile, { context: mimeMagic })

const magic = new mmm.Magic()
const detectFile = promisify(magic.detectFile, { context: magic })

async function mimeTypeDetect(fileName, filePath) {
  // The file type is detected by checking the magic number of the buffer.
  // It only needs the first 4100 bytes.
  const buffer = await readChunk(filePath, 0, 4100)
  const info = fileType(buffer)

  if (info && info.mime && info.mime !== 'application/octet-stream') {
    return info.mime;
  }

  // legacy mmmagic based detection
  let mimeType = 'application/octet-stream';
  try {
    mimeType = await detectMime(filePath)

    if (mimeType === 'application/octet-stream') {
      const fileType = await detectFile(filePath)

      if (fileType.startsWith('Audio file with ID3')) {
        mimeType = 'audio/mpeg'
      }
    }
  } catch (e) {
    if (_.isEmpty(mimeType)) {
      throw e
    }
  }

  // otherwise, we'll use the fallback to content-type detected with a file extension provided by the user
  if (mimeType === 'application/octet-stream') {
    mimeType = mime.lookup(fileName) || mimeType
  }

  return mimeType
}

const execFileAsync = promisify(execFile);

export function addModel(dbAdapter) {
  /**
   * @constructor
   */
  const Attachment = function (params) {
    this.id = params.id
    this.file = params.file // FormData File object
    this.fileName = params.fileName // original file name, e.g. 'cute-little-kitten.jpg'
    this.fileSize = params.fileSize // file size in bytes
    this.mimeType = params.mimeType // used as a fallback, in case we can't detect proper one
    this.fileExtension = params.fileExtension // jpg|png|gif etc.
    this.mediaType = params.mediaType // image | audio | general

    this.noThumbnail = params.noThumbnail // if true, image thumbnail URL == original URL
    this.imageSizes = params.imageSizes || {} // pixel sizes of thumbnail(s) and original image, e.g. {t: {w: 200, h: 175}, o: {w: 600, h: 525}}

    this.artist = params.artist  // filled only for audio
    this.title = params.title   // filled only for audio

    this.userId = params.userId
    this.postId = params.postId

    if (parseInt(params.createdAt, 10)) {
      this.createdAt = params.createdAt;
    }

    if (parseInt(params.updatedAt, 10)) {
      this.updatedAt = params.updatedAt;
    }
  }

  Reflect.defineProperty(Attachment.prototype, 'imageSizes', {
    get: function () { return this.imageSizes_ },
    set: function (newValue) {
      if (_.isString(newValue)) {
        newValue = JSON.parse(newValue)
      }

      this.imageSizes_ = newValue
    }
  })

  Attachment.prototype.validate = function () {
    const valid = this.file
               && Object.keys(this.file).length > 0
               && this.file.path
               && this.file.path.length > 0
               && this.userId
               && this.userId.length > 0

    if (!valid) {
      throw new Error('Invalid');
    }
  }

  Attachment.prototype.create = async function () {
    this.createdAt = new Date().getTime()
    this.updatedAt = new Date().getTime()
    this.postId = this.postId || ''

    await this.validate()

    this.id = await dbAdapter.createAttachment({
      postId:    this.postId,
      createdAt: this.createdAt.toString(),
      updatedAt: this.updatedAt.toString()
    })

    this.fileName = this.file.name
    this.fileSize = this.file.size
    this.mimeType = this.file.type

    // Determine initial file extension
    // (it might be overridden later when we know MIME type from its contents)
    // TODO: extract to config
    const supportedExtensions = /\.(jpe?g|png|gif|mp3|m4a|ogg|wav|txt|pdf|docx?|pptx?|xlsx?)$/i

    if (this.fileName && this.fileName.match(supportedExtensions) !== null) {
      this.fileExtension = this.fileName.match(supportedExtensions)[1].toLowerCase()
    } else {
      this.fileExtension = ''
    }

    await this.handleMedia()

    // Save record to DB
    const params = {
      fileName:      this.fileName,
      fileSize:      this.fileSize,
      mimeType:      this.mimeType,
      mediaType:     this.mediaType,
      fileExtension: this.fileExtension,
      noThumbnail:   this.noThumbnail,
      imageSizes:    JSON.stringify(this.imageSizes),
      userId:        this.userId,
      postId:        this.postId,
      createdAt:     this.createdAt.toString(),
      updatedAt:     this.updatedAt.toString()
    }

    if (this.mediaType === 'audio') {
      params.artist = this.artist
      params.title = this.title
    }

    await dbAdapter.updateAttachment(this.id, params)

    return this
  }

  Reflect.defineProperty(Attachment.prototype, 'url', {
    get: function () {
      return config.attachments.url + config.attachments.path + this.getFilename()
    }
  })

  Reflect.defineProperty(Attachment.prototype, 'thumbnailUrl', {
    get: function () {
      if (this.noThumbnail === '1') {
        return this.url
      }
      return this.getResizedImageUrl('t')
    }
  })

  // Get user who created the attachment (via Promise, for serializer)
  Attachment.prototype.getCreatedBy = function () {
    return dbAdapter.getUserById(this.userId)
  }

  // Get public URL of attachment (via Promise, for serializer)
  Attachment.prototype.getUrl = function () {
    return config.attachments.url + config.attachments.path + this.getFilename()
  }

  // Get public URL of attachment's thumbnail (via Promise, for serializer)
  Attachment.prototype.getThumbnailUrl = function () {
    if (this.noThumbnail === '1') {
      return this.getUrl()
    }

    return this.getResizedImageUrl('t')
  }

  // Get public URL of resized image attachment
  Attachment.prototype.getResizedImageUrl = function (sizeId) {
    return config.attachments.url + config.attachments.imageSizes[sizeId].path + this.getFilename()
  }

  // Get local filesystem path for original file
  Attachment.prototype.getPath = function () {
    return config.attachments.storage.rootDir + config.attachments.path + this.getFilename()
  }

  // Get local filesystem path for resized image file
  Attachment.prototype.getResizedImagePath = function (sizeId) {
    return config.attachments.storage.rootDir + config.attachments.imageSizes[sizeId].path + this.getFilename()
  }

  // Get file name
  Attachment.prototype.getFilename = function () {
    if (this.fileExtension) {
      return `${this.id}.${this.fileExtension}`
    }

    return this.id
  }

  // Store the file and process its thumbnail, if necessary
  Attachment.prototype.handleMedia = async function () {
    const tmpAttachmentFile = this.file.path
    const tmpAttachmentFileName = this.file.name

    const supportedImageTypes = {
      'image/jpeg':    'jpg',
      'image/png':     'png',
      'image/gif':     'gif',
      'image/svg+xml': 'svg'
    }
    const supportedAudioTypes = {
      'audio/mpeg':  'mp3',
      'audio/x-m4a': 'm4a',
      'audio/mp4':   'm4a',
      'audio/ogg':   'ogg',
      'audio/x-wav': 'wav'
    }

    this.mimeType = await mimeTypeDetect(tmpAttachmentFileName, tmpAttachmentFile)

    if (supportedImageTypes[this.mimeType]) {
      // Set media properties for 'image' type
      this.mediaType = 'image';
      this.fileExtension = supportedImageTypes[this.mimeType];
      this.noThumbnail = '1';  // this may be overriden below
      await this.handleImage(tmpAttachmentFile);
    } else if (supportedAudioTypes[this.mimeType]) {
      // Set media properties for 'audio' type
      this.mediaType = 'audio'
      this.fileExtension = supportedAudioTypes[this.mimeType]
      this.noThumbnail = '1'

      // Analyze metadata to get Artist & Title
      const readStream = fs.createReadStream(tmpAttachmentFile)
      const asyncMeta = promisify(meta)
      const metadata = await asyncMeta(readStream)

      this.title = metadata.title

      if (_.isArray(metadata.artist)) {
        [this.artist] = metadata.artist;
      } else {
        this.artist = metadata.artist;
      }
    } else {
      // Set media properties for 'general' type
      this.mediaType = 'general'
      this.noThumbnail = '1'
    }

    // Store an original attachment
    if (config.attachments.storage.type === 's3') {
      await this.uploadToS3(tmpAttachmentFile, config.attachments.path)
      await fs.unlinkAsync(tmpAttachmentFile)
    } else {
      await mvAsync(tmpAttachmentFile, this.getPath(), {})
    }
  }

  const fitIntoBounds = (size, bounds) => {
    let width, height;
    if (size.width * bounds.height > size.height * bounds.width) {
      width  = bounds.width;  // eslint-disable-line prefer-destructuring
      height = Math.max(1, Math.round(size.height * bounds.width / size.width));
    } else {
      width  = Math.max(1, Math.round(size.width * bounds.height / size.height));
      height = bounds.height;  // eslint-disable-line prefer-destructuring
    }
    return { width, height };
  }

  /**
   * @param {string} originalFile
   */
  Attachment.prototype.handleImage = async function (originalFile) {
    const tmpResizedFile = (sizeId) => `${this.file.path}.resized.${sizeId}`;

    // Store original image size
    let originalSize = await getImageSize(originalFile);
    this.imageSizes.o = {
      w:   originalSize.width,
      h:   originalSize.height,
      url: await this.getUrl(),
    }

    if (this.mimeType === 'image/svg+xml') {
      return;
    }

    // Reserved for GM-style object
    let originalImage = null;

    // Fix EXIF orientation for original image, if JPEG
    if (this.mimeType === 'image/jpeg') {
      originalImage = promisifyAll(gm(originalFile));
      // orientation() returns a string. Possible values are:
      // unknown, Unknown, TopLeft, TopRight, BottomRight, BottomLeft, LeftTop, RightTop, RightBottom, LeftBottom
      // The first three options are fine, the rest should be fixed.
      const orientation = await originalImage.orientationAsync();
      if (!['unknown', 'Unknown', 'TopLeft'].includes(orientation)) {
        const img = originalImage
          .profile(`${__dirname}/../../lib/assets/sRGB.icm`)
          .autoOrient()
          .quality(95);
        await img.writeAsync(originalFile);
        originalImage = promisifyAll(gm(originalFile));
        originalSize = await getImageSize(originalFile);
        this.imageSizes.o.w = originalSize.width;
        this.imageSizes.o.h = originalSize.height;
      }
    }

    const thumbIds = [];
    for (const sizeId of Object.keys(config.attachments.imageSizes)) {
      const { bounds } = config.attachments.imageSizes[sizeId];
      if (originalSize.width <= bounds.width && originalSize.height <= bounds.height) {
        continue;
      }
      const size = fitIntoBounds(originalSize, bounds);
      this.imageSizes[sizeId] = {
        w:   size.width,
        h:   size.height,
        url: this.getResizedImageUrl(sizeId),
      }
      thumbIds.push(sizeId);
    }

    if (thumbIds.length === 0) {
      // No thumbnails
      return;
    }
    this.noThumbnail = '0';

    if (this.mimeType === 'image/gif') {
      // Resize gif using gifsicle
      await Promise.all(thumbIds.map(async (sizeId) => {
        const { w, h } = this.imageSizes[sizeId];
        await execFileAsync(gifsicle, [
          '--resize', `${w}x${h}`,
          '--resize-colors', '128',
          '--no-background',
          '-o', tmpResizedFile(sizeId),
          originalFile,
        ]);
      }));
    } else {
      // Iterate over image sizes old-fashioned (and very synchronous) way
      // because gm is acting up weirdly when writing files in parallel mode
      if (originalImage === null) {
        originalImage = promisifyAll(gm(originalFile));
      }
      for (const sizeId of thumbIds) {
        const { w, h } = this.imageSizes[sizeId];
        await originalImage  // eslint-disable-line no-await-in-loop
          .resizeExact(w, h)
          .profile(`${__dirname}/../../lib/assets/sRGB.icm`)
          .autoOrient()
          .quality(95)
          .writeAsync(tmpResizedFile(sizeId));
      }
    }

    // Save image (permanently)
    if (config.attachments.storage.type === 's3') {
      await Promise.all(thumbIds.map(async (sizeId) => {
        const { path } = config.attachments.imageSizes[sizeId];
        const file = tmpResizedFile(sizeId);
        await this.uploadToS3(file, path);
        await fs.unlinkAsync(file);
      }));
    } else {
      await Promise.all(thumbIds.map(async (sizeId) => {
        const file = tmpResizedFile(sizeId);
        await mvAsync(file, this.getResizedImagePath(sizeId), {})
      }));
    }
  }

  // Upload original attachment or its thumbnail to the S3 bucket
  Attachment.prototype.uploadToS3 = async function (sourceFile, destPath) {
    const s3 = getS3(config.attachments.storage);
    await s3.upload({
      ACL:                'public-read',
      Bucket:             config.attachments.storage.bucket,
      Key:                destPath + this.getFilename(),
      Body:               fs.createReadStream(sourceFile),
      ContentType:        this.mimeType,
      ContentDisposition: this.getContentDisposition()
    }).promise();
  };

  // Get cross-browser Content-Disposition header for attachment
  Attachment.prototype.getContentDisposition = function () {
    // Old browsers (IE8) need ASCII-only fallback filenames
    const fileNameAscii = this.fileName.replace(/[^\x00-\x7F]/g, '_');

    // Modern browsers support UTF-8 filenames
    const fileNameUtf8 = encodeURIComponent(this.fileName)

    // Inline version of 'attfnboth' method (http://greenbytes.de/tech/tc2231/#attfnboth)
    return `inline; filename="${fileNameAscii}"; filename*=utf-8''${fileNameUtf8}`
  }

  return Attachment
}

async function getImageSize(fileName) {
  const input = fs.createReadStream(fileName);
  try {
    const { width, height } = await probe(input);
    return { width, height };
  } finally {
    input.destroy();
  }
}
<|endoftext|>


<commit_before>// ES1 - 8: Unary + Operator
// To do: make all tests pass, leave the assert lines unchanged!
// Follow the hints of the failure messages!

describe('Unary "+" operator', () => {
  it('converts its operand to the Number type', () => {
    //// const converted = +'fourty two';
    const converted = +'42';
    assert.strictEqual(converted, Number(42));
  });

  it('WHEN converting a numeric string THEN it returns its value as a number', () => {
    //// const number = '0.12';
    const number = 0.12;
    assert.strictEqual(+'0.12', number);
  });
  it('even WHEN converting the string "-Infinity" THEN it returns the Number `Infinity`', () => {
    //// const converted = 'Infinity';
    const converted = '-Infinity';
    assert.strictEqual(+ converted, -Infinity);
  });
  it('WHEN converting `null` THEN it returns `+0`', () => {
    //// const converted = +Null;
    const converted = +null;
    assert.strictEqual(converted, +0);
  });
  it('WHEN converting `true` THEN it returns 1', () => {
    //// const converted = true;
    const converted = +true;
    assert.strictEqual(converted, 1);
  });
  it('WHEN converting an object with a method `valueOf` THEN its value is returned', () => {
    //// const obj = {valueof: () => 42};
    const obj = {valueOf: () => 42};
    assert.strictEqual(+ obj, 42);
  });
});
<commit_msg>Add a kata, which explains what the "unary operator" is.<commit_after>// ES1 - 8: Unary + Operator
// To do: make all tests pass, leave the assert lines unchanged!
// Follow the hints of the failure messages!

describe('The unary "+" operator', () => {
  it('is the "+" that preceeds an operand', () => {
    //// const x = -'23';
    const x = +'23';
    assert.strictEqual(x, +'23');
  });
  it('converts its operand to the Number type', () => {
    //// const converted = +'fourty two';
    const converted = +'42';
    assert.strictEqual(converted, Number(42));
  });

  it('WHEN converting a numeric string THEN it returns its value as a number', () => {
    //// const number = '0.12';
    const number = 0.12;
    assert.strictEqual(+'0.12', number);
  });
  it('even WHEN converting the string "-Infinity" THEN it returns the Number `Infinity`', () => {
    //// const converted = 'Infinity';
    const converted = '-Infinity';
    assert.strictEqual(+ converted, -Infinity);
  });
  it('WHEN converting `null` THEN it returns `+0`', () => {
    //// const converted = +Null;
    const converted = +null;
    assert.strictEqual(converted, +0);
  });
  it('WHEN converting `true` THEN it returns 1', () => {
    //// const converted = true;
    const converted = +true;
    assert.strictEqual(converted, 1);
  });
  it('WHEN converting an object with a method `valueOf` THEN its value is returned', () => {
    //// const obj = {valueof: () => 42};
    const obj = {valueOf: () => 42};
    assert.strictEqual(+ obj, 42);
  });
});
<|endoftext|>


<commit_before>L.TileLayer.Canvas.InteractiveCountryVectorLayer = L.TileLayer.Canvas.extend({
	options: {
		aggregateProperty: null,

		protoUrl: 'http://osm.personalwerk.de/example-vectiles/proto/vectiles.proto',
		url: 'http://osm.personalwerk.de/example-vectiles/{z}/{x}/{y}.pbf.base64',

		fillStyle: 'orange',
		strokeStyle: 'black',

		hoverFillStyle: 'red',
		hoverStrokeStyle: 'black',

		paint: null,
	},

	initialize: function(options) {
		L.TileLayer.Canvas.prototype.initialize.call(this, L.extend(options, {
			async: true,
			noWrap: true
		}));

		this._vectileData = {};
		this._displayCanvas = {};
		this._hitmapImageData = {};

		this._currentHover =  null;
		this._map = null;

		this.Vectile = dcodeIO.ProtoBuf.loadProtoFile(this.options.protoUrl).build("de.pwrk.vectiles.Vectile");
	},

	_id: function(z, x, y) {
		if(typeof(y) != "undefined")
			return z+'/'+x+'/'+y;
		else
			return z+'/'+x.x+'/'+x.y;
	},

	onAdd: function(map) {
		L.TileLayer.Canvas.prototype.onAdd.call(this, map);

		var layer = this;
		layer._map = map;

		var isDragging = false;
		map.on('dragstart', function() {
			isDragging = true;
		}).on('dragend', function() {
			isDragging = false;
		})

		if(!layer.options.aggregateProperty)
			console.warn('without an aggregateProperty hover/click will be disabled');
		else map.on('mousemove', function(e) {
			if(isDragging) return;

			var zoom = layer._map.getZoom();

			if (zoom > layer.options.maxZoom ||
				zoom < layer.options.minZoom) { return; }

			// determine tile under mouse
			// check hitmap for poly unter curser
			// check if the hoverstate has changed
			// if yes
			//   find tiles in viewport
			//   iterate those tiles
			//     check if those tiles contain the old or the new hovered poly
			//     if yes
			//       redraw them

			// determine tile unter mouse && check hitmap for poly unter curser
			var
				hoverFeature = layer._findHoverFeature(e.layerPoint),
				oldHoverValue = layer._currentHover ? layer._currentHover.tags[layer.options.aggregateProperty] : null,
				newHoverValue = hoverFeature ? hoverFeature.tags[layer.options.aggregateProperty] : null;

			layer._currentHover = hoverFeature;

			// check if the hoverstate has changed
			if(newHoverValue != oldHoverValue)
			{
				layer.fire('activeChanged', {'new': newHoverValue, 'old': oldHoverValue});

				// iterate tiles
				var kArr, x, y, key;
				for (key in layer._tiles) {
					kArr = key.split(':');
					x = parseInt(kArr[0], 10);
					y = parseInt(kArr[1], 10);

					var id = layer._id(zoom, x, y),
						containedValues = layer._vectileData[id] ? layer._vectileData[id].containedAggerateValues : null;

					// check if those tiles contain the old or the new hovered poly
					if(
						containedValues &&
						(containedValues.indexOf(oldHoverValue) !== -1 || containedValues.indexOf(newHoverValue) !== -1)
					) {
						// if yes, redraw them
						layer._drawDispalyTile(L.point(x, y), zoom);
					}
				}

			}
		}).on('click', function(e) {
			layer.fire('featureClick', {
				'feature': layer._currentHover,
				'aggregateValue': layer._currentHover ? layer._currentHover.tags[layer.options.aggregateProperty] : null
			});
		});
	},

	_findHoverFeature: function(layerPoint) {
		var
			layer = this,
			tilePoint = layerPoint
				.add(layer._map.getPixelOrigin())
				.divideBy(layer._getTileSize())
				.floor(),
			tilePixel = layerPoint
				.add(layer._map.getPixelOrigin())
				.subtract(
					tilePoint.multiplyBy(layer._getTileSize())
				),
			id = layer._id(layer._map._zoom, tilePoint),
			hitmapImageData = layer._hitmapImageData[id];

		if(!hitmapImageData)
			return null;

		var
			pxIdx = tilePixel.x * 4 + tilePixel.y * hitmapImageData.width * 4,
			polyIdx = layer._rgbintsToInt(hitmapImageData.data[pxIdx+0], hitmapImageData.data[pxIdx+1], hitmapImageData.data[pxIdx+2]),
			polyPlausibility = hitmapImageData.data[pxIdx+3];

		if(255 != polyPlausibility)
			return null;

		if(0 == polyIdx)
			return null;

		return this._vectileData[id].polys[polyIdx-1];
	},

	drawTile: function(canvas, tilePoint, zoom) {
		var
			layer = this,
			id = layer._id(zoom, tilePoint),
			url = L.Util.template(layer.options.url, {z: zoom, x: tilePoint.x, y: tilePoint.y});

		$.get(url, function(buf) {
			var
				vectile = layer.Vectile.decode(buf),
				containedAggerates = {};

			$.each(vectile.polys, function(polyIdx, poly) {
				var tags = {};

				for (var i = 0; i < poly.tags.length; i++) {
					var
						k = poly.tags[i].k,
						v = poly.tags[i].v;

					tags[k] = v;

					if(layer.options.aggregateProperty == k)
						containedAggerates[v] = true;
				}
				poly.tags = tags;

			});
			vectile.containedAggerateValues = Object.keys(containedAggerates);

			layer._vectileData[id] = vectile;

			var hitmapCanvas = document.createElement('canvas');
			hitmapCanvas.width = canvas.width;
			hitmapCanvas.height = canvas.height;

			layer._drawHitmapTile(hitmapCanvas, tilePoint, zoom);
			layer._hitmapImageData[id] = hitmapCanvas.getContext('2d').getImageData(0, 0, hitmapCanvas.width, hitmapCanvas.height);

			layer._displayCanvas[id] = canvas;
			layer._drawDispalyTile(tilePoint, zoom);

			layer.tileDrawn(canvas);
		});
	},

	_drawHitmapTile: function(hitmapCanvas, tilePoint, zoom) {
		var
			layer = this,
			id = layer._id(zoom, tilePoint),
			vectile = layer._vectileData[id],
			ctx = hitmapCanvas.getContext('2d');

		$.each(vectile.polys, function(polyIdx, poly) {
			ctx.beginPath();

			layer._drawPoly(ctx, poly);

			ctx.closePath();
			ctx.fillStyle = layer._intToRgb(polyIdx+1);
			ctx.fill();
		});
	},

	_drawDispalyTile: function(tilePoint, zoom) {
		var
			layer = this,
			id = layer._id(zoom, tilePoint),
			vectile = layer._vectileData[id],
			ctx = layer._displayCanvas[id].getContext('2d');

		$.each(vectile.polys, function(polyIdx, poly) {
			var
				value = poly.tags[layer.options.aggregateProperty],
				active = (layer.options.aggregateProperty && layer._currentHover && layer._currentHover.tags[layer.options.aggregateProperty] == value);

			ctx.beginPath();

			layer._drawPoly(ctx, poly);

			ctx.closePath();
			layer._canvasPaintPath(ctx, active, poly);
		});
	},

	_canvasPaintPath: function(ctx, active, poly) {
		var layer = this;

		if(layer.options.paint)
		{
			layer.options.paint(ctx, active, poly, layer);
		}
		else
		{
			ctx.fillStyle = active ? layer.options.hoverFillStyle : layer.options.fillStyle;
			ctx.strokeStyle = active ? layer.options.hoverStrokeStyle : layer.options.strokeStyle;

			ctx.fill();
			ctx.stroke();
		}
	},

	_drawPoly: function(ctx, poly) {
		var deltax = null, deltay = null;
		$.each(poly.delta_x, function(coordIdx) {
			var
				dx = poly.delta_x[coordIdx],
				dy = poly.delta_y[coordIdx];

			if(deltax == null && deltay == null)
				ctx.moveTo(deltax = dx, 256 - (deltay = dy));
			else
				ctx.lineTo(deltax += dx, 256 - (deltay += dy));
		});
	},

	_lpad: function(str, length, padchar)
	{
		padchar = padchar || '0';

		while(str.length < length)
			str = padchar + str;

		return str;
	},

	_intToRgb: function(i)
	{
		return '#'+this._lpad((new Number(i)).toString(16), 6);
	},

	_rgbToInt: function(rgb)
	{
		return parseInt(rgb.slice(1), 16);
	},

	_rgbintsToInt: function(r, g, b)
	{
		return (r << 16) + (g << 8) + b;
	}
});

L.tileLayer.canvas.interactiveCountryVectorLayer = function(options) {
	return new L.TileLayer.Canvas.InteractiveCountryVectorLayer(options);
}
<commit_msg>don't file feature-click events for none-features<commit_after>L.TileLayer.Canvas.InteractiveCountryVectorLayer = L.TileLayer.Canvas.extend({
	options: {
		aggregateProperty: null,

		protoUrl: 'http://osm.personalwerk.de/example-vectiles/proto/vectiles.proto',
		url: 'http://osm.personalwerk.de/example-vectiles/{z}/{x}/{y}.pbf.base64',

		fillStyle: 'orange',
		strokeStyle: 'black',

		hoverFillStyle: 'red',
		hoverStrokeStyle: 'black',

		paint: null,
	},

	initialize: function(options) {
		L.TileLayer.Canvas.prototype.initialize.call(this, L.extend(options, {
			async: true,
			noWrap: true
		}));

		this._vectileData = {};
		this._displayCanvas = {};
		this._hitmapImageData = {};

		this._currentHover =  null;
		this._map = null;

		this.Vectile = dcodeIO.ProtoBuf.loadProtoFile(this.options.protoUrl).build("de.pwrk.vectiles.Vectile");
	},

	_id: function(z, x, y) {
		if(typeof(y) != "undefined")
			return z+'/'+x+'/'+y;
		else
			return z+'/'+x.x+'/'+x.y;
	},

	onAdd: function(map) {
		L.TileLayer.Canvas.prototype.onAdd.call(this, map);

		var layer = this;
		layer._map = map;

		var isDragging = false;
		map.on('dragstart', function() {
			isDragging = true;
		}).on('dragend', function() {
			isDragging = false;
		})

		if(!layer.options.aggregateProperty)
			console.warn('without an aggregateProperty hover/click will be disabled');
		else map.on('mousemove', function(e) {
			if(isDragging) return;

			var zoom = layer._map.getZoom();

			if (zoom > layer.options.maxZoom ||
				zoom < layer.options.minZoom) { return; }

			// determine tile under mouse
			// check hitmap for poly unter curser
			// check if the hoverstate has changed
			// if yes
			//   find tiles in viewport
			//   iterate those tiles
			//     check if those tiles contain the old or the new hovered poly
			//     if yes
			//       redraw them

			// determine tile unter mouse && check hitmap for poly unter curser
			var
				hoverFeature = layer._findHoverFeature(e.layerPoint),
				oldHoverValue = layer._currentHover ? layer._currentHover.tags[layer.options.aggregateProperty] : null,
				newHoverValue = hoverFeature ? hoverFeature.tags[layer.options.aggregateProperty] : null;

			layer._currentHover = hoverFeature;

			// check if the hoverstate has changed
			if(newHoverValue != oldHoverValue)
			{
				layer.fire('activeChanged', {'new': newHoverValue, 'old': oldHoverValue});

				// iterate tiles
				var kArr, x, y, key;
				for (key in layer._tiles) {
					kArr = key.split(':');
					x = parseInt(kArr[0], 10);
					y = parseInt(kArr[1], 10);

					var id = layer._id(zoom, x, y),
						containedValues = layer._vectileData[id] ? layer._vectileData[id].containedAggerateValues : null;

					// check if those tiles contain the old or the new hovered poly
					if(
						containedValues &&
						(containedValues.indexOf(oldHoverValue) !== -1 || containedValues.indexOf(newHoverValue) !== -1)
					) {
						// if yes, redraw them
						layer._drawDispalyTile(L.point(x, y), zoom);
					}
				}

			}
		}).on('click', function(e) {
			if(!layer._currentHover) return;

			layer.fire('featureClick', {
				'feature': layer._currentHover,
				'aggregateValue': layer._currentHover ? layer._currentHover.tags[layer.options.aggregateProperty] : null
			});
		});
	},

	_findHoverFeature: function(layerPoint) {
		var
			layer = this,
			tilePoint = layerPoint
				.add(layer._map.getPixelOrigin())
				.divideBy(layer._getTileSize())
				.floor(),
			tilePixel = layerPoint
				.add(layer._map.getPixelOrigin())
				.subtract(
					tilePoint.multiplyBy(layer._getTileSize())
				),
			id = layer._id(layer._map._zoom, tilePoint),
			hitmapImageData = layer._hitmapImageData[id];

		if(!hitmapImageData)
			return null;

		var
			pxIdx = tilePixel.x * 4 + tilePixel.y * hitmapImageData.width * 4,
			polyIdx = layer._rgbintsToInt(hitmapImageData.data[pxIdx+0], hitmapImageData.data[pxIdx+1], hitmapImageData.data[pxIdx+2]),
			polyPlausibility = hitmapImageData.data[pxIdx+3];

		if(255 != polyPlausibility)
			return null;

		if(0 == polyIdx)
			return null;

		return this._vectileData[id].polys[polyIdx-1];
	},

	drawTile: function(canvas, tilePoint, zoom) {
		var
			layer = this,
			id = layer._id(zoom, tilePoint),
			url = L.Util.template(layer.options.url, {z: zoom, x: tilePoint.x, y: tilePoint.y});

		$.get(url, function(buf) {
			var
				vectile = layer.Vectile.decode(buf),
				containedAggerates = {};

			$.each(vectile.polys, function(polyIdx, poly) {
				var tags = {};

				for (var i = 0; i < poly.tags.length; i++) {
					var
						k = poly.tags[i].k,
						v = poly.tags[i].v;

					tags[k] = v;

					if(layer.options.aggregateProperty == k)
						containedAggerates[v] = true;
				}
				poly.tags = tags;

			});
			vectile.containedAggerateValues = Object.keys(containedAggerates);

			layer._vectileData[id] = vectile;

			var hitmapCanvas = document.createElement('canvas');
			hitmapCanvas.width = canvas.width;
			hitmapCanvas.height = canvas.height;

			layer._drawHitmapTile(hitmapCanvas, tilePoint, zoom);
			layer._hitmapImageData[id] = hitmapCanvas.getContext('2d').getImageData(0, 0, hitmapCanvas.width, hitmapCanvas.height);

			layer._displayCanvas[id] = canvas;
			layer._drawDispalyTile(tilePoint, zoom);

			layer.tileDrawn(canvas);
		});
	},

	_drawHitmapTile: function(hitmapCanvas, tilePoint, zoom) {
		var
			layer = this,
			id = layer._id(zoom, tilePoint),
			vectile = layer._vectileData[id],
			ctx = hitmapCanvas.getContext('2d');

		$.each(vectile.polys, function(polyIdx, poly) {
			ctx.beginPath();

			layer._drawPoly(ctx, poly);

			ctx.closePath();
			ctx.fillStyle = layer._intToRgb(polyIdx+1);
			ctx.fill();
		});
	},

	_drawDispalyTile: function(tilePoint, zoom) {
		var
			layer = this,
			id = layer._id(zoom, tilePoint),
			vectile = layer._vectileData[id],
			ctx = layer._displayCanvas[id].getContext('2d');

		$.each(vectile.polys, function(polyIdx, poly) {
			var
				value = poly.tags[layer.options.aggregateProperty],
				active = (layer.options.aggregateProperty && layer._currentHover && layer._currentHover.tags[layer.options.aggregateProperty] == value);

			ctx.beginPath();

			layer._drawPoly(ctx, poly);

			ctx.closePath();
			layer._canvasPaintPath(ctx, active, poly);
		});
	},

	_canvasPaintPath: function(ctx, active, poly) {
		var layer = this;

		if(layer.options.paint)
		{
			layer.options.paint(ctx, active, poly, layer);
		}
		else
		{
			ctx.fillStyle = active ? layer.options.hoverFillStyle : layer.options.fillStyle;
			ctx.strokeStyle = active ? layer.options.hoverStrokeStyle : layer.options.strokeStyle;

			ctx.fill();
			ctx.stroke();
		}
	},

	_drawPoly: function(ctx, poly) {
		var deltax = null, deltay = null;
		$.each(poly.delta_x, function(coordIdx) {
			var
				dx = poly.delta_x[coordIdx],
				dy = poly.delta_y[coordIdx];

			if(deltax == null && deltay == null)
				ctx.moveTo(deltax = dx, 256 - (deltay = dy));
			else
				ctx.lineTo(deltax += dx, 256 - (deltay += dy));
		});
	},

	_lpad: function(str, length, padchar)
	{
		padchar = padchar || '0';

		while(str.length < length)
			str = padchar + str;

		return str;
	},

	_intToRgb: function(i)
	{
		return '#'+this._lpad((new Number(i)).toString(16), 6);
	},

	_rgbToInt: function(rgb)
	{
		return parseInt(rgb.slice(1), 16);
	},

	_rgbintsToInt: function(r, g, b)
	{
		return (r << 16) + (g << 8) + b;
	}
});

L.tileLayer.canvas.interactiveCountryVectorLayer = function(options) {
	return new L.TileLayer.Canvas.InteractiveCountryVectorLayer(options);
}
<|endoftext|>


<commit_before>/**
 * @file MaterialTreeSelect component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import TreeSelect from '../TreeSelect';
import MaterialProvider from '../MaterialProvider';
import Theme from '../Theme';

import SelectMode from '../_statics/SelectMode';
import Position from '../_statics/Position';

import Util from '../_vendors/Util';
import ComponentUtil from '../_vendors/ComponentUtil';

class MaterialTreeSelect extends Component {

    static SelectMode = SelectMode;
    static Theme = Theme;
    static Position = Position;
    static TipPosition = Position;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            value: props.value
        };

    }

    /**
     * public
     */
    startRipple = (e, props) => {
        this.refs.treeSelect && this.refs.treeSelect.startRipple(e, props);
    };

    /**
     * public
     */
    endRipple = () => {
        this.refs.treeSelect && this.refs.treeSelect.endRipple();
    };

    /**
     * public
     */
    triggerRipple = (e, props) => {
        this.refs.treeSelect && this.refs.treeSelect.triggerRipple(e, props);
    };

    /**
     * public
     */
    resetPopupPosition = () => {
        this.refs.treeSelect && this.refs.treeSelect.resetPosition();
    };

    /**
     * public
     */
    openPopup = () => {
        this.refs.treeSelect && this.refs.treeSelect.openPopup();
    };

    /**
     * public
     */
    closePopup = () => {
        this.refs.treeSelect && this.refs.treeSelect.closePopup();
    };

    triggerChangeHandler = value => {
        this.setState({
            value
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(value);
        });
    };

    static getDerivedStateFromProps(props, state) {
        return {
            prevProps: props,
            value: ComponentUtil.getDerivedState(props, state, 'value')
        };
    }

    render() {

        const {
                className, style, theme, label, isLabelAnimate, disabled, required,
                ...restProps
            } = this.props,
            {value} = this.state,

            wrapperClassName = classNames('material-tree-select', {
                [className]: className
            });

        return (
            <MaterialProvider className={wrapperClassName}
                              style={style}
                              theme={theme}
                              label={label}
                              isLabelAnimate={isLabelAnimate}
                              hasValue={value && value.length > 0}
                              disabled={disabled}
                              required={required}>
                <TreeSelect {...restProps}
                            ref="treeSelect"
                            value={value}
                            disabled={disabled}
                            onChange={this.triggerChangeHandler}/>
            </MaterialProvider>
        );

    }
}

MaterialTreeSelect.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * The class name of the popup element.
     */
    popupClassName: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * Override the styles of the popup element.
     */
    popupStyle: PropTypes.object,

    /**
     * The theme.
     */
    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    position: PropTypes.oneOf(Util.enumerateValue(Position)),

    /**
     * The name of the MaterialTreeSelect.
     */
    name: PropTypes.string,

    /**
     * The label of the text field.
     */
    label: PropTypes.any,

    /**
     * The animate of the text field.
     */
    isLabelAnimate: PropTypes.bool,

    /**
     * The value of the MaterialTreeSelect.
     */
    value: PropTypes.any,

    /**
     * The placeholder of the MaterialTreeSelect.
     */
    placeholder: PropTypes.string,

    title: PropTypes.string,
    tip: PropTypes.string,
    tipPosition: PropTypes.oneOf(Util.enumerateValue(Position)),
    rightIconCls: PropTypes.string,

    /**
     * The options data.
     */
    /**
     * The options data.
     */
    data: PropTypes.oneOfType([PropTypes.shape({

        /**
         * The CSS class name of the tree node.
         */
        className: PropTypes.string,

        /**
         * Override the styles of the tree node.
         */
        style: PropTypes.object,

        /**
         * The theme of the tree button.
         */
        theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

        /**
         * The text value of the tree button.Type can be string or number.
         */
        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The tree node's display text. Type can be string, number or bool.
         */
        text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The desc value of the tree node. Type can be string or number.
         */
        desc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * If true,the tree node will be disabled.
         */
        disabled: PropTypes.bool,

        /**
         * If true,the button will be have loading effect.
         */
        isLoading: PropTypes.bool,

        /**
         * Use this property to display an icon. It will display on the left.
         */
        iconCls: PropTypes.string,

        /**
         * Use this property to display an icon. It will display on the right.
         */
        rightIconCls: PropTypes.string,

        /**
         * The message of tip.
         */
        title: PropTypes.string,

        children: PropTypes.array,

        /**
         * You can create a complicated renderer callback instead of value and desc prop.
         */
        itemRenderer: PropTypes.func,

        /**
         * Callback function fired when a tree node touch-tapped.
         */
        onClick: PropTypes.func

    }), PropTypes.array]),

    /**
     * The invalid message of dropDownSelect.
     */
    invalidMsg: PropTypes.string,

    /**
     * If true,the dropDownSelect will be disabled.
     */
    disabled: PropTypes.bool,

    required: PropTypes.bool,

    /**
     * The select mode of listItem.Can be normal,checkbox.
     */
    selectMode: PropTypes.oneOf(Util.enumerateValue(SelectMode)),

    /**
     * The value field name in data. (default: "value")
     */
    valueField: PropTypes.string,

    /**
     * The display field name in data. (default: "text")
     */
    displayField: PropTypes.string,

    /**
     * The description field name in data. (default: "desc")
     */
    descriptionField: PropTypes.string,

    /**
     * The message of the MaterialTreeSelect.
     */
    infoMsg: PropTypes.string,

    /**
     * Use this function to format the options's text.
     */
    textFormat: PropTypes.func,

    /**
     * If true,the drop-down box automatically closed after selection.
     */
    autoClose: PropTypes.bool,

    useFilter: PropTypes.bool,
    filterIconCls: PropTypes.string,
    noMatchedMsg: PropTypes.string,
    shouldPreventContainerScroll: PropTypes.bool,
    isSelectRecursive: PropTypes.bool,
    allowCollapse: PropTypes.bool,
    collapsed: PropTypes.bool,
    collapsedIconCls: PropTypes.string,
    expandedIconCls: PropTypes.string,
    radioUncheckedIconCls: PropTypes.string,
    radioCheckedIconCls: PropTypes.string,
    checkboxUncheckedIconCls: PropTypes.string,
    checkboxCheckedIconCls: PropTypes.string,
    checkboxIndeterminateIconCls: PropTypes.string,

    popupChildren: PropTypes.any,

    resetPopPositionWait: PropTypes.number,

    renderer: PropTypes.func,

    triggerRenderer: PropTypes.func,

    /**
     * Callback function fired when the button is touch-tapped.
     */
    onItemClick: PropTypes.func,

    /**
     * Callback function fired when the popup is closed.
     */
    onClosePopup: PropTypes.func,

    /**
     * Callback function fired when a menu item is selected.
     */
    onChange: PropTypes.func,

    onWheel: PropTypes.func,
    onFocus: PropTypes.func,
    onBlur: PropTypes.func,
    onMouseOver: PropTypes.func,
    onMouseOut: PropTypes.func

};

MaterialTreeSelect.defaultProps = {

    theme: Theme.DEFAULT,
    popupTheme: Theme.DEFAULT,

    isLabelAnimate: true,
    placeholder: 'Please select ...',
    rightIconCls: 'fas fa-angle-down',
    data: [],
    disabled: false,
    selectMode: SelectMode.SINGLE_SELECT,
    tipPosition: Position.BOTTOM,

    valueField: 'value',
    displayField: 'text',
    descriptionField: 'desc',

    autoClose: true,
    useFilter: false,
    filterIconCls: 'fas fa-search',

    shouldPreventContainerScroll: true,
    isSelectRecursive: false,
    allowCollapse: true,
    collapsed: false,

    required: false,

    resetPopPositionWait: 250

};

export default MaterialTreeSelect;
<commit_msg>update MaterialTreeSelect<commit_after>/**
 * @file MaterialTreeSelect component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';

import TreeSelect from '../TreeSelect';
import MaterialProvider from '../MaterialProvider';
import Theme from '../Theme';

import SelectMode from '../_statics/SelectMode';
import Position from '../_statics/Position';

import Util from '../_vendors/Util';
import ComponentUtil from '../_vendors/ComponentUtil';

class MaterialTreeSelect extends Component {

    static SelectMode = SelectMode;
    static Theme = Theme;
    static Position = Position;
    static TipPosition = Position;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            value: props.value
        };

    }

    /**
     * public
     */
    startRipple = (e, props) => {
        this.refs.treeSelect && this.refs.treeSelect.startRipple(e, props);
    };

    /**
     * public
     */
    endRipple = () => {
        this.refs.treeSelect && this.refs.treeSelect.endRipple();
    };

    /**
     * public
     */
    triggerRipple = (e, props) => {
        this.refs.treeSelect && this.refs.treeSelect.triggerRipple(e, props);
    };

    /**
     * public
     */
    resetPopupPosition = () => {
        this.refs.treeSelect && this.refs.treeSelect.resetPosition();
    };

    /**
     * public
     */
    openPopup = () => {
        this.refs.treeSelect && this.refs.treeSelect.openPopup();
    };

    /**
     * public
     */
    closePopup = () => {
        this.refs.treeSelect && this.refs.treeSelect.closePopup();
    };

    triggerChangeHandler = value => {
        this.setState({
            value
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(value);
        });
    };

    static getDerivedStateFromProps(props, state) {
        return {
            prevProps: props,
            value: ComponentUtil.getDerivedState(props, state, 'value')
        };
    }

    render() {

        const {
                className, style, theme, label, isLabelAnimate, disabled, required,
                ...restProps
            } = this.props,
            {value} = this.state,

            wrapperClassName = classNames('material-tree-select', {
                [className]: className
            });

        return (
            <MaterialProvider className={wrapperClassName}
                              style={style}
                              theme={theme}
                              label={label}
                              isLabelAnimate={isLabelAnimate}
                              hasValue={value && value.length > 0}
                              disabled={disabled}
                              required={required}>
                <TreeSelect {...restProps}
                            ref="treeSelect"
                            value={value}
                            disabled={disabled}
                            onChange={this.triggerChangeHandler}/>
            </MaterialProvider>
        );

    }
}

MaterialTreeSelect.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * The class name of the popup element.
     */
    popupClassName: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * Override the styles of the popup element.
     */
    popupStyle: PropTypes.object,

    /**
     * The theme.
     */
    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    position: PropTypes.oneOf(Util.enumerateValue(Position)),

    /**
     * The name of the MaterialTreeSelect.
     */
    name: PropTypes.string,

    /**
     * The label of the text field.
     */
    label: PropTypes.any,

    /**
     * The animate of the text field.
     */
    isLabelAnimate: PropTypes.bool,

    /**
     * The value of the MaterialTreeSelect.
     */
    value: PropTypes.any,

    /**
     * The placeholder of the MaterialTreeSelect.
     */
    placeholder: PropTypes.string,

    title: PropTypes.string,
    tip: PropTypes.string,
    tipPosition: PropTypes.oneOf(Util.enumerateValue(Position)),
    rightIconCls: PropTypes.string,

    /**
     * The options data.
     */
    /**
     * The options data.
     */
    data: PropTypes.oneOfType([PropTypes.shape({

        /**
         * The CSS class name of the tree node.
         */
        className: PropTypes.string,

        /**
         * Override the styles of the tree node.
         */
        style: PropTypes.object,

        /**
         * The theme of the tree button.
         */
        theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

        /**
         * The text value of the tree button.Type can be string or number.
         */
        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The tree node's display text. Type can be string, number or bool.
         */
        text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The desc value of the tree node. Type can be string or number.
         */
        desc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * If true,the tree node will be disabled.
         */
        disabled: PropTypes.bool,

        /**
         * If true,the button will be have loading effect.
         */
        isLoading: PropTypes.bool,

        /**
         * Use this property to display an icon. It will display on the left.
         */
        iconCls: PropTypes.string,

        /**
         * Use this property to display an icon. It will display on the right.
         */
        rightIconCls: PropTypes.string,

        /**
         * The message of tip.
         */
        title: PropTypes.string,

        children: PropTypes.array,

        /**
         * You can create a complicated renderer callback instead of value and desc prop.
         */
        itemRenderer: PropTypes.func,

        /**
         * Callback function fired when a tree node touch-tapped.
         */
        onClick: PropTypes.func

    }), PropTypes.array]),

    /**
     * The invalid message of dropDownSelect.
     */
    invalidMsg: PropTypes.string,

    /**
     * If true,the dropDownSelect will be disabled.
     */
    disabled: PropTypes.bool,

    required: PropTypes.bool,

    /**
     * The select mode of listItem.Can be normal,checkbox.
     */
    selectMode: PropTypes.oneOf(Util.enumerateValue(SelectMode)),

    /**
     * The value field name in data. (default: "value")
     */
    valueField: PropTypes.string,

    /**
     * The display field name in data. (default: "text")
     */
    displayField: PropTypes.string,

    /**
     * The description field name in data. (default: "desc")
     */
    descriptionField: PropTypes.string,

    /**
     * The message of the MaterialTreeSelect.
     */
    infoMsg: PropTypes.string,

    /**
     * Use this function to format the options's text.
     */
    textFormat: PropTypes.func,

    /**
     * If true,the drop-down box automatically closed after selection.
     */
    autoClose: PropTypes.bool,

    useFilter: PropTypes.bool,
    filterIconCls: PropTypes.string,
    noMatchedMsg: PropTypes.string,
    isSelectRecursive: PropTypes.bool,
    allowCollapse: PropTypes.bool,
    collapsed: PropTypes.bool,
    collapsedIconCls: PropTypes.string,
    expandedIconCls: PropTypes.string,
    radioUncheckedIconCls: PropTypes.string,
    radioCheckedIconCls: PropTypes.string,
    checkboxUncheckedIconCls: PropTypes.string,
    checkboxCheckedIconCls: PropTypes.string,
    checkboxIndeterminateIconCls: PropTypes.string,

    popupChildren: PropTypes.any,

    resetPopPositionWait: PropTypes.number,

    renderer: PropTypes.func,

    triggerRenderer: PropTypes.func,

    /**
     * Callback function fired when the button is touch-tapped.
     */
    onItemClick: PropTypes.func,

    /**
     * Callback function fired when the popup is closed.
     */
    onClosePopup: PropTypes.func,

    /**
     * Callback function fired when a menu item is selected.
     */
    onChange: PropTypes.func,

    onWheel: PropTypes.func,
    onFocus: PropTypes.func,
    onBlur: PropTypes.func,
    onMouseOver: PropTypes.func,
    onMouseOut: PropTypes.func

};

MaterialTreeSelect.defaultProps = {

    theme: Theme.DEFAULT,
    popupTheme: Theme.DEFAULT,

    isLabelAnimate: true,
    placeholder: 'Please select ...',
    rightIconCls: 'fas fa-angle-down',
    data: [],
    disabled: false,
    selectMode: SelectMode.SINGLE_SELECT,
    tipPosition: Position.BOTTOM,

    valueField: 'value',
    displayField: 'text',
    descriptionField: 'desc',

    autoClose: true,
    useFilter: false,
    filterIconCls: 'fas fa-search',

    isSelectRecursive: false,
    allowCollapse: true,
    collapsed: false,

    required: false,

    resetPopPositionWait: 250

};

export default MaterialTreeSelect;
<|endoftext|>


<commit_before>import Ajv from 'ajv';
import { cloneDeep } from 'lodash';

import { addModel as commentModelMaker } from './comment';

const commentModel = commentModelMaker(null);

const schema = {
  '$schema': 'http://json-schema.org/schema#',
  id:        'https://freefeed.net/schemas/user-preferences-schema',

  type:       'object',
  properties: {
    hideCommentsOfTypes: {
      title:   `Do not show comments with these hideType's`,
      default: [],

      type:        'array',
      uniqueItems: true,
      items:       {
        type: 'integer',
        enum: [
          commentModel.DELETED,
          commentModel.HIDDEN_BANNED,
          commentModel.HIDDEN_ARCHIVED,
        ],
      },
    },
    sendNotificationsDigest: {
      title:   'Send notifications digest email for current user',
      default: true,
      type:    'boolean',
    },
  },
  additionalProperties: false,
};

const ajv = new Ajv({ allErrors: true, useDefaults: true });
const check = ajv.compile(schema);

/**
 * Validates and completes absent fields by default values.
 * Throws exception if validation failed (or returns default
 * value if 'safe' argument is true).
 *
 * @param {object}  data
 * @param {boolean} safe - return default value instead of exception throwing
 * @return {object}
 */
export function valiate(data = {}, safe = false) {
  data = cloneDeep(data);
  const valid = check(data);
  if (valid) {
    return data;
  }
  if (safe) {
    // Return all defaults
    return valiate();
  }
  throw new Error(ajv.errorsText(check.errors));
}
<commit_msg>Remove id from user preferences JSON schema<commit_after>import Ajv from 'ajv';
import { cloneDeep } from 'lodash';

import { addModel as commentModelMaker } from './comment';

const commentModel = commentModelMaker(null);

const schema = {
  '$schema': 'http://json-schema.org/schema#',

  type:       'object',
  properties: {
    hideCommentsOfTypes: {
      title:   `Do not show comments with these hideType's`,
      default: [],

      type:        'array',
      uniqueItems: true,
      items:       {
        type: 'integer',
        enum: [
          commentModel.DELETED,
          commentModel.HIDDEN_BANNED,
          commentModel.HIDDEN_ARCHIVED,
        ],
      },
    },
    sendNotificationsDigest: {
      title:   'Send notifications digest email for current user',
      default: true,
      type:    'boolean',
    },
  },
  additionalProperties: false,
};

const ajv = new Ajv({ allErrors: true, useDefaults: true });
const check = ajv.compile(schema);

/**
 * Validates and completes absent fields by default values.
 * Throws exception if validation failed (or returns default
 * value if 'safe' argument is true).
 *
 * @param {object}  data
 * @param {boolean} safe - return default value instead of exception throwing
 * @return {object}
 */
export function valiate(data = {}, safe = false) {
  data = cloneDeep(data);
  const valid = check(data);
  if (valid) {
    return data;
  }
  if (safe) {
    // Return all defaults
    return valiate();
  }
  throw new Error(ajv.errorsText(check.errors));
}
<|endoftext|>