<commit_before>/*eslint-env node */
'use strict';

var
  http = require('http'),
  httpProxy = require('http-proxy'),
  validUrl = require('valid-url'),
  parseUrl = require('url').parse,
  path = require('path'),
  _ = require('lodash'),
  bunyan = require('bunyan'),
  cluster = require('cluster');

function ReverseProxy(opts){
  if(!(this instanceof ReverseProxy)){
    return new ReverseProxy(opts);
  }

  this.opts = opts = opts || {};

  var log;
  if(opts.bunyan !== false){
    log = this.log = bunyan.createLogger(opts.bunyan || {
      name: 'redbird'
    });
  }

  var _this = this;

  if(opts.cluster && cluster.isMaster){
    for(var i = 0; i < opts.cluster; i++){
      cluster.fork();
    }

    cluster.on('exit', function(worker, code, signal) {
      // Fork if a worker dies.
      log && log.error({
        code: code,
        signal: signal},
        'worker died un-expectedly... restarting it.');
      cluster.fork();
    });
  }else{
    opts.port = opts.port || 8080;

    //
    // Routing table.
    //
    this.routing = {};

    //
    // Create a proxy server with custom application logic
    //
    var proxy = this.proxy = httpProxy.createProxyServer({
      xfwd: (opts.xfwd != false) ? true : false,
      prependPath: false,
      secure: (opts.secure != false) ? true : false
    });

    proxy.on('proxyReq', function(p, req){
      if(req.host != null){
        p.setHeader('host', req.host);
      }
    });

    //
    // Support NTLM auth
    //
    if(opts.ntlm){
      proxy.on('proxyRes', function (proxyRes) {
        var key = 'www-authenticate';
        proxyRes.headers[key] = proxyRes.headers[key] && proxyRes.headers[key].split(',');
      });
    }

    //
    // Standard HTTP Proxy Server.
    //
    var server = this.server = http.createServer(function(req, res){
      var src = getSource(req);
      var target = _this._getTarget(src, req);
      if(target){
        if(shouldRedirectToHttps(certs, src, target)){
          redirectToHttps(req, res, target, opts.ssl, log);
        }else{
          proxy.web(req, res, {target: target});
        }
      }else{
        notFound(res);
      }
    });

    //
    // Listen to the `upgrade` event and proxy the
    // WebSocket requests as well.
    //
    server.on('upgrade', websocketsUpgrade);

    server.on('error', function(err){
      log && log.error(err, 'Server Error');
    });

    //
    // Optionally create an https proxy server.
    //
    if(opts.ssl){
      var https = require('https');
      var certs = this.certs = {};

      var ssl = {
        SNICallback: function(hostname, cb){
          if(cb){
            cb(null, certs[hostname]);
          }else{
            return certs[hostname];
          }
        },
        //
        // Default certs for clients that do not support SNI.
        //
        key: getCertData(opts.ssl.key),
        cert: getCertData(opts.ssl.cert)
      };

      if(opts.ssl.ca){
        ssl.ca = getCertData(opts.ssl.ca, true);
      }

      if(opts.ssl.opts){
        ssl = _.defaults(ssl, opts.ssl.opts);
      }

      var httpsServer = this.httpsServer = https.createServer(ssl, function(req, res){

        var src = getSource(req);

        var target = _this._getTarget(src, req);
        if(target){
          proxy.web(req, res, {target: target});
        }else{
          notFound(res);
        }
      });

      httpsServer.on('upgrade', websocketsUpgrade);

      httpsServer.on('error', function(err){
        log && log.error(err, 'HTTPS Server Error');
      });

      httpsServer.on('clientError', function(err){
        log && log.error(err, 'HTTPS Client  Error');
      });

      log && log.info('Listening to HTTPS requests on port %s', opts.ssl.port);
      httpsServer.listen(opts.ssl.port);
    }

    proxy.on('error', handleProxyError);

    server.listen(opts.port);

    log && log.info(opts.port, 'Started a Redbird reverse proxy server');
  }

  function shouldRedirectToHttps(certs, src, target){
    return certs && src in certs && target.sslRedirect;
  }

  function websocketsUpgrade(req, socket, head){
    var src = getSource(req);
    var target = _this._getTarget(src, req);
    log && log.info({headers: req.headers, target: target}, 'upgrade to websockets');
    if(target){
      proxy.ws(req, socket, head, {target: target});
    }else{
      notFound(socket);
    }
  }

  function handleProxyError(err, req, res){
    //
    // Send a 500 http status if headers have been sent
    //
    if(!res.headersSent){
      res.writeHead && res.writeHead(500);
    }

    //
    // Do not log this common error
    //
    if(err.message !== 'socket hang up'){
      log && log.error(err, 'Proxy Error');
    }

    //
    // TODO: if err.code=ECONNREFUSED and there are more servers
    // for this route, try another one.
    //
  }
}

/**
  Register a new route.

  @src {String|URL} A string or a url parsed by node url module.
  Note that port is ignored, since the proxy just listens to one port.

  @target {String|URL} A string or a url parsed by node url module.
  @opts {Object} Route options.
*/
ReverseProxy.prototype.register = function(src, target, opts){
  if(this.opts.cluster && cluster.isMaster) return this;

  if(!src || !target){
    throw Error('Cannot register a new route with unspecified src or target');
  }

  var routing = this.routing;

  src = prepareUrl(src);
  target = prepareUrl(target);

  target.sslRedirect = true;
  target.useTargetHostHeader = false;

  if(opts){
    if(opts.ssl){
      if(!this.httpsServer){
        throw Error('Cannot register https routes without defining a ssl port');
      }

      target.sslRedirect = opts.ssl.redirect === false ? false : true;

      if(!this.certs[src.hostname]){
        if(opts.ssl.key || opts.ssl.cert || opts.ssl.ca){
          var cert = createCredentialContext(opts.ssl.key, opts.ssl.cert, opts.ssl.ca);
          this.certs[src.hostname] = cert;
        }else{
          // Trigger the use of the default certificates.
          this.certs[src.hostname] = void 0;
        }
      }
    }
    target.useTargetHostHeader = opts.useTargetHostHeader === true ? true : false;
  }

  var host = routing[src.hostname] = routing[src.hostname] || [];
  var pathname = src.pathname || '/';
  var route = _.find(host, {path: pathname});

  if(!route){
    route = {path: pathname, rr: 0, urls: []};
    host.push(route);

    //
    // Sort routes
    //
    routing[src.hostname] = _.sortBy(host, function(_route){
      return -_route.path.length;
    });
  }

  route.urls.push(target);

  this.log && this.log.info({from: src, to: target}, 'Registered a new route');
  return this;
};

ReverseProxy.prototype.unregister = function(src, target){
  if(this.opts.cluster && cluster.isMaster) return this;

  if(!src){
    return this;
  }

  src = prepareUrl(src);
  var routes = this.routing[src.hostname] || [];
  var pathname = src.pathname || '/';
  var i;

  for(i = 0; i < routes.length; i++){
    if(routes[i].path === pathname){
      break;
    }
  }

  if(i < routes.length){
    var route = routes[i];

    if(target){
      target = prepareUrl(target);
      _.remove(route.urls, function(url){
        return url.href === target.href;
      });
    }else{
      route.urls = [];
    }

    if(route.urls.length === 0){
      routes.splice(i, 1);
    }

    this.log && this.log.info({from: src, to: target}, 'Unregistered a route');
  }
  return this;
};

ReverseProxy.prototype.resolve = function(host, url){
  // Given a src resolve it to a target route if any available.
  if(!host){
    return;
  }

  url = url || '/';

  var routes = this.routing[host];
  var i = 0;

  if(routes){
    var len = routes.length;

    //
    // Find path that matches the start of req.url
    //
    for(i = 0; i < len; i++){
      var route = routes[i];

      if(route.path === '/' || startsWith(url, route.path)){
        return route;
      }
    }
  }
};

ReverseProxy.prototype._getTarget = function(src, req){
  var url = req.url;
  var route = this.resolve(src, url);

  if(!route){
    this.log && this.log.warn({src: src, url: req.url}, 'no valid route found for given source');
    return;
  }

  var pathname = route.path;
  if(pathname.length > 1){
    //
    // remove prefix from src
    //
    req._url = url; // save original url
    req.url = url.substr(pathname.length) || '/';
  }

  //
  // Perform Round Robin on the available targets
  // TODO: if target errors with EHOSTUNREACH we should skip this
  // target and try with another.
  //
  var urls = route.urls;
  var j = route.rr;
  route.rr = (j + 1) % urls.length; // get and update Round robin index.
  var target = route.urls[j];

  //
  // Fix request url if targetname specified.
  //
  if(target.pathname){
    req.url = path.join(target.pathname, req.url);
  }

  //
  // Host headers are passed through from the source by default
  // Often we want to use the host header of the target instead
  //
  if(target.useTargetHostHeader === true){
    req.host = target.host;
  }

  this.log && this.log.info('Proxying %s to %s', src + url, path.join(target.host, req.url));

  return target;
};

ReverseProxy.prototype.close = function(){
  try{
    this.server.close();
    this.httpsServer && this.httpsServer.close();
  }catch(err){
    // Ignore for now...
  }
};

//
// Helpers
//
function getSource(req){
  if(req.headers.host){
    return req.headers.host.split(':')[0];
  }
}

/**
  Routing table structure. An object with hostname as key, and an array as value.
  The array has one element per path associated to the given hostname.
  Every path has a RoundRobin value (rr) and urls array, with all the urls available
  for this target route.

  {
    hostA :
      [
        {
          path: '/',
          rr: 3,
          urls: []
        }
      ]
  }
*/

function notFound(res){
  res.statusCode = 404;
  res.write('Not Found');
  res.end();
}

//
// Redirect to the HTTPS proxy
//
function redirectToHttps(req, res, target, ssl, log){
  req.url = req._url || req.url; // Get the original url since we are going to redirect.

  var hostname = req.headers.host.split(':')[0] + ':' + (ssl.redirectPort || ssl.port);
  var url = 'https://' + path.join(hostname, req.url);
  log && log.info('Redirecting %s to %s', path.join(req.headers.host, req.url), url);
  //
  // We can use 301 for permanent redirect, but its bad for debugging, we may have it as
  // a configurable option.
  //
  res.writeHead(302, {Location: url});
  res.end();
}

function startsWith(input, str){
  return input.slice(0, str.length) === str &&
         (input.length === str.length || input[str.length] === '/')
}

function prepareUrl(url){
  url = _.clone(url);
  if(_.isString(url)){
    url = setHttp(url);

    if(!validUrl.isHttpUri(url) && !validUrl.isHttpsUri(url)){
      throw Error('uri is not a valid http uri ' + url);
    }

    url = parseUrl(url);
  }
  return url;
}

function getCertData(pathname, unbundle){
  var fs = require('fs');

  // TODO: Support input as Buffer, Stream or Pathname.

  if(pathname){
    if(_.isArray(pathname)){
      var pathnames = pathname;
      return _.flatten(_.map(pathnames, function(_pathname){
        return getCertData(_pathname, unbundle);
      }));
    }else if(fs.existsSync(pathname)){
      if(unbundle){
        return unbundleCert(fs.readFileSync(pathname, 'utf8'));
      }else{
        return fs.readFileSync(pathname, 'utf8');
      }
    }
  }
}

/**
  Unbundles a file composed of several certificates.
  http://www.benjiegillam.com/2012/06/node-dot-js-ssl-certificate-chain/
*/
function unbundleCert(bundle){
  var chain = bundle.trim().split('\n');

  var ca = [];
  var cert = [];

  for(var i = 0, len = chain.length; i < len; i++){
    var line = chain[i].trim();
    if(!(line.length !== 0)){
      continue;
    }
    cert.push(line);
    if(line.match(/-END CERTIFICATE-/)){
      var joined = cert.join('\n');
      ca.push(joined);
      cert = [];
    }
  }
  return ca;
}

var tls = require('tls');
function createCredentialContext(key, cert, ca){
  var opts = {};

  opts.key = getCertData(key);
  opts.cert = getCertData(cert);
  if(ca){
    opts.ca = getCertData(ca, true);
  }

  var credentials = tls.createSecureContext(opts);

  return credentials.context;
}

//
// https://stackoverflow.com/questions/18052919/javascript-regular-expression-to-add-protocol-to-url-string/18053700#18053700
// Adds http protocol if non specified.
function setHttp(link){
  if(link.search(/^http[s]?\:\/\//) === -1){
    link = 'http://' + link;
  }
  return link;
}

module.exports = ReverseProxy;
<commit_msg>Add notFound function to set respondNotFound callback (#44)<commit_after>/*eslint-env node */
'use strict';

var
  http = require('http'),
  httpProxy = require('http-proxy'),
  validUrl = require('valid-url'),
  parseUrl = require('url').parse,
  path = require('path'),
  _ = require('lodash'),
  bunyan = require('bunyan'),
  cluster = require('cluster');

function ReverseProxy(opts){
  if(!(this instanceof ReverseProxy)){
    return new ReverseProxy(opts);
  }

  this.opts = opts = opts || {};

  var log;
  if(opts.bunyan !== false){
    log = this.log = bunyan.createLogger(opts.bunyan || {
      name: 'redbird'
    });
  }

  var _this = this;

  if(opts.cluster && cluster.isMaster){
    for(var i = 0; i < opts.cluster; i++){
      cluster.fork();
    }

    cluster.on('exit', function(worker, code, signal) {
      // Fork if a worker dies.
      log && log.error({
        code: code,
        signal: signal},
        'worker died un-expectedly... restarting it.');
      cluster.fork();
    });
  }else{
    opts.port = opts.port || 8080;

    //
    // Routing table.
    //
    this.routing = {};

    //
    // Create a proxy server with custom application logic
    //
    var proxy = this.proxy = httpProxy.createProxyServer({
      xfwd: (opts.xfwd != false) ? true : false,
      prependPath: false,
      secure: (opts.secure != false) ? true : false
    });

    proxy.on('proxyReq', function(p, req){
      if(req.host != null){
        p.setHeader('host', req.host);
      }
    });

    //
    // Support NTLM auth
    //
    if(opts.ntlm){
      proxy.on('proxyRes', function (proxyRes) {
        var key = 'www-authenticate';
        proxyRes.headers[key] = proxyRes.headers[key] && proxyRes.headers[key].split(',');
      });
    }

    //
    // Standard HTTP Proxy Server.
    //
    var server = this.server = http.createServer(function(req, res){
      var src = getSource(req);
      var target = _this._getTarget(src, req);
      if(target){
        if(shouldRedirectToHttps(certs, src, target)){
          redirectToHttps(req, res, target, opts.ssl, log);
        }else{
          proxy.web(req, res, {target: target});
        }
      }else{
        respondNotFound(res);
      }
    });

    //
    // Listen to the `upgrade` event and proxy the
    // WebSocket requests as well.
    //
    server.on('upgrade', websocketsUpgrade);

    server.on('error', function(err){
      log && log.error(err, 'Server Error');
    });

    //
    // Optionally create an https proxy server.
    //
    if(opts.ssl){
      var https = require('https');
      var certs = this.certs = {};

      var ssl = {
        SNICallback: function(hostname, cb){
          if(cb){
            cb(null, certs[hostname]);
          }else{
            return certs[hostname];
          }
        },
        //
        // Default certs for clients that do not support SNI.
        //
        key: getCertData(opts.ssl.key),
        cert: getCertData(opts.ssl.cert)
      };

      if(opts.ssl.ca){
        ssl.ca = getCertData(opts.ssl.ca, true);
      }

      if(opts.ssl.opts){
        ssl = _.defaults(ssl, opts.ssl.opts);
      }

      var httpsServer = this.httpsServer = https.createServer(ssl, function(req, res){

        var src = getSource(req);

        var target = _this._getTarget(src, req);
        if(target){
          proxy.web(req, res, {target: target});
        }else{
          respondNotFound(res);
        }
      });

      httpsServer.on('upgrade', websocketsUpgrade);

      httpsServer.on('error', function(err){
        log && log.error(err, 'HTTPS Server Error');
      });

      httpsServer.on('clientError', function(err){
        log && log.error(err, 'HTTPS Client  Error');
      });

      log && log.info('Listening to HTTPS requests on port %s', opts.ssl.port);
      httpsServer.listen(opts.ssl.port);
    }

    proxy.on('error', handleProxyError);

    server.listen(opts.port);

    log && log.info(opts.port, 'Started a Redbird reverse proxy server');
  }

  function shouldRedirectToHttps(certs, src, target){
    return certs && src in certs && target.sslRedirect;
  }

  function websocketsUpgrade(req, socket, head){
    var src = getSource(req);
    var target = _this._getTarget(src, req);
    log && log.info({headers: req.headers, target: target}, 'upgrade to websockets');
    if(target){
      proxy.ws(req, socket, head, {target: target});
    }else{
      respondNotFound(socket);
    }
  }

  function handleProxyError(err, req, res){
    //
    // Send a 500 http status if headers have been sent
    //
    if(!res.headersSent){
      res.writeHead && res.writeHead(500);
    }

    //
    // Do not log this common error
    //
    if(err.message !== 'socket hang up'){
      log && log.error(err, 'Proxy Error');
    }

    //
    // TODO: if err.code=ECONNREFUSED and there are more servers
    // for this route, try another one.
    //
  }
}

/**
  Register a new route.

  @src {String|URL} A string or a url parsed by node url module.
  Note that port is ignored, since the proxy just listens to one port.

  @target {String|URL} A string or a url parsed by node url module.
  @opts {Object} Route options.
*/
ReverseProxy.prototype.register = function(src, target, opts){
  if(this.opts.cluster && cluster.isMaster) return this;

  if(!src || !target){
    throw Error('Cannot register a new route with unspecified src or target');
  }

  var routing = this.routing;

  src = prepareUrl(src);
  target = prepareUrl(target);

  target.sslRedirect = true;
  target.useTargetHostHeader = false;

  if(opts){
    if(opts.ssl){
      if(!this.httpsServer){
        throw Error('Cannot register https routes without defining a ssl port');
      }

      target.sslRedirect = opts.ssl.redirect === false ? false : true;

      if(!this.certs[src.hostname]){
        if(opts.ssl.key || opts.ssl.cert || opts.ssl.ca){
          var cert = createCredentialContext(opts.ssl.key, opts.ssl.cert, opts.ssl.ca);
          this.certs[src.hostname] = cert;
        }else{
          // Trigger the use of the default certificates.
          this.certs[src.hostname] = void 0;
        }
      }
    }
    target.useTargetHostHeader = opts.useTargetHostHeader === true ? true : false;
  }

  var host = routing[src.hostname] = routing[src.hostname] || [];
  var pathname = src.pathname || '/';
  var route = _.find(host, {path: pathname});

  if(!route){
    route = {path: pathname, rr: 0, urls: []};
    host.push(route);

    //
    // Sort routes
    //
    routing[src.hostname] = _.sortBy(host, function(_route){
      return -_route.path.length;
    });
  }

  route.urls.push(target);

  this.log && this.log.info({from: src, to: target}, 'Registered a new route');
  return this;
};

ReverseProxy.prototype.unregister = function(src, target){
  if(this.opts.cluster && cluster.isMaster) return this;

  if(!src){
    return this;
  }

  src = prepareUrl(src);
  var routes = this.routing[src.hostname] || [];
  var pathname = src.pathname || '/';
  var i;

  for(i = 0; i < routes.length; i++){
    if(routes[i].path === pathname){
      break;
    }
  }

  if(i < routes.length){
    var route = routes[i];

    if(target){
      target = prepareUrl(target);
      _.remove(route.urls, function(url){
        return url.href === target.href;
      });
    }else{
      route.urls = [];
    }

    if(route.urls.length === 0){
      routes.splice(i, 1);
    }

    this.log && this.log.info({from: src, to: target}, 'Unregistered a route');
  }
  return this;
};

ReverseProxy.prototype.resolve = function(host, url){
  // Given a src resolve it to a target route if any available.
  if(!host){
    return;
  }

  url = url || '/';

  var routes = this.routing[host];
  var i = 0;

  if(routes){
    var len = routes.length;

    //
    // Find path that matches the start of req.url
    //
    for(i = 0; i < len; i++){
      var route = routes[i];

      if(route.path === '/' || startsWith(url, route.path)){
        return route;
      }
    }
  }
};

ReverseProxy.prototype._getTarget = function(src, req){
  var url = req.url;
  var route = this.resolve(src, url);

  if(!route){
    this.log && this.log.warn({src: src, url: req.url}, 'no valid route found for given source');
    return;
  }

  var pathname = route.path;
  if(pathname.length > 1){
    //
    // remove prefix from src
    //
    req._url = url; // save original url
    req.url = url.substr(pathname.length) || '/';
  }

  //
  // Perform Round Robin on the available targets
  // TODO: if target errors with EHOSTUNREACH we should skip this
  // target and try with another.
  //
  var urls = route.urls;
  var j = route.rr;
  route.rr = (j + 1) % urls.length; // get and update Round robin index.
  var target = route.urls[j];

  //
  // Fix request url if targetname specified.
  //
  if(target.pathname){
    req.url = path.join(target.pathname, req.url);
  }

  //
  // Host headers are passed through from the source by default
  // Often we want to use the host header of the target instead
  //
  if(target.useTargetHostHeader === true){
    req.host = target.host;
  }

  this.log && this.log.info('Proxying %s to %s', src + url, path.join(target.host, req.url));

  return target;
};

ReverseProxy.prototype.close = function(){
  try{
    this.server.close();
    this.httpsServer && this.httpsServer.close();
  }catch(err){
    // Ignore for now...
  }
};

//
// Helpers
//
function getSource(req){
  if(req.headers.host){
    return req.headers.host.split(':')[0];
  }
}

/**
  Routing table structure. An object with hostname as key, and an array as value.
  The array has one element per path associated to the given hostname.
  Every path has a RoundRobin value (rr) and urls array, with all the urls available
  for this target route.

  {
    hostA :
      [
        {
          path: '/',
          rr: 3,
          urls: []
        }
      ]
  }
*/

var respondNotFound = function(res){
  res.statusCode = 404;
  res.write('Not Found');
  res.end();
};

ReverseProxy.prototype.notFound = function(callback){
  if(typeof callback == "function")
    respondNotFound = callback;
  else
    throw Error('notFound callback is not a function');
};

//
// Redirect to the HTTPS proxy
//
function redirectToHttps(req, res, target, ssl, log){
  req.url = req._url || req.url; // Get the original url since we are going to redirect.

  var hostname = req.headers.host.split(':')[0] + ':' + (ssl.redirectPort || ssl.port);
  var url = 'https://' + path.join(hostname, req.url);
  log && log.info('Redirecting %s to %s', path.join(req.headers.host, req.url), url);
  //
  // We can use 301 for permanent redirect, but its bad for debugging, we may have it as
  // a configurable option.
  //
  res.writeHead(302, {Location: url});
  res.end();
}

function startsWith(input, str){
  return input.slice(0, str.length) === str &&
         (input.length === str.length || input[str.length] === '/')
}

function prepareUrl(url){
  url = _.clone(url);
  if(_.isString(url)){
    url = setHttp(url);

    if(!validUrl.isHttpUri(url) && !validUrl.isHttpsUri(url)){
      throw Error('uri is not a valid http uri ' + url);
    }

    url = parseUrl(url);
  }
  return url;
}

function getCertData(pathname, unbundle){
  var fs = require('fs');

  // TODO: Support input as Buffer, Stream or Pathname.

  if(pathname){
    if(_.isArray(pathname)){
      var pathnames = pathname;
      return _.flatten(_.map(pathnames, function(_pathname){
        return getCertData(_pathname, unbundle);
      }));
    }else if(fs.existsSync(pathname)){
      if(unbundle){
        return unbundleCert(fs.readFileSync(pathname, 'utf8'));
      }else{
        return fs.readFileSync(pathname, 'utf8');
      }
    }
  }
}

/**
  Unbundles a file composed of several certificates.
  http://www.benjiegillam.com/2012/06/node-dot-js-ssl-certificate-chain/
*/
function unbundleCert(bundle){
  var chain = bundle.trim().split('\n');

  var ca = [];
  var cert = [];

  for(var i = 0, len = chain.length; i < len; i++){
    var line = chain[i].trim();
    if(!(line.length !== 0)){
      continue;
    }
    cert.push(line);
    if(line.match(/-END CERTIFICATE-/)){
      var joined = cert.join('\n');
      ca.push(joined);
      cert = [];
    }
  }
  return ca;
}

var tls = require('tls');
function createCredentialContext(key, cert, ca){
  var opts = {};

  opts.key = getCertData(key);
  opts.cert = getCertData(cert);
  if(ca){
    opts.ca = getCertData(ca, true);
  }

  var credentials = tls.createSecureContext(opts);

  return credentials.context;
}

//
// https://stackoverflow.com/questions/18052919/javascript-regular-expression-to-add-protocol-to-url-string/18053700#18053700
// Adds http protocol if non specified.
function setHttp(link){
  if(link.search(/^http[s]?\:\/\//) === -1){
    link = 'http://' + link;
  }
  return link;
}

module.exports = ReverseProxy;
<|endoftext|>


<commit_before>/*
 * Url module. Required for resolving relative paths.
 */

var url = require("url");

/*
 * Regexp from origin Arc90 Readability.
 */

var regexps = {
  unlikelyCandidatesRe: /combx|pager|comment|disqus|foot|header|menu|meta|nav|rss|shoutbox|sidebar|sponsor|share|bookmark|social|advert|leaderboard|instapaper_ignore|entry-unrelated/i,
  okMaybeItsACandidateRe: /and|article|body|column|main/i,
  positiveRe: /article|body|content|entry|hentry|page|pagination|post|text/i,
  negativeRe: /combx|comment|captcha|contact|foot|footer|footnote|link|media|meta|promo|related|scroll|shoutbox|sponsor|utility|tags|widget|tip|dialog/i,
  divToPElementsRe: /<(a|blockquote|dl|div|img|ol|p|pre|table|ul)/i,
  replaceBrsRe: /(
]*>[ \n\r\t]*){2,}/gi,
  replaceFontsRe: /<(\/?)font[^>]*>/gi,
  trimRe: /^\s+|\s+$/g,
  normalizeRe: /\s{2,}/g,
  killBreaksRe: /(
(\s| ?)*){1,}/g,
  videoRe: /http:\/\/(www\.)?(youtube|vimeo|youku|tudou|56|yinyuetai)\.com/i
};

/**
 * Node Types and their classification
 **/

var nodeTypes = {
  'mostPositive': ['div'],
  'positive': ['pre', 'td', 'blockquote'],
  'negative': ['address', 'ol', 'ul', 'dl', 'dd', 'dt', 'li'],
  'mostNegative': ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'th']
}

/**
 * We're getting only the readable content + images, no other nodes are needed.
 **/

var trashNodes = 'meta,iframe,noscript,style,aside,object,form,script';

/**
 * Select the TopCandidate from all possible candidates
 **/

function getArticle(candidates, $, options) {
  var topCandidate = null;

  candidates.forEach(function(elem) {
    var linkDensity = getLinkDensity(elem, $);
    var score = elem.data('readabilityScore');
    // actually say, we can't score node by children length
    // e.g.: sometimes, the comments of an article have lots of children,
    // such as: like, dislike, follow, print, star+/-...
    // but most of them worthless.
    // var siblings = elem.children().length;

    elem.data('readabilityScore', /*Math.max(2, siblings) * */ score * (1 - linkDensity));
    if (!topCandidate || elem.data('readabilityScore') > topCandidate.data('readabilityScore')) {
      topCandidate = elem;
    }
  });
  /**
   * If we still have no top candidate, just use the body as a last resort.
   * We also have to copy the body node so it is something we can modify.
   **/
  if (topCandidate === null) {
    // With no top candidate, bail out if no body tag exists as last resort.
    if (!$('body')) return new Error("No body tag was found.");
    return $('body');
  }
  // Perhaps the topCandidate is the parent?
  var parent;
  if (!(parent = topCandidate.parent()) || parent.length == 0 || topCandidate.children().length > 5) {
    return filterCandidates(topCandidate, topCandidate.children(), $);
  } else {
    return filterCandidates(topCandidate, parent.children(), $);
  }
}

/**
 * Filter TopCandidate Siblings (Children) based on their Link Density, and readabilityScore
 * Append the nodes to articleContent
 **/

function filterCandidates(topCandidate, siblings, $) {
  var articleContent = $("

");
  var siblingScoreThreshold = Math.max(10, topCandidate.data('readabilityScore') * 0.2);
  siblings.each(function(index, elem) {
    var sibling = $(this);
    var append = false;
    var type = sibling.get(0).name;
    var score = sibling.data('readabilityScore');
    var children = siblings.contents().length;
    if (sibling === topCandidate || score > siblingScoreThreshold) {
      append = true;
    }
    if (children > 0) {
      siblings.contents().each(function(index, elem) {
        if (elem.name == "img") append = true;
      });
    }

    if (!append && type == 'p') {
      var linkDensity = getLinkDensity(sibling, $);
      var len = sibling.text().length;
      if (len < 3) return;

      if (len > 80 && linkDensity < 0.25) {
        append = true;
      } else if (len < 80 && linkDensity == 0 && sibling.text().replace(regexps.trimRe, "")) {
        append = true;
      }
    }
    if (append) {
      articleContent.append(sibling);
    }
  });
  return articleContent;
}

/**
 * Traverse all Nodes and remove unlikely Candidates.
 **/

function getCandidates($, base, options) {

  $(trashNodes).remove();
  // Candidates Array
  var candidates = [];

  // Iterate over all Nodes in body
  $('*', 'body').each(function(index, element) {
    var node = $(this);

    // If node is null, return, otherwise Illegal Access Error
    if (!node || node.length == 0) return;
    var nodeType = node.get(0).name;

    // Remove Unlikely Candidates
    var classAndID = (node.attr('class') || "") + (node.attr('id') || "");
    if (classAndID.search(regexps.unlikelyCandidatesRe) !== -1 && classAndID.search(regexps.okMaybeItsACandidateRe) == -1) {
      node.remove();
      return;
    }

    // Remove Elements that have no children and have no content
    if (nodeType == "div" && node.children().length < 1 && node.text().trim().length < 1) {
      node.remove();
      return;
    }
    // Remove Style
    node.removeAttr('style');

    // turn all divs that don't have children block level elements into p's
    if (nodeType === "div" && options.considerDIVs) {
      if (node.html().search(regexps.divToPElementsRe) === -1) {
        node.replaceWith('
' + node.html() + '
');
      } else {
        node.contents().each(function(index, element){
          var child = $(this), childEntity;
          if(!child || !(childEntity = child.get(0))){
            return;
          }
          if(childEntity.type == 'text' && childEntity.data && childEntity.data.replace(regexps.trimRe, '').length > 0){
            child.replaceWith('
' + childEntity.data + '
');
          }
        });
      }
    }

    // score paragraphs.
    if (nodeType == "p") {
      calculateNodeScore(node, candidates);
      return;
    }

    // Resolve URLs
    if (base) {
      if (nodeType == "img" && typeof node.attr('href') != "undefined") {
        node.attr('src', url.resolve(base, node.attr('src')));
      }
      if (nodeType == "a" && typeof node.attr('href') != "undefined") {
        node.attr('href', url.resolve(base, node.attr('href')));
      }
    }

    // Clean the headers
    if (["h1", "h2", "h3", "h4", "h5", "h6"].indexOf(nodeType) !== -1) {
      var weight = getClassWeight(node, $);
      var density = getLinkDensity(node, $);
      if (weight < 0 || density > 0.3) {
        node.remove();
      }
    }

  });
  // calculate scores of `P`s that were turned from DIV by us.
  $('p.node-read-div2p', 'body').each(function(){
    calculateNodeScore($(this), candidates);
  });
  return candidates;
}

/**
 * save score datas to nodes need score.
 * @param node
 * @param contentScore
 * @param candidates
 */
function scoreCandidate(node, contentScore, candidates) {
  var score;
  if (typeof node.data('readabilityScore') == "undefined") {
    score = initializeNode(node);
    candidates.push(node);
  } else {
    score = node.data('readabilityScore') || 0;
  }
  node.data('readabilityScore', score + contentScore)
}

/**
 * calculate score of specified node.
 * @param node
 * @param candidates
 */
function calculateNodeScore(node, candidates){
  var txt = node.text();
  // Ignore too small nodes
  if (txt.length < 25) return;

  var contentScore = 0;

  // Add a point for the paragraph itself as a base.
  ++contentScore;

  // Add points for any commas within this paragraph
  // support Chinese commas.
  var commas = txt.match(/[,，.。;；?？、]/g);
  if (commas && commas.length) {
    contentScore += commas.length;
  }

  // For every 100 characters in this paragraph, add another point. Up to 3 points.
  contentScore += Math.min(Math.floor(txt.length / 100), 3);

  // Initialize Parent and Grandparent
  // First initialize the parent node with contentScore / 1, then grandParentNode with contentScore / 2
  var parent = node.parent();

  if (parent && parent.length > 0) {
    scoreCandidate(parent, contentScore, candidates);
    var grandParent = parent.parent();
    if (grandParent && grandParent.length > 0) {
      scoreCandidate(grandParent, contentScore / 2, candidates);
    }
  }
}

/**
 * Check the type of node, and get its Weight
 **/

function initializeNode(node) {
  if (!node || node.length == 0) return 0;
  var tag = node.get(0).name;
  if (nodeTypes['mostPositive'].indexOf(tag)) return 5 + getClassWeight(node);
  if (nodeTypes['positive'].indexOf(tag)) return 3 + getClassWeight(node);
  if (nodeTypes['negative'].indexOf(tag)) return -3 + getClassWeight(node);
  if (nodeTypes['mostNegative'].indexOf(tag)) return -5 + getClassWeight(node);
}

/**
 * Node Weight is calculated based on className and ID of the node.
 **/

function getClassWeight(node) {
  if (node == null || node.length == 0) return 0;
  var classAndID = (node.attr('class') || "") + (node.attr('id') || "");
  var weight = 0;

  if (node.get(0).name == "article") weight += 25;
  if (classAndID.search(regexps.negativeRe) !== -1) weight -= 25;
  if (classAndID.search(regexps.positiveRe) !== -1) weight += 25;

  return weight;
}

/**
 * Get Link density of this node.
 * Total length of link text in this node divided by the total text of the node.
 * Relative links are not included.
 **/

function getLinkDensity(node, $) {
  var links = node.find('a');
  var textLength = node.text().length;
  var linkLength = 0;
  links.each(function(index, elem) {
    var href = $(this).attr('href');
    if (!href || (href.length > 0 && href[0] === '#')) return;
    linkLength += $(this).text().length;
  });
  return (linkLength / textLength) || 0;
}

/**
 * Main method
 * If the first run does not succeed, try the body element;
 **/

module.exports.extract = function($, base, options) {
  var candidates = getCandidates($, base, options);
  article = getArticle(candidates, $, options);
  if (article.length < 1) {
    article = getArticle([$('body')], $)
  }
  return article;
}<commit_msg>BUG FIX: content and trash tag<commit_after>/*
 * Url module. Required for resolving relative paths.
 */

var url = require("url");

/*
 * Regexp from origin Arc90 Readability.
 */

var regexps = {
  unlikelyCandidatesRe: /combx|pager|comment|disqus|foot|header|menu|meta|nav|rss|shoutbox|sidebar|sponsor|share|bookmark|social|advert|leaderboard|instapaper_ignore|entry-unrelated/i,
  okMaybeItsACandidateRe: /and|article|body|column|main/i,
  positiveRe: /article|body|content|entry|hentry|page|pagination|post|text/i,
  negativeRe: /combx|comment|captcha|contact|foot|footer|footnote|link|media|meta|promo|related|scroll|shoutbox|sponsor|utility|tags|widget|tip|dialog/i,
  divToPElementsRe: /<(a|blockquote|dl|div|img|ol|p|pre|table|ul)/i,
  replaceBrsRe: /(
]*>[ \n\r\t]*){2,}/gi,
  replaceFontsRe: /<(\/?)font[^>]*>/gi,
  trimRe: /^\s+|\s+$/g,
  normalizeRe: /\s{2,}/g,
  killBreaksRe: /(
(\s| ?)*){1,}/g,
  videoRe: /http:\/\/(www\.)?(youtube|vimeo|youku|tudou|56|yinyuetai)\.com/i
};

/**
 * Node Types and their classification
 **/

var nodeTypes = {
  'mostPositive': ['div'],
  'positive': ['pre', 'td', 'blockquote'],
  'negative': ['address', 'ol', 'ul', 'dl', 'dd', 'dt', 'li'],
  'mostNegative': ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'th']
}

/**
 * We're getting only the readable content + images, no other nodes are needed.
 **/

var trashNodes = 'meta,iframe,noscript,style,aside,object,script';

/**
 * Select the TopCandidate from all possible candidates
 **/

function getArticle(candidates, $, options) {
  var topCandidate = null;

  candidates.forEach(function(elem) {
    var linkDensity = getLinkDensity(elem, $);
    var score = elem.data('readabilityScore');
    // actually say, we can't score node by children length
    // e.g.: sometimes, the comments of an article have lots of children,
    // such as: like, dislike, follow, print, star+/-...
    // but most of them worthless.
    // var siblings = elem.children().length;

    elem.data('readabilityScore', /*Math.max(2, siblings) * */ score * (1 - linkDensity));
    if (!topCandidate || elem.data('readabilityScore') > topCandidate.data('readabilityScore')) {
      topCandidate = elem;
    }
  });
  /**
   * If we still have no top candidate, just use the body as a last resort.
   * We also have to copy the body node so it is something we can modify.
   **/
  if (topCandidate === null) {
    // With no top candidate, bail out if no body tag exists as last resort.
    if (!$('body')) return new Error("No body tag was found.");
    return $('body');
  }
  // Perhaps the topCandidate is the parent?
  var parent;
  if (!(parent = topCandidate.parent()) || parent.length == 0 || topCandidate.children('p').length > 5) {
    return filterCandidates(topCandidate, topCandidate.children(), $);
  } else {
    return filterCandidates(topCandidate, parent.children(), $);
  }
}

/**
 * Filter TopCandidate Siblings (Children) based on their Link Density, and readabilityScore
 * Append the nodes to articleContent
 **/

function filterCandidates(topCandidate, siblings, $) {
  var articleContent = $("

");
  var siblingScoreThreshold = Math.max(10, topCandidate.data('readabilityScore') * 0.2);
  siblings.each(function(index, elem) {
    var sibling = $(this);
    var append = false;
    var type = sibling.get(0).name;
    var score = sibling.data('readabilityScore');
    var children = siblings.contents().length;
    if (sibling === topCandidate || score > siblingScoreThreshold) {
      append = true;
    }
    if (children > 0) {
      siblings.contents().each(function(index, elem) {
        if (elem.name == "img") append = true;
      });
    }

    if (!append && type == 'p') {
      var linkDensity = getLinkDensity(sibling, $);
      var len = sibling.text().length;
      if (len < 3) return;

      if (len > 80 && linkDensity < 0.25) {
        append = true;
      } else if (len < 80 && linkDensity == 0 && sibling.text().replace(regexps.trimRe, "")) {
        append = true;
      }
    }
    if (append) {
      articleContent.append(sibling);
    }
  });
  return articleContent;
}

/**
 * Traverse all Nodes and remove unlikely Candidates.
 **/

function getCandidates($, base, options) {

  $(trashNodes).remove();
  // Candidates Array
  var candidates = [];

  // Iterate over all Nodes in body
  $('*', 'body').each(function(index, element) {
    var node = $(this);

    // If node is null, return, otherwise Illegal Access Error
    if (!node || node.length == 0) return;
    var nodeType = node.get(0).name;

    // Remove Unlikely Candidates
    var classAndID = (node.attr('class') || "") + (node.attr('id') || "");
    if (classAndID.search(regexps.unlikelyCandidatesRe) !== -1 && classAndID.search(regexps.okMaybeItsACandidateRe) == -1) {
      node.remove();
      return;
    }

    // Remove Elements that have no children and have no content
    if (nodeType == "div" && node.children().length < 1 && node.text().trim().length < 1) {
      node.remove();
      return;
    }
    // Remove Style
    node.removeAttr('style');

    // turn all divs that don't have children block level elements into p's
    if (nodeType === "div" && options.considerDIVs) {
      if (node.html().search(regexps.divToPElementsRe) === -1) {
        node.replaceWith('
' + node.html() + '
');
      } else {
        node.contents().each(function(index, element){
          var child = $(this), childEntity;
          if(!child || !(childEntity = child.get(0))){
            return;
          }
          if(childEntity.type == 'text' && childEntity.data && childEntity.data.replace(regexps.trimRe, '').length > 0){
            child.replaceWith('
' + childEntity.data + '
');
          }
        });
      }
    }

    // score paragraphs.
    if (nodeType == "p") {
      calculateNodeScore(node, candidates);
      return;
    }

    // Resolve URLs
    if (base) {
      if (nodeType == "img" && typeof node.attr('href') != "undefined") {
        node.attr('src', url.resolve(base, node.attr('src')));
      }
      if (nodeType == "a" && typeof node.attr('href') != "undefined") {
        node.attr('href', url.resolve(base, node.attr('href')));
      }
    }

    // Clean the headers
    if (["h1", "h2", "h3", "h4", "h5", "h6"].indexOf(nodeType) !== -1) {
      var weight = getClassWeight(node, $);
      var density = getLinkDensity(node, $);
      if (weight < 0 || density > 0.3) {
        node.remove();
      }
    }

  });
  // calculate scores of `P`s that were turned from DIV by us.
  $('p.node-read-div2p', 'body').each(function(){
    calculateNodeScore($(this), candidates);
  });
  return candidates;
}

/**
 * save score datas to nodes need score.
 * @param node
 * @param contentScore
 * @param candidates
 */
function scoreCandidate(node, contentScore, candidates) {
  var score;
  if (typeof node.data('readabilityScore') == "undefined") {
    score = initializeNode(node);
    candidates.push(node);
  } else {
    score = node.data('readabilityScore') || 0;
  }
  node.data('readabilityScore', score + contentScore)
}

/**
 * calculate score of specified node.
 * @param node
 * @param candidates
 */
function calculateNodeScore(node, candidates){
  var txt = node.text();
  // Ignore too small nodes
  if (txt.length < 25) return;

  var contentScore = 0;

  // Add a point for the paragraph itself as a base.
  ++contentScore;

  // Add points for any commas within this paragraph
  // support Chinese commas.
  var commas = txt.match(/[,，.。;；?？、]/g);
  if (commas && commas.length) {
    contentScore += commas.length;
  }

  // For every 100 characters in this paragraph, add another point. Up to 3 points.
  contentScore += Math.min(Math.floor(txt.length / 100), 3);

  // Initialize Parent and Grandparent
  // First initialize the parent node with contentScore / 1, then grandParentNode with contentScore / 2
  var parent = node.parent();

  if (parent && parent.length > 0) {
    scoreCandidate(parent, contentScore, candidates);
    var grandParent = parent.parent();
    if (grandParent && grandParent.length > 0) {
      scoreCandidate(grandParent, contentScore / 2, candidates);
    }
  }
}

/**
 * Check the type of node, and get its Weight
 **/

function initializeNode(node) {
  if (!node || node.length == 0) return 0;
  var tag = node.get(0).name;
  if (nodeTypes['mostPositive'].indexOf(tag)) return 5 + getClassWeight(node);
  if (nodeTypes['positive'].indexOf(tag)) return 3 + getClassWeight(node);
  if (nodeTypes['negative'].indexOf(tag)) return -3 + getClassWeight(node);
  if (nodeTypes['mostNegative'].indexOf(tag)) return -5 + getClassWeight(node);
}

/**
 * Node Weight is calculated based on className and ID of the node.
 **/

function getClassWeight(node) {
  if (node == null || node.length == 0) return 0;
  var classAndID = (node.attr('class') || "") + (node.attr('id') || "");
  var weight = 0;

  if (node.get(0).name == "article") weight += 25;
  if (classAndID.search(regexps.negativeRe) !== -1) weight -= 25;
  if (classAndID.search(regexps.positiveRe) !== -1) weight += 25;

  return weight;
}

/**
 * Get Link density of this node.
 * Total length of link text in this node divided by the total text of the node.
 * Relative links are not included.
 **/

function getLinkDensity(node, $) {
  var links = node.find('a');
  var textLength = node.text().length;
  var linkLength = 0;
  links.each(function(index, elem) {
    var href = $(this).attr('href');
    if (!href || (href.length > 0 && href[0] === '#')) return;
    linkLength += $(this).text().length;
  });
  return (linkLength / textLength) || 0;
}

/**
 * Main method
 * If the first run does not succeed, try the body element;
 **/

module.exports.extract = function($, base, options) {
  var candidates = getCandidates($, base, options);
  article = getArticle(candidates, $, options);
  if (article.length < 1) {
    article = getArticle([$('body')], $)
  }
  return article;
}<|endoftext|>


<commit_before>'use strict';

var fs = require('fs') ;
var path = require('path') ;
var _ = require('lodash') ;
var allPossibleTargets = [] ;
var dest = [] ;
var debug ;
var config ;

//watch config file for changes - we allow the user to dynamically add or remove targets
var configPath = path.resolve(__dirname) + '/../config.js' ;
fs.watchFile(configPath, function () {
  try {
    delete require.cache[require.resolve(configPath)] ;
    config = require(configPath) ;
    createTargets( config ) ;
    console.log('config.js was modified, proxy targets are now: ', allPossibleTargets) ;
  } catch( err ) {
    console.error('Error re-reading config.js after modification; check to ensure there are no syntax errors: ', err) ;
  }
}) ;

function createTargets( config ) {
  allPossibleTargets = _.filter( config.targets, function(t) { return t.enabled !== false; }) ;
}

module.exports = function(app) {
  debug = app.debug ;
  config = app.config ;

  createTargets(config) ;

  app.invite( function(req,res) {
    var dest = _.map( allPossibleTargets, function(t) { return t.address + ':' + (t.port || 5060); }) ;
    debug('proxy destinations are: ', dest) ;
    req.proxy({
      remainInDialog: false,
      destination: dest
    }, function(err, results) {
      if( err ) console.error('Error proxying: ', err) ;
      debug('proxy result: ', JSON.stringify(results)) ;
    }) ;

    //round-robin the next starting server in the list
    var item = allPossibleTargets.shift() ;
    allPossibleTargets.push( item ) ;
  }) ;

} ;

<commit_msg>use a closure to fix issue with wrong targets being updated in setInterval call<commit_after>'use strict';

var fs = require('fs') ;
var path = require('path') ;
var _ = require('lodash') ;
var allPossibleTargets = [] ;
var dest = [] ;
var debug ;
var config ;

//watch config file for changes - we allow the user to dynamically add or remove targets
var configPath = path.resolve(__dirname) + '/../config.js' ;
fs.watchFile(configPath, function () {
  try {
    delete require.cache[require.resolve(configPath)] ;
    config = require(configPath) ;
    createTargets( config ) ;
    console.log('config.js was modified, proxy targets are now: ', allPossibleTargets) ;
  } catch( err ) {
    console.error('Error re-reading config.js after modification; check to ensure there are no syntax errors: ', err) ;
  }
}) ;

function createTargets( config ) {
  allPossibleTargets = _.filter( config.targets, function(t) { return t.enabled !== false; }) ;
}

function pingTarget( app, target ) {
  //start a timer
  target.to = setTimeout( function() {
    //if we get here we timed out
    if( !target.offline ) {
        target.offline = true ;
        target.to = null ;
        debug('taking target offline: ', JSON.stringify(target)) ;
    }
  }, 2000 ) ;

  //send the OPTIONS ping
  var dest =  'sip:' + target.address + ':' + (target.port || 5060) ;
  app.request({
      uri: dest,
      method: 'OPTIONS',
      headers: {
        'User-Agent': 'drachtio simple proxy'
      }
  }, function( err, req ) {
    if( err ) { return console.error('Error pinging %s', target) ;}
    debug('sent PING to %s', dest) ;
    req.on('response', function(res){
      debug('PING response from %s was %s', dest, res.status) ;

      if( target.to ) {
        debug('clearing timeout for: ', dest) ;
        clearTimeout( target.to ) ;
      }
      if( 200 === res.status ) {
        if( target.offline ) {
           target.offline = false ;
           debug('bringing target back online: ', JSON.stringify(target)) ;
        }
      }
    }) ;
  }) ;
}

function startPinging( app ) {
  for( var i = 0; i < allPossibleTargets.length; i++ ) {
    var t = allPossibleTargets[i] ;
    (function(target) {
      setInterval( function() { pingTarget(app,target); }, 5000) ;
    })(t) ;
  }
}

module.exports = function(app) {
  debug = app.debug ;
  config = app.config ;

  createTargets(config) ;
  startPinging(app) ;

  app.invite( function(req,res) {

    //filter out offline servers
    var onlineServers = _.filter( allPossibleTargets, function(t) { return !t.offline; }) ;
    var dest = _.map(  onlineServers, function(t) { return t.address + ':' + (t.port || 5060); }) ;

    debug('proxy destinations are: ', dest) ;
    req.proxy({
      remainInDialog: false,
      destination: dest
    }, function(err, results) {
      if( err ) console.error('Error proxying: ', err) ;
      debug('proxy result: ', JSON.stringify(results)) ;
    }) ;

    //round-robin the next starting server in the list
    var item = allPossibleTargets.shift() ;
    allPossibleTargets.push( item ) ;
  }) ;
} ;

<|endoftext|>


<commit_before>var Utils = {
  dominantMovement: function(startPosition, endPosition) {
    // calculate the details of the movement
    // leap gestures positions are [x, y, z]
    var movements = [
      {direction: "x", distance: endPosition[0] - startendPosition[0]},
      {direction: "y", distance: endPosition[1] - startendPosition[1]},
      {direction: "z", distance: endPosition[2] - startendPosition[2]}
    ];

    return movements.sort(function(a, b) {
      // we sort the movement components by which one traverse the greatest
      // distance
      return Math.abs(a.distance) > Math.abs(b.distance) ? -1 : 1
    })[0];
  }
}

module.exports = Utils;
<commit_msg>Fix bug<commit_after>var Utils = {
  dominantMovement: function(startPosition, endPosition) {
    // calculate the details of the movement
    // leap gestures positions are [x, y, z]
    var movements = [
      {direction: "x", distance: endPosition[0] - startPosition[0]},
      {direction: "y", distance: endPosition[1] - startPosition[1]},
      {direction: "z", distance: endPosition[2] - startPosition[2]}
    ];

    return movements.sort(function(a, b) {
      // we sort the movement components by which one traverse the greatest
      // distance
      return Math.abs(a.distance) > Math.abs(b.distance) ? -1 : 1
    })[0];
  }
}

module.exports = Utils;
<|endoftext|>


<commit_before>'use strict';

var traverse = require('traverse');
var ObjectID = require('mongodb').ObjectID;

var TWO_DAYS = 172800000;

function default_options (opts) {
  opts = opts || { };
  if (opts) {
    var keys = [null].concat(Object.keys(opts));
    // if (keys.indexOf('laxDate') < 1) { opts.laxDate = false; }
    if (keys.indexOf('deltaAgo') < 1) {
      opts.deltaAgo = ( TWO_DAYS * 2 );
    }
    if (keys.indexOf('walker') < 1) {
      opts.walker = { date: parseInt, sgv: parseInt };
    }
  }
  return opts;
}

function create (params, opts) {
  opts = default_options(opts);
  var finder = walker(opts.walker)(params);
  var query = finder && finder.find ? finder.find : { };
  var dateField = opts.dateField || 'date';
  var dateValue = query[dateField];

  if (!dateValue && !query.dateString) {
    // if (!laxDate) { }
    var minDate = Date.now( ) - opts.deltaAgo;
    query[dateField] = {
      $gte: opts.useEpoch ? minDate : new Date(minDate).toISOString()
    };
  }
  if (query._id && query._id.length) {
    query._id = ObjectID(query._id);
  }
  return query;
}

function walker (spec) {
  var fns = [ ];
  var keys = Object.keys(spec);
  keys.forEach(function config (prop) {
    var typer = spec[prop];
    fns.push(walk_prop(prop, typer));
  });

  function exec (obj) {
    var fn;
    while (fns.length > 0) {
      fn = fns.shift( );
      obj = fn(obj);
    }
    return obj;
  }
  return exec;
}

function walk_prop (prop, typer) {
  function iter (opts) {
    if (opts && opts.find && opts.find[prop]) {
      traverse(opts.find[prop]).forEach(function (x) {
        if (this.isLeaf) {
          this.update(typer(x));
        }
      });
    }
    return opts;
  }
  return iter;
}

walker.walk_prop = walk_prop;
create.walker = walker;
create.default_options = default_options;

exports = module.exports = create;

<commit_msg>try some docs<commit_after>'use strict';

var traverse = require('traverse');
var ObjectID = require('mongodb').ObjectID;

var TWO_DAYS = 172800000;
/**
 * @module query
 * Assist in translating objects from query-string representation into
 * mongo-style queries by performing type translation.
 */

/**
  * Default options for query.
  * Interpret and return the options to use for building our query.
  */
function default_options (opts) {
  opts = opts || { };
  if (opts) {
    var keys = [null].concat(Object.keys(opts));
    // if (keys.indexOf('laxDate') < 1) { opts.laxDate = false; }
    if (keys.indexOf('deltaAgo') < 1) {
      opts.deltaAgo = ( TWO_DAYS * 2 );
    }
    if (keys.indexOf('walker') < 1) {
      opts.walker = { date: parseInt, sgv: parseInt };
    }
  }
  return opts;
}

/**
  * @param QueryParams params Object returned by qs.parse or https://github.com/hapijs/qs
  * @param BuilderOpts opts Options for how to translate types.
  *
  * Allows performing logic described by a model's attributes.
  * Specifically, we try to ensure that all queries have some kind of query
  * body to filter the rows mongodb will spool.  The defaults, such as name and
  * representation of a date field can be configured via the `opts` passed in.
  *
  * @returns Object An object which can be passed to `mongodb.find( )`
  */
function create (params, opts) {
  opts = default_options(opts);
  var finder = walker(opts.walker)(params);
  var query = finder && finder.find ? finder.find : { };
  var dateField = opts.dateField || 'date';
  var dateValue = query[dateField];

  if (!dateValue && !query.dateString) {
    // if (!laxDate) { }
    var minDate = Date.now( ) - opts.deltaAgo;
    query[dateField] = {
      $gte: opts.useEpoch ? minDate : new Date(minDate).toISOString()
    };
  }
  if (query._id && query._id.length) {
    query._id = ObjectID(query._id);
  }
  return query;
}

function walker (spec) {
  var fns = [ ];
  var keys = Object.keys(spec);
  keys.forEach(function config (prop) {
    var typer = spec[prop];
    fns.push(walk_prop(prop, typer));
  });

  function exec (obj) {
    var fn;
    while (fns.length > 0) {
      fn = fns.shift( );
      obj = fn(obj);
    }
    return obj;
  }
  return exec;
}

function walk_prop (prop, typer) {
  function iter (opts) {
    if (opts && opts.find && opts.find[prop]) {
      traverse(opts.find[prop]).forEach(function (x) {
        if (this.isLeaf) {
          this.update(typer(x));
        }
      });
    }
    return opts;
  }
  return iter;
}

walker.walk_prop = walk_prop;
create.walker = walker;
create.default_options = default_options;

exports = module.exports = create;

<|endoftext|>


<commit_before>#!/usr/bin/env node
// Guidelines
// aug2uag © 2015

var http = require('http')
  , fs = require('fs')
  , was = this;

exports.getDisciplines = function(cb) {
	var _p = '../guidelines/';
	fs.readdir(_p, function (err, files) {
	    if (err) return cb(err);
	    var acc = files.map(function (file) {
	        return path.join(_p, file);
	    }).filter(function (file) {
	        return fs.statSync(file).isDirectory();
	    });
	    var agg=[];
	    function final() {cb(null,agg)}
		function series(dir) {
			if(dir) {
				return series(acc.shift());
		  } else {
			return final();
		  }
		}
		series(acc.shift());
	});
}

exports.getDiscipline = function(discipline, cb) {
	discipline = './guidelines/'+discipline+'/'+discipline+'_guidelines.json';
	fs.readFile(discipline, 'utf8', function(err, contents) {
		if (err&&/ENOENT/.test(err.code)) return cb(new Error('invalid discipline '+err.path));
		if (err) return cb(err);
	    contents = JSON.parse(contents);
	    cb(null,contents);
	});
}

exports.getPDF = function(discipline, document, cb) {
	was.getDiscipline(discipline, function(err, docs) {
		if (err) return cb(err);
		console.log(docs)
		console.log(document)
		var url = docs[document];
		http.get(url, function(response) {
			var chunks = [];
			response.on('data', function(chunk) {
				chunks.push(chunk);
			});
			response.on("end", function() {
				var pdf = new Buffer.concat(chunks).toString('base64');
				cb(pdf);
			});
		}).on("error", function(err) {
			cb(err);
		});
	});
}<commit_msg>debug path in helper<commit_after>#!/usr/bin/env node
// Guidelines
// aug2uag © 2015

var http = require('http')
  , fs = require('fs')
  , path = require('path')
  , was = this;

exports.getDisciplines = function(cb) {
	var _p = './guidelines/';
	fs.readdir(_p, function (err, files) {
	    if (err) return cb(err);
	    var acc = files.map(function (file) {
	        return path.join(_p, file);
	    }).filter(function (file) {
	        return fs.statSync(file).isDirectory();
	    });
	    var agg=[];
	    function final() {cb(null,agg)}
		function series(dir) {
			if(dir) {
				agg.push(dir.split('guidelines/')[1]);
				return series(acc.shift());
		  } else {
			return final();
		  }
		}
		series(acc.shift());
	});
}

exports.getDiscipline = function(discipline, cb) {
	discipline = './guidelines/'+discipline+'/'+discipline+'_guidelines.json';
	fs.readFile(discipline, 'utf8', function(err, contents) {
		if (err&&/ENOENT/.test(err.code)) return cb(new Error('invalid discipline '+err.path));
		if (err) return cb(err);
	    contents = JSON.parse(contents);
	    cb(null,contents);
	});
}

exports.getPDF = function(discipline, document, cb) {
	was.getDiscipline(discipline, function(err, docs) {
		if (err) return cb(err);
		var url = docs[document];
		http.get(url, function(response) {
			var chunks = [];
			response.on('data', function(chunk) {
				chunks.push(chunk);
			});
			response.on("end", function() {
				var pdf = new Buffer.concat(chunks).toString('base64');
				cb(pdf);
			});
		}).on("error", function(err) {
			cb(err);
		});
	});
}<|endoftext|>


<commit_before>docTests.wrongWrappedSidebarMacros = {
  name: "wrong_wrapped_sidebar_macros",
  desc: "wrong_wrapped_sidebar_macros_desc",
  check: function checkWrongWrappedSidebarMacros(rootElement) {
    const allowedMacros = /^(?:apiref|cssref|htmlref|makesimplequicklinks|mathmlref|svgrefelem)$|sidebar$/i;

    let treeWalker = document.createTreeWalker(
        rootElement,
        NodeFilter.SHOW_TEXT,
        {
          acceptNode: (node) => {
            return node.textContent.match(/\{\{.*?\}\}/) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
          }
        }
    );
    let matches = [];

    while(treeWalker.nextNode()) {
      let reMacroName = /\{\{\s*([^\(\}\s]+).*?\}\}/g;
      let macroNameMatch = reMacroName.exec(treeWalker.currentNode.textContent);
      while (macroNameMatch) {
        if (macroNameMatch[1].match(allowedMacros) !== null &&
            treeWalker.currentNode.parentElement.localName !== 'div') {
          matches.push({
            msg: "wrong_element_wrapping_sidebar_macro",
            msgParams: [macroNameMatch[0], treeWalker.currentNode.parentElement.localName],
            type: ERROR
          });
        }
        macroNameMatch = reMacroName.exec(treeWalker.currentNode.textContent);
      }
    }

    return matches;
  }
};<commit_msg>Fixed naming of incorrectly wrapped sidebar macros test<commit_after>docTests.incorrectlyWrappedSidebarMacros = {
  name: "incorrectly_wrapped_sidebar_macros",
  desc: "incorrectly_wrapped_sidebar_macros_desc",
  check: function checkIncorrectlyWrappedSidebarMacros(rootElement) {
    const allowedMacros = /^(?:apiref|cssref|htmlref|makesimplequicklinks|mathmlref|svgrefelem)$|sidebar$/i;

    let treeWalker = document.createTreeWalker(
        rootElement,
        NodeFilter.SHOW_TEXT,
        {
          acceptNode: (node) => {
            return node.textContent.match(/\{\{.*?\}\}/) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
          }
        }
    );
    let matches = [];

    while(treeWalker.nextNode()) {
      let reMacroName = /\{\{\s*([^\(\}\s]+).*?\}\}/g;
      let macroNameMatch = reMacroName.exec(treeWalker.currentNode.textContent);
      while (macroNameMatch) {
        if (macroNameMatch[1].match(allowedMacros) !== null &&
            treeWalker.currentNode.parentElement.localName !== 'div') {
          matches.push({
            msg: "wrong_element_wrapping_sidebar_macro",
            msgParams: [macroNameMatch[0], treeWalker.currentNode.parentElement.localName],
            type: ERROR
          });
        }
        macroNameMatch = reMacroName.exec(treeWalker.currentNode.textContent);
      }
    }

    return matches;
  }
};<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */

var _ = require('lodash'),
  util = require('./util'),
  Types = require('./types'),
  SchemaType = require('./schematype'),
  getProperty = util.getProperty,
  getType = util.getType;

var schemaType = new SchemaType();

var types = {
  Array: new Types.Array(),
  Boolean: new Types.Boolean(),
  Date: new Types.Date(),
  Object: new Types.Object(),
  Number: new Types.Number(),
  String: new Types.String()
};

/**
 * Creates a new query instance.
 *
 * @param {Array} index
 * @api public
 */

var Query = module.exports = function(index, populates){
  this._index = index;
  this._populates = populates || [];

  this.__defineGetter__('length', function(){
    return index.length;
  });
};

/**
 * Creates a new query instance.
 *
 * @param {Array} index
 * @return {Query}
 * @api private
 */

Query.prototype._createQuery = function(index){
  if (!index) index = this._index.slice();

  return new this._model._query(index, this._populates.slice());
};

/**
 * Iterates over the query.
 *
 * @param {Function} iterator
 * @return {Query}
 * @api public
 */

Query.prototype.forEach = Query.prototype.each = function(iterator){
  var index = this._index,
    model = this._model,
    populates = this._populates;

  for (var i = 0, len = index.length; i < len; i++){
    iterator(model._populate(model.get(index[i]), populates), i);
  }

  return this;
};

/**
 * Returns an array containing all items.
 *
 * @return {Array}
 * @api public
 */

Query.prototype.toArray = function(){
  var arr = [];

  this.each(function(item){
    arr.push(item);
  });

  return arr;
};

/**
 * Returns the number of items in the query.
 *
 * @return {Number}
 * @api public
 */

Query.prototype.count = function(){
  return this.length;
};

/**
 * Updates all items in the query.
 *
 * @param {Object} obj
 * @param {Function} [callback]
 * @return {Query}
 * @api public
 */

Query.prototype.update = function(obj, callback){
  var model = this._model,
    index = this._index,
    arr = [];

  for (var i = 0, len = index.length; i < len; i++){
    model.updateById(index[i], obj, function(item){
      if (item) arr.push(item);
    });
  }

  callback && callback(arr);

  return this;
};

/**
 * Replaces all items in the query.
 *
 * @param {Object} obj
 * @param {Function} [callback]
 * @return {Query}
 * @api public
 */

Query.prototype.replace = function(obj, callback){
  var model = this._model,
    index = this._index,
    arr = [];

  for (var i = 0, len = index.length; i < len; i++){
    model.replaceById(index[i], obj, function(item){
      if (item) arr.push(item);
    });
  }

  callback && callback(arr);

  return this;
};

/**
 * Removes all items in the query.
 *
 * @param {Function} [callback]
 * @return {Query}
 * @api public
 */

Query.prototype.remove = function(callback){
  var model = this._model,
    index = this._index,
    arr = [];

  for (var i = 0, len = index.length; i < len; i++){
    model.removeById(index[i], function(item){
      if (item) arr.push(item);
    });
  }

  callback && callback(arr);

  return this;
};

/**
 * Gets the item at the specified index.
 *
 * @param {Number} num
 * @return {Document}
 * @api public
 */

Query.prototype.eq = function(num){
  if (this.length){
    if (num < 0) num = this.length + num;
    return this._model.get(this._index[num]);
  }
};

/**
 * Returns the first item in the query.
 *
 * @return {Document}
 * @api public
 */

Query.prototype.first = function(){
  return this.eq(0);
};

/**
 * Returns the last item in the query.
 *
 * @return {Document}
 * @api public
 */

Query.prototype.last = function(){
  return this.eq(-1);
};

/**
 * Returns the specified range of the query.
 *
 * @param {Number} start
 * @param {Number} [end]
 * @return {Query}
 * @api public
 */

Query.prototype.slice = function(start, end){
  return this._createQuery([].slice.apply(this._index, arguments));
};

/**
 * Limits the number of items returned.
 *
 * @param {Number} num
 * @return {Query}
 * @api public
 */

Query.prototype.limit = function(num){
  return this.slice(0, num);
};

/**
 * Specifies the number of items to skip.
 *
 * @param {Number} num
 * @return {Query}
 * @api public
 */

Query.prototype.skip = function(num){
  return this.slice(num);
};

/**
 * Returns the query in reversed order.
 *
 * @return {Query}
 * @api public
 */

Query.prototype.reverse = function(){
  return this._createQuery(util.reverse(this._index));
};

/**
 * Sorts the model.
 *
 * @param {String|Object} orderby
 * @param {Number|String} [order]
 * @return {Query}
 * @api public
 */

Query.prototype.sort = function(orderby, order){
  if (_.isObject(orderby)){
    var query = this._createQuery();

    for (var i in orderby){
      (function(i){
        query = query.sort(i, orderby[i]);
      })(i);
    }

    return query;
  }

  var data = {};

  this.each(function(item){
    data[item._id] = item;
  });

  var arr = this._index.slice().sort(function(a, b){
    var orderA = getProperty(data[a], orderby),
      orderB = getProperty(data[b], orderby);

    if (orderA < orderB){
      return -1;
    } else if (orderA > orderB){
      return 1;
    } else {
      return 0;
    }
  });

  if (order){
    order = order.toString().toLowerCase();
    if (order == -1 || order == 'desc' || order == 'descending') arr.reverse();
  }

  return this._createQuery(arr);
};

/**
 * Returns the model in random order.
 *
 * @return {Query}
 * @api public
 */

Query.prototype.random = Query.prototype.shuffle = function(){
  return this._createQuery(util.shuffle(this._index));
};

/**
 * Finds the matching items.
 *
 * @param {Object} data
 * @param {Object} conditions
 * @return {Boolean}
 * @api private
 */

Query.prototype._query = function(data, conditions){
  var match = true,
    keys = Object.keys(conditions);

  for (var i = 0, len = keys.length; i < len; i++){
    var key = keys[i],
      query = conditions[key];

    switch (key){
      case '$or':
        var subMatch = false;

        for (var j = 0, qLen = query.length; j < qLen; j++){
          if (this._query(data, query[j])){
            subMatch = true;
            break;
          }
        }

        match = subMatch;

        break;

      case '$and':
        for (var j = 0, qLen = query.length; j < qLen; j++){
          if (!this._query(data, query[j])){
            match = false;
            break;
          }
        }

        break;

      case '$not':
        match = !this._query(data, query);
        break;

      case '$nor':
        for (var j = 0, qLen = query.length; j < qLen; j++){
          if (this._query(data, query[j])){
            match = false;
            break;
          }
        }

        break;

      default:
        var path = this._model.schema.path(key);

        if (!path){
          path = item == null ? schemaType : types[item.constructor.name];
        }

        var item = path.cast(getProperty(data, key));

        if (getType(query) === 'Object'){
          var qKeys = Object.keys(query);

          for (var j = 0, qLen = qKeys.length; j < qLen; j++){
            var qKey = qKeys[j];

            if (/^\$/.test(qKey)){
              var operator = path['q' + qKey];
              if (operator == null) continue;

              match = operator(item, query[qKey]);
            } else {
              match = this._query(item, query[qKey]);
            }

            if (!match) break;
          }
        } else {
          match = path.compare(item, query);
        }
    }

    if (!match) break;
  }

  return match;
};

/**
 * Finds the matching items.
 *
 * @param {Object} conditions
 * @param {Object} [options]
 * @return {Query}
 * @api public
 */

Query.prototype.find = function(conditions, options){
  var index = this._index,
    model = this._model,
    arr = [];

  for (var i = 0, len = this.length; i < len; i++){
    if (this._query(model._getRaw(index[i]), conditions)) arr.push(index[i]);
    if (options && arr.length === options.limit) break;
  }

  return this._createQuery(arr);
};

/**
 * Finds the first matching item.
 *
 * @param {Object} conditions
 * @return {Document}
 * @api public
 */

Query.prototype.findOne = function(conditions){
  return this.find(conditions, {limit: 1}).first();
};

/**
 * Sets the path to populate.
 *
 * @param {String} name
 * @return {Query}
 * @api public
 */

Query.prototype.populate = function(name){
  this._populates.push(name);

  return this;
};<commit_msg>Fixed copy populate when creating query<commit_after>/**
 * Module dependencies.
 */

var _ = require('lodash'),
  util = require('./util'),
  Types = require('./types'),
  SchemaType = require('./schematype'),
  getProperty = util.getProperty,
  getType = util.getType;

var schemaType = new SchemaType();

var types = {
  Array: new Types.Array(),
  Boolean: new Types.Boolean(),
  Date: new Types.Date(),
  Object: new Types.Object(),
  Number: new Types.Number(),
  String: new Types.String()
};

/**
 * Creates a new query instance.
 *
 * @param {Array} index
 * @api public
 */

var Query = module.exports = function(index){
  this._index = index;
  this._populates = [];

  this.__defineGetter__('length', function(){
    return index.length;
  });
};

/**
 * Creates a new query instance.
 *
 * @param {Array} index
 * @return {Query}
 * @api private
 */

Query.prototype._createQuery = function(index){
  if (!index) index = this._index.slice();

  var query = new this._model._query(index);

  query._populates = this._populates.slice();

  return query;
};

/**
 * Iterates over the query.
 *
 * @param {Function} iterator
 * @return {Query}
 * @api public
 */

Query.prototype.forEach = Query.prototype.each = function(iterator){
  var index = this._index,
    model = this._model,
    populates = this._populates;

  for (var i = 0, len = index.length; i < len; i++){
    iterator(model._populate(model.get(index[i]), populates), i);
  }

  return this;
};

/**
 * Returns an array containing all items.
 *
 * @return {Array}
 * @api public
 */

Query.prototype.toArray = function(){
  var arr = [];

  this.each(function(item){
    arr.push(item);
  });

  return arr;
};

/**
 * Returns the number of items in the query.
 *
 * @return {Number}
 * @api public
 */

Query.prototype.count = function(){
  return this.length;
};

/**
 * Updates all items in the query.
 *
 * @param {Object} obj
 * @param {Function} [callback]
 * @return {Query}
 * @api public
 */

Query.prototype.update = function(obj, callback){
  var model = this._model,
    index = this._index,
    arr = [];

  for (var i = 0, len = index.length; i < len; i++){
    model.updateById(index[i], obj, function(item){
      if (item) arr.push(item);
    });
  }

  callback && callback(arr);

  return this;
};

/**
 * Replaces all items in the query.
 *
 * @param {Object} obj
 * @param {Function} [callback]
 * @return {Query}
 * @api public
 */

Query.prototype.replace = function(obj, callback){
  var model = this._model,
    index = this._index,
    arr = [];

  for (var i = 0, len = index.length; i < len; i++){
    model.replaceById(index[i], obj, function(item){
      if (item) arr.push(item);
    });
  }

  callback && callback(arr);

  return this;
};

/**
 * Removes all items in the query.
 *
 * @param {Function} [callback]
 * @return {Query}
 * @api public
 */

Query.prototype.remove = function(callback){
  var model = this._model,
    index = this._index,
    arr = [];

  for (var i = 0, len = index.length; i < len; i++){
    model.removeById(index[i], function(item){
      if (item) arr.push(item);
    });
  }

  callback && callback(arr);

  return this;
};

/**
 * Gets the item at the specified index.
 *
 * @param {Number} num
 * @return {Document}
 * @api public
 */

Query.prototype.eq = function(num){
  if (this.length){
    if (num < 0) num = this.length + num;
    return this._model.get(this._index[num]);
  }
};

/**
 * Returns the first item in the query.
 *
 * @return {Document}
 * @api public
 */

Query.prototype.first = function(){
  return this.eq(0);
};

/**
 * Returns the last item in the query.
 *
 * @return {Document}
 * @api public
 */

Query.prototype.last = function(){
  return this.eq(-1);
};

/**
 * Returns the specified range of the query.
 *
 * @param {Number} start
 * @param {Number} [end]
 * @return {Query}
 * @api public
 */

Query.prototype.slice = function(start, end){
  return this._createQuery([].slice.apply(this._index, arguments));
};

/**
 * Limits the number of items returned.
 *
 * @param {Number} num
 * @return {Query}
 * @api public
 */

Query.prototype.limit = function(num){
  return this.slice(0, num);
};

/**
 * Specifies the number of items to skip.
 *
 * @param {Number} num
 * @return {Query}
 * @api public
 */

Query.prototype.skip = function(num){
  return this.slice(num);
};

/**
 * Returns the query in reversed order.
 *
 * @return {Query}
 * @api public
 */

Query.prototype.reverse = function(){
  return this._createQuery(util.reverse(this._index));
};

/**
 * Sorts the model.
 *
 * @param {String|Object} orderby
 * @param {Number|String} [order]
 * @return {Query}
 * @api public
 */

Query.prototype.sort = function(orderby, order){
  if (_.isObject(orderby)){
    var query = this._createQuery();

    for (var i in orderby){
      (function(i){
        query = query.sort(i, orderby[i]);
      })(i);
    }

    return query;
  }

  var data = {};

  this.each(function(item){
    data[item._id] = item;
  });

  var arr = this._index.slice().sort(function(a, b){
    var orderA = getProperty(data[a], orderby),
      orderB = getProperty(data[b], orderby);

    if (orderA < orderB){
      return -1;
    } else if (orderA > orderB){
      return 1;
    } else {
      return 0;
    }
  });

  if (order){
    order = order.toString().toLowerCase();
    if (order == -1 || order == 'desc' || order == 'descending') arr.reverse();
  }

  return this._createQuery(arr);
};

/**
 * Returns the model in random order.
 *
 * @return {Query}
 * @api public
 */

Query.prototype.random = Query.prototype.shuffle = function(){
  return this._createQuery(util.shuffle(this._index));
};

/**
 * Finds the matching items.
 *
 * @param {Object} data
 * @param {Object} conditions
 * @return {Boolean}
 * @api private
 */

Query.prototype._query = function(data, conditions){
  var match = true,
    keys = Object.keys(conditions);

  for (var i = 0, len = keys.length; i < len; i++){
    var key = keys[i],
      query = conditions[key];

    switch (key){
      case '$or':
        var subMatch = false;

        for (var j = 0, qLen = query.length; j < qLen; j++){
          if (this._query(data, query[j])){
            subMatch = true;
            break;
          }
        }

        match = subMatch;

        break;

      case '$and':
        for (var j = 0, qLen = query.length; j < qLen; j++){
          if (!this._query(data, query[j])){
            match = false;
            break;
          }
        }

        break;

      case '$not':
        match = !this._query(data, query);
        break;

      case '$nor':
        for (var j = 0, qLen = query.length; j < qLen; j++){
          if (this._query(data, query[j])){
            match = false;
            break;
          }
        }

        break;

      default:
        var path = this._model.schema.path(key);

        if (!path){
          path = item == null ? schemaType : types[item.constructor.name];
        }

        var item = path.cast(getProperty(data, key));

        if (getType(query) === 'Object'){
          var qKeys = Object.keys(query);

          for (var j = 0, qLen = qKeys.length; j < qLen; j++){
            var qKey = qKeys[j];

            if (/^\$/.test(qKey)){
              var operator = path['q' + qKey];
              if (operator == null) continue;

              match = operator(item, query[qKey]);
            } else {
              match = this._query(item, query[qKey]);
            }

            if (!match) break;
          }
        } else {
          match = path.compare(item, query);
        }
    }

    if (!match) break;
  }

  return match;
};

/**
 * Finds the matching items.
 *
 * @param {Object} conditions
 * @param {Object} [options]
 * @return {Query}
 * @api public
 */

Query.prototype.find = function(conditions, options){
  var index = this._index,
    model = this._model,
    arr = [];

  for (var i = 0, len = this.length; i < len; i++){
    if (this._query(model._getRaw(index[i]), conditions)) arr.push(index[i]);
    if (options && arr.length === options.limit) break;
  }

  return this._createQuery(arr);
};

/**
 * Finds the first matching item.
 *
 * @param {Object} conditions
 * @return {Document}
 * @api public
 */

Query.prototype.findOne = function(conditions){
  return this.find(conditions, {limit: 1}).first();
};

/**
 * Sets the path to populate.
 *
 * @param {String} name
 * @return {Query}
 * @api public
 */

Query.prototype.populate = function(name){
  this._populates.push(name);

  return this;
};<|endoftext|>


<commit_before>/**
 * vCard Constructor
 * @param  {String|Object} data
 * @return {vCard}
 */
function vCard( data ) {

  if( !(this instanceof vCard) )
    return new vCard( data )

  this.version = '4.0'

  if( typeof data === 'string' ) {
    this.parse( data )
  } else if( data != null ) {
    this.set( data )
  }

}

// Exports
module.exports = vCard

// Expose data about fields 'n' stuff for external use
vCard.PROPERTIES = require( './properties' )
vCard.PARAMETERS = require( './parameters' )
vCard.DATA_TYPES = require( './data-types' )

/**
 * Parses a string into a vCard object
 * @param  {String} data
 * @return {vCard}
 */
vCard.parse = function parse( data ) {
  return new vCard().parse( data )
}

/**
 * Merges two vCard objects
 * @param  {vCard} a
 * @param  {vCard} b
 * @return {vCard}
 */
vCard.merge = function merge( a, b ) {

  if( !(a instanceof vCard) || !(b instanceof vCard) )
    throw new TypeError( 'Cards to be merged must be of type vCard' )

  throw new Error( 'Not implemented' )

}

/**
 * vCard prototype
 * @type {Object}
 */
vCard.prototype = {

  constructor: vCard,

  /**
   * [set description]
   * @param {String} key
   * @param {Mixed}  value
   * @param {Object} params
   */
  set: function( key, value, params ) {

    // Enable omitting the value argument
    if( arguments.length === 2 ) {
      params = value
      value = null
    } else {
      params.data = value
    }

    // Handle multiple entries with the same key
    if( this[ key ] ) {
      Array.isArray( this[ key ] ) ?
        this[ key ].push( params ) :
        this[ key ] = [ this[ key ], params ]
    } else {
      this[ key ] = params
    }

    return this

  },

  /**
   * [parse description]
   * @param  {String} data
   * @return {vCard}
   */
  parse: function( data ) {

    // Normalize and split into lines
    var lines = ( data + '' )
      // Trim whitespace
      .replace( /^\s+|\s+$/g, '' )
      // Trim blank lines
      .replace( /\r?\n(\s+|$)/g, '' )
      // Unfold folded lines
      .replace( /\r?\n[\x20\x09]+/g, '' )
      // Split into lines
      .split( /\r?\n/g )

    // Bail out, if we got nothing to parse
    if( !lines.length )
      return

    // Keep begin and end markers
    // for eventual error messages
    var begin, version, end, line

    // Check for begin marker
    if( begin = lines.shift() && /BEGIN:VCARD/i.test( begin ) ) {
      throw new SyntaxError(
        'Invalid format: expected "BEGIN:VCARD" but found "'+ begin +'"'
      )
    }

    // Check for the end marker
    if( end = lines.pop() && /END:VCARD/i.test( end ) ) {
      throw new SyntaxError(
        'Invalid format: expected "END:VCARD" but found "'+ end +'"'
      )
    }

    // Check for version number
    if( version = lines.pop() && /VERSION:\d\.\d/i.test( version ) ) {
      throw new SyntaxError(
        'Invalid format: expected "VERSION:\\d.\\d" but found "'+ version +'"'
      )
    }

    // Process each line
    while( line = lines.shift() ) {
      this.parseLine( line )
    }

    return this

  },

  /**
   * [parseLine description]
   * @param  {String} line
   * @return {vCard}
   */
  parseLine: function( line ) {

    var match = line
      // Trim whitespace
      .replace( /^\s+|\s+$/g, '' )
      // Match key / value
      .match( /^([^:;]+)[;:](.*)$/i )

    if( match == null ) {
      throw new SyntaxError(
        'Invalid format: "'+ line + '"'
      )
    }

    // Normalize key & value
    var key = match[1].toLowerCase().replace( /-/g, '_' )
    var value = match[2].replace( /^\s+|\s+$/g, '' )

    // Primitive fields (Cardinality=1)
    if( key.match( /version|gender|fn/ ) ) {
      this[ key ] = value
      return this
    }

    // Field data object
    var field = {}

    // Extract PARAM="VAL,VAL;VAL" (escaped value)
    // and PARAM=VAL,VAL (multi value)
    value = value.replace(
      /(?:(?:([a-z0-9-]+)=(")([^"]+)")|(?:([a-z0-9-]+)=([^";:]+)))(?:[;:]|$)/ig,
      function param( match ) {

        var escaped = arguments[2] === '"'
        var value = arguments[3] || arguments[5]
        var key = ( arguments[1] || arguments[4] )
          .toLowerCase().replace( /-/g, '_' )

        var values = value.split( /\s*,\s*/g )

        values.length < 2 || escaped ?
          field[ key ] = value :
          field[ key ] = values

        return ''

      }
    )

    // Set the actual literal value
    if( value.length ) {
      field.data = value
    }

    // ...
    this.set( key, field )

    return this

  },

  /**
   * [toString description]
   * @param  {Number} version
   * @return {String}
   */
  toString: function( version ) {

    version = version || this.version

    throw new Error( 'Not implemented' )

  }

}
<commit_msg>Removed setting of data in constructor<commit_after>/**
 * vCard Constructor
 * @param  {String|Object} data
 * @return {vCard}
 */
function vCard( data ) {

  if( !(this instanceof vCard) )
    return new vCard( data )

  this.version = '4.0'

  if( data != null ) {
    this.parse( data )
  }

}

// Exports
module.exports = vCard

// Expose data about fields 'n' stuff for external use
vCard.PROPERTIES = require( './properties' )
vCard.PARAMETERS = require( './parameters' )
vCard.DATA_TYPES = require( './data-types' )

/**
 * Parses a string into a vCard object
 * @param  {String} data
 * @return {vCard}
 */
vCard.parse = function parse( data ) {
  return new vCard().parse( data )
}

/**
 * Merges two vCard objects
 * @param  {vCard} a
 * @param  {vCard} b
 * @return {vCard}
 */
vCard.merge = function merge( a, b ) {

  if( !(a instanceof vCard) || !(b instanceof vCard) )
    throw new TypeError( 'Cards to be merged must be of type vCard' )

  throw new Error( 'Not implemented' )

}

/**
 * vCard prototype
 * @type {Object}
 */
vCard.prototype = {

  constructor: vCard,

  /**
   * [set description]
   * @param {String} key
   * @param {Mixed}  value
   * @param {Object} params
   */
  set: function( key, value, params ) {

    // Enable omitting the value argument
    if( arguments.length === 2 ) {
      params = value
      value = null
    } else {
      params.data = value
    }

    // Handle multiple entries with the same key
    if( this[ key ] ) {
      Array.isArray( this[ key ] ) ?
        this[ key ].push( params ) :
        this[ key ] = [ this[ key ], params ]
    } else {
      this[ key ] = params
    }

    return this

  },

  /**
   * [parse description]
   * @param  {String} data
   * @return {vCard}
   */
  parse: function( data ) {

    // Normalize and split into lines
    var lines = ( data + '' )
      // Trim whitespace
      .replace( /^\s+|\s+$/g, '' )
      // Trim blank lines
      .replace( /\r?\n(\s+|$)/g, '' )
      // Unfold folded lines
      .replace( /\r?\n[\x20\x09]+/g, '' )
      // Split into lines
      .split( /\r?\n/g )

    // Bail out, if we got nothing to parse
    if( !lines.length )
      return

    // Keep begin and end markers
    // for eventual error messages
    var begin, version, end, line

    // Check for begin marker
    if( begin = lines.shift() && /BEGIN:VCARD/i.test( begin ) ) {
      throw new SyntaxError(
        'Invalid format: expected "BEGIN:VCARD" but found "'+ begin +'"'
      )
    }

    // Check for the end marker
    if( end = lines.pop() && /END:VCARD/i.test( end ) ) {
      throw new SyntaxError(
        'Invalid format: expected "END:VCARD" but found "'+ end +'"'
      )
    }

    // Check for version number
    if( version = lines.pop() && /VERSION:\d\.\d/i.test( version ) ) {
      throw new SyntaxError(
        'Invalid format: expected "VERSION:\\d.\\d" but found "'+ version +'"'
      )
    }

    // Process each line
    while( line = lines.shift() ) {
      this.parseLine( line )
    }

    return this

  },

  /**
   * [parseLine description]
   * @param  {String} line
   * @return {vCard}
   */
  parseLine: function( line ) {

    var match = line
      // Trim whitespace
      .replace( /^\s+|\s+$/g, '' )
      // Match key / value
      .match( /^([^:;]+)[;:](.*)$/i )

    if( match == null ) {
      throw new SyntaxError(
        'Invalid format: "'+ line + '"'
      )
    }

    // Normalize key & value
    var key = match[1].toLowerCase().replace( /-/g, '_' )
    var value = match[2].replace( /^\s+|\s+$/g, '' )

    // Primitive fields (Cardinality=1)
    if( key.match( /version|gender|fn/ ) ) {
      this[ key ] = value
      return this
    }

    // Field data object
    var field = {}

    // Extract PARAM="VAL,VAL;VAL" (escaped value)
    // and PARAM=VAL,VAL (multi value)
    value = value.replace(
      /(?:(?:([a-z0-9-]+)=(")([^"]+)")|(?:([a-z0-9-]+)=([^";:]+)))(?:[;:]|$)/ig,
      function param( match ) {

        var escaped = arguments[2] === '"'
        var value = arguments[3] || arguments[5]
        var key = ( arguments[1] || arguments[4] )
          .toLowerCase().replace( /-/g, '_' )

        var values = value.split( /\s*,\s*/g )

        values.length < 2 || escaped ?
          field[ key ] = value :
          field[ key ] = values

        return ''

      }
    )

    // Set the actual literal value
    if( value.length ) {
      field.data = value
    }

    // ...
    this.set( key, field )

    return this

  },

  /**
   * [toString description]
   * @param  {Number} version
   * @return {String}
   */
  toString: function( version ) {

    version = version || this.version

    throw new Error( 'Not implemented' )

  }

}
<|endoftext|>


<commit_before>module.exports = Queue

function Queue(stream) {
    var ended = false
        , array = []
        , queue = {
            shift: shift
            , push: push
            , end: end
            , error: error
            , length: 0
        }

    return queue

    function shift() {
        var chunk = array.shift()
        if (ended && array.length === 0) {
            process.nextTick(emitEnd)
        }
        queue.length = array.length
        return chunk
    }

    function push(chunk) {
        if (ended) {
            return
        } else if (chunk === null) {
            return end()
        }

        array.push(chunk)
        if (array.length === 1) {
            stream.emit("readable")
        }

        queue.length = array.length
    }

    function end(chunk) {
        if (ended) {
            return
        }

        ended = true

        if (arguments.length) {
            push(chunk)
        }

        if (array.length === 0) {
            stream.emit("end")
        }
    }

    function error(err) {
        stream.emit("error", err)
    }

    function emitEnd() {
        stream.emit("end")
    }
}
<commit_msg>remove gaurd in push<commit_after>module.exports = Queue

function Queue(stream) {
    var ended = false
        , array = []
        , queue = {
            shift: shift
            , push: push
            , end: end
            , error: error
            , length: 0
        }

    return queue

    function shift() {
        var chunk = array.shift()
        if (ended && array.length === 0) {
            process.nextTick(emitEnd)
        }
        queue.length = array.length
        return chunk
    }

    function push(chunk) {
        if (chunk === null) {
            return end()
        }

        array.push(chunk)
        if (array.length === 1) {
            stream.emit("readable")
        }

        queue.length = array.length
    }

    function end(chunk) {
        if (ended) {
            return
        }

        ended = true

        if (arguments.length) {
            push(chunk)
        }

        if (array.length === 0) {
            stream.emit("end")
        }
    }

    function error(err) {
        stream.emit("error", err)
    }

    function emitEnd() {
        stream.emit("end")
    }
}
<|endoftext|>