<commit_before>/**
 * Copyright reelyActive 2015
 * We believe in an open Internet of Things
 */

var nedb = require('nedb');

var ASSOCIATION_DB = "associations.db";
var TEST_TRANSMITTER = { "_id": "001bc50940100000",
                         "url": "http://reelyactive.com/metadata/test.json" };
var TEST_RECEIVER_0 = { "_id": "001bc50940800000", "tags": [ "test" ] };
var TEST_RECEIVER_1 = { "_id": "001bc50940800001", "tags": [ "test" ] };
var TEST_RECEIVER_2 = { "_id": "001bc50940810000", "tags": [ "test" ] };
var TEST_RECEIVER_3 = { "_id": "001bc50940810001", "tags": [ "test" ] };

var TAGS_SPLIT_CHAR = ',';

/**
 * AssociationManager Class
 * Manages the association of identifiers with URLs
 * @constructor
 */
function AssociationManager() {
  var self = this;
  this.db = new nedb({filename: ASSOCIATION_DB, autoload: true });
  this.db.insert(TEST_TRANSMITTER);
  this.db.insert(TEST_RECEIVER_0);
  this.db.insert(TEST_RECEIVER_1);
  this.db.insert(TEST_RECEIVER_2);
  this.db.insert(TEST_RECEIVER_3);
}

/**
 * Find and append any URL associated with each device identifier
 * @param {Object} state State of devices.
 * @param {function} callback Function to call on completion.
 */
AssociationManager.prototype.addUrls = function(state, callback) {
  var self = this;
  var devices = state.devices;
  var transmitterIDs = Object.keys(devices);
  var decoders = extractRadioDecoders(devices);
  var decoderIDs = Object.keys(decoders);
  var ids = transmitterIDs.concat(decoderIDs);

  this.db.find({ _id: { $in: ids } }, function(err, urls) {
    var lookup = {};

    // Create a lookup table of all registered associations
    for(cUrl = 0; cUrl < urls.length; cUrl++) {
      if(urls[cUrl].url) {
        lookup[urls[cUrl]._id] = urls[cUrl].url;
      }
    }

    // Add each radioDecoder to the list of devices
    for(var cDecoder = 0; cDecoder < decoderIDs.length; cDecoder++) {
      var decoderId = decoderIDs[cDecoder];
      devices[decoderId] = decoders[decoderId];
    }

    // Add a url to each device and remove the identifier
    for(var deviceId in devices) {
      var device = devices[deviceId];
      var lookupExists = lookup.hasOwnProperty(deviceId);
      if(lookupExists) {
        device.url = lookup[deviceId];
      }
      else {
        makeExternalAssociation(device);
      }
      delete device.identifier;

      // Substitute radioDecodings for nearest
      if(device.radioDecodings) {
        device.nearest = [];

        for(var cDecoding = 0; cDecoding < device.radioDecodings.length;
            cDecoding++) {
          var decoding = device.radioDecodings[cDecoding];
          device.nearest[cDecoding] = { device: decoding.identifier.value,
                                        rssi: decoding.rssi };
        }
        delete device.radioDecodings;
      }
    }
    callback(state);
  });
};

/**
 * Attempts to associate a device with an external API
 * https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers
 * @param {Object} device The given device.
 */
function makeExternalAssociation(device) {
  if(device.identifier.type === "EUI-64") {
    var oui36 = device.identifier.value.slice(0,9);
    switch(oui36) {
      case "001bc5094": // reelyActive
        if(device.identifier.value.substr(9,2) === '01') {
          device.url = "http://reelyactive.com/metadata/ra-t411.json";
        }
        else {
          device.url = "http://reelyactive.com/metadata/ra-rxxx.json";
        }
        return;
      default: // Unknown
        return;
    }
  }
  var advData = device.identifier.advData;
  var advHeader = device.identifier.advHeader;
  if(advData) {
    var complete128BitUUIDs = advData.complete128BitUUIDs;
    var nonComplete128BitUUIDs = advData.nonComplete128BitUUIDs;
    var complete16BitUUIDs = advData.complete16BitUUIDs;
    var nonComplete16BitUUIDs = advData.nonComplete16BitUUIDs;
    var uuid = complete128BitUUIDs || nonComplete128BitUUIDs ||
               complete16BitUUIDs || nonComplete16BitUUIDs;
    switch(uuid) {
      case "7265656c794163746976652055554944":  // RA-R436
        device.url = "http://reelyactive.com/metadata/ra-r436.json";
        return;
      case "2f521f8c4d6f12269c600050e4c00067":  // WNDR
        device.url = "http://reelyactive.com/metadata/wndr.json";
                     // "http://getwndr.com/reelyactive/user/" +
                     // advData.completeLocalName;
        return;
      case "adabfb006e7d4601bda2bffaa68956ba":  // Fitbit
        device.url = "http://reelyactive.com/metadata/fitbit.json";
        return;
      case "d5060001a904deb947482c7f4a124842":  // MYO
        device.url = "http://reelyactive.com/metadata/myo.json";
        return;
      case "a495ff10c5b14b44b5121370f02d74de":  // Bean (PunchThrough)
        device.url = "http://reelyactive.com/metadata/bean.json";
        return;
      case "bd4ac6100b4511e38ffd0800200c9a66":  // August Smart Lock
        device.url = "http://reelyactive.com/metadata/august.json";
        return;
      case "9ac78e8d1e9943ce83637c1b1e003a10":  // VanHawks Valour
        device.url = "http://reelyactive.com/metadata/vanhawks.json";
        return;
      case "f81e00015aa144bda4806ab7767d47e3":  // Mightycast NEX Band
        device.url = "http://reelyactive.com/metadata/nexband.json";
        return;
      case "0f3e":  // TrackR
        device.url = "http://reelyactive.com/metadata/trackr.json";
        return;
      case "febf":  // Nod (OpenSpatial)
        device.url = "http://reelyactive.com/metadata/nod.json";
        return;
      case "fed8":  // Physical Web (Google)
        if(advData.serviceData && advData.serviceData.uriBeacon &&
           advData.serviceData.uriBeacon.url) {
          device.url = advData.serviceData.uriBeacon.url;
        }
        else {
          device.url = "http://reelyactive.com/metadata/physicalweb.json";
        }
        return;
      case "feed":  // Tile
        device.url = "http://reelyactive.com/metadata/tile.json";
        return;
    }
    var manufacturerSpecificData = advData.manufacturerSpecificData;
    if(manufacturerSpecificData) {
      var iBeacon = manufacturerSpecificData.iBeacon;
      if(iBeacon) {
        var beaconId = iBeacon.uuid;
        switch(beaconId) {
          case "b9407f30f5f8466eaff925556b57fe6d": // Estimote
            device.url = "http://reelyactive.com/metadata/estimote.json";
            return;
          case "f7826da64fa24e988024bc5b71e0893e": // Kontakt.io
            device.url = "http://reelyactive.com/metadata/kontakt.json";
            return;
          case "8deefbb9f7384297804096668bb44281": // Roximity
            device.url = "http://reelyactive.com/metadata/roximity.json";
            return;
          case "f0018b9b75094c31a9051a27d39c003c": // LocosLab
            device.url = "http://reelyactive.com/metadata/locoslab.json";
            return;
          case "dab59c4fa4d6ee286bfe8e0000bbc2bb": // eNote
            device.url = "http://reelyactive.com/metadata/enote.json";
            return;
          default:
            device.url = "http://reelyactive.com/metadata/ibeacon.json";
            return;
        }
      }
      var companyIdentifierCode = manufacturerSpecificData.companyIdentifierCode;
      switch(companyIdentifierCode) {
        case "004c": // Apple inc.
          device.url = "http://reelyactive.com/metadata/apple.json";
          return;
        case "008c": // Gimbal inc.
          device.url = "http://reelyactive.com/metadata/gimbal.json";
          return;
        case "beba": // Nymi inc. ???
          device.url = "http://reelyactive.com/metadata/nymi.json";
          return;
      }
    }
    var serviceData = advData.serviceData;
    if(serviceData) {
      switch(serviceData.uuid) {
        case "fee6":  // Seed
          device.url = "http://reelyactive.com/metadata/seedlabs.json";
          return;
      }
    }
  }
  if(advHeader) {
    var type = advHeader.type;
    if(type === "SCAN_REQ") { // Curious device
      device.url = "http://reelyactive.com/metadata/curious.json";
      return;
    }
    else { // Unknown Bluetooth Smart device
      device.url = "http://reelyactive.com/metadata/bluetoothsmart.json";
    }
  }
}

/**
 * Retrieve an association from the database
 * @param {String} id The id of the association.
 * @param {String} property A specific association property to retrieve.
 * @param {function} callback Function to call on completion.
 */
AssociationManager.prototype.retrieve = function(id, property, callback) {
  var self = this;
  var projection;

  switch(property) {
    case 'url':
      projection = { url: 1, _id: 0 };
      break;
    case 'directory':
      projection = { directory: 1, _id: 0 };
      break;
    case 'tags':
      projection = { tags: 1, _id: 0 };
      break;
    default:
      projection = { _id: 0 };
  }

  self.db.find({ _id: id }, projection, function(err, devices) {
    var device = devices[0];
    if(err) { err = err.message; }
    else if(!device || (Object.keys(device).length === 0)) { device = null; }
    callback(err, id, device);
  });
};

/**
 * Replace an association in the database, create if it doesn't exist
 * @param {String} id The id of the association.
 * @param {String} url URL to associate.
 * @param {String} directory Directory to associate.
 * @param {Array} tags The tags to associate.
 * @param {function} callback Function to call on completion.
 */
AssociationManager.prototype.replace = function(id, url, directory, tags,
                                                callback) {
  var self = this;
  var association = {};

  if(url) {
    association.url = url;
  }
  if(directory) {
    association.directory = directory;
  }
  if(tags) {
    association.tags = tags;
  }

  this.db.update({ _id: id }, { $set: association }, { upsert: true },
                 function(err, numReplaced, device) {
    if(err) { err = err.message; }
    if(device) {
      id = device._id;
      delete device._id;
    }
    else { device = association; }
    callback(err, id, device);
  });
};

/**
 * Remove an association from the database
 * @param {String} id Identifier to remove.
 * @param {String} property A specific association property to retrieve.
 * @param {function} callback Function to call on completion.
 */
AssociationManager.prototype.remove = function(id, property, callback) {
  var self = this;

  if(!property) {
    self.db.remove({ _id: id }, function(err, numRemoved) {
      if(err) { err = err.message; }
      else if(numRemoved === 0) { err = "Identifier not found"; }
      callback(err);
    });
  }
  else {
    var unset = {};
    if(property === 'url') {
      unset = { url: true };
    }
    else if(property === 'directory') {
      unset = { directory: true };
    }
    else if(property === 'tags') {
      unset = { tags: true };
    }
    self.db.update({ _id: id }, { $unset: unset }, {},
                   function(err, numReplaced) {
      if(err) { err = err.message; }
      else if(numReplaced === 0) { err = "Identifier not found"; }
      callback(err);
    });
  }
};

/**
 * Retrieve the ids associated with the given parameters
 * @param {String} directory The association directory to search for.
 * @param {String} tags The assocation tags to search for.
 * @param {function} callback Function to call on completion.
 */
AssociationManager.prototype.retrieveIdsByParams = function(directory, tags,
                                                            callback) {
  var self = this;
  var query = {};
  var ids = [];

  if(directory) {
    var regex = new RegExp('^' + directory + '([:]+|$)');
    query = { directory: { $regex: regex } };
  }
  else if(tags) {
    var tagsArray = tags.split(TAGS_SPLIT_CHAR);
    query = { tags: { $in: tagsArray } };
  }
  else {
    callback(ids);
  }

  self.db.find(query, { _id: 1 }, function(err, devices) {
    if(!devices) {
      callback(ids);
    }
    else {
      for(var cDevice = 0; cDevice < devices.length; cDevice++) {
        ids.push(devices[cDevice]._id);
      }
      callback(ids);
    }
  });
};

/**
 * Extract the radio decoders from the given list of devices.
 * @param {Object} devices The given devices.
 */
function extractRadioDecoders(devices) {
  var radioDecoders = {};

  for(var deviceId in devices) {
    var radioDecodings = devices[deviceId].radioDecodings || [];
    for(var cDecoding = 0; cDecoding < radioDecodings.length; cDecoding++) {
      var radioDecoder = radioDecodings[cDecoding].identifier;
      var radioDecoderId = radioDecoder.value;
      radioDecoders[radioDecoderId] = { identifier: radioDecoder };
    }
  }

  return radioDecoders;
}

module.exports = AssociationManager;
module.exports.makeExternalAssociation = makeExternalAssociation;
module.exports.extractRadioDecoders = extractRadioDecoders;
<commit_msg>Added Xiaomi Mi band<commit_after>/**
 * Copyright reelyActive 2015
 * We believe in an open Internet of Things
 */

var nedb = require('nedb');

var ASSOCIATION_DB = "associations.db";
var TEST_TRANSMITTER = { "_id": "001bc50940100000",
                         "url": "http://reelyactive.com/metadata/test.json" };
var TEST_RECEIVER_0 = { "_id": "001bc50940800000", "tags": [ "test" ] };
var TEST_RECEIVER_1 = { "_id": "001bc50940800001", "tags": [ "test" ] };
var TEST_RECEIVER_2 = { "_id": "001bc50940810000", "tags": [ "test" ] };
var TEST_RECEIVER_3 = { "_id": "001bc50940810001", "tags": [ "test" ] };

var TAGS_SPLIT_CHAR = ',';

/**
 * AssociationManager Class
 * Manages the association of identifiers with URLs
 * @constructor
 */
function AssociationManager() {
  var self = this;
  this.db = new nedb({filename: ASSOCIATION_DB, autoload: true });
  this.db.insert(TEST_TRANSMITTER);
  this.db.insert(TEST_RECEIVER_0);
  this.db.insert(TEST_RECEIVER_1);
  this.db.insert(TEST_RECEIVER_2);
  this.db.insert(TEST_RECEIVER_3);
}

/**
 * Find and append any URL associated with each device identifier
 * @param {Object} state State of devices.
 * @param {function} callback Function to call on completion.
 */
AssociationManager.prototype.addUrls = function(state, callback) {
  var self = this;
  var devices = state.devices;
  var transmitterIDs = Object.keys(devices);
  var decoders = extractRadioDecoders(devices);
  var decoderIDs = Object.keys(decoders);
  var ids = transmitterIDs.concat(decoderIDs);

  this.db.find({ _id: { $in: ids } }, function(err, urls) {
    var lookup = {};

    // Create a lookup table of all registered associations
    for(cUrl = 0; cUrl < urls.length; cUrl++) {
      if(urls[cUrl].url) {
        lookup[urls[cUrl]._id] = urls[cUrl].url;
      }
    }

    // Add each radioDecoder to the list of devices
    for(var cDecoder = 0; cDecoder < decoderIDs.length; cDecoder++) {
      var decoderId = decoderIDs[cDecoder];
      devices[decoderId] = decoders[decoderId];
    }

    // Add a url to each device and remove the identifier
    for(var deviceId in devices) {
      var device = devices[deviceId];
      var lookupExists = lookup.hasOwnProperty(deviceId);
      if(lookupExists) {
        device.url = lookup[deviceId];
      }
      else {
        makeExternalAssociation(device);
      }
      delete device.identifier;

      // Substitute radioDecodings for nearest
      if(device.radioDecodings) {
        device.nearest = [];

        for(var cDecoding = 0; cDecoding < device.radioDecodings.length;
            cDecoding++) {
          var decoding = device.radioDecodings[cDecoding];
          device.nearest[cDecoding] = { device: decoding.identifier.value,
                                        rssi: decoding.rssi };
        }
        delete device.radioDecodings;
      }
    }
    callback(state);
  });
};

/**
 * Attempts to associate a device with an external API
 * https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers
 * @param {Object} device The given device.
 */
function makeExternalAssociation(device) {
  if(device.identifier.type === "EUI-64") {
    var oui36 = device.identifier.value.slice(0,9);
    switch(oui36) {
      case "001bc5094": // reelyActive
        if(device.identifier.value.substr(9,2) === '01') {
          device.url = "http://reelyactive.com/metadata/ra-t411.json";
        }
        else {
          device.url = "http://reelyactive.com/metadata/ra-rxxx.json";
        }
        return;
      default: // Unknown
        return;
    }
  }
  var advData = device.identifier.advData;
  var advHeader = device.identifier.advHeader;
  if(advData) {
    var complete128BitUUIDs = advData.complete128BitUUIDs;
    var nonComplete128BitUUIDs = advData.nonComplete128BitUUIDs;
    var complete16BitUUIDs = advData.complete16BitUUIDs;
    var nonComplete16BitUUIDs = advData.nonComplete16BitUUIDs;
    var uuid = complete128BitUUIDs || nonComplete128BitUUIDs ||
               complete16BitUUIDs || nonComplete16BitUUIDs;
    switch(uuid) {
      case "7265656c794163746976652055554944":  // RA-R436
        device.url = "http://reelyactive.com/metadata/ra-r436.json";
        return;
      case "2f521f8c4d6f12269c600050e4c00067":  // WNDR
        device.url = "http://reelyactive.com/metadata/wndr.json";
                     // "http://getwndr.com/reelyactive/user/" +
                     // advData.completeLocalName;
        return;
      case "adabfb006e7d4601bda2bffaa68956ba":  // Fitbit
        device.url = "http://reelyactive.com/metadata/fitbit.json";
        return;
      case "d5060001a904deb947482c7f4a124842":  // MYO
        device.url = "http://reelyactive.com/metadata/myo.json";
        return;
      case "a495ff10c5b14b44b5121370f02d74de":  // Bean (PunchThrough)
        device.url = "http://reelyactive.com/metadata/bean.json";
        return;
      case "bd4ac6100b4511e38ffd0800200c9a66":  // August Smart Lock
        device.url = "http://reelyactive.com/metadata/august.json";
        return;
      case "9ac78e8d1e9943ce83637c1b1e003a10":  // VanHawks Valour
        device.url = "http://reelyactive.com/metadata/vanhawks.json";
        return;
      case "f81e00015aa144bda4806ab7767d47e3":  // Mightycast NEX Band
        device.url = "http://reelyactive.com/metadata/nexband.json";
        return;
      case "0f3e":  // TrackR
        device.url = "http://reelyactive.com/metadata/trackr.json";
        return;
      case "febf":  // Nod (OpenSpatial)
        device.url = "http://reelyactive.com/metadata/nod.json";
        return;
      case "fed8":  // Physical Web (Google)
        if(advData.serviceData && advData.serviceData.uriBeacon &&
           advData.serviceData.uriBeacon.url) {
          device.url = advData.serviceData.uriBeacon.url;
        }
        else {
          device.url = "http://reelyactive.com/metadata/physicalweb.json";
        }
        return;
      case "feed":  // Tile
        device.url = "http://reelyactive.com/metadata/tile.json";
        return;
    }
    var manufacturerSpecificData = advData.manufacturerSpecificData;
    if(manufacturerSpecificData) {
      var iBeacon = manufacturerSpecificData.iBeacon;
      if(iBeacon) {
        var beaconId = iBeacon.uuid;
        switch(beaconId) {
          case "b9407f30f5f8466eaff925556b57fe6d": // Estimote
            device.url = "http://reelyactive.com/metadata/estimote.json";
            return;
          case "f7826da64fa24e988024bc5b71e0893e": // Kontakt.io
            device.url = "http://reelyactive.com/metadata/kontakt.json";
            return;
          case "8deefbb9f7384297804096668bb44281": // Roximity
            device.url = "http://reelyactive.com/metadata/roximity.json";
            return;
          case "f0018b9b75094c31a9051a27d39c003c": // LocosLab
            device.url = "http://reelyactive.com/metadata/locoslab.json";
            return;
          case "dab59c4fa4d6ee286bfe8e0000bbc2bb": // eNote
            device.url = "http://reelyactive.com/metadata/enote.json";
            return;
          default:
            device.url = "http://reelyactive.com/metadata/ibeacon.json";
            return;
        }
      }
      var companyIdentifierCode = manufacturerSpecificData.companyIdentifierCode;
      switch(companyIdentifierCode) {
        case "004c": // Apple inc.
          device.url = "http://reelyactive.com/metadata/apple.json";
          return;
        case "008c": // Gimbal inc.
          device.url = "http://reelyactive.com/metadata/gimbal.json";
          return;
        case "0157": // Anhui Huami Information Technology Co., Ltd. (Xiaomi)
          device.url = "http://reelyactive.com/metadata/xiaomi.json";
          return;
        case "beba": // Nymi inc. ???
          device.url = "http://reelyactive.com/metadata/nymi.json";
          return;
      }
    }
    var serviceData = advData.serviceData;
    if(serviceData) {
      switch(serviceData.uuid) {
        case "fee6":  // Seed
          device.url = "http://reelyactive.com/metadata/seedlabs.json";
          return;
      }
    }
  }
  if(advHeader) {
    var type = advHeader.type;
    if(type === "SCAN_REQ") { // Curious device
      device.url = "http://reelyactive.com/metadata/curious.json";
      return;
    }
    else { // Unknown Bluetooth Smart device
      device.url = "http://reelyactive.com/metadata/bluetoothsmart.json";
    }
  }
}

/**
 * Retrieve an association from the database
 * @param {String} id The id of the association.
 * @param {String} property A specific association property to retrieve.
 * @param {function} callback Function to call on completion.
 */
AssociationManager.prototype.retrieve = function(id, property, callback) {
  var self = this;
  var projection;

  switch(property) {
    case 'url':
      projection = { url: 1, _id: 0 };
      break;
    case 'directory':
      projection = { directory: 1, _id: 0 };
      break;
    case 'tags':
      projection = { tags: 1, _id: 0 };
      break;
    default:
      projection = { _id: 0 };
  }

  self.db.find({ _id: id }, projection, function(err, devices) {
    var device = devices[0];
    if(err) { err = err.message; }
    else if(!device || (Object.keys(device).length === 0)) { device = null; }
    callback(err, id, device);
  });
};

/**
 * Replace an association in the database, create if it doesn't exist
 * @param {String} id The id of the association.
 * @param {String} url URL to associate.
 * @param {String} directory Directory to associate.
 * @param {Array} tags The tags to associate.
 * @param {function} callback Function to call on completion.
 */
AssociationManager.prototype.replace = function(id, url, directory, tags,
                                                callback) {
  var self = this;
  var association = {};

  if(url) {
    association.url = url;
  }
  if(directory) {
    association.directory = directory;
  }
  if(tags) {
    association.tags = tags;
  }

  this.db.update({ _id: id }, { $set: association }, { upsert: true },
                 function(err, numReplaced, device) {
    if(err) { err = err.message; }
    if(device) {
      id = device._id;
      delete device._id;
    }
    else { device = association; }
    callback(err, id, device);
  });
};

/**
 * Remove an association from the database
 * @param {String} id Identifier to remove.
 * @param {String} property A specific association property to retrieve.
 * @param {function} callback Function to call on completion.
 */
AssociationManager.prototype.remove = function(id, property, callback) {
  var self = this;

  if(!property) {
    self.db.remove({ _id: id }, function(err, numRemoved) {
      if(err) { err = err.message; }
      else if(numRemoved === 0) { err = "Identifier not found"; }
      callback(err);
    });
  }
  else {
    var unset = {};
    if(property === 'url') {
      unset = { url: true };
    }
    else if(property === 'directory') {
      unset = { directory: true };
    }
    else if(property === 'tags') {
      unset = { tags: true };
    }
    self.db.update({ _id: id }, { $unset: unset }, {},
                   function(err, numReplaced) {
      if(err) { err = err.message; }
      else if(numReplaced === 0) { err = "Identifier not found"; }
      callback(err);
    });
  }
};

/**
 * Retrieve the ids associated with the given parameters
 * @param {String} directory The association directory to search for.
 * @param {String} tags The assocation tags to search for.
 * @param {function} callback Function to call on completion.
 */
AssociationManager.prototype.retrieveIdsByParams = function(directory, tags,
                                                            callback) {
  var self = this;
  var query = {};
  var ids = [];

  if(directory) {
    var regex = new RegExp('^' + directory + '([:]+|$)');
    query = { directory: { $regex: regex } };
  }
  else if(tags) {
    var tagsArray = tags.split(TAGS_SPLIT_CHAR);
    query = { tags: { $in: tagsArray } };
  }
  else {
    callback(ids);
  }

  self.db.find(query, { _id: 1 }, function(err, devices) {
    if(!devices) {
      callback(ids);
    }
    else {
      for(var cDevice = 0; cDevice < devices.length; cDevice++) {
        ids.push(devices[cDevice]._id);
      }
      callback(ids);
    }
  });
};

/**
 * Extract the radio decoders from the given list of devices.
 * @param {Object} devices The given devices.
 */
function extractRadioDecoders(devices) {
  var radioDecoders = {};

  for(var deviceId in devices) {
    var radioDecodings = devices[deviceId].radioDecodings || [];
    for(var cDecoding = 0; cDecoding < radioDecodings.length; cDecoding++) {
      var radioDecoder = radioDecodings[cDecoding].identifier;
      var radioDecoderId = radioDecoder.value;
      radioDecoders[radioDecoderId] = { identifier: radioDecoder };
    }
  }

  return radioDecoders;
}

module.exports = AssociationManager;
module.exports.makeExternalAssociation = makeExternalAssociation;
module.exports.extractRadioDecoders = extractRadioDecoders;
<|endoftext|>


<commit_before>/* eslint no-undefined: 0 */
'use strict';

const path      = require('path');
const assert    = require('assert');
const https     = require('https');
const walk      = require('fs-walk');
const async     = require('async');
const semver    = require('semver');
const digest    = require('../lib/helpers.js').sri.digest;
const helpers   = require('./test_helper.js');

const config    = helpers.config();

const expectedHeaders = {
    'accept-ranges': 'bytes',
    'access-control-allow-origin': '*',
    'cache-control': undefined,

    'connection': 'Keep-Alive',
    'content-length': undefined,
    'content-type': undefined,
    'date': undefined,
    'etag': undefined,
    'last-modified': undefined,
    'vary': 'Accept-Encoding',
    'x-cache': undefined,
    'x-hello-human': 'Say hello back! @getBootstrapCDN on Twitter'
};

const responses = {};

function request(uri, cb) {
    // return memoized response to avoid making the same http call twice
    if (Object.prototype.hasOwnProperty.call(responses, uri)) {
        return cb(responses[uri]);
    }

    // build memoized response
    return helpers.preFetch(uri, (res) => {
        responses[uri] = res;
        cb(res);
    }, https);
}

function assertSRI(uri, sri, done) {
    request(uri, (response) => {
        assert.equal(200, response.statusCode);

        const expected = digest(response.body, true);

        assert.equal(expected, sri);
        done();
    });
}

const s3include = ['content-type'];

function assertHeader(uri, header, value) {
    if (typeof process.env.TEST_S3 !== 'undefined' && !s3include.includes(header)) {
        it.skip(`has ${header}`);
    } else {
        it(`has ${header}`, (done) => {
            request(uri, (response) => {
                assert.equal(200, response.statusCode);
                assert(Object.prototype.hasOwnProperty.call(response.headers, header),
                    'Expected: ${header} in: ${Object.keys(response.headers).join(", ")}');

                if (typeof value !== 'undefined') {
                    assert.equal(response.headers[header], value);
                } else if (expectedHeaders[header]) {
                    assert.equal(response.headers[header], expectedHeaders[header]);
                }

                done();
            });
        });
    }
}

describe('functional', () => {
    config.bootstrap.forEach((self) => {
        describe(helpers.domainCheck(self.javascript), () => {
            const uri = helpers.domainCheck(self.javascript);

            Object.keys(expectedHeaders).forEach((header) => {
                assertHeader(uri, header);
            });

            assertHeader(uri, 'content-type', 'application/javascript; charset=utf-8');

            it('has integrity', (done) => {
                assertSRI(uri, self.javascriptSri, done);
            });
        });

        describe(helpers.domainCheck(self.stylesheet), () => {
            const uri = helpers.domainCheck(self.stylesheet);

            Object.keys(expectedHeaders).forEach((header) => {
                assertHeader(uri, header);
            });

            assertHeader(uri, 'content-type', 'text/css; charset=utf-8');

            it('has integrity', (done) => {
                assertSRI(uri, self.stylesheetSri, done);
            });
        });
    });

    describe('bootswatch3', () => {
        config.bootswatch3.themes.forEach((theme) => {
            const uri = helpers.domainCheck(config.bootswatch3.bootstrap
                .replace('SWATCH_VERSION', config.bootswatch3.version)
                .replace('SWATCH_NAME', theme.name));

            describe(uri, () => {
                Object.keys(expectedHeaders).forEach((header) => {
                    assertHeader(uri, header);
                });

                assertHeader(uri, 'content-type', 'text/css; charset=utf-8');

                it('has integrity', (done) => {
                    assertSRI(uri, theme.sri, done);
                });
            });
        });
    });

    describe('bootswatch4', () => {
        config.bootswatch4.themes.forEach((theme) => {
            const uri = helpers.domainCheck(config.bootswatch4.bootstrap
                .replace('SWATCH_VERSION', config.bootswatch4.version)
                .replace('SWATCH_NAME', theme.name));

            describe(uri, () => {
                Object.keys(expectedHeaders).forEach((header) => {
                    assertHeader(uri, header);
                });

                assertHeader(uri, 'content-type', 'text/css; charset=utf-8');

                it('has integrity', (done) => {
                    assertSRI(uri, theme.sri, done);
                });
            });
        });
    });

    describe('bootlint', () => {
        config.bootlint.forEach((self) => {
            const uri = helpers.domainCheck(self.javascript);

            describe(uri, () => {
                Object.keys(expectedHeaders).forEach((header) => {
                    assertHeader(uri, header);
                });

                assertHeader(uri, 'content-type', 'application/javascript; charset=utf-8');

                it('has integrity', (done) => {
                    assertSRI(uri, self.javascriptSri, done);
                });
            });
        });
    });

    describe('fontawesome', () => {
        config.fontawesome.forEach((self) => {
            const uri = helpers.domainCheck(self.stylesheet);

            describe(uri, () => {
                Object.keys(expectedHeaders).forEach((header) => {
                    assertHeader(uri, header);
                });

                assertHeader(uri, 'content-type', 'text/css; charset=utf-8');

                it('has integrity', (done) => {
                    assertSRI(uri, self.stylesheetSri, done);
                });
            });
        });
    });

    describe('public/**/*.*', () => {
        // Build File List
        const whitelist = [
            'bootlint',
            'bootstrap',
            'bootswatch',
            'font-awesome',
            'twitter-bootstrap',
            'css',
            'js'
        ];

        const publicURIs = [];

        walk.filesSync(path.join(__dirname, '..', 'public'), (base, name) => {
            let root = base.split(`${path.sep}public${path.sep}`)[1];

            // ensure file is in whitelisted directory
            if (typeof root === 'undefined' || !whitelist.includes(root.split(path.sep)[0])) {
                return;
            }

            // replace Windows backslashes with forward ones
            root = root.replace(/\\/g, '/');
            const domain = helpers.domainCheck('https://stackpath.bootstrapcdn.com/');
            const uri = `${domain + root}/${name}`;
            const ext = helpers.extension(name);

            // ignore unknown / unsupported types
            if (typeof helpers.CONTENT_TYPE_MAP[ext] === 'undefined') {
                return;
            }

            // ignore twitter-bootstrap versions after 2.3.2
            if (uri.includes('twitter-bootstrap')) {
                const m = uri.match(/(\d+\.\d+\.\d+)/);

                // err on the side of testing things that can't be abstracted
                if (m && m[1] && semver.valid(m[1]) && semver.gt(m[1], '2.3.2')) {
                    return; // don't add the file
                }
            }

            publicURIs.push(uri);
        });

        // Run Tests
        async.eachSeries(publicURIs, (uri, callback) => {
            describe(uri, () => {
                it('content-type', (done) => {
                    request(uri, (response) => {
                        assert.equal(200, response.statusCode, 'file missing or forbidden');

                        helpers.assert.contentType(uri, response.headers['content-type']);
                        done();
                        callback();
                    });
                });
            });
        });
    });
});
<commit_msg>functional_test.js: fix template literals.<commit_after>/* eslint no-undefined: 0 */
'use strict';

const path      = require('path');
const assert    = require('assert');
const https     = require('https');
const walk      = require('fs-walk');
const async     = require('async');
const semver    = require('semver');
const digest    = require('../lib/helpers.js').sri.digest;
const helpers   = require('./test_helper.js');

const config    = helpers.config();

const expectedHeaders = {
    'accept-ranges': 'bytes',
    'access-control-allow-origin': '*',
    'cache-control': undefined,

    'connection': 'Keep-Alive',
    'content-length': undefined,
    'content-type': undefined,
    'date': undefined,
    'etag': undefined,
    'last-modified': undefined,
    'vary': 'Accept-Encoding',
    'x-cache': undefined,
    'x-hello-human': 'Say hello back! @getBootstrapCDN on Twitter'
};

const responses = {};

function request(uri, cb) {
    // return memoized response to avoid making the same http call twice
    if (Object.prototype.hasOwnProperty.call(responses, uri)) {
        return cb(responses[uri]);
    }

    // build memoized response
    return helpers.preFetch(uri, (res) => {
        responses[uri] = res;
        cb(res);
    }, https);
}

function assertSRI(uri, sri, done) {
    request(uri, (response) => {
        assert.equal(200, response.statusCode);

        const expected = digest(response.body, true);

        assert.equal(expected, sri);
        done();
    });
}

const s3include = ['content-type'];

function assertHeader(uri, header, value) {
    if (typeof process.env.TEST_S3 !== 'undefined' && !s3include.includes(header)) {
        it.skip(`has ${header}`);
    } else {
        it(`has ${header}`, (done) => {
            request(uri, (response) => {
                assert.equal(200, response.statusCode);
                assert(Object.prototype.hasOwnProperty.call(response.headers, header),
                    `Expected: ${header} in: ${Object.keys(response.headers).join(', ')}`);

                if (typeof value !== 'undefined') {
                    assert.equal(response.headers[header], value);
                } else if (expectedHeaders[header]) {
                    assert.equal(response.headers[header], expectedHeaders[header]);
                }

                done();
            });
        });
    }
}

describe('functional', () => {
    config.bootstrap.forEach((self) => {
        describe(helpers.domainCheck(self.javascript), () => {
            const uri = helpers.domainCheck(self.javascript);

            Object.keys(expectedHeaders).forEach((header) => {
                assertHeader(uri, header);
            });

            assertHeader(uri, 'content-type', 'application/javascript; charset=utf-8');

            it('has integrity', (done) => {
                assertSRI(uri, self.javascriptSri, done);
            });
        });

        describe(helpers.domainCheck(self.stylesheet), () => {
            const uri = helpers.domainCheck(self.stylesheet);

            Object.keys(expectedHeaders).forEach((header) => {
                assertHeader(uri, header);
            });

            assertHeader(uri, 'content-type', 'text/css; charset=utf-8');

            it('has integrity', (done) => {
                assertSRI(uri, self.stylesheetSri, done);
            });
        });
    });

    describe('bootswatch3', () => {
        config.bootswatch3.themes.forEach((theme) => {
            const uri = helpers.domainCheck(config.bootswatch3.bootstrap
                .replace('SWATCH_VERSION', config.bootswatch3.version)
                .replace('SWATCH_NAME', theme.name));

            describe(uri, () => {
                Object.keys(expectedHeaders).forEach((header) => {
                    assertHeader(uri, header);
                });

                assertHeader(uri, 'content-type', 'text/css; charset=utf-8');

                it('has integrity', (done) => {
                    assertSRI(uri, theme.sri, done);
                });
            });
        });
    });

    describe('bootswatch4', () => {
        config.bootswatch4.themes.forEach((theme) => {
            const uri = helpers.domainCheck(config.bootswatch4.bootstrap
                .replace('SWATCH_VERSION', config.bootswatch4.version)
                .replace('SWATCH_NAME', theme.name));

            describe(uri, () => {
                Object.keys(expectedHeaders).forEach((header) => {
                    assertHeader(uri, header);
                });

                assertHeader(uri, 'content-type', 'text/css; charset=utf-8');

                it('has integrity', (done) => {
                    assertSRI(uri, theme.sri, done);
                });
            });
        });
    });

    describe('bootlint', () => {
        config.bootlint.forEach((self) => {
            const uri = helpers.domainCheck(self.javascript);

            describe(uri, () => {
                Object.keys(expectedHeaders).forEach((header) => {
                    assertHeader(uri, header);
                });

                assertHeader(uri, 'content-type', 'application/javascript; charset=utf-8');

                it('has integrity', (done) => {
                    assertSRI(uri, self.javascriptSri, done);
                });
            });
        });
    });

    describe('fontawesome', () => {
        config.fontawesome.forEach((self) => {
            const uri = helpers.domainCheck(self.stylesheet);

            describe(uri, () => {
                Object.keys(expectedHeaders).forEach((header) => {
                    assertHeader(uri, header);
                });

                assertHeader(uri, 'content-type', 'text/css; charset=utf-8');

                it('has integrity', (done) => {
                    assertSRI(uri, self.stylesheetSri, done);
                });
            });
        });
    });

    describe('public/**/*.*', () => {
        // Build File List
        const whitelist = [
            'bootlint',
            'bootstrap',
            'bootswatch',
            'font-awesome',
            'twitter-bootstrap',
            'css',
            'js'
        ];

        const publicURIs = [];

        walk.filesSync(path.join(__dirname, '..', 'public'), (base, name) => {
            let root = base.split(`${path.sep}public${path.sep}`)[1];

            // ensure file is in whitelisted directory
            if (typeof root === 'undefined' || !whitelist.includes(root.split(path.sep)[0])) {
                return;
            }

            // replace Windows backslashes with forward ones
            root = root.replace(/\\/g, '/');
            const domain = helpers.domainCheck('https://stackpath.bootstrapcdn.com/');
            const uri = `${domain + root}/${name}`;
            const ext = helpers.extension(name);

            // ignore unknown / unsupported types
            if (typeof helpers.CONTENT_TYPE_MAP[ext] === 'undefined') {
                return;
            }

            // ignore twitter-bootstrap versions after 2.3.2
            if (uri.includes('twitter-bootstrap')) {
                const m = uri.match(/(\d+\.\d+\.\d+)/);

                // err on the side of testing things that can't be abstracted
                if (m && m[1] && semver.valid(m[1]) && semver.gt(m[1], '2.3.2')) {
                    return; // don't add the file
                }
            }

            publicURIs.push(uri);
        });

        // Run Tests
        async.eachSeries(publicURIs, (uri, callback) => {
            describe(uri, () => {
                it('content-type', (done) => {
                    request(uri, (response) => {
                        assert.equal(200, response.statusCode, 'file missing or forbidden');

                        helpers.assert.contentType(uri, response.headers['content-type']);
                        done();
                        callback();
                    });
                });
            });
        });
    });
});
<|endoftext|>


<commit_before>var websocket = {
    socket: null,
    retrySocketDelay: 1,
    buffer: new Map(),
    messageCounter: 0,
    hasConnected: false,
    reconnectAttempts: 0,
    open: function() {
        console.info("Trying to open websocket...");

        websocket.socket = new WebSocket(location.protocol.replace(/^http/, 'ws') + location.host + "/florence/websocket");
        websocket.reconnectAttempts++;

        websocket.socket.onopen = function() {
            console.info("Websocket has been opened");
            websocket.hasConnected = true;
            websocket.retrySocketDelay = 1;
            websocket.buffer.forEach(function(message) {
                websocket.socket.send(message);
            });
            // FIXME we should be logging that the socket is open - but there's a horrible cyclical dependency on startup atm
        }

        websocket.socket.onerror = function() {
            console.error("Error opening websocket");
        }

        websocket.socket.onmessage = function(message) {
            message = JSON.parse(message.data);
            if (message.type === "version") {
                // TODO Not being used yet to check version but should replace existing implementation at some point
                Florence.version = message.payload.version;
                return;
            }
            if (message.type !== "ack") {
                return;
            }
            websocket.buffer.delete(parseInt(message.payload));
        }

        websocket.socket.onclose = function() {
            console.info("Websocket has been closed");
            websocket.retrySocketDelay *= 2;
            if (websocket.retrySocketDelay >= 300000) {
                websocket.retrySocketDelay = 300000;
            }

            if (!websocket.hasConnected && websocket.reconnectAttempts >= 100) {
                console.info("Unable to connect websocket after 100 attempts, so stop retrying websocket");
                return;
            }

            setTimeout(() => {
                // setTimeout sets 'this' to window wrapping 'this.open()' in a lambda function
                // (rather than passing it inline), stops it.
                // Ref: https://stackoverflow.com/questions/2130241/pass-correct-this-context-to-settimeout-callback
                websocket.open();
            }, websocket.retrySocketDelay);
        }
    },
    send: function(message) {
        websocket.messageCounter++;
        message = websocket.messageCounter + ":" + message;

        if (websocket.buffer.size >= 50) {
            console.warn(`Websocket buffer has reached it's limit, so message will not be sent to server. Message: \n`, message);
            log.add(log.eventTypes.socketBufferFull); // This has to be excluded from being sent to the server or else we'll have an infinite loop
            return;
        }

        websocket.buffer.set(websocket.messageCounter, message);

        if (websocket.socket.readyState === 1) {
            websocket.socket.send(message);
        }
    }
};<commit_msg>Stop logging full websocket buffer to fix infinite loop<commit_after>var websocket = {
    socket: null,
    retrySocketDelay: 1,
    buffer: new Map(),
    messageCounter: 0,
    hasConnected: false,
    reconnectAttempts: 0,
    open: function() {
        console.info("Trying to open websocket...");

        websocket.socket = new WebSocket(location.protocol.replace(/^http/, 'ws') + location.host + "/florence/websocket");
        websocket.reconnectAttempts++;

        websocket.socket.onopen = function() {
            console.info("Websocket has been opened");
            websocket.hasConnected = true;
            websocket.retrySocketDelay = 1;
            websocket.buffer.forEach(function(message) {
                websocket.socket.send(message);
            });
            // FIXME we should be logging that the socket is open - but there's a horrible cyclical dependency on startup atm
        }

        websocket.socket.onerror = function() {
            console.error("Error opening websocket");
        }

        websocket.socket.onmessage = function(message) {
            message = JSON.parse(message.data);
            if (message.type === "version") {
                // TODO Not being used yet to check version but should replace existing implementation at some point
                Florence.version = message.payload.version;
                return;
            }
            if (message.type !== "ack") {
                return;
            }
            websocket.buffer.delete(parseInt(message.payload));
        }

        websocket.socket.onclose = function() {
            console.info("Websocket has been closed");
            websocket.retrySocketDelay *= 2;
            if (websocket.retrySocketDelay >= 300000) {
                websocket.retrySocketDelay = 300000;
            }

            if (!websocket.hasConnected && websocket.reconnectAttempts >= 100) {
                console.info("Unable to connect websocket after 100 attempts, so stop retrying websocket");
                return;
            }

            setTimeout(() => {
                // setTimeout sets 'this' to window wrapping 'this.open()' in a lambda function
                // (rather than passing it inline), stops it.
                // Ref: https://stackoverflow.com/questions/2130241/pass-correct-this-context-to-settimeout-callback
                websocket.open();
            }, websocket.retrySocketDelay);
        }
    },
    send: function(message) {
        websocket.messageCounter++;
        message = websocket.messageCounter + ":" + message;

        if (websocket.buffer.size >= 50) {
            console.warn(`Websocket buffer has reached it's limit, so message will not be sent to server. Message: \n`, message);
            //log.add(log.eventTypes.socketBufferFull); // This has to be excluded from being sent to the server or else we'll have an infinite loop
            return;
        }

        websocket.buffer.set(websocket.messageCounter, message);

        if (websocket.socket.readyState === 1) {
            websocket.socket.send(message);
        }
    }
};<|endoftext|>


<commit_before>/**
 * Some UI stuff like pretty boxes, lists, a console, dividing panels
 * By convention, inheritance with protected-like visibility is achieved with
 * closures and a "spec" parameter to constructor calls.
 * Said "spec" is optionally defined and accessed from a class that wishes to
 * expose a method only to sub-classes but not to the outside.
 * Any srcjs.ui.* constructor shoudl return an object with at least a method
 * "panel" which sould return the main widget/element, and in most cases, a
 * method "append".
 */
srcjs.ui = (function() {

	// private to all members of package
	var TextboxHistory = function() {
		// notice importance of pre- and post-increment on "index" (it's for brevity)
		var history = [];
		var index = 0;
		var size = 0;
		var o = {
			push: function(command) {
				history[index++] = command;
				history.splice(index + 1);
				size++;
			},
			back: function(input) {
				if (index > 0) {
					input.val(history[--index]);
				}
			},
			forth: function(input) {
				if (typeof history[index + 1] != 'undefined') {
					input.val(history[++index]);
				} else {
					if (index < size) {
						index++;
					}
					input.val('');
				}
			},
			listen: function(input, listener) {
				input.keyup(function(e) {
					if (e.which == 13) { // enter
						if (listener(input.val())) {
							input.val('');
						}
					} else if (e.which == 38) {
						o.back(input);
					} else if (e.which == 40) {
						o.forth(input);
					}
				});
			}
		};
		return o;
	};

	var ui = {

		SplitPanel: function() {
			var panel = $('
');

			var setWidths = function() {
				var childWidth = Math.floor(100 / panel.children().length);
				var children = panel.children();

				$(children).each(function(i, child) {
					if (i < children.length) {
						$(child).css({
							width: (childWidth - 1)+'%',
							marginLeft: '1%'
						});
					} else {
						$(child).css({width: childWidth+'%'});
					}
				});
			};

			var o = {
				panel: panel,
				append: function(element) {
					panel.append(element);
					setWidths();
				},
			};
			return o;
		},

		Table: function(options) {
			var panel = $('
');
			var header = $('
').text(options.title);
			panel.append(header);
			var table = $('
');
			panel.append(table);

			var o = {
				panel: panel,
				addRow: function() {
					var tr = $('
');
					for(var i = 0; i < arguments.length; i++) {
						if (typeof arguments[i] == 'string' || typeof arguments[i] == 'number') {
							tr.append($('').text(arguments[i]));
						} else if (typeof arguments[i] == 'object') {
							if (typeof arguments[i].length != 'undefined') {
								for(var j = 0; j < arguments[i].length; j++) {
									tr.append($('').append(arguments[i][j]));
								}
							} else {
								tr.append($('').append(arguments[i]));
							}
						} else if (typeof arguments[i] == 'function') {
							tr.append($('').append(arguments[i]()));
						} else {
							tr.append($('').text(arguments[i].toString()));
						}
					}
					table.append(tr);
				},
				clear: function() {
					table.html('');
				}
			};
			return o;
		},

		/**
		 * "Box" has a header and a footer with some nice css.
		 * There is a css class "srcjsPanelBody" which is suitable for elements
		 * that are appended to it.
		 */
		Box: function(options, spec) {
			spec = spec || {};
			spec.setTitle = spec.setTitle || function(title) {
				titleBar.text(title);
			};
			spec.setTitleButton = spec.setTitleButton || function(icon, handler, text) {
				text = text || titleBar.text();
				titleBar.html('');
				titleBar.append($('').click(handler));
				if (text) {
					titleBar.append(text);
				}
			};
			var div = $('
');
			if (options.classNames) {
				$(options.classNames).each(function(i, className) {
					div.addClass(className);
				});
			}
			if (options.css) {
				div.css(options.css);
			}

			var titleBar = $('
');
			if (options.title) {
				titleBar.text(options.title);
			}
			div.append(titleBar);

			var footer = $('
');
			div.append(footer);

			var o = {};
			// protected methods:
			o.panel = spec.panel || function() {
				return div;
			};
			o.append = spec.append || function(element) {
				element.insertBefore(footer);
			};
			return o;
		},

		/**
		 * Simple readonly list of strings
		 */
		ListBox: function(options, spec) {
			spec = spec || {};
			// get inner content
			spec.getItem = spec.getItem || function(item, index) {
				return $('').text(item);
			};
			// get
 wrapper, with custom styles or events
			spec.getListItem = spec.getListItem || function(index) {
				return $('
');
			};
			spec.list = $('
');

			var box = srcjs.ui.Box(options, spec);

			if (spec.listBoxClassNames) {
				$(spec.listBoxClassNames).each(function(i, className) {
					spec.list.addClass(className);
				});
			}

			var body = $('
');
			box.append(body.append(spec.list));

			// set items to list of strings
			box.set = function(items) {
				spec.list.html('');
				$(items).each(function(i, item) {
					var li = spec.getListItem(i);
					$(spec.getItem(item, i)).each(function(i, item) {
						li.append(item);
					});
					spec.list.append(li);
				});
			};

			box.getBody = function() {
				return body;
			};

			return box;
		},

		/**
		 * Column-formatted readonly list, wih custom formatting callback
		 * options.formatColumns accepts index (to tell column) and a string parameter
		 * andmust return a new element.
		 */
		ListBoxFormatted: function(options, spec) {
			spec = spec || {};
			if (spec.listBoxClassNames) {
				spec.listBoxClassNames.push('srcjsListBoxFormatted');
			} else {
				spec.listBoxClassNames = ['srcjsListBoxFormatted'];
			}

			options.formatColumns = options.formatColumns || function(index, item, listIndex) {
				return item;
			};
			spec.getItem = spec.getItem || function(item, index) {
				var data = [];
				for(var i = 0; i < item.length; i++) {
					data[i] = $('').append(options.formatColumns(i, item[i], index));

				}
				return data;
			};
			return srcjs.ui.ListBox(options, spec);
		},

		ListBoxLinksFormatted: function(options, spec) {
			spec = spec || {};
			if (spec.listBoxClassNames) {
				spec.listBoxClassNames.push('srcjsListBoxLinksFormatted');
			} else {
				spec.listBoxClassNames = ['srcjsListBoxLinksFormatted'];
			}
			spec.getListItem = spec.getListItem || function(index) {
				return $('
').click(function() {
					list.openItem(index);
				});
			};

			var list = srcjs.ui.ListBoxFormatted(options, spec);

			list.openItem = function(index) {
				spec.list.detach();
				var onRemoveCallback = function() {
				};
				var body = $('
');

				var view = options.getItemView(index);

				list.append(body.append(view.element));
				spec.setTitleButton('back', function() {
					spec.setTitle(options.title);
					if (view.onRemove) {
						view.onRemove(function() {
							body.remove();
							list.append(spec.list);
						});
					} else {
						body.remove();
						list.append(spec.list);
					}
				}, view.title);
			};
			return list;
		},

		/**
		 * Get a console in a box, ready with command history etc.
		 * inputListener will be called when enter is pressed on input; it must return boolean false if the input field
		 * should NOT be cleared
		 */
		Console: function(options, spec) {
			spec = spec || {};

			if (options.classNames) {
				options.classNames.push('srcjsConsole');
			} else {
				options.classNames = ['srcjsConsole'];
			}

			var box = srcjs.ui.Box(options, spec);

			/**
			 * Add a line of text, an element, or an array of elements
			 */
			box.addLines = function(data) {
				var div = $('
');
				if (typeof data == 'string') {
					div.text(data);
				} else if (typeof data == 'object') {
					if (typeof data.length != 'undefined') {
						$(data).each(function(index, element) {
							div.append(element);
						});
					} else {
						div.append(data[i]);
					}
				} else {
					div.text(data.toString());
				}
				// limit scroll history
				if (lineDiv.children().size() > 1000) {
					lineDiv.children().filter(':lt(100)').remove();
				}
				lineDiv.append(div);
				lineDiv.scrollTop(lineDiv[0].scrollHeight);
			};

			height = typeof options.height != 'undefined' ? options.height : 100;
			height -= 30 + 10; // take off top and bottom bar - yes, this bad practice

			var lineDiv = $('
');
			box.append(lineDiv);

			// if no inputlistener, assume that console is read only
			if (options.inputListener) {
				var inputDiv = $('
');

				var btn = $('<button class="srcjsInputButton"/>');
				btn.append('');
				inputDiv.append(btn);

				var wrapper = $('
');
				var input = $('<input type="text" autocomplete="false" class="srcjsConsoleInput"/>');
				wrapper.append(input);
				inputDiv.append(wrapper);

				input.keyup(function(e) {
					if (e.which == 13) { // enter
						if (options.inputListener(input.val()) !== false) {
							input.val('');
						}
					} else if (e.which == 38) { // up
						history.back(input);
					} else if (e.which == 40) { // down
						history.forth(input);
					}
				});

				box.append(inputDiv);
				var history = TextboxHistory();
				history.listen(lineDiv, options.inputListener);

				height -= 30; // take off input height
			}

			lineDiv.css({height: height+'px'});

			return box;
		}
	};
	return ui;
})();<commit_msg>fix console-history somewhat<commit_after>/**
 * Some UI stuff like pretty boxes, lists, a console, dividing panels
 * By convention, inheritance with protected-like visibility is achieved with
 * closures and a "spec" parameter to constructor calls.
 * Said "spec" is optionally defined and accessed from a class that wishes to
 * expose a method only to sub-classes but not to the outside.
 * Any srcjs.ui.* constructor shoudl return an object with at least a method
 * "panel" which sould return the main widget/element, and in most cases, a
 * method "append".
 */
srcjs.ui = (function() {

	// private to all members of package
	var TextboxHistory = function(input, listener) {

		// notice importance of pre- and post-increment on "index" (it's for brevity)
		var history = [];
		var index = 0;

		var o = {
			push: function(command) {
				index = history.length;
				history[index] = command;
				history.splice(index + 1);
			},
			back: function(input) {
				if (index > 0) {
					return history[--index];
				}
				return null;
			},
			forth: function(input) {
				if (typeof history[index + 1] != 'undefined') {
					return history[++index];
				} else {
					if (index < history.length) {
						index++;
					}
					return null;
				}
			},
			listen: function(input, listener) {
				input.keyup(function(e) {
					var newVal = null;
					if (e.which == 13) { // enter
						if (listener(input.val()) !== false) {
							o.push(input.val());
							input.val('');
							console.log(history);
						}
					} else if (e.which == 38) { // arrow up
						newVal = o.back();
						if (newVal !== null) {
							input.val(newVal);
						}
					} else if (e.which == 40) { // arrow down
						newVal = o.forth();
						if (newVal !== null) {
							input.val(newVal);
						} else {
							input.val('');
						}
					}

				});
			}
		};
		if (input && listener) {
			o.listen(input, listener);
		}
		return o;
	};

	var ui = {

		SplitPanel: function() {
			var panel = $('
');

			var setWidths = function() {
				var childWidth = Math.floor(100 / panel.children().length);
				var children = panel.children();

				$(children).each(function(i, child) {
					if (i < children.length) {
						$(child).css({
							width: (childWidth - 1)+'%',
							marginLeft: '1%'
						});
					} else {
						$(child).css({width: childWidth+'%'});
					}
				});
			};

			var o = {
				panel: panel,
				append: function(element) {
					panel.append(element);
					setWidths();
				},
			};
			return o;
		},

		Table: function(options) {
			var panel = $('
');
			var header = $('
').text(options.title);
			panel.append(header);
			var table = $('
');
			panel.append(table);

			var o = {
				panel: panel,
				addRow: function() {
					var tr = $('
');
					for(var i = 0; i < arguments.length; i++) {
						if (typeof arguments[i] == 'string' || typeof arguments[i] == 'number') {
							tr.append($('').text(arguments[i]));
						} else if (typeof arguments[i] == 'object') {
							if (typeof arguments[i].length != 'undefined') {
								for(var j = 0; j < arguments[i].length; j++) {
									tr.append($('').append(arguments[i][j]));
								}
							} else {
								tr.append($('').append(arguments[i]));
							}
						} else if (typeof arguments[i] == 'function') {
							tr.append($('').append(arguments[i]()));
						} else {
							tr.append($('').text(arguments[i].toString()));
						}
					}
					table.append(tr);
				},
				clear: function() {
					table.html('');
				}
			};
			return o;
		},

		/**
		 * "Box" has a header and a footer with some nice css.
		 * There is a css class "srcjsPanelBody" which is suitable for elements
		 * that are appended to it.
		 */
		Box: function(options, spec) {
			spec = spec || {};
			spec.setTitle = spec.setTitle || function(title) {
				titleBar.text(title);
			};
			spec.setTitleButton = spec.setTitleButton || function(icon, handler, text) {
				text = text || titleBar.text();
				titleBar.html('');
				titleBar.append($('').click(handler));
				if (text) {
					titleBar.append(text);
				}
			};
			var div = $('
');
			if (options.classNames) {
				$(options.classNames).each(function(i, className) {
					div.addClass(className);
				});
			}
			if (options.css) {
				div.css(options.css);
			}

			var titleBar = $('
');
			if (options.title) {
				titleBar.text(options.title);
			}
			div.append(titleBar);

			var footer = $('
');
			div.append(footer);

			var o = {};
			// protected methods:
			o.panel = spec.panel || function() {
				return div;
			};
			o.append = spec.append || function(element) {
				element.insertBefore(footer);
			};
			return o;
		},

		/**
		 * Simple readonly list of strings
		 */
		ListBox: function(options, spec) {
			spec = spec || {};
			// get inner content
			spec.getItem = spec.getItem || function(item, index) {
				return $('').text(item);
			};
			// get
 wrapper, with custom styles or events
			spec.getListItem = spec.getListItem || function(index) {
				return $('
');
			};
			spec.list = $('
');

			var box = srcjs.ui.Box(options, spec);

			if (spec.listBoxClassNames) {
				$(spec.listBoxClassNames).each(function(i, className) {
					spec.list.addClass(className);
				});
			}

			var body = $('
');
			box.append(body.append(spec.list));

			// set items to list of strings
			box.set = function(items) {
				spec.list.html('');
				$(items).each(function(i, item) {
					var li = spec.getListItem(i);
					$(spec.getItem(item, i)).each(function(i, item) {
						li.append(item);
					});
					spec.list.append(li);
				});
			};

			box.getBody = function() {
				return body;
			};

			return box;
		},

		/**
		 * Column-formatted readonly list, wih custom formatting callback
		 * options.formatColumns accepts index (to tell column) and a string parameter
		 * andmust return a new element.
		 */
		ListBoxFormatted: function(options, spec) {
			spec = spec || {};
			if (spec.listBoxClassNames) {
				spec.listBoxClassNames.push('srcjsListBoxFormatted');
			} else {
				spec.listBoxClassNames = ['srcjsListBoxFormatted'];
			}

			options.formatColumns = options.formatColumns || function(index, item, listIndex) {
				return item;
			};
			spec.getItem = spec.getItem || function(item, index) {
				var data = [];
				for(var i = 0; i < item.length; i++) {
					data[i] = $('').append(options.formatColumns(i, item[i], index));

				}
				return data;
			};
			return srcjs.ui.ListBox(options, spec);
		},

		ListBoxLinksFormatted: function(options, spec) {
			spec = spec || {};
			if (spec.listBoxClassNames) {
				spec.listBoxClassNames.push('srcjsListBoxLinksFormatted');
			} else {
				spec.listBoxClassNames = ['srcjsListBoxLinksFormatted'];
			}
			spec.getListItem = spec.getListItem || function(index) {
				return $('
').click(function() {
					list.openItem(index);
				});
			};

			var list = srcjs.ui.ListBoxFormatted(options, spec);

			list.openItem = function(index) {
				spec.list.detach();
				var onRemoveCallback = function() {
				};
				var body = $('
');

				var view = options.getItemView(index);

				list.append(body.append(view.element));
				spec.setTitleButton('back', function() {
					spec.setTitle(options.title);
					if (view.onRemove) {
						view.onRemove(function() {
							body.remove();
							list.append(spec.list);
						});
					} else {
						body.remove();
						list.append(spec.list);
					}
				}, view.title);
			};
			return list;
		},

		/**
		 * Get a console in a box, ready with command history etc.
		 * inputListener will be called when enter is pressed on input; it must return boolean false if the input field
		 * should NOT be cleared
		 */
		Console: function(options, spec) {
			spec = spec || {};

			if (options.classNames) {
				options.classNames.push('srcjsConsole');
			} else {
				options.classNames = ['srcjsConsole'];
			}

			var box = srcjs.ui.Box(options, spec);

			/**
			 * Add a line of text, an element, or an array of elements
			 */
			box.addLines = function(data) {
				var div = $('
');
				if (typeof data == 'string') {
					div.text(data);
				} else if (typeof data == 'object') {
					if (typeof data.length != 'undefined') {
						$(data).each(function(index, element) {
							div.append(element);
						});
					} else {
						div.append(data[i]);
					}
				} else {
					div.text(data.toString());
				}
				// limit scroll history
				if (lineDiv.children().size() > 1000) {
					lineDiv.children().filter(':lt(100)').remove();
				}
				lineDiv.append(div);
				lineDiv.scrollTop(lineDiv[0].scrollHeight);
			};

			height = typeof options.height != 'undefined' ? options.height : 100;
			height -= 30 + 10; // take off top and bottom bar - yes, this bad practice

			var lineDiv = $('
');
			box.append(lineDiv);

			// if no inputlistener, assume that console is read only
			if (options.inputListener) {
				var inputDiv = $('
');

				var btn = $('<button class="srcjsInputButton"/>');
				btn.append('');
				inputDiv.append(btn);

				var wrapper = $('
');
				var input = $('<input type="text" autocomplete="false" class="srcjsConsoleInput"/>');
				wrapper.append(input);
				inputDiv.append(wrapper);

				box.append(inputDiv);
				var history = TextboxHistory(input, options.inputListener);

				height -= 30; // take off input height
			}

			lineDiv.css({height: height+'px'});

			return box;
		}
	};
	return ui;
})();<|endoftext|>


<commit_before>// Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details.
/*global desc, task, jake, fail, complete, directory*/

(function() {
	"use strict";

	if (!process.env.loose) console.log("For more forgiving test settings, use 'loose=true'");

	var fs = require("fs");
	var shell = require("shelljs");

	var browserify = require("browserify");
	var lint = require("./build/util/lint_runner.js");
	var nodeunit = require("./build/util/nodeunit_runner.js");
	var karma = require("./build/util/karma_runner.js");
	var versionChecker = require("./build/util/version_checker.js");
	var runServer = require("./src/_run_server.js");
	var path = require("path");

	var REQUIRED_BROWSERS = [
		"IE 8.0.0 (Windows 7)",
		"IE 9.0.0 (Windows 7)",
		"Firefox 24.0.0 (Mac OS X 10.8)",
		"Chrome 30.0.1599 (Mac OS X 10.8.5)",
		"Safari 6.0.5 (Mac OS X 10.8.5)",
		"Mobile Safari 6.0.0 (iOS 6.1)"
	];

	var GENERATED_DIR = "generated";
	var TEMP_TESTFILE_DIR = GENERATED_DIR + "/test";
	var BUILD_DIR = GENERATED_DIR + "/build";
	var BUILD_CLIENT_DIR = BUILD_DIR + "/client";

	directory(TEMP_TESTFILE_DIR);
	directory(BUILD_DIR);
	directory(BUILD_CLIENT_DIR);

	desc("Delete all generated files");
	task("clean", [], function() {
		jake.rmRf(GENERATED_DIR);
	});

	desc("Build and test");
	task("default", ["lint", "test"], function() {
		console.log("\n\nOK");
	});

	desc("Start Karma server for testing");
	task("karma", function() {
		karma.serve("build/karma.conf.js", complete, fail);
	}, {async: true});

	desc("Start WeeWikiPaint server for manual testing");
	task("run", ["build"], function() {
		console.log("Running server. Press Ctrl-C to stop.");
		runServer.runInteractively();
		// We never call complete() because we want the task to hang until the user presses 'Ctrl-C'.
	}, {async: true});

	desc("Lint everything");
	task("lint", ["lintNode", "lintClient"]);

	task("lintNode", ["nodeVersion"], function() {
		var passed = lint.validateFileList(nodeLintFiles(), nodeLintOptions(), {});
		if (!passed) fail("Lint failed");
	});

	task("lintClient", function() {
		var passed = lint.validateFileList(clientLintFiles(), clientLintOptions(), clientGlobals());
		if (!passed) fail("Lint failed");
	});

	desc("Test everything");
	task("test", ["testServer", "testClient", "testSmoke"]);

	desc("Test server code");
	task("testServer", ["nodeVersion", TEMP_TESTFILE_DIR], function() {
		nodeunit.runTests(serverTestFiles(), complete, fail);
	}, {async: true});

	desc("Test client code");
	task("testClient", [], function() {
		karma.runTests(REQUIRED_BROWSERS, complete, fail);
	}, {async: true});

	desc("End-to-end smoke tests");
	task("testSmoke", ["build"], function() {
		nodeunit.runTests(smokeTestFiles(), complete, fail);
	}, {async: true});

	desc("Bundle and build code");
	task("build", [BUILD_CLIENT_DIR], function() {
		shell.rm("-rf", BUILD_CLIENT_DIR + "/*");
		shell.cp("-R", "src/client/*.html", "src/client/vendor", BUILD_CLIENT_DIR);

		console.log("Bundling client files with Browserify...");
		var b = browserify();
		b.require("./src/client/client.js", {expose: "./client.js"} );
		b.require("./src/client/html_element.js", {expose: "./html_element.js"} );
		b.bundle({ debug: true }, function(err, bundle) {
			if (err) fail(err);
			fs.writeFileSync(BUILD_CLIENT_DIR + "/bundle.js", bundle);
			complete();
		});
	}, {async: true});

	desc("Deploy to Heroku");
	task("deploy", ["default"], function() {
		console.log("1. Make sure 'git status' is clean.");

		// Correction: Use "git push heroku integration:master" to deploy from integration branch.
		// Thanks to Jüri A, http://www.letscodejavascript.com/v3/comments/live/32#comment-798947003 .
		console.log("2. 'git push heroku master' (or integration:master)");
		console.log("3. 'jake test'");
	});

//	desc("Ensure correct version of Node is present.");
	task("nodeVersion", [], function() {
		var deployedVersion = "v" + require("./package.json").engines.node;
		versionChecker.check("Node", !process.env.loose, deployedVersion, process.version, fail);
	});

	desc("Integration checklist");
	task("integrate", ["default"], function() {
		console.log("1. Make sure 'git status' is clean.");
		console.log("2. Build on the integration box.");
		console.log("   a. Walk over to integration box.");
		console.log("   b. 'git pull'");
		console.log("   c. 'npm rebuild'");
		console.log("   d. Check status for files that need to be .gitignore'd");
		console.log("   e. 'jake'");
		console.log("   f. If jake fails, stop! Try again after fixing the issue.");
		console.log("3. 'git checkout integration'");
		console.log("4. 'git merge master --no-ff --log'");
		console.log("5. 'git checkout master'");
	});

	desc("End-of-episode checklist");
	task("episode", [], function() {
		console.log("1. Save recording.");
		console.log("2. Double-check sound and framing.");
		console.log("3. Commit source code.");
		console.log("4. Check Windows compatibility");
		console.log("   a. Switch to Windows VM");
		console.log("   b. 'git pull'");
		console.log("   c. 'npm rebuild'");
		console.log("   d. Check status for files that need to be .gitignore'd");
		console.log("   e. 'jake'");
		console.log("5. Tag episode: 'git tag -a episodeXX -m \"End of episode XX\"'");
	});

	function serverTestFiles() {
		var testFiles = new jake.FileList();
		testFiles.include("src/server/**/_*_test.js");
		testFiles = testFiles.toArray();
		return testFiles;
	}

	function smokeTestFiles() {
		var testFiles = new jake.FileList();
		testFiles.include("src/_*_test.js");
		testFiles = testFiles.toArray();
		return testFiles;
	}

	function nodeLintFiles() {
		var javascriptFiles = new jake.FileList();
		javascriptFiles.include("*.js");
		javascriptFiles.include("build/util/*.js");
		javascriptFiles.include("src/server/**/*.js");
		javascriptFiles.include("src/*.js");
		return javascriptFiles.toArray();
	}

	function clientLintFiles() {
		var javascriptFiles = new jake.FileList();
		javascriptFiles.include("src/client/*.js");
		return javascriptFiles.toArray();
	}

	function nodeLintOptions() {
		var options = sharedLintOptions();
		options.node = true;
		return options;
	}

	function clientLintOptions() {
		var options = sharedLintOptions();
		options.browser = true;
		return options;
	}

	function sharedLintOptions() {
		return {
			bitwise: true,
			curly: false,
			eqeqeq: true,
			forin: true,
			immed: true,
			latedef: false,
			newcap: true,
			noarg: true,
			noempty: true,
			nonew: true,
			regexp: true,
			undef: true,
			strict: true,
			trailing: true
		};
	}

	function clientGlobals() {
		return {
			// Browserify
			require: false,
			module: false,
			exports: false,

			// Mocha / expect.js
			describe: false,
			it: false,
			expect: false,
			dump: false,
			beforeEach: false,
			afterEach: false,

			// Browser
			console: false
		};
	}

}());
<commit_msg>Updated Mobile Safari 6.0 --> 7.0<commit_after>// Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details.
/*global desc, task, jake, fail, complete, directory*/

(function() {
	"use strict";

	if (!process.env.loose) console.log("For more forgiving test settings, use 'loose=true'");

	var fs = require("fs");
	var shell = require("shelljs");

	var browserify = require("browserify");
	var lint = require("./build/util/lint_runner.js");
	var nodeunit = require("./build/util/nodeunit_runner.js");
	var karma = require("./build/util/karma_runner.js");
	var versionChecker = require("./build/util/version_checker.js");
	var runServer = require("./src/_run_server.js");
	var path = require("path");

	var REQUIRED_BROWSERS = [
		"IE 8.0.0 (Windows 7)",
		"IE 9.0.0 (Windows 7)",
		"Firefox 24.0.0 (Mac OS X 10.8)",
		"Chrome 30.0.1599 (Mac OS X 10.8.5)",
		"Safari 6.0.5 (Mac OS X 10.8.5)",
		"Mobile Safari 7.0.0 (iOS 7.0)"
	];

	var GENERATED_DIR = "generated";
	var TEMP_TESTFILE_DIR = GENERATED_DIR + "/test";
	var BUILD_DIR = GENERATED_DIR + "/build";
	var BUILD_CLIENT_DIR = BUILD_DIR + "/client";

	directory(TEMP_TESTFILE_DIR);
	directory(BUILD_DIR);
	directory(BUILD_CLIENT_DIR);

	desc("Delete all generated files");
	task("clean", [], function() {
		jake.rmRf(GENERATED_DIR);
	});

	desc("Build and test");
	task("default", ["lint", "test"], function() {
		console.log("\n\nOK");
	});

	desc("Start Karma server for testing");
	task("karma", function() {
		karma.serve("build/karma.conf.js", complete, fail);
	}, {async: true});

	desc("Start WeeWikiPaint server for manual testing");
	task("run", ["build"], function() {
		console.log("Running server. Press Ctrl-C to stop.");
		runServer.runInteractively();
		// We never call complete() because we want the task to hang until the user presses 'Ctrl-C'.
	}, {async: true});

	desc("Lint everything");
	task("lint", ["lintNode", "lintClient"]);

	task("lintNode", ["nodeVersion"], function() {
		var passed = lint.validateFileList(nodeLintFiles(), nodeLintOptions(), {});
		if (!passed) fail("Lint failed");
	});

	task("lintClient", function() {
		var passed = lint.validateFileList(clientLintFiles(), clientLintOptions(), clientGlobals());
		if (!passed) fail("Lint failed");
	});

	desc("Test everything");
	task("test", ["testServer", "testClient", "testSmoke"]);

	desc("Test server code");
	task("testServer", ["nodeVersion", TEMP_TESTFILE_DIR], function() {
		nodeunit.runTests(serverTestFiles(), complete, fail);
	}, {async: true});

	desc("Test client code");
	task("testClient", [], function() {
		karma.runTests(REQUIRED_BROWSERS, complete, fail);
	}, {async: true});

	desc("End-to-end smoke tests");
	task("testSmoke", ["build"], function() {
		nodeunit.runTests(smokeTestFiles(), complete, fail);
	}, {async: true});

	desc("Bundle and build code");
	task("build", [BUILD_CLIENT_DIR], function() {
		shell.rm("-rf", BUILD_CLIENT_DIR + "/*");
		shell.cp("-R", "src/client/*.html", "src/client/vendor", BUILD_CLIENT_DIR);

		console.log("Bundling client files with Browserify...");
		var b = browserify();
		b.require("./src/client/client.js", {expose: "./client.js"} );
		b.require("./src/client/html_element.js", {expose: "./html_element.js"} );
		b.bundle({ debug: true }, function(err, bundle) {
			if (err) fail(err);
			fs.writeFileSync(BUILD_CLIENT_DIR + "/bundle.js", bundle);
			complete();
		});
	}, {async: true});

	desc("Deploy to Heroku");
	task("deploy", ["default"], function() {
		console.log("1. Make sure 'git status' is clean.");

		// Correction: Use "git push heroku integration:master" to deploy from integration branch.
		// Thanks to Jüri A, http://www.letscodejavascript.com/v3/comments/live/32#comment-798947003 .
		console.log("2. 'git push heroku master' (or integration:master)");
		console.log("3. 'jake test'");
	});

//	desc("Ensure correct version of Node is present.");
	task("nodeVersion", [], function() {
		var deployedVersion = "v" + require("./package.json").engines.node;
		versionChecker.check("Node", !process.env.loose, deployedVersion, process.version, fail);
	});

	desc("Integration checklist");
	task("integrate", ["default"], function() {
		console.log("1. Make sure 'git status' is clean.");
		console.log("2. Build on the integration box.");
		console.log("   a. Walk over to integration box.");
		console.log("   b. 'git pull'");
		console.log("   c. 'npm rebuild'");
		console.log("   d. Check status for files that need to be .gitignore'd");
		console.log("   e. 'jake'");
		console.log("   f. If jake fails, stop! Try again after fixing the issue.");
		console.log("3. 'git checkout integration'");
		console.log("4. 'git merge master --no-ff --log'");
		console.log("5. 'git checkout master'");
	});

	desc("End-of-episode checklist");
	task("episode", [], function() {
		console.log("1. Save recording.");
		console.log("2. Double-check sound and framing.");
		console.log("3. Commit source code.");
		console.log("4. Check Windows compatibility");
		console.log("   a. Switch to Windows VM");
		console.log("   b. 'git pull'");
		console.log("   c. 'npm rebuild'");
		console.log("   d. Check status for files that need to be .gitignore'd");
		console.log("   e. 'jake'");
		console.log("5. Tag episode: 'git tag -a episodeXX -m \"End of episode XX\"'");
	});

	function serverTestFiles() {
		var testFiles = new jake.FileList();
		testFiles.include("src/server/**/_*_test.js");
		testFiles = testFiles.toArray();
		return testFiles;
	}

	function smokeTestFiles() {
		var testFiles = new jake.FileList();
		testFiles.include("src/_*_test.js");
		testFiles = testFiles.toArray();
		return testFiles;
	}

	function nodeLintFiles() {
		var javascriptFiles = new jake.FileList();
		javascriptFiles.include("*.js");
		javascriptFiles.include("build/util/*.js");
		javascriptFiles.include("src/server/**/*.js");
		javascriptFiles.include("src/*.js");
		return javascriptFiles.toArray();
	}

	function clientLintFiles() {
		var javascriptFiles = new jake.FileList();
		javascriptFiles.include("src/client/*.js");
		return javascriptFiles.toArray();
	}

	function nodeLintOptions() {
		var options = sharedLintOptions();
		options.node = true;
		return options;
	}

	function clientLintOptions() {
		var options = sharedLintOptions();
		options.browser = true;
		return options;
	}

	function sharedLintOptions() {
		return {
			bitwise: true,
			curly: false,
			eqeqeq: true,
			forin: true,
			immed: true,
			latedef: false,
			newcap: true,
			noarg: true,
			noempty: true,
			nonew: true,
			regexp: true,
			undef: true,
			strict: true,
			trailing: true
		};
	}

	function clientGlobals() {
		return {
			// Browserify
			require: false,
			module: false,
			exports: false,

			// Mocha / expect.js
			describe: false,
			it: false,
			expect: false,
			dump: false,
			beforeEach: false,
			afterEach: false,

			// Browser
			console: false
		};
	}

}());
<|endoftext|>


<commit_before>define(function (require, exports, module) {

/**
 * 基类
 *
 * @module Base
 */

'use strict';

var Class = require('class'),
  Events = require('events');

var Aspect = require('./aspect');

/**
 * 基类
 *
 * 实现 事件订阅 与 Aspect (AOP)
 *
 * @class Base
 * @constructor
 * @implements Events
 * @implements Aspect
 *
 * @example
 * ```
 * // 创建子类
 * var SomeBase = Base.extend({
 *   someMethod: function () {
 *     this.fire('someEvent');
 *   }
 * });
 * // 实例化
 * var someBase = new SomeBase({
 *   events: {
 *     someEvent: function () {
 *       console.log('someEvent fired');
 *     }
 *   }
 * });
 * // 调用方法
 * someBase.someMethod();
 * // 控制台将输出：
 * // someEvent fired
 * ```
 */
var Base = Class.create({

  /**
   * 初始化函数，将自动执行；执行参数初始化与订阅事件初始化
   *
   * @method initialize
   * @param {Object} options 参数
   */
  initialize: function (options) {
    Base.superclass.initialize.apply(this, arguments);

    // 初始化参数
    this.__options = mergeDefaults(this, options || {});

    // 初始化订阅事件
    this.initEvents();
  },

  mixins: [Events.prototype, Aspect.prototype],

  /**
   * 默认参数，子类自动继承并覆盖
   *
   * @property {Object} defaults
   * @type {Object}
   */
  defaults: { },

  /**
   * 存取状态
   *
   * @method state
   * @param {Number} [state] 状态值
   * @return {Mixed} 当前状态值或当前实例
   */
  state: function (state) {
    if (state === undefined) {
      return this.__state;
    }

    this.__state = state;
    return this;
  },

  /**
   * 存取初始化后的数据/参数，支持多级存取，
   * 如：this.option('rules/remote') 对应于 { rules: { remote: ... } }
   *
   * @method option
   * @param {String} [key] 键
   * @param {Mixed} [value] 值
   * @param {Object} [context] 上下文
   * @return {Mixed} 整个参数列表或指定参数值
   */
  option: function (key, value, context, override) {
    var options = context || this.__options;

    if (key === undefined) {
      return options;
    }

    function get () {
      var keyArr = key.split('/');

      while ((key = keyArr.shift())) {
        if (options.hasOwnProperty(key)) {
          options = options[key];
        } else {
          return undefined;
        }
      }

      return options;
    }

    function set () {
      var keyMap = {};

      function recruit (obj, arr) {
        key = arr.shift();

        if (key) {
          obj[key] = recruit({}, arr);
          return obj;
        }

        return value;
      }

      recruit(keyMap, key.split('/'));

      copy(options, keyMap, override);
    }

    function isObj (obj) {
      return Object.prototype.toString.call(obj) === '[object Object]';
    }

    function copy (target, source, override) {
      var p, obj;
      for (p in source) {
        obj = source[p];

        if (!override && isObj(obj)) {
          isObj(target[p]) || (target[p] = {});
          copy(target[p], obj, false);
        } else {
          target[p] = obj;
        }
      }
    }

    if (isObj(key)) {
      copy(options, key, override);
    } else {
      if (value === undefined) {
        return get();
      } else {
        set();
      }
    }

    return this;
  },

  /**
   * 事件订阅，以及AOP
   *
   * @method initEvents
   * @param {Object|Function} [events] 事件订阅列表
   * @return {Object} 当前实例
   */
  initEvents: function (events) {
    var self = this;

    events || (events = this.option('events'));

    if (!events) {
      return this;
    }

    $.each(events, function (event, callback) {
      var match;

      if (typeof callback === 'string') {
        callback = self[callback];
      }

      if (typeof callback !== 'function') {
        return true;
      }

      match = /^(before|after):(\w+)$/.exec(event);

      if (match) {
        // AOP
        self[match[1]](match[2], callback);
      } else {
        // Subscriber
        self.on(event, callback);
      }
    });

    return this;
  },

  /**
   * 销毁当前组件实例
   * @method destroy
   */
  destroy: function () {
    var prop;

    // 移除事件订阅
    this.off();

    // 移除属性
    for (prop in this) {
      if (this.hasOwnProperty(prop)) {
        delete this[prop];
      }
    }

    this.destroy = function() { };
  }

});

function mergeDefaults(instance, options) {
  var arr = [options],
    proto = instance.constructor.prototype;

  while (proto) {
    if (proto.hasOwnProperty('defaults')) {
      arr.unshift(proto.defaults);
    }

    proto = proto.constructor.superclass;
  }

  arr.unshift(true, {});

  return $.extend.apply(null, arr);
}

module.exports = Base;

});
<commit_msg>修改 context 参数类型<commit_after>define(function (require, exports, module) {

/**
 * 基类
 *
 * @module Base
 */

'use strict';

var Class = require('class'),
  Events = require('events');

var Aspect = require('./aspect');

/**
 * 基类
 *
 * 实现 事件订阅 与 Aspect (AOP)
 *
 * @class Base
 * @constructor
 * @implements Events
 * @implements Aspect
 *
 * @example
 * ```
 * // 创建子类
 * var SomeBase = Base.extend({
 *   someMethod: function () {
 *     this.fire('someEvent');
 *   }
 * });
 * // 实例化
 * var someBase = new SomeBase({
 *   events: {
 *     someEvent: function () {
 *       console.log('someEvent fired');
 *     }
 *   }
 * });
 * // 调用方法
 * someBase.someMethod();
 * // 控制台将输出：
 * // someEvent fired
 * ```
 */
var Base = Class.create({

  /**
   * 初始化函数，将自动执行；执行参数初始化与订阅事件初始化
   *
   * @method initialize
   * @param {Object} options 参数
   */
  initialize: function (options) {
    Base.superclass.initialize.apply(this, arguments);

    // 初始化参数
    this.__options = mergeDefaults(this, options || {});

    // 初始化订阅事件
    this.initEvents();
  },

  mixins: [Events.prototype, Aspect.prototype],

  /**
   * 默认参数，子类自动继承并覆盖
   *
   * @property {Object} defaults
   * @type {Object}
   */
  defaults: { },

  /**
   * 存取状态
   *
   * @method state
   * @param {Number} [state] 状态值
   * @return {Mixed} 当前状态值或当前实例
   */
  state: function (state) {
    if (state === undefined) {
      return this.__state;
    }

    this.__state = state;
    return this;
  },

  /**
   * 存取初始化后的数据/参数，支持多级存取，
   * 如：this.option('rules/remote') 对应于 { rules: { remote: ... } }
   *
   * @method option
   * @param {String} [key] 键
   * @param {Mixed} [value] 值
   * @param {String} [context] 上下文
   * @return {Mixed} 整个参数列表或指定参数值
   */
  option: function (key, value, context, override) {
    var options = context ? this.__options[context] : this.__options;

    if (key === undefined) {
      return options;
    }

    function get () {
      var keyArr = key.split('/');

      while ((key = keyArr.shift())) {
        if (options.hasOwnProperty(key)) {
          options = options[key];
        } else {
          return undefined;
        }
      }

      return options;
    }

    function set () {
      var keyMap = {};

      function recruit (obj, arr) {
        key = arr.shift();

        if (key) {
          obj[key] = recruit({}, arr);
          return obj;
        }

        return value;
      }

      recruit(keyMap, key.split('/'));

      copy(options, keyMap, override);
    }

    function isObj (obj) {
      return Object.prototype.toString.call(obj) === '[object Object]';
    }

    function copy (target, source, override) {
      var p, obj;
      for (p in source) {
        obj = source[p];

        if (!override && isObj(obj)) {
          isObj(target[p]) || (target[p] = {});
          copy(target[p], obj, false);
        } else {
          target[p] = obj;
        }
      }
    }

    if (isObj(key)) {
      copy(options, key, override);
    } else {
      if (value === undefined) {
        return get();
      } else {
        set();
      }
    }

    return this;
  },

  /**
   * 事件订阅，以及AOP
   *
   * @method initEvents
   * @param {Object|Function} [events] 事件订阅列表
   * @return {Object} 当前实例
   */
  initEvents: function (events) {
    var self = this;

    events || (events = this.option('events'));

    if (!events) {
      return this;
    }

    $.each(events, function (event, callback) {
      var match;

      if (typeof callback === 'string') {
        callback = self[callback];
      }

      if (typeof callback !== 'function') {
        return true;
      }

      match = /^(before|after):(\w+)$/.exec(event);

      if (match) {
        // AOP
        self[match[1]](match[2], callback);
      } else {
        // Subscriber
        self.on(event, callback);
      }
    });

    return this;
  },

  /**
   * 销毁当前组件实例
   * @method destroy
   */
  destroy: function () {
    var prop;

    // 移除事件订阅
    this.off();

    // 移除属性
    for (prop in this) {
      if (this.hasOwnProperty(prop)) {
        delete this[prop];
      }
    }

    this.destroy = function() { };
  }

});

function mergeDefaults(instance, options) {
  var arr = [options],
    proto = instance.constructor.prototype;

  while (proto) {
    if (proto.hasOwnProperty('defaults')) {
      arr.unshift(proto.defaults);
    }

    proto = proto.constructor.superclass;
  }

  arr.unshift(true, {});

  return $.extend.apply(null, arr);
}

module.exports = Base;

});
<|endoftext|>


<commit_before>import React from 'react';
import GraphQLQueryInput from './GraphQLQueryInput';
import GraphQLQueryResults from './GraphQLQueryResults';

var parentDivStyle = {
  display: "flex",
  flexWrap: "wrap"
};
var divStyle = {
  margin: "auto",
  width: "49%",
  border: "1px dotted #ccc"
};
var divStyleQuerying = {
  margin: "auto",
  width: "49%",
  border: "1px solid #aaa"
};
var divStylePreQuerying = {
  margin: "auto",
  width: "49%",
  border: "1px solid #000"
};
var styles = [divStyle, divStylePreQuerying, divStyleQuerying];

export default class GraphQLWebClient extends React.Component {
  defaultProps: {
	extraheaders: []
  }
  constructor(props) {
    super(props);
    this.state = {
      query: this.props.defaultQuery,
      queryState: 0,
      response: `(no response received yet)`
    };
    this.queryEvent = null;
    this.queryDelay = 500;
    this.xhr = null;
  }
  onInputChange(value) {
    window.location.hash = value;
    this.setState({query: value});
    if (this.props.autoRun) {this.queryBackend(); }
  }
  componentDidMount() {
    this.queryBackend();
  }
  componentWillReceiveProps(nextProps) {
    if (nextProps.defaultQuery !== this.props.defaultQuery) {
      this.setState({query: nextProps.defaultQuery});
    }
    if (!this.props.autoRun && nextProps.autoRun) {this.queryBackend(); }
  }
  setQueryState(newState) {
    if (this.props.onQueryState) {
      this.props.onQueryState(newState);
    }
    this.setState({queryState: newState});
  }
  queryBackend() {
    var queryDelay = this.queryDelay;
    if (this.queryEvent !== null) { clearTimeout(this.queryEvent); }
    if (this.xhr !== null) {
      this.xhr.abort();
    } else {
      queryDelay = 0;
    }
    this.setQueryState(1);
    this.queryEvent = setTimeout(() => {
      this.setQueryState(2);
      var xhr = new XMLHttpRequest();
      xhr.open('get', `${this.props.endpoint}?q=${encodeURIComponent(this.state.query)}`, true);
      xhr.setRequestHeader('X-Trace-Id', '1');
      if (this.props.showParseResult) {
        xhr.setRequestHeader('X-GraphQL-Only-Parse', '1');
      }
      this.props.extraHeaders.forEach((h) => {
        xhr.setRequestHeader(h[0], h[1]);
      });
      xhr.onload = () => {
          this.setState({response: xhr.responseText});
          this.setQueryState(0);
      };
      xhr.send();
      this.xhr = xhr;
    }, queryDelay);
  }
  render() {
    return (

       <GraphQLQueryInput query={this.state.query} onChange={this.onInputChange.bind(this)} />

       <GraphQLQueryResults results={this.state.response} />

    );
  }
}

<commit_msg>clean up repetitive styles<commit_after>import React from 'react';
import GraphQLQueryInput from './GraphQLQueryInput';
import GraphQLQueryResults from './GraphQLQueryResults';

var parentDivStyle = {
  display: "flex",
  flexWrap: "wrap"
};
var divStyle = {
  margin: "auto",
  width: "49%",
  border: "1px dotted #ccc"
};
var divStyleQuerying = { border: "1px solid #aaa" };
var divStylePreQuerying = { border: "1px solid #000" };
Object.assign(divStyleQuerying, divStyle);
Object.assign(divStylePreQuerying, divStyle);

var styles = [divStyle, divStylePreQuerying, divStyleQuerying];

export default class GraphQLWebClient extends React.Component {
  defaultProps: {
	extraheaders: []
  }
  constructor(props) {
    super(props);
    this.state = {
      query: this.props.defaultQuery,
      queryState: 0,
      response: `(no response received yet)`
    };
    this.queryEvent = null;
    this.queryDelay = 500;
    this.xhr = null;
  }
  onInputChange(value) {
    window.location.hash = value;
    this.setState({query: value});
    if (this.props.autoRun) {this.queryBackend(); }
  }
  componentDidMount() {
    this.queryBackend();
  }
  componentWillReceiveProps(nextProps) {
    if (nextProps.defaultQuery !== this.props.defaultQuery) {
      this.setState({query: nextProps.defaultQuery});
    }
    if (!this.props.autoRun && nextProps.autoRun) {this.queryBackend(); }
  }
  setQueryState(newState) {
    if (this.props.onQueryState) {
      this.props.onQueryState(newState);
    }
    this.setState({queryState: newState});
  }
  queryBackend() {
    var queryDelay = this.queryDelay;
    if (this.queryEvent !== null) { clearTimeout(this.queryEvent); }
    if (this.xhr !== null) {
      this.xhr.abort();
    } else {
      queryDelay = 0;
    }
    this.setQueryState(1);
    this.queryEvent = setTimeout(() => {
      this.setQueryState(2);
      var xhr = new XMLHttpRequest();
      xhr.open('get', `${this.props.endpoint}?q=${encodeURIComponent(this.state.query)}`, true);
      xhr.setRequestHeader('X-Trace-Id', '1');
      if (this.props.showParseResult) {
        xhr.setRequestHeader('X-GraphQL-Only-Parse', '1');
      }
      this.props.extraHeaders.forEach((h) => {
        xhr.setRequestHeader(h[0], h[1]);
      });
      xhr.onload = () => {
          this.setState({response: xhr.responseText});
          this.setQueryState(0);
      };
      xhr.send();
      this.xhr = xhr;
    }, queryDelay);
  }
  render() {
    return (

       <GraphQLQueryInput query={this.state.query} onChange={this.onInputChange.bind(this)} />

       <GraphQLQueryResults results={this.state.response} />

    );
  }
}

<|endoftext|>