mjackson/mach

View on GitHub
modules/Location.js

Summary

Maintainability
F
1 wk
Test Coverage
var d = require('describe-property');
var mergeQuery = require('./utils/mergeQuery');
var stringifyQuery = require('./utils/stringifyQuery');
var parseQuery = require('./utils/parseQuery');
var parseURL = require('./utils/parseURL');

/**
 * Standard ports for HTTP protocols.
 */
var STANDARD_PORTS = {
  'http:': '80',
  'https:': '443'
};

function propertyAlias(propertyName, defaultValue) {
  return d.gs(function () {
    return this.properties[propertyName] || (defaultValue == null ? null : defaultValue);
  }, function (value) {
    this.properties[propertyName] = value;
  });
}

// Order is important here. Later properties take priority.
var PROPERTY_NAMES = [ 'protocol', 'auth', 'hostname', 'port', 'host', 'pathname', 'search', 'queryString', 'query', 'path' ];

function setProperties(location, properties) {
  var propertyName;
  for (var i = 0, len = PROPERTY_NAMES.length; i < len; ++i) {
    propertyName = PROPERTY_NAMES[i];

    if (properties.hasOwnProperty(propertyName) && propertyName in location)
      location[propertyName] = properties[propertyName];
  }
}

/**
 * A URL location, analogous to window.location.
 *
 * Options may be any of the following:
 *
 * - protocol
 * - auth
 * - hostname
 * - port
 * - host (overrides hostname and port)
 * - pathname
 * - search
 * - queryString (overrides search)
 * - query (overrides queryString/search)
 * - path (overrides pathname and query/queryString/search)
 *
 * Alternatively, options may be a URL string.
 */
function Location(options) {
  this.properties = {};

  if (typeof options === 'string') {
    this.href = options;
  } else if (options) {
    setProperties(this, options);
  }
}

Object.defineProperties(Location.prototype, {

  /**
   * Creates and returns a new Location with the path and query of
   * the given location appended.
   */
  concat: d(function (location) {
    if (!(location instanceof Location))
      location = new Location(location);

    var pathname = this.pathname;
    var extraPathname = location.pathname;

    if (extraPathname !== '/')
      pathname = pathname.replace(/\/*$/, '/') + extraPathname.replace(/^\/*/, '');

    var query = mergeQuery(this.query, location.query);

    return new Location({
      protocol: location.protocol || this.protocol,
      auth: location.auth || this.auth,
      hostname: location.hostname || this.hostname,
      port: location.port || this.port,
      pathname: pathname,
      query: query
    });
  }),

  /**
   * The full URL.
   */
  href: d.gs(function () {
    var auth = this.auth;
    var host = this.host;
    var path = this.path;

    return host ? (this.protocol + '//' + (auth ? auth + '@' : '') + host + path) : path;
  }, function (value) {
    var parsed = parseURL(value);

    setProperties(this, {
      protocol: parsed.protocol,
      auth: parsed.auth,
      hostname: parsed.hostname,
      port: parsed.port,
      pathname: parsed.pathname,
      search: parsed.search
    });
  }),

  /**
   * The portion of the URL that denotes the protocol, including the
   * trailing colon (e.g. "http:" or "https:").
   */
  protocol: propertyAlias('protocol'),

  /**
   * The username:password used in the URL, if any.
   */
  auth: propertyAlias('auth', ''),

  /**
   * The full name of the host, including the port number when using
   * a non-standard port.
   */
  host: d.gs(function () {
    var protocol = this.protocol;
    var host = this.hostname;
    var port = this.port;

    if (port != null && port !== STANDARD_PORTS[protocol])
      host += ':' + port;

    return host;
  }, function (value) {
    var index;

    if (typeof value === 'string' && (index = value.indexOf(':')) !== -1) {
      this.hostname = value.substring(0, index);
      this.port = value.substring(index + 1);
    } else {
      this.hostname = value;
      this.port = null;
    }
  }),

  /**
   * The name of the host without the port.
   */
  hostname: propertyAlias('hostname'),

  /**
   * The port number as a string.
   */
  port: d.gs(function () {
    return this.properties.port || (this.protocol ? STANDARD_PORTS[this.protocol] : null);
  }, function (value) {
    this.properties.port = value ? String(value) : null;
  }),

  /**
   * The URL path without the query string.
   */
  pathname: propertyAlias('pathname', '/'),

  /**
   * The URL path with query string.
   */
  path: d.gs(function () {
    return this.pathname + this.search;
  }, function (value) {
    var index;

    if (typeof value === 'string' && (index = value.indexOf('?')) !== -1) {
      this.pathname = value.substring(0, index);
      this.search = value.substring(index);
    } else {
      this.pathname = value;
      this.search = null;
    }
  }),

  /**
   * The query string, including the preceeding ?.
   */
  search: propertyAlias('search', ''),

  /**
   * The query string of the URL, without the preceeding ?.
   */
  queryString: d.gs(function () {
    return this.search.substring(1);
  }, function (value) {
    this.search = value && '?' + value;
  }),

  /**
   * An object of data in the query string.
   */
  query: d.gs(function () {
    return parseQuery(this.queryString);
  }, function (value) {
    this.queryString = stringifyQuery(value);
  }),

  toJSON: d(function () {
    return this.href;
  }),

  toString: d(function () {
    return this.href;
  })

});

module.exports = Location;