<commit_before>/* global BreakString:true */

/*
* memoize.js
* by @philogb and @addyosmani
* with further optimizations by @mathias
* and @DmitryBaranovsk
* perf tests: http://bit.ly/q3zpG3
* Released under an MIT license.
*
* modified with cap by Sam
*/
function cappedMemoize(fn, max) {
    fn.maxMemoize = max;
    fn.memoizeLength = 0;

    return function () {
        const args = Array.prototype.slice.call(arguments);
        let hash = "";
        let i = args.length;
        let currentArg = null;
        while (i--) {
            currentArg = args[i];
            hash += (currentArg === new Object(currentArg)) ?
            JSON.stringify(currentArg) : currentArg;
            if(!fn.memoize) {
              fn.memoize = {};
            }
        }
        if (hash in fn.memoize) {
          return fn.memoize[hash];
        } else {
          fn.memoizeLength++;
          if(fn.memoizeLength > max) {
            fn.memoizeLength = 0;
            fn.memoize = {};
          }
          const result = fn.apply(this, args);
          fn.memoize[hash] = result;
          return result;
        }
    };
}

const breakUp = cappedMemoize(function(str, hint){
  return new BreakString(str).break(hint);
}, 100);
export { breakUp };

export function shortDate(date){
  return moment(date).format(I18n.t("dates.medium.date_year"));
}

function shortDateNoYear(date) {
  return moment(date).format(I18n.t("dates.tiny.date_month"));
}

// Suppress year if it's this year
export function smartShortDate(date, withYear=tinyDateYear) {
  return (date.getFullYear() === new Date().getFullYear()) ? shortDateNoYear(date) : withYear(date);
}

export function tinyDateYear(date) {
  return moment(date).format(I18n.t("dates.tiny.date_year"));
}

// http://stackoverflow.com/questions/196972/convert-string-to-title-case-with-javascript
// TODO: locale support ?
export function toTitleCase(str) {
  return str.replace(/\w\S*/g, function(txt){
    return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
  });
}

export function longDate(dt) {
  if (!dt) return;
  return moment(dt).longDate();
}

// suppress year, if current year
export function longDateNoYear(dt) {
  if (!dt) return;

  if ((new Date()).getFullYear() !== dt.getFullYear()) {
    return moment(dt).format(I18n.t("dates.long_date_with_year"));
  } else {
    return moment(dt).format(I18n.t("dates.long_date_without_year"));
  }
}

export function updateRelativeAge(elems) {
  // jQuery .each
  elems.each(function(){
    const $this = $(this);
    $this.html(relativeAge(new Date($this.data('time')), {format: $this.data('format'), wrapInSpan: false}));
  });
}

export function autoUpdatingRelativeAge(date,options) {
  if (!date) return "";
  if (+date === +new Date(0)) return "";

  options = options || {};
  let format = options.format || "tiny";

  let append = "";
  if (format === 'medium') {
    append = " date";
    if(options.leaveAgo) {
      format = 'medium-with-ago';
    }
    options.wrapInSpan = false;
  }

  const relAge = relativeAge(date, options);

  if (format === 'tiny' && relativeAgeTinyShowsYear(relAge)) {
    append += " with-year";
  }

  if (options.title) {
    append += "' title='" + longDate(date);
  }

  return "" + relAge  + "";
}

function wrapAgo(dateStr) {
  return I18n.t("dates.wrap_ago", { date: dateStr });
}

function relativeAgeTiny(date, ageOpts) {
  const format = "tiny";
  const distance = Math.round((new Date() - date) / 1000);
  const distanceInMinutes = Math.round(distance / 60.0);

  let formatted;
  const t = function(key, opts) {
    const result = I18n.t("dates." + format + "." + key, opts);
    return (ageOpts && ageOpts.addAgo) ? wrapAgo(result) : result;
  };

  switch(true) {
    case(distanceInMinutes < 1):
      formatted = t("less_than_x_minutes", {count: 1});
      break;
    case(distanceInMinutes >= 1 && distanceInMinutes <= 44):
      formatted = t("x_minutes", {count: distanceInMinutes});
      break;
    case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
      formatted = t("about_x_hours", {count: 1});
      break;
    case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
      formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
      break;
    case(Discourse.SiteSettings.relative_date_duration === 0 && distanceInMinutes <= 525599):
      formatted = shortDateNoYear(date);
      break;
    case(distanceInMinutes >= 1410 && distanceInMinutes <= 2519):
      formatted = t("x_days", {count: 1});
      break;
    case(distanceInMinutes >= 2520 && distanceInMinutes <= ((Discourse.SiteSettings.relative_date_duration||14) * 1440)):
      formatted = t("x_days", {count: Math.round(distanceInMinutes / 1440.0)});
      break;
    default:
      formatted = (ageOpts.defaultFormat || smartShortDate)(date);
      break;
  }

  return formatted;
}

/*
 * Returns true if the given tiny date string includes the year.
 * Useful for checking if the string isn't so tiny.
 */
function relativeAgeTinyShowsYear(relativeAgeString) {
  return relativeAgeString.match(/'[\d]{2}$/);
}

function relativeAgeMediumSpan(distance, leaveAgo) {
  let formatted;
  const distanceInMinutes = Math.round(distance / 60.0);

  const t = function(key, opts){
    return I18n.t("dates.medium" + (leaveAgo?"_with_ago":"") + "." + key, opts);
  };

  switch(true){
  case(distanceInMinutes >= 1 && distanceInMinutes <= 55):
    formatted = t("x_minutes", {count: distanceInMinutes});
    break;
  case(distanceInMinutes >= 56 && distanceInMinutes <= 89):
    formatted = t("x_hours", {count: 1});
    break;
  case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
    formatted = t("x_hours", {count: Math.round(distanceInMinutes / 60.0)});
    break;
  case(distanceInMinutes >= 1410 && distanceInMinutes <= 2159):
    formatted = t("x_days", {count: 1});
    break;
  case(distanceInMinutes >= 2160):
    formatted = t("x_days", {count: Math.round((distanceInMinutes - 720.0) / 1440.0)});
    break;
  }
  return formatted || '&mdash';
}

function relativeAgeMedium(date, options) {
  const wrapInSpan = options.wrapInSpan !== false;
  const leaveAgo = options.leaveAgo;
  const distance = Math.round((new Date() - date) / 1000);

  if (!date) {
    return "—";
  }

  const fullReadable = longDate(date);
  const fiveDaysAgo = 432000;
  const oneMinuteAgo = 60;

  let displayDate = "";
  if (distance < oneMinuteAgo) {
    displayDate = I18n.t("now");
  } else if (distance > fiveDaysAgo) {
    displayDate = smartShortDate(date, shortDate);
  } else {
    displayDate = relativeAgeMediumSpan(distance, leaveAgo);
  }
  if(wrapInSpan) {
    return "" + displayDate + "";
  } else {
    return displayDate;
  }
}

// mostly lifted from rails with a few amendments
export function relativeAge(date, options) {
  options = options || {};
  const format = options.format || "tiny";

  if (format === "tiny") {
    return relativeAgeTiny(date, options);
  } else if (format === "medium") {
    return relativeAgeMedium(date, options);
  } else if (format === 'medium-with-ago') {
    return relativeAgeMedium(date, _.extend(options, {format: 'medium', leaveAgo: true}));
  }

  return "UNKNOWN FORMAT";
}

export function number(val) {
  let formattedNumber;

  val = parseInt(val, 10);
  if (isNaN(val)) val = 0;

  if (val > 999999) {
    formattedNumber = I18n.toNumber(val / 1000000, {precision: 1});
    return I18n.t("number.short.millions", {number: formattedNumber});
  }
  if (val > 999) {
    formattedNumber = I18n.toNumber(val / 1000, {precision: 1});
    return I18n.t("number.short.thousands", {number: formattedNumber});
  }
  return val.toString();
}
<commit_msg>FIX: `mdash` being displayed when a date was invalid<commit_after>/* global BreakString:true */

/*
* memoize.js
* by @philogb and @addyosmani
* with further optimizations by @mathias
* and @DmitryBaranovsk
* perf tests: http://bit.ly/q3zpG3
* Released under an MIT license.
*
* modified with cap by Sam
*/
function cappedMemoize(fn, max) {
    fn.maxMemoize = max;
    fn.memoizeLength = 0;

    return function () {
        const args = Array.prototype.slice.call(arguments);
        let hash = "";
        let i = args.length;
        let currentArg = null;
        while (i--) {
            currentArg = args[i];
            hash += (currentArg === new Object(currentArg)) ?
            JSON.stringify(currentArg) : currentArg;
            if(!fn.memoize) {
              fn.memoize = {};
            }
        }
        if (hash in fn.memoize) {
          return fn.memoize[hash];
        } else {
          fn.memoizeLength++;
          if(fn.memoizeLength > max) {
            fn.memoizeLength = 0;
            fn.memoize = {};
          }
          const result = fn.apply(this, args);
          fn.memoize[hash] = result;
          return result;
        }
    };
}

const breakUp = cappedMemoize(function(str, hint){
  return new BreakString(str).break(hint);
}, 100);
export { breakUp };

export function shortDate(date){
  return moment(date).format(I18n.t("dates.medium.date_year"));
}

function shortDateNoYear(date) {
  return moment(date).format(I18n.t("dates.tiny.date_month"));
}

// Suppress year if it's this year
export function smartShortDate(date, withYear=tinyDateYear) {
  return (date.getFullYear() === new Date().getFullYear()) ? shortDateNoYear(date) : withYear(date);
}

export function tinyDateYear(date) {
  return moment(date).format(I18n.t("dates.tiny.date_year"));
}

// http://stackoverflow.com/questions/196972/convert-string-to-title-case-with-javascript
// TODO: locale support ?
export function toTitleCase(str) {
  return str.replace(/\w\S*/g, function(txt){
    return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
  });
}

export function longDate(dt) {
  if (!dt) return;
  return moment(dt).longDate();
}

// suppress year, if current year
export function longDateNoYear(dt) {
  if (!dt) return;

  if ((new Date()).getFullYear() !== dt.getFullYear()) {
    return moment(dt).format(I18n.t("dates.long_date_with_year"));
  } else {
    return moment(dt).format(I18n.t("dates.long_date_without_year"));
  }
}

export function updateRelativeAge(elems) {
  // jQuery .each
  elems.each(function(){
    const $this = $(this);
    $this.html(relativeAge(new Date($this.data('time')), {format: $this.data('format'), wrapInSpan: false}));
  });
}

export function autoUpdatingRelativeAge(date,options) {
  if (!date) return "";
  if (+date === +new Date(0)) return "";

  options = options || {};
  let format = options.format || "tiny";

  let append = "";
  if (format === 'medium') {
    append = " date";
    if(options.leaveAgo) {
      format = 'medium-with-ago';
    }
    options.wrapInSpan = false;
  }

  const relAge = relativeAge(date, options);

  if (format === 'tiny' && relativeAgeTinyShowsYear(relAge)) {
    append += " with-year";
  }

  if (options.title) {
    append += "' title='" + longDate(date);
  }

  return "" + relAge  + "";
}

function wrapAgo(dateStr) {
  return I18n.t("dates.wrap_ago", { date: dateStr });
}

function relativeAgeTiny(date, ageOpts) {
  const format = "tiny";
  const distance = Math.round((new Date() - date) / 1000);
  const distanceInMinutes = Math.round(distance / 60.0);

  let formatted;
  const t = function(key, opts) {
    const result = I18n.t("dates." + format + "." + key, opts);
    return (ageOpts && ageOpts.addAgo) ? wrapAgo(result) : result;
  };

  switch(true) {
    case(distanceInMinutes < 1):
      formatted = t("less_than_x_minutes", {count: 1});
      break;
    case(distanceInMinutes >= 1 && distanceInMinutes <= 44):
      formatted = t("x_minutes", {count: distanceInMinutes});
      break;
    case(distanceInMinutes >= 45 && distanceInMinutes <= 89):
      formatted = t("about_x_hours", {count: 1});
      break;
    case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
      formatted = t("about_x_hours", {count: Math.round(distanceInMinutes / 60.0)});
      break;
    case(Discourse.SiteSettings.relative_date_duration === 0 && distanceInMinutes <= 525599):
      formatted = shortDateNoYear(date);
      break;
    case(distanceInMinutes >= 1410 && distanceInMinutes <= 2519):
      formatted = t("x_days", {count: 1});
      break;
    case(distanceInMinutes >= 2520 && distanceInMinutes <= ((Discourse.SiteSettings.relative_date_duration||14) * 1440)):
      formatted = t("x_days", {count: Math.round(distanceInMinutes / 1440.0)});
      break;
    default:
      formatted = (ageOpts.defaultFormat || smartShortDate)(date);
      break;
  }

  return formatted;
}

/*
 * Returns true if the given tiny date string includes the year.
 * Useful for checking if the string isn't so tiny.
 */
function relativeAgeTinyShowsYear(relativeAgeString) {
  return relativeAgeString.match(/'[\d]{2}$/);
}

function relativeAgeMediumSpan(distance, leaveAgo) {
  let formatted;
  const distanceInMinutes = Math.round(distance / 60.0);

  const t = function(key, opts){
    return I18n.t("dates.medium" + (leaveAgo?"_with_ago":"") + "." + key, opts);
  };

  switch(true){
  case(distanceInMinutes >= 1 && distanceInMinutes <= 55):
    formatted = t("x_minutes", {count: distanceInMinutes});
    break;
  case(distanceInMinutes >= 56 && distanceInMinutes <= 89):
    formatted = t("x_hours", {count: 1});
    break;
  case(distanceInMinutes >= 90 && distanceInMinutes <= 1409):
    formatted = t("x_hours", {count: Math.round(distanceInMinutes / 60.0)});
    break;
  case(distanceInMinutes >= 1410 && distanceInMinutes <= 2159):
    formatted = t("x_days", {count: 1});
    break;
  case(distanceInMinutes >= 2160):
    formatted = t("x_days", {count: Math.round((distanceInMinutes - 720.0) / 1440.0)});
    break;
  }
  return formatted || '—';
}

function relativeAgeMedium(date, options) {
  const wrapInSpan = options.wrapInSpan !== false;
  const leaveAgo = options.leaveAgo;
  const distance = Math.round((new Date() - date) / 1000);

  if (!date) {
    return "—";
  }

  const fullReadable = longDate(date);
  const fiveDaysAgo = 432000;
  const oneMinuteAgo = 60;

  let displayDate = "";
  if (distance < oneMinuteAgo) {
    displayDate = I18n.t("now");
  } else if (distance > fiveDaysAgo) {
    displayDate = smartShortDate(date, shortDate);
  } else {
    displayDate = relativeAgeMediumSpan(distance, leaveAgo);
  }
  if(wrapInSpan) {
    return "" + displayDate + "";
  } else {
    return displayDate;
  }
}

// mostly lifted from rails with a few amendments
export function relativeAge(date, options) {
  options = options || {};
  const format = options.format || "tiny";

  if (format === "tiny") {
    return relativeAgeTiny(date, options);
  } else if (format === "medium") {
    return relativeAgeMedium(date, options);
  } else if (format === 'medium-with-ago') {
    return relativeAgeMedium(date, _.extend(options, {format: 'medium', leaveAgo: true}));
  }

  return "UNKNOWN FORMAT";
}

export function number(val) {
  let formattedNumber;

  val = parseInt(val, 10);
  if (isNaN(val)) val = 0;

  if (val > 999999) {
    formattedNumber = I18n.toNumber(val / 1000000, {precision: 1});
    return I18n.t("number.short.millions", {number: formattedNumber});
  }
  if (val > 999) {
    formattedNumber = I18n.toNumber(val / 1000, {precision: 1});
    return I18n.t("number.short.thousands", {number: formattedNumber});
  }
  return val.toString();
}
<|endoftext|>


<commit_before>//////////////////////////////////////////
// Needle -- Node.js HTTP Client
// Written by Tomás Pollak <tomas@forkhq.com>
// (c) 2012-2013 - Fork Ltd.
// MIT Licensed
//////////////////////////////////////////

var fs          = require('fs'),
    http        = require('http'),
    https       = require('https'),
    url         = require('url'),
    Stream      = require('stream'),
    stringify   = require('qs').stringify,
    multipart   = require('./multipart'),
    auth        = require('./auth'),
    assert      = require('assert');

//////////////////////////////////////////
// variabilia
//////////////////////////////////////////

var version     = JSON.parse(fs.readFileSync(__dirname + '/../package.json').toString()).version,
    debugging   = !!process.env.DEBUG;

var user_agent = 'Needle/' + version;
user_agent    += ' (Node.js ' + process.version + '; ' + process.platform + ' ' + process.arch + ')';

var node_tls_opts = 'agent pfx key passphrase cert ca ciphers rejectUnauthorized secureProtocol';

var debug = function() {
  if (debugging)
    console.log(arguments);
}

//////////////////////////////////////////
// decompressors mappings for content-encoding
//////////////////////////////////////////
var decompressors = {};
try {
  var zlib = require('zlib')

  decompressors['deflate'] = zlib.Inflate;
  decompressors['gzip']    = zlib.Gunzip;

} catch(e) { /* zlib not available */ }

//////////////////////////////////////////
// buffering stream class for our parsers
//////////////////////////////////////////

function parserFactory (fn) {
  return function () {
      var chunks = []
      var stream = new Stream.Transform({objectMode: true});

      // Buffer all our data
      stream._transform = function (chunk, encoding, done) {
          assert.ok(Buffer.isBuffer(chunk))
          chunks.push(chunk)
          done()
      },

      // And flush our tree..
      stream._flush = function (done) {
          var self = this;
          var data = Buffer.concat(chunks)

          try {
              fn(data,function(err, result) {
                  if (err) throw err

                  self.push(result);
              });

          } catch (err) {
              // If we were unable to parse, instead of bailing out, just pass the
              // original unparsed data.
              debug('Error while parsing: ', err)
              self.push(data);
          } finally {
              done();
          }
      }

      return stream;
  }
}

//////////////////////////////////////////
// content type parsers
//////////////////////////////////////////

var parsers = {};

parsers['application/json'] = parserFactory(function (buffer, callback) {
  var err, data;

  try {
    data = JSON.parse(buffer)
  } catch (e) {
    err = e;
  }

  callback(err, data);
});

parsers['text/javascript'] = parsers['application/json'];

try {
  var xml2js = require('xml2js');

  // xml2js.Parser.parseString() has the exact same function signature as our ParseStream
  // expects, so we can reuse this.
  parsers['application/xml'] =
        parserFactory(new xml2js.Parser({
          explicitRoot: true,
          explicitArray: false
        }).parseString);

  parsers['text/xml'] = parsers['application/xml'];
  parsers['application/rss+xml'] = parsers['application/xml'];
} catch(e) { /* xml2js not found */ }

//////////////////////////////////////////
// defaults
//////////////////////////////////////////

var defaults = {
  accept          : '*/*',
  connection      : 'close',
  user_agent      : user_agent,
  follow          : 0,
  decode_response : true,
  parse_response  : true,
  timeout         : 10000,
  encoding        : 'utf8',
  boundary        : '--------------------NODENEEDLEHTTPCLIENT'
}

//////////////////////////////////////////
// the main act
//////////////////////////////////////////

var Needle = {

  request: function(method, uri, data, options, callback) {

    var self     = this;
    var callback = (typeof options == 'function') ? options : callback;
    var options  = options || {};

    // if no 'http' is found on URL, prepend it
    if (uri.indexOf('http') == -1) uri = 'http://' + uri;

    var config = {
      base_opts       : {},
      proxy           : options.proxy,
      output          : options.output,
      encoding        : options.encoding || (options.multipart ? 'binary' : defaults.encoding),
      decode_response : options.decode === false ? false : defaults.decode_response,
      parse_response  : options.parse === false ? false : defaults.parse_response,
      follow          : options.follow === true ? 10 : typeof options.follow == 'number' ? options.follow : defaults.follow,
      timeout         : (typeof options.timeout == 'number') ? options.timeout : defaults.timeout
    }

    // if any of node's TLS options are passed, let them be passed to https.request()
    node_tls_opts.split(' ').forEach(function(key) {
      if (typeof options[key] != 'undefined') {
        config.base_opts[key] = options[key];
        if (typeof options.agent == 'undefined')
          config.base_opts.agent = false; // otherwise tls options are skipped
      }
    });

    config.headers = {
      'Accept'     : options.accept     || defaults.accept,
      'Connection' : options.connection || defaults.connection,
      'User-Agent' : options.user_agent || defaults.user_agent
    }

    if (options.compressed && typeof unzip != 'undefined')
      config.headers['Accept-Encoding'] = 'gzip,deflate';

    for (var h in options.headers)
      config.headers[h] = options.headers[h];

    if (options.username && options.password) {
      if (options.auth && (options.auth == 'auto' || options.auth == 'digest')) {
        config.credentials = [options.username, options.password];
      } else {
        var auth_header = options.proxy ? 'Proxy-Authorization' : 'Authorization';
        config.headers[auth_header] = auth.basic(options.username, options.password);
      }
    }

    if (data) {
      if (options.multipart) {
        var boundary = options.boundary || defaults.boundary;
        return multipart.build(data, boundary, function(err, body) {
          if (err) throw(err);
          config.headers['Content-Type'] = 'multipart/form-data; boundary=' + boundary;
          config.headers['Content-Length'] = body.length;
          self.send_request(1, method, uri, config, body, callback);
        });

      } else {
        var post_data = (typeof(data) === 'string') ? data :
            options.json ? JSON.stringify(data) : stringify(data);

        if (!config.headers['Content-Type']) {
          config.headers['Content-Type'] = options.json
          ? 'application/json'
          : 'application/x-www-form-urlencoded';
        }

        post_data = new Buffer(post_data, config.encoding)
        config.headers['Content-Length'] = post_data.length;
      }
    }

    return this.send_request(1, method, uri, config, post_data, callback);
  },

  get_request_opts: function(method, uri, config) {
    var opts      = config.base_opts, proxy = config.proxy;
    var remote    = proxy ? url.parse(proxy) : url.parse(uri);

    opts.protocol = remote.protocol;
    opts.host     = remote.hostname;
    opts.port     = remote.port || (remote.protocol == 'https:' ? 443 : 80);
    opts.path     = proxy ? uri : remote.pathname + (remote.search || '');
    opts.method   = method;
    opts.headers  = config.headers;
    opts.headers['Host'] = proxy ? url.parse(uri).hostname : remote.hostname;
    if (opts.port != 80 && opts.port != 443) opts.headers['Host'] += ':' + opts.port;

    return opts;
  },

  get_auth_header: function(header, credentials, request_opts) {
    var type = header.split(' ')[0],
        user = credentials[0],
        pass = credentials[1];

    if (type == 'Digest') {
      return auth.digest(header, user, pass, request_opts.method, request_opts.path);
    } else if (type == 'Basic') {
      return auth.basic(user, pass);
    }
  },

  send_request: function(count, method, uri, config, post_data, callback) {

    var timer,
        self = this,
        request_opts = this.get_request_opts(method, uri, config),
        protocol = request_opts.protocol == 'https:' ? https : http,
        i_am_stream = (callback ? false : true);

    // config.out is where all our body is streamed twards
    //
    // FIXME: set objectMode: true only when we're using a parser that emits
    // objects -- buffer/strings are far more performant that way.
    config.out = config.out || new Stream.PassThrough({objectMode: true, allowHalfOpen: false})

    debug('Making request #' + count, request_opts);
    var request = protocol.request(request_opts, function(resp) {

      var headers = resp.headers;
      debug('Got response', headers);
      if (timer) clearTimeout(timer);

      // if redirect code is found, send a GET request to that location if enabled via 'follow' option
      if ([301, 302].indexOf(resp.statusCode) != -1 && headers.location) {
        if (count <= config.follow)
          return self.send_request(++count, 'GET', url.resolve(uri, headers.location), config, null, callback);
        else if (config.follow > 0)
          return callback(new Error('Max redirects reached. Possible loop in: ' + headers.location));
      }

      // if authentication is requested and credentials were not passed, resend request if we have user/pass
      if (resp.statusCode == 401 && headers['www-authenticate'] && config.credentials) {
        if (!config.headers['Authorization']) { // only if authentication hasn't been sent
          var auth_header = self.get_auth_header(headers['www-authenticate'], config.credentials, request_opts);

          if (auth_header) {
            config.headers['Authorization'] = auth_header;
            return self.send_request(count, method, uri, config, post_data, callback);
          }
        }
      }

      var length = 0,
          mime = self.parse_content_type(headers['content-type']);

      var response_opts = {
        output       : config.output,
        parse        : config.parse_response, // parse XML or JSON
        content_type : mime.type,
        text         : mime.type && mime.type.indexOf('text/') != -1,
        charset      : mime.charset
      }

      if (response_opts.text)
        response_opts.decode = config.decode_response; // only allow iconv decoding on text bodies

      // `resp` at this point is our stream with raw body data coming over
      // the write. Let's initialize our streams pipiline to have the eventual
      // destination `config.out`.
      var pipeline = [];

      // First of all, if our body is compressed and we are able to decompress it,
      // decompress it.
      if (headers['content-encoding'] && decompressors[headers['content-encoding']]) {
        pipeline.push(decompressors[headers['content-encoding']]());
      }

      // Parsing the 'content-type' (xml, json, whatever) is one of the last
      // things we should do.
      if (response_opts.parse && parsers[response_opts.content_type]) {
        pipeline.push(parsers[response_opts.content_type]())

      } else {
        // If charset is not UTF-8 and the users wants to decode, decode. Note
        // that this is mutually exclusive with parsing JSON/XML/etc.
        if (response_opts.decode && opts.charset && !response_opts.charset.match(/utf-?8$/i)) {
          // TODO: in case of error, this emits 'error' on the stream -- is this
          // what we want?
          try {
            pipeline.push(require('iconv').Iconv('UTF-8', response_opts.charset))
          } catch (err) {
            // iconv library wasn't installed, do not do any decoding
            debug('iconv not installed!')
          }
        }
      }

      // And config.out is the stream we eventually push our sanitized body data to.
      pipeline.push(config.out)

      while (pipeline.length) {
        resp = resp.pipe(pipeline.shift())
      }

      // If the user has requested that we should write the data to a file, pipe the
      // output stream to this file stream.
      //
      // Note: since we do not make this part of the `pipeline` above, the user will still
      // receive all the response data, we just sniff on the stream and write the file.
      //
      // :FIXME: does this belong here anyway? Isn't it sufficient to let the end-user
      // do a needle.get(url).pipe(fs.createWriteStream()) ?
      if (response_opts.output && resp.statusCode == 200) {
        resp.pipe(fs.createWriteStream(response_opts.output))
      }

      // Since we do not know whether our underlying streams all behave properly,
      // let's fix it no matter what. This also has the added advantage that we will
      // not expose a witable stream to the end user.
      config.out = new Stream.Readable().wrap(config.out);

      resp.body = []

      // We do not want to aggregate the body contents if we are a stream, for performance
      // and scalability reasons.
      if (i_am_stream == false) {

        // Ah, but a callback has been requested, with all our data. Let's first aggregate
        // incoming data from the stream...
        config.out.on('readable', function () {
          while (chunk = this.read()) {
            // We're either pushing buffers or objects, never strings.
            if (typeof chunk == 'String') chunk = new Buffer(chunk)

            // What we will do here is simply make resp.body a mapping of all
            // our chunks. Later, in resp.end(), we reduce this to a single buffer.
            resp.body.push(chunk);
          }
        })

        // And set the .body property once all data is in.
        config.out.on('end', function () {
          // If our body is a string (Buffer), we need to merge those into one big buffer.
          if (resp.body.length != 0 && Buffer.isBuffer(resp.body[0])) {
            resp.body = Buffer.concat(resp.body);
          } else if (resp.body.length == 1) {
            assert(Buffer.isBuffer(resp.body[0]) == false, 'Internal error: body should be an array of objects, is a buffer?')
            // Do not treat body as an array when we have only one object
            resp.body = resp.body[0]
          }

          // :TODO: This doesn't make sense if resp.body is an array of objects.
          resp.bytes = resp.body.length;
        });

      };
    });

    // unless timeout was disabled, set a timeout to abort the request
    if (config.timeout > 0) {
      timer = setTimeout(function() {
        request.abort();
      }, config.timeout)
    }

    request.on('error', function(err) {
      debug('Request error', err);
      if (timer) clearTimeout(timer);
      if (callback) callback(err || new Error('Unknown error when making request.'));
    });

    if (post_data) request.write(post_data, config.encoding);
    request.end();

    return (i_am_stream ? config.out : request);

  },

  parse_content_type: function(header) {
    if (!header || header == '') return {};

    var charset = 'iso-8859-1', arr = header.split(';');
    try { charset = arr[1].match(/charset=(.+)/)[1] } catch (e) { /* not found */ }

    return { type: arr[0], charset: charset };
  },
}

exports.version = version;

exports.defaults = function(obj) {
  for (var key in obj) {
    if (defaults[key] && typeof obj[key] != 'undefined')
      defaults[key] = obj[key];
  }
  return defaults;
}

'head get'.split(' ').forEach(function(method) {
  exports[method] = function(uri, options, callback) {
    return Needle.request(method, uri, null, options, callback);
  }
})

'post put delete'.split(' ').forEach(function(method) {
  exports[method] = function(uri, data, options, callback) {
    return Needle.request(method, uri, data, options, callback);
  }
})

exports.request = function(method, uri, data, opts, callback) {
  return Needle.request(method.toUpperCase(), uri, data, opts, callback);
};
<commit_msg>Ensures content-length is set to the correct value.<commit_after>//////////////////////////////////////////
// Needle -- Node.js HTTP Client
// Written by Tomás Pollak <tomas@forkhq.com>
// (c) 2012-2013 - Fork Ltd.
// MIT Licensed
//////////////////////////////////////////

var fs          = require('fs'),
    http        = require('http'),
    https       = require('https'),
    url         = require('url'),
    Stream      = require('stream'),
    stringify   = require('qs').stringify,
    multipart   = require('./multipart'),
    auth        = require('./auth'),
    assert      = require('assert');

//////////////////////////////////////////
// variabilia
//////////////////////////////////////////

var version     = JSON.parse(fs.readFileSync(__dirname + '/../package.json').toString()).version,
    debugging   = !!process.env.DEBUG;

var user_agent = 'Needle/' + version;
user_agent    += ' (Node.js ' + process.version + '; ' + process.platform + ' ' + process.arch + ')';

var node_tls_opts = 'agent pfx key passphrase cert ca ciphers rejectUnauthorized secureProtocol';

var debug = function() {
  if (debugging)
    console.log(arguments);
}

//////////////////////////////////////////
// decompressors mappings for content-encoding
//////////////////////////////////////////
var decompressors = {};
try {
  var zlib = require('zlib')

  decompressors['deflate'] = zlib.Inflate;
  decompressors['gzip']    = zlib.Gunzip;

} catch(e) { /* zlib not available */ }

//////////////////////////////////////////
// buffering stream class for our parsers
//////////////////////////////////////////

function parserFactory (fn) {
  return function () {
      var chunks = []
      var stream = new Stream.Transform({objectMode: true});

      // Buffer all our data
      stream._transform = function (chunk, encoding, done) {
          assert.ok(Buffer.isBuffer(chunk))
          chunks.push(chunk)
          done()
      },

      // And flush our tree..
      stream._flush = function (done) {
          var self = this;
          var data = Buffer.concat(chunks)

          try {
              fn(data,function(err, result) {
                  if (err) throw err

                  self.push(result);
              });

          } catch (err) {
              // If we were unable to parse, instead of bailing out, just pass the
              // original unparsed data.
              debug('Error while parsing: ', err)
              self.push(data);
          } finally {
              done();
          }
      }

      return stream;
  }
}

//////////////////////////////////////////
// content type parsers
//////////////////////////////////////////

var parsers = {};

parsers['application/json'] = parserFactory(function (buffer, callback) {
  var err, data;

  try {
    data = JSON.parse(buffer)
  } catch (e) {
    err = e;
  }

  callback(err, data);
});

parsers['text/javascript'] = parsers['application/json'];

try {
  var xml2js = require('xml2js');

  // xml2js.Parser.parseString() has the exact same function signature as our ParseStream
  // expects, so we can reuse this.
  parsers['application/xml'] =
        parserFactory(new xml2js.Parser({
          explicitRoot: true,
          explicitArray: false
        }).parseString);

  parsers['text/xml'] = parsers['application/xml'];
  parsers['application/rss+xml'] = parsers['application/xml'];
} catch(e) { /* xml2js not found */ }

//////////////////////////////////////////
// defaults
//////////////////////////////////////////

var defaults = {
  accept          : '*/*',
  connection      : 'close',
  user_agent      : user_agent,
  follow          : 0,
  decode_response : true,
  parse_response  : true,
  timeout         : 10000,
  encoding        : 'utf8',
  boundary        : '--------------------NODENEEDLEHTTPCLIENT'
}

//////////////////////////////////////////
// the main act
//////////////////////////////////////////

var Needle = {

  request: function(method, uri, data, options, callback) {

    var self     = this;
    var callback = (typeof options == 'function') ? options : callback;
    var options  = options || {};

    // if no 'http' is found on URL, prepend it
    if (uri.indexOf('http') == -1) uri = 'http://' + uri;

    var config = {
      base_opts       : {},
      proxy           : options.proxy,
      output          : options.output,
      encoding        : options.encoding || (options.multipart ? 'binary' : defaults.encoding),
      decode_response : options.decode === false ? false : defaults.decode_response,
      parse_response  : options.parse === false ? false : defaults.parse_response,
      follow          : options.follow === true ? 10 : typeof options.follow == 'number' ? options.follow : defaults.follow,
      timeout         : (typeof options.timeout == 'number') ? options.timeout : defaults.timeout
    }

    // if any of node's TLS options are passed, let them be passed to https.request()
    node_tls_opts.split(' ').forEach(function(key) {
      if (typeof options[key] != 'undefined') {
        config.base_opts[key] = options[key];
        if (typeof options.agent == 'undefined')
          config.base_opts.agent = false; // otherwise tls options are skipped
      }
    });

    config.headers = {
      'Accept'     : options.accept     || defaults.accept,
      'Connection' : options.connection || defaults.connection,
      'User-Agent' : options.user_agent || defaults.user_agent
    }

    if (options.compressed && typeof unzip != 'undefined')
      config.headers['Accept-Encoding'] = 'gzip,deflate';

    for (var h in options.headers)
      config.headers[h] = options.headers[h];

    if (options.username && options.password) {
      if (options.auth && (options.auth == 'auto' || options.auth == 'digest')) {
        config.credentials = [options.username, options.password];
      } else {
        var auth_header = options.proxy ? 'Proxy-Authorization' : 'Authorization';
        config.headers[auth_header] = auth.basic(options.username, options.password);
      }
    }

    if (data) {
      if (options.multipart) {
        var boundary = options.boundary || defaults.boundary;
        return multipart.build(data, boundary, function(err, body) {
          if (err) throw(err);
          config.headers['Content-Type'] = 'multipart/form-data; boundary=' + boundary;
          config.headers['Content-Length'] = body.length;
          self.send_request(1, method, uri, config, body, callback);
        });

      } else {
        var post_data = (typeof(data) === 'string') ? data :
            options.json ? JSON.stringify(data) : stringify(data);

        if (!config.headers['Content-Type']) {
          config.headers['Content-Type'] = options.json
          ? 'application/json'
          : 'application/x-www-form-urlencoded';
        }

        post_data = new Buffer(post_data, config.encoding)
        config.headers['Content-Length'] = post_data.length;
      }
    }

    return this.send_request(1, method, uri, config, post_data, callback);
  },

  get_request_opts: function(method, uri, config) {
    var opts      = config.base_opts, proxy = config.proxy;
    var remote    = proxy ? url.parse(proxy) : url.parse(uri);

    opts.protocol = remote.protocol;
    opts.host     = remote.hostname;
    opts.port     = remote.port || (remote.protocol == 'https:' ? 443 : 80);
    opts.path     = proxy ? uri : remote.pathname + (remote.search || '');
    opts.method   = method;
    opts.headers  = config.headers;
    opts.headers['Host'] = proxy ? url.parse(uri).hostname : remote.hostname;
    if (opts.port != 80 && opts.port != 443) opts.headers['Host'] += ':' + opts.port;

    return opts;
  },

  get_auth_header: function(header, credentials, request_opts) {
    var type = header.split(' ')[0],
        user = credentials[0],
        pass = credentials[1];

    if (type == 'Digest') {
      return auth.digest(header, user, pass, request_opts.method, request_opts.path);
    } else if (type == 'Basic') {
      return auth.basic(user, pass);
    }
  },

  send_request: function(count, method, uri, config, post_data, callback) {

    var timer,
        self = this,
        request_opts = this.get_request_opts(method, uri, config),
        protocol = request_opts.protocol == 'https:' ? https : http,
        i_am_stream = (callback ? false : true);

    // config.out is where all our body is streamed twards
    //
    // FIXME: set objectMode: true only when we're using a parser that emits
    // objects -- buffer/strings are far more performant that way.
    config.out = config.out || new Stream.PassThrough({objectMode: true, allowHalfOpen: false})

    debug('Making request #' + count, request_opts);
    var request = protocol.request(request_opts, function(resp) {

      var headers = resp.headers;
      debug('Got response', headers);
      if (timer) clearTimeout(timer);

      // if redirect code is found, send a GET request to that location if enabled via 'follow' option
      if ([301, 302].indexOf(resp.statusCode) != -1 && headers.location) {
        if (count <= config.follow)
          return self.send_request(++count, 'GET', url.resolve(uri, headers.location), config, null, callback);
        else if (config.follow > 0)
          return callback(new Error('Max redirects reached. Possible loop in: ' + headers.location));
      }

      // if authentication is requested and credentials were not passed, resend request if we have user/pass
      if (resp.statusCode == 401 && headers['www-authenticate'] && config.credentials) {
        if (!config.headers['Authorization']) { // only if authentication hasn't been sent
          var auth_header = self.get_auth_header(headers['www-authenticate'], config.credentials, request_opts);

          if (auth_header) {
            config.headers['Authorization'] = auth_header;
            return self.send_request(count, method, uri, config, post_data, callback);
          }
        }
      }

      var length = 0,
          mime = self.parse_content_type(headers['content-type']);

      var response_opts = {
        output       : config.output,
        parse        : config.parse_response, // parse XML or JSON
        content_type : mime.type,
        text         : mime.type && mime.type.indexOf('text/') != -1,
        charset      : mime.charset
      }

      if (response_opts.text)
        response_opts.decode = config.decode_response; // only allow iconv decoding on text bodies

      // `resp` at this point is our stream with raw body data coming over
      // the write. Let's initialize our streams pipiline to have the eventual
      // destination `config.out`.
      var pipeline = [];

      // First of all, if our body is compressed and we are able to decompress it,
      // decompress it.
      if (headers['content-encoding'] && decompressors[headers['content-encoding']]) {
        pipeline.push(decompressors[headers['content-encoding']]());
      }

      // Parsing the 'content-type' (xml, json, whatever) is one of the last
      // things we should do.
      if (response_opts.parse && parsers[response_opts.content_type]) {
        pipeline.push(parsers[response_opts.content_type]())

      } else {
        // If charset is not UTF-8 and the users wants to decode, decode. Note
        // that this is mutually exclusive with parsing JSON/XML/etc.
        if (response_opts.decode && opts.charset && !response_opts.charset.match(/utf-?8$/i)) {
          // TODO: in case of error, this emits 'error' on the stream -- is this
          // what we want?
          try {
            pipeline.push(require('iconv').Iconv('UTF-8', response_opts.charset))
          } catch (err) {
            // iconv library wasn't installed, do not do any decoding
            debug('iconv not installed!')
          }
        }
      }

      // And config.out is the stream we eventually push our sanitized body data to.
      pipeline.push(config.out)

      var tmp = resp;
      while (pipeline.length) {
        tmp = tmp.pipe(pipeline.shift())
      }

      // If the user has requested that we should write the data to a file, pipe the
      // output stream to this file stream.
      //
      // Note: since we do not make this part of the `pipeline` above, the user will still
      // receive all the response data, we just sniff on the stream and write the file.
      //
      // :FIXME: does this belong here anyway? Isn't it sufficient to let the end-user
      // do a needle.get(url).pipe(fs.createWriteStream()) ?
      if (response_opts.output && resp.statusCode == 200) {
        resp.pipe(fs.createWriteStream(response_opts.output))
      }

      // We do not want to aggregate the body contents if we are a stream, for performance
      // and scalability reasons.
      if (i_am_stream == false) {
        resp.body = []
        resp.bytes = 0;

        // Ensure that we count the amount of (raw) bytes we see over the wire. Note that
        // pipe through resp, which is our 'raw' body data.
        var bytesCounter = new Stream.PassThrough();
        resp.pipe(bytesCounter);
        bytesCounter.on('readable', function () {
          while (chunk = this.read()) {
            resp.bytes += chunk.length;
          }
        })

        // Ah, but a callback has been requested, with all our data. Let's first aggregate
        // incoming data from the stream...
        config.out.on('readable', function () {
          while (chunk = this.read()) {
            // We're either pushing buffers or objects, never strings.
            if (typeof chunk == 'String') chunk = new Buffer(chunk)

            // What we will do here is simply make resp.body a mapping of all
            // our chunks. Later, in resp.end(), we reduce this to a single buffer.
            resp.body.push(chunk);
          }
        })

        // And set the .body property once all data is in.
        config.out.on('end', function () {
          // If our body is a string (Buffer), we need to merge those into one big buffer.
          if (resp.body.length != 0 && Buffer.isBuffer(resp.body[0])) {
            resp.body = Buffer.concat(resp.body);
          } else if (resp.body.length == 1) {
            assert(Buffer.isBuffer(resp.body[0]) == false, 'Internal error: body should be an array of objects, is a buffer?')
            // Do not treat body as an array when we have only one object
            resp.body = resp.body[0]
          }

          // :TODO: This doesn't make sense if resp.body is an array of objects.
          callback(null, resp, resp.body);
        });

      };
    });

    // unless timeout was disabled, set a timeout to abort the request
    if (config.timeout > 0) {
      timer = setTimeout(function() {
        request.abort();
      }, config.timeout)
    }

    request.on('error', function(err) {
      debug('Request error', err);
      if (timer) clearTimeout(timer);
      if (callback) callback(err || new Error('Unknown error when making request.'));
    });

    if (post_data) request.write(post_data, config.encoding);
    request.end();

    return (i_am_stream ? config.out : request);

  },

  parse_content_type: function(header) {
    if (!header || header == '') return {};

    var charset = 'iso-8859-1', arr = header.split(';');
    try { charset = arr[1].match(/charset=(.+)/)[1] } catch (e) { /* not found */ }

    return { type: arr[0], charset: charset };
  },
}

exports.version = version;

exports.defaults = function(obj) {
  for (var key in obj) {
    if (defaults[key] && typeof obj[key] != 'undefined')
      defaults[key] = obj[key];
  }
  return defaults;
}

'head get'.split(' ').forEach(function(method) {
  exports[method] = function(uri, options, callback) {
    return Needle.request(method, uri, null, options, callback);
  }
})

'post put delete'.split(' ').forEach(function(method) {
  exports[method] = function(uri, data, options, callback) {
    return Needle.request(method, uri, data, options, callback);
  }
})

exports.request = function(method, uri, data, opts, callback) {
  return Needle.request(method.toUpperCase(), uri, data, opts, callback);
};
<|endoftext|>


<commit_before>//= require ../../vendor/raphael-min
//= require ../../vendor/g.raphael-min
//= require ../../vendor/g.pie-min
//= require ../../vendor/g.line-min

var IslayShop = {};

/* -------------------------------------------------------------------------- */
/* UTILS
/* -------------------------------------------------------------------------- */
IslayShop.u = {
  formatMoney: function(v) {
    return '$' + v.toFixed(2);
  }
};

/* -------------------------------------------------------------------------- */
/* LINE GRAPH
/* Wraps gRaphael line graphs. Makes it easier to render.
/* -------------------------------------------------------------------------- */
IslayShop.LineGraph = Backbone.View.extend({
  className: 'graph',

  initialize: function() {
    this.x = this.options.values.x;
    this.y = this.options.values.y;
    this.xLabels = this.options.values.xLabels;
  },

  render: function() {
    this.paper = Raphael(this.el);

    var opts = {symbol: 'circle', axis: '0 0 1 1', axisxstep: this.x.length - 1, axisystep: 5, gutter: 10};
    this.line = this.paper.linechart(30, 0, 500, 250, this.x, this.y, opts);

    // These callbacks are defined inline, and we use the behaviour of closures
    // to keep our view in scope and call our own handlers. This is because of
    // gRaphael's limited callbacks.
    var view = this;
    this.line.hoverColumn(function() {view.hoverIn(this);}, function() {view.hoverOut(this);});

    if (this.xLabels) {this.renderXLabels(this.xLabels)};

    return this;
  },

  hoverIn: function(cover) {
    this.tags = []

    for (var i = 0, ii = cover.y.length; i < ii; i++) {
      var tag = $H('div.tag', IslayShop.u.formatMoney(cover.values[i]));
      this.tags.push(tag);
      this.$el.append(tag);
      tag.css({left: cover.x, top: cover.y[i] - (tag.outerHeight() / 2)});
    }
  },

  hoverOut: function() {
    this.tags && _.each(this.tags, function(tag) {tag.remove();});
  },

  renderXLabels: function(labels) {
    var els = this.line.axis[0].text.items;
    _.each(labels, function(label, i) {els[i].attr({text: label});});
  }
});

/* -------------------------------------------------------------------------- */
/* SERIES GRAPH
/* -------------------------------------------------------------------------- */
IslayShop.SeriesGraph = Backbone.View.extend({
  className: 'series-graph',

  initialize: function() {
    this.values = {
      value:      {x: [], y: [], xLabels: []},
      volume:     {x: [], y: [], xLabels: []},
      sku_volume: {x: [], y: [], xLabels: []}
    };

    _.each(this.options.table.find('tbody tr'), function(el, i) {
      var values = _.map($(el).find('td:not(:first-child)'), function(el) {
        return parseInt($(el).text());
      })

      this.update('value', i, values[0], i + 1);
      this.update('volume', i, values[1], i + 1);
      this.update('sku_volume', i, values[2], i + 1);
    }, this);

    this.graphs = _.map(this.values, function(v) {
      return new IslayShop.LineGraph({color: 'blue', values: v});
    });

    this.render();
  },

  update: function(key, x, y, label) {
    var v = this.values[key];
    v.x.push(x);
    v.y.push(y);
    if (label) {v.xLabels.push(label);}
  },

  render: function() {
    this.options.table.before(this.$el).remove();
    _.each(this.graphs, function(view) {
      this.$el.append(view.el);
      view.render();
    }, this);
    return this;
  }
});

/* -------------------------------------------------------------------------- */
/* SKU TOP TEN
/* -------------------------------------------------------------------------- */
IslayShop.TopTen = Backbone.View.extend({
  events: {'click .segmented li': 'click'},

  initialize: function() {
    this.tables = [];

    _.each(this.$el.find('table'), function(table, i) {
      var $table = $(table);
      this.tables.push({table: $table, caption: $table.find('caption').text()});
    }, this);

    this.render();
  },

  click: function(e) {
    var $target = $(e.target),
        index = parseInt($target.attr('data-index'));

    if (index !== this.current) {
      var current = this.tables[this.current];
      current.li.removeClass('current');
      current.table.hide();

      $target.addClass('current');
      this.tables[index].table.show();
      this.current = index;
    }
  },

  render: function() {
    this.links = $H('ul.segmented');

    _.each(this.tables, function(c, i) {
      var li = $H('li[data-index=' + i + ']', c.caption);
      this.links.append(li);
      c.li = li;

      if (i > 0) {
        c.table.hide();
        this.current = i;
        li.addClass('current');
      }
    }, this);

    this.$el.find('h3').after(this.links);

    return this;
  }
});

$SP.where('#islay-shop-admin-reports.index').run(function() {
  var graph = new IslayShop.SeriesGraph({table: $('.series-graph')});
  var topTen = new IslayShop.TopTen({el: $("#top-ten")});
});
<commit_msg>Modify the top ten view to use a sub view for the segmented control.<commit_after>//= require ../../vendor/raphael-min
//= require ../../vendor/g.raphael-min
//= require ../../vendor/g.pie-min
//= require ../../vendor/g.line-min

var IslayShop = {};

/* -------------------------------------------------------------------------- */
/* UTILS
/* -------------------------------------------------------------------------- */
IslayShop.u = {
  formatMoney: function(v) {
    return '$' + v.toFixed(2);
  }
};

/* -------------------------------------------------------------------------- */
/* SEGMENTED CONTROL
/* Simplistic, report-specific segmented control.
/* -------------------------------------------------------------------------- */
IslayShop.SegmentedControl = Backbone.View.extend({
  tagName: 'ul',
  className: 'segmented',
  events: {'click li': 'click'},

  initialize: function() {
    _.bindAll(this, 'click');
    this.lis = [];
  },

  click: function(e) {
    var $target = $(e.target),
        index = parseInt($target.attr('data-index'));

    if (index !== this.current) {
      var current = this.lis[this.current];
      current.removeClass('current');
      $target.addClass('current');
      this.current = index;
      this.trigger('selected', index);
    }
  },

  render: function() {
    _.each(this.options.labels, function(label, i) {
      var li = $H('li[data-index=' + i + ']', label);
      this.$el.append(li);
      this.lis.push(li);

      if (i === 0) {
        this.current = i;
        li.addClass('current');
      }
    }, this);

    return this;
  }
});

/* -------------------------------------------------------------------------- */
/* LINE GRAPH
/* Wraps gRaphael line graphs. Makes it easier to render.
/* -------------------------------------------------------------------------- */
IslayShop.LineGraph = Backbone.View.extend({
  className: 'graph',

  initialize: function() {
    this.x = this.options.values.x;
    this.y = this.options.values.y;
    this.xLabels = this.options.values.xLabels;
  },

  render: function() {
    this.paper = Raphael(this.el);

    var opts = {symbol: 'circle', axis: '0 0 1 1', axisxstep: this.x.length - 1, axisystep: 5, gutter: 10};
    this.line = this.paper.linechart(30, 0, 500, 250, this.x, this.y, opts);

    // These callbacks are defined inline, and we use the behaviour of closures
    // to keep our view in scope and call our own handlers. This is because of
    // gRaphael's limited callbacks.
    var view = this;
    this.line.hoverColumn(function() {view.hoverIn(this);}, function() {view.hoverOut(this);});

    if (this.xLabels) {this.renderXLabels(this.xLabels)};

    return this;
  },

  hoverIn: function(cover) {
    this.tags = []

    for (var i = 0, ii = cover.y.length; i < ii; i++) {
      var tag = $H('div.tag', IslayShop.u.formatMoney(cover.values[i]));
      this.tags.push(tag);
      this.$el.append(tag);
      tag.css({left: cover.x, top: cover.y[i] - (tag.outerHeight() / 2)});
    }
  },

  hoverOut: function() {
    this.tags && _.each(this.tags, function(tag) {tag.remove();});
  },

  renderXLabels: function(labels) {
    var els = this.line.axis[0].text.items;
    _.each(labels, function(label, i) {els[i].attr({text: label});});
  }
});

/* -------------------------------------------------------------------------- */
/* SERIES GRAPH
/* -------------------------------------------------------------------------- */
IslayShop.SeriesGraph = Backbone.View.extend({
  className: 'series-graph',

  initialize: function() {
    this.values = {
      value:      {x: [], y: [], xLabels: []},
      volume:     {x: [], y: [], xLabels: []},
      sku_volume: {x: [], y: [], xLabels: []}
    };

    _.each(this.options.table.find('tbody tr'), function(el, i) {
      var values = _.map($(el).find('td:not(:first-child)'), function(el) {
        return parseInt($(el).text());
      })

      this.update('value', i, values[0], i + 1);
      this.update('volume', i, values[1], i + 1);
      this.update('sku_volume', i, values[2], i + 1);
    }, this);

    this.graphs = _.map(this.values, function(v) {
      return new IslayShop.LineGraph({color: 'blue', values: v});
    });

    this.render();
  },

  update: function(key, x, y, label) {
    var v = this.values[key];
    v.x.push(x);
    v.y.push(y);
    if (label) {v.xLabels.push(label);}
  },

  render: function() {
    this.options.table.before(this.$el).remove();
    _.each(this.graphs, function(view) {
      this.$el.append(view.el);
      view.render();
    }, this);
    return this;
  }
});

/* -------------------------------------------------------------------------- */
/* SKU TOP TEN
/* -------------------------------------------------------------------------- */
IslayShop.TopTen = Backbone.View.extend({
  initialize: function() {
    _.bindAll(this, 'toggle');

    this.tables = [];
    this.current = 0;
    var labels = [];

    _.each(this.$el.find('table'), function(table, i) {
      var $table = $(table);
      this.tables.push($table);
      labels.push($table.find('caption').text());
    }, this);

    this.controls = new IslayShop.SegmentedControl({labels: labels});
    this.controls.on('selected', this.toggle);

    this.render();
  },

  toggle: function(index) {
    this.tables[this.current].hide();
    this.tables[index].show();
    this.current = index;
  },

  render: function() {
    this.$el.find('h3').after(this.controls.render().el);
    this.tables[1].hide();

    return this;
  }
});

$SP.where('#islay-shop-admin-reports.index').run(function() {
  var graph = new IslayShop.SeriesGraph({table: $('.series-graph')});
  var topTen = new IslayShop.TopTen({el: $("#top-ten")});
});
<|endoftext|>


<commit_before>//////////////////////////////////////////
// Needle -- Node Simple HTTP Client
// Written by Tomás Pollak <tomas@forkhq.com>
// (c) 2011 - Fork Ltd.
// MIT Licensed
//////////////////////////////////////////

var util = require('util'),
		fs = require('fs'),
		path = require('path'),
		http = require('http'),
		https = require('https'),
		url = require('url'),
		stringify = require('qs').stringify,
		version = JSON.parse(fs.readFileSync(__dirname + '/../package.json').toString()).version;

http.globalAgent.maxSockets = 128;
exports.version = version;
try { var unzip = require('zlib').unzip; } catch(e){ /* zlib not supported */ }

var parsers = {
	'application/json': function(data, callback){
		callback(data && JSON.parse(data));
	}
};

try {
	var xml2js = require('xml2js');

	parsers['application/xml'] = function(data, callback){

		var xml_parser = new xml2js.Parser();

		xml_parser.on('end', callback);
		xml_parser.on('error', function(result) {
			// throw("Error parsing XML!")
			callback(null);
		});

		xml_parser.parseString(data);

	};

} catch(e) { }

// utility function for flattening params in multipart POST's
function flatten(object, into, prefix){

	into = into || {};

	for(key in object){
		var prefix_key = prefix ? prefix + "[" + key + "]" : key;
		var prop = object[key];

		if(prop && typeof prop === "object" && !((prop.buffer || prop.file) && prop.content_type))
			flatten(prop, into, prefix_key)
		else
			into[prefix_key] = prop;

	}

	return into;

}

var Needle = {

	default_boundary: '--------------------NODENEEDLEHTTPCLIENT',

	request: function(uri, method, data, options, callback){

		var self = this;
		var callback = (typeof options == 'function') ? options : callback;
		var options = options || {};
		if(uri.indexOf('http') == -1) uri = 'http://' + uri;

		var remote = options.proxy ? url.parse(options.proxy) : url.parse(uri);
		var is_https = remote.protocol === "https:";
		var port = remote.port || (is_https ? 443 : 80);
		var request_compressed = (options.compressed && typeof unzip != 'undefined') || false;
		var post_data = null;

		var request_opts = {
			encoding: options.multipart ? 'binary' : 'utf8',
			protocol: is_https ? https : http,
			parse_response: options.parse === false ? false : true,
			timeout: options.timeout || 10000 // 10 seconds timeout
		}

		var headers = {
			"Host" : options.proxy ? url.parse(uri).hostname : remote.hostname,
			"User-Agent": "Needle/" + version + " (NodeJS " + process.version + ")",
			"Connection": "close",
			"Accept": "*/*"
		}

		if(request_compressed) headers['Accept-Encoding'] = 'gzip,deflate';

		for(h in options.headers)
			headers[h] = options.headers[h];

		if(options.username && options.password){
			var b = new Buffer([options.username, options.password].join(':'));
			headers['Authorization'] = "Basic " + b.toString('base64');
		}

		var http_opts = {
			host: remote.hostname,
			port: port,
			path: options.proxy ? uri : remote.pathname + (remote.search || ''),
			method: method,
			headers: headers,
			agent: options.agent || http.globalAgent
		}

		if(data) {
			if(options.multipart){

				var boundary = options.boundary || this.default_boundary;
				return this.build_multipart_body(data, boundary, function(err, body){

					if(err) throw(err);
					headers['Content-Type'] = 'multipart/form-data; boundary=' + boundary;
					headers['Content-Length'] = body.length;
					self.send_request(request_opts, http_opts, body, callback);

				});

			} else {
				post_data = (typeof(data) === "string") ? data : stringify(data);
				headers['Content-Type'] = 'application/x-www-form-urlencoded';
				headers['Content-Length'] = post_data.length;
			}

		}

		this.send_request(request_opts, http_opts, post_data, callback);

	},

	send_request: function(request_opts, http_opts, post_data, callback){

		if(process.env.DEBUG) console.log(http_opts + "\n\n" + post_data);
		var self = this, timer, response_opts = {parse_response: request_opts.parse_response};

		var request = request_opts.protocol.request(http_opts, function(response){

			if(timer) clearTimeout(timer);

			var body = '';
			var compressed = /gzip|deflate/.test(response.headers['content-encoding']);
			response.setEncoding(compressed ? 'binary' : 'utf8');

			response.on('data', function(chunk){
				body += chunk;
			});

			response.on('end', function() {
				if(typeof unzip != 'undefined' && compressed)
					unzip(new Buffer(body, 'binary'), function(err, buff){
						self.response_end(response_opts, response, buff.toString(), callback);
					});
				else
					self.response_end(response_opts, response, body, callback);
			});

		});

		if(request_opts.timeout) {
			timer = setTimeout(function() {
				request.abort();
			}, request_opts.timeout)
		}

		request.on('error', function(err) {
			if(process.env.DEBUG) console.log('Error on request: ' + err.toString());
			if(timer) clearTimeout(timer);
			if(callback) callback(err || new Error("Unkown error on request."));
		});

		if(post_data) request.write(post_data, request_opts.encoding);
		request.end();

	},

	response_end: function(opts, response, body, callback){

		if(process.env.DEBUG) console.log(response.headers);
		var content_type = response.headers['content-type'] && response.headers['content-type'].split(';')[0];

		if(opts.parse_response && parsers[content_type]) {
			parsers[content_type](body, function(result){
				if(callback) callback(null, response, result);
			});
		} else {
			if(callback) callback(null, response, body);
		}

	},

	build_multipart_body: function(data, boundary, callback){

		var body = '';
		var object = flatten(data);
		var count = Object.keys(object).length;

		for(var key in object){

			var value = object[key];
			if(value === null || typeof value == 'undefined') return --count;

			var part = (value.buffer || value.file) && value.content_type ? value : {value: value};

			this.generate_part(key, part, boundary, function(err, section){
				if(err) return callback(err);
				body += section;
				--count || callback(null, body + '--' + boundary + '--');
			});

		}

	},

	generate_part: function(name, part, boundary, callback){

		var return_part = '--' + boundary + "\r\n";
		return_part += "Content-Disposition: form-data; name=\"" + name + "\"";

		var append = function(data, filename){

			if(data){
				return_part += "; filename=\"" + encodeURIComponent(filename) + "\"\r\n";
				return_part += "Content-Type: " + part.content_type + "\r\n\r\n";
				return_part += (part.content_type.indexOf('text') == -1)
					? data.toString('binary')
					: data.toString('utf8');
			}

			callback(null, return_part + '\r\n');
		};

		if((part.file || part.buffer) && part.content_type){

			var filename = part.filename || part.file ? path.basename(part.file) : name;
			if(part.buffer) return append(part.buffer, filename);

			fs.readFile(part.file, function(err, data){

				if(err) return callback(err);
				append(data, filename);

			});

		} else {

			return_part += "\r\n\r\n";
			return_part += part.value;
			append();

		}

	}

}

exports.get = function(uri, options, callback){
	return Needle.request(uri, "GET", null, options, callback);
}

exports.post = function(uri, data, options, callback){
	if(!data) throw("POST request expects data.");
	return Needle.request(uri, "POST", data, options, callback);
}

exports.put = function(uri, data, options, callback){
	if(!data) throw("PUT request expects data.");
	return Needle.request(uri, "PUT", data, options, callback);
}

exports.delete = function(uri, data, options, callback){
	return Needle.request(uri, "DELETE", null, options, callback);
}
<commit_msg>Commented globalAgent.maxSockets. User can always modify it before calling needle<commit_after>//////////////////////////////////////////
// Needle -- Node Simple HTTP Client
// Written by Tomás Pollak <tomas@forkhq.com>
// (c) 2011 - Fork Ltd.
// MIT Licensed
//////////////////////////////////////////

var util = require('util'),
		fs = require('fs'),
		path = require('path'),
		http = require('http'),
		https = require('https'),
		url = require('url'),
		stringify = require('qs').stringify,
		version = JSON.parse(fs.readFileSync(__dirname + '/../package.json').toString()).version;

// http.globalAgent.maxSockets = 128;
exports.version = version;
try { var unzip = require('zlib').unzip; } catch(e){ /* zlib not supported */ }

var parsers = {
	'application/json': function(data, callback){
		callback(data && JSON.parse(data));
	}
};

try {
	var xml2js = require('xml2js');

	parsers['application/xml'] = function(data, callback){

		var xml_parser = new xml2js.Parser();

		xml_parser.on('end', callback);
		xml_parser.on('error', function(result) {
			// throw("Error parsing XML!")
			callback(null);
		});

		xml_parser.parseString(data);

	};

} catch(e) { }

// utility function for flattening params in multipart POST's
function flatten(object, into, prefix){

	into = into || {};

	for(key in object){
		var prefix_key = prefix ? prefix + "[" + key + "]" : key;
		var prop = object[key];

		if(prop && typeof prop === "object" && !((prop.buffer || prop.file) && prop.content_type))
			flatten(prop, into, prefix_key)
		else
			into[prefix_key] = prop;

	}

	return into;

}

var Needle = {

	default_boundary: '--------------------NODENEEDLEHTTPCLIENT',

	request: function(uri, method, data, options, callback){

		var self = this;
		var callback = (typeof options == 'function') ? options : callback;
		var options = options || {};
		if(uri.indexOf('http') == -1) uri = 'http://' + uri;

		var remote = options.proxy ? url.parse(options.proxy) : url.parse(uri);
		var is_https = remote.protocol === "https:";
		var port = remote.port || (is_https ? 443 : 80);
		var request_compressed = (options.compressed && typeof unzip != 'undefined') || false;
		var post_data = null;

		var request_opts = {
			encoding: options.multipart ? 'binary' : 'utf8',
			protocol: is_https ? https : http,
			parse_response: options.parse === false ? false : true,
			timeout: options.timeout || 10000 // 10 seconds timeout
		}

		var headers = {
			"Host" : options.proxy ? url.parse(uri).hostname : remote.hostname,
			"User-Agent": "Needle/" + version + " (NodeJS " + process.version + ")",
			"Connection": "close",
			"Accept": "*/*"
		}

		if(request_compressed) headers['Accept-Encoding'] = 'gzip,deflate';

		for(h in options.headers)
			headers[h] = options.headers[h];

		if(options.username && options.password){
			var b = new Buffer([options.username, options.password].join(':'));
			headers['Authorization'] = "Basic " + b.toString('base64');
		}

		var http_opts = {
			host: remote.hostname,
			port: port,
			path: options.proxy ? uri : remote.pathname + (remote.search || ''),
			method: method,
			headers: headers,
			agent: options.agent || http.globalAgent
		}

		if(data) {
			if(options.multipart){

				var boundary = options.boundary || this.default_boundary;
				return this.build_multipart_body(data, boundary, function(err, body){

					if(err) throw(err);
					headers['Content-Type'] = 'multipart/form-data; boundary=' + boundary;
					headers['Content-Length'] = body.length;
					self.send_request(request_opts, http_opts, body, callback);

				});

			} else {
				post_data = (typeof(data) === "string") ? data : stringify(data);
				headers['Content-Type'] = 'application/x-www-form-urlencoded';
				headers['Content-Length'] = post_data.length;
			}

		}

		this.send_request(request_opts, http_opts, post_data, callback);

	},

	send_request: function(request_opts, http_opts, post_data, callback){

		if(process.env.DEBUG) console.log(http_opts + "\n\n" + post_data);
		var self = this, timer, response_opts = {parse_response: request_opts.parse_response};

		var request = request_opts.protocol.request(http_opts, function(response){

			if(timer) clearTimeout(timer);

			var body = '';
			var compressed = /gzip|deflate/.test(response.headers['content-encoding']);
			response.setEncoding(compressed ? 'binary' : 'utf8');

			response.on('data', function(chunk){
				body += chunk;
			});

			response.on('end', function() {
				if(typeof unzip != 'undefined' && compressed)
					unzip(new Buffer(body, 'binary'), function(err, buff){
						self.response_end(response_opts, response, buff.toString(), callback);
					});
				else
					self.response_end(response_opts, response, body, callback);
			});

		});

		if(request_opts.timeout) {
			timer = setTimeout(function() {
				request.abort();
			}, request_opts.timeout)
		}

		request.on('error', function(err) {
			if(process.env.DEBUG) console.log('Error on request: ' + err.toString());
			if(timer) clearTimeout(timer);
			if(callback) callback(err || new Error("Unkown error on request."));
		});

		if(post_data) request.write(post_data, request_opts.encoding);
		request.end();

	},

	response_end: function(opts, response, body, callback){

		if(process.env.DEBUG) console.log(response.headers);
		var content_type = response.headers['content-type'] && response.headers['content-type'].split(';')[0];

		if(opts.parse_response && parsers[content_type]) {
			parsers[content_type](body, function(result){
				if(callback) callback(null, response, result);
			});
		} else {
			if(callback) callback(null, response, body);
		}

	},

	build_multipart_body: function(data, boundary, callback){

		var body = '';
		var object = flatten(data);
		var count = Object.keys(object).length;

		for(var key in object){

			var value = object[key];
			if(value === null || typeof value == 'undefined') return --count;

			var part = (value.buffer || value.file) && value.content_type ? value : {value: value};

			this.generate_part(key, part, boundary, function(err, section){
				if(err) return callback(err);
				body += section;
				--count || callback(null, body + '--' + boundary + '--');
			});

		}

	},

	generate_part: function(name, part, boundary, callback){

		var return_part = '--' + boundary + "\r\n";
		return_part += "Content-Disposition: form-data; name=\"" + name + "\"";

		var append = function(data, filename){

			if(data){
				return_part += "; filename=\"" + encodeURIComponent(filename) + "\"\r\n";
				return_part += "Content-Type: " + part.content_type + "\r\n\r\n";
				return_part += (part.content_type.indexOf('text') == -1)
					? data.toString('binary')
					: data.toString('utf8');
			}

			callback(null, return_part + '\r\n');
		};

		if((part.file || part.buffer) && part.content_type){

			var filename = part.filename || part.file ? path.basename(part.file) : name;
			if(part.buffer) return append(part.buffer, filename);

			fs.readFile(part.file, function(err, data){

				if(err) return callback(err);
				append(data, filename);

			});

		} else {

			return_part += "\r\n\r\n";
			return_part += part.value;
			append();

		}

	}

}

exports.get = function(uri, options, callback){
	return Needle.request(uri, "GET", null, options, callback);
}

exports.post = function(uri, data, options, callback){
	if(!data) throw("POST request expects data.");
	return Needle.request(uri, "POST", data, options, callback);
}

exports.put = function(uri, data, options, callback){
	if(!data) throw("PUT request expects data.");
	return Needle.request(uri, "PUT", data, options, callback);
}

exports.delete = function(uri, data, options, callback){
	return Needle.request(uri, "DELETE", null, options, callback);
}
<|endoftext|>


<commit_before>var React = require('react');
var ActiveState = require('../mixins/ActiveState');
var ActiveStore = require('../helpers/ActiveStore');
var mergeProperties = require('../helpers/mergeProperties');
var withoutProperties = require('../helpers/withoutProperties');
var transitionTo = require('../helpers/transitionTo');
var makeHref = require('../helpers/makeHref');

var RESERVED_PROPS = {
  to: true,
  className: true,
  activeClassName: true,
  query: true,
  children: true // ReactChildren
};

/**
 * <Link> components are used to create an <a> element that links to a route.
 * When that route is active, the link gets an "active" class name (or the
 * value of its `activeClassName` prop).
 *
 * For example, assuming you have the following route:
 *
 *   <Route name="showPost" path="/posts/:postId" handler={Post}/>
 *
 * You could use the following component to link to that route:
 *
 *   <Link to="showPost" postId="123"/>
 *
 * In addition to params, links may pass along query string parameters
 * using the `query` prop.
 *
 *   <Link to="showPost" postId="123" query={{show:true}}/>
 */
var Link = React.createClass({

  displayName: 'Link',

  mixins: [ ActiveState ],

  statics: {

    getUnreservedProps: function (props) {
      return withoutProperties(props, RESERVED_PROPS);
    }

  },

  propTypes: {
    to: React.PropTypes.string.isRequired,
    activeClassName: React.PropTypes.string.isRequired,
    query: React.PropTypes.object
  },

  getDefaultProps: function () {
    return {
      activeClassName: 'active'
    };
  },

  getInitialState: function () {
    return {
      isActive: false
    };
  },

  /**
   * Returns a hash of URL parameters to use in this <Link>'s path.
   */
  getParams: function () {
    // Links may omit active params. See #89
    return mergeProperties(ActiveStore.getActiveParams(), Link.getUnreservedProps(this.props));
  },

  /**
   * Returns the value of the "href" attribute to use on the DOM element.
   */
  getHref: function () {
    return makeHref(this.props.to, this.getParams(), this.props.query);
  },

  /**
   * Returns the value of the "class" attribute to use on the DOM element, which contains
   * the value of the activeClassName property when this <Link> is active.
   */
  getClassName: function () {
    var className = this.props.className || '';

    if (this.state.isActive)
      return className + ' ' + this.props.activeClassName;

    return className;
  },

  componentWillReceiveProps: function (nextProps) {
    var params = Link.getUnreservedProps(nextProps);

    this.setState({
      isActive: Link.isActive(nextProps.to, params, nextProps.query)
    });
  },

  updateActiveState: function () {
    this.setState({
      isActive: Link.isActive(this.props.to, this.getParams(), this.props.query)
    });
  },

  handleClick: function (event) {
    if (isModifiedEvent(event))
      return;

    event.preventDefault();

    transitionTo(this.props.to, this.getParams(), this.props.query);
  },

  render: function () {
    var props = {
      href: this.getHref(),
      className: this.getClassName(),
      onClick: this.handleClick
    };

    return React.DOM.a(props, this.props.children);
  }

});

function isModifiedEvent(event) {
  return !!(event.metaKey || event.ctrlKey || event.shiftKey);
}

module.exports = Link;
<commit_msg>Fix require path<commit_after>var React = require('react');
var ActiveState = require('../mixins/ActiveState');
var ActiveStore = require('../stores/ActiveStore');
var mergeProperties = require('../helpers/mergeProperties');
var withoutProperties = require('../helpers/withoutProperties');
var transitionTo = require('../helpers/transitionTo');
var makeHref = require('../helpers/makeHref');

var RESERVED_PROPS = {
  to: true,
  className: true,
  activeClassName: true,
  query: true,
  children: true // ReactChildren
};

/**
 * <Link> components are used to create an <a> element that links to a route.
 * When that route is active, the link gets an "active" class name (or the
 * value of its `activeClassName` prop).
 *
 * For example, assuming you have the following route:
 *
 *   <Route name="showPost" path="/posts/:postId" handler={Post}/>
 *
 * You could use the following component to link to that route:
 *
 *   <Link to="showPost" postId="123"/>
 *
 * In addition to params, links may pass along query string parameters
 * using the `query` prop.
 *
 *   <Link to="showPost" postId="123" query={{show:true}}/>
 */
var Link = React.createClass({

  displayName: 'Link',

  mixins: [ ActiveState ],

  statics: {

    getUnreservedProps: function (props) {
      return withoutProperties(props, RESERVED_PROPS);
    }

  },

  propTypes: {
    to: React.PropTypes.string.isRequired,
    activeClassName: React.PropTypes.string.isRequired,
    query: React.PropTypes.object
  },

  getDefaultProps: function () {
    return {
      activeClassName: 'active'
    };
  },

  getInitialState: function () {
    return {
      isActive: false
    };
  },

  /**
   * Returns a hash of URL parameters to use in this <Link>'s path.
   */
  getParams: function () {
    // Links may omit active params. See #89
    return mergeProperties(ActiveStore.getActiveParams(), Link.getUnreservedProps(this.props));
  },

  /**
   * Returns the value of the "href" attribute to use on the DOM element.
   */
  getHref: function () {
    return makeHref(this.props.to, this.getParams(), this.props.query);
  },

  /**
   * Returns the value of the "class" attribute to use on the DOM element, which contains
   * the value of the activeClassName property when this <Link> is active.
   */
  getClassName: function () {
    var className = this.props.className || '';

    if (this.state.isActive)
      return className + ' ' + this.props.activeClassName;

    return className;
  },

  componentWillReceiveProps: function (nextProps) {
    var params = Link.getUnreservedProps(nextProps);

    this.setState({
      isActive: Link.isActive(nextProps.to, params, nextProps.query)
    });
  },

  updateActiveState: function () {
    this.setState({
      isActive: Link.isActive(this.props.to, this.getParams(), this.props.query)
    });
  },

  handleClick: function (event) {
    if (isModifiedEvent(event))
      return;

    event.preventDefault();

    transitionTo(this.props.to, this.getParams(), this.props.query);
  },

  render: function () {
    var props = {
      href: this.getHref(),
      className: this.getClassName(),
      onClick: this.handleClick
    };

    return React.DOM.a(props, this.props.children);
  }

});

function isModifiedEvent(event) {
  return !!(event.metaKey || event.ctrlKey || event.shiftKey);
}

module.exports = Link;
<|endoftext|>