<commit_before>/*
 * Copyright 2014-2015 Fabian Tollenaar <fabian@starting-point.nl>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

if (typeof [].includes != 'function') {
  console.log("Minimum required Node.js version is 6, please update.")
  process.exit(-1)
}

var express = require('express'),
  _ = require('lodash'),
  debug = require('debug')('signalk-server'),
  path = require('path'),
  http = require('http'),
  https = require('https'),
  httpolyglot = require('httpolyglot'),
  pem = require('pem'),
  fs = require('fs'),
  FullSignalK = require('signalk-schema').FullSignalK,
  StreamBundle = require('./streambundle'),
  SubscriptionManager = require('./subscriptionmanager'),
  Mode = require('stat-mode')

function Server(opts) {
  this.params = opts || {};
  this.app = express();
  this.app.started = false;

  this.app.overrides = {
    settings: this.params.settings || null,
    port: this.params.port || null
  };

  var app = this.app;
  require('./config/config')(app);
  app.selfContext = 'vessels.' + app.selfId;
  app.version = "0.0.1"

  app.use(require('body-parser').json())
  app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html')
  })

  app.get('/apps', (req, res, next) => {
    var html = fs.readFileSync(__dirname + '/appindex.html', {encoding: 'utf8'})
    var insertionIndex = html.indexOf('
');
    var sliceToInsertion = html.slice(0, insertionIndex);
    var sliceToEnd = html.slice(insertionIndex);

    var result = sliceToInsertion;
    result += '
';
    result += app.webapps.reduce(function(result, componentInfo) {
      result += '
';
      result += '' + componentInfo.name + ' ';
      result += componentInfo.description;
      result += '
\n';
      return result;
    }, '');
    result += '
';
    result += sliceToEnd;
    res.send(result);
  })

  app.signalk = new FullSignalK(app.selfId, app.selfType, app.config.defaults);

  app.handleMessage = function(providerId, data) {
    if(data.updates) {
      if(typeof data.context === 'undefined') {
        data.context = 'vessels.' + app.selfId;
      }
      data.updates.forEach(function(update) {
        if(typeof update.source != "undefined") {
          update.source.label = providerId;
        }
        if(!update.timestamp) {
          update.timestamp = (new Date()).toISOString();
        }
      })
      app.signalk.addDelta(data);
    }
  }

  app.webapps = []

  app.streambundle = new StreamBundle(app.selfId);
  app.signalk.on('delta', app.streambundle.pushDelta.bind(app.streambundle))
  app.subscriptionmanager = new SubscriptionManager(app);
}

module.exports = Server;

Server.prototype.start = function() {
  var self = this;
  var app = this.app;

  this.app.pruneInterval = setInterval(app.signalk.pruneContexts.bind(app.signalk, 60 * 60), 60 * 1000);
  this.app.providers = [];

  return new Promise((resolve, reject) => {
    createServer(app, function(err, server) {
      if (err) {
        reject(err)
        return
      }
      app.server = server;
      app.interfaces = {};
      app.connections = {};
      app.clients = 0;

      debug("ID type: " + app.selfType);
      debug("ID: " + app.selfId);

      startInterfaces(app);
      startMdns(app);
      app.providers = require('./pipedproviders')(app).start();

      var SD_LISTEN_FDS_START = 3
      var port = process.env.LISTEN_FDS > 0 ?
        {
          fd: SD_LISTEN_FDS_START
        } :
        app.config.port;

      server.listen(port, function() {
        console.log('signalk-server running at 0.0.0.0:' + port + "\n");
        app.started = true;
        resolve(self);
      });
    })
  })
};

function createServer(app, cb) {
  if(typeof app.config.settings.ssl === "undefined" || app.config.settings.ssl) {
    getCertificateOptions(function(err, options) {
      if(err) {
        cb(err)
      } else {
        debug("Starting server to serve both http and https")
        cb(null, httpolyglot.createServer(options, app));
      }
    });
    return;
  };
  var server;
  try {
    debug("Starting server to serve only http")
    server = http.createServer(app);
  } catch(e) {
    cb(e);
    return;
  }
  cb(null, server);
}

function hasStrictPermissions(stat) {
  return new Mode(stat).toString() === '-r--------'
}

function getCertificateOptions(cb) {
  if(fs.existsSync('./settings/ssl-key.pem') && fs.existsSync('./settings/ssl-cert.pem')) {
    if(!hasStrictPermissions(fs.statSync('./settings/ssl-key.pem'))) {
      cb(new Error('./settings/ssl-key.pem must be accessible only by the user that is running the server, refusing to start'))
      return
    }
    if(!hasStrictPermissions(fs.statSync('./settings/ssl-cert.pem'))) {
      cb(new Error('./settings/ssl-cert.pem must be accessible only by the user that is running the server, refusing to start'))
      return
    }
    debug("Using certificate ssl-key.pem and ssl-cert.pem in ./settings/");
    cb(null, {
      key: fs.readFileSync('./settings/ssl-key.pem'),
      cert: fs.readFileSync('./settings/ssl-cert.pem')
    });
    return;
  } else {
    createCertificateOptions(cb);
  }
}

function createCertificateOptions(cb) {
  debug("Creating certificate files in ./settings/");
  pem.createCertificate({
    days: 360,
    selfSigned: true
  }, function(err, keys) {
    if(err) {
      console.error("Could not create SSL certificate:" + err.message)
      throw err
    } else {
      fs.writeFileSync('./settings/ssl-key.pem', keys.serviceKey);
      fs.chmodSync('./settings/ssl-key.pem', '400');
      fs.writeFileSync('./settings/ssl-cert.pem', keys.certificate);
      fs.chmodSync('./settings/ssl-cert.pem', '400');
      cb(null, {
        key: keys.serviceKey,
        cert: keys.certificate
      })
    }
  });
}

function startMdns(app) {
  if(_.isUndefined(app.config.settings.mdns) || app.config.settings.mdns) {
    debug("Starting interface 'mDNS'");
    try {
      app.interfaces['mdns'] = require('./mdns')(app);
    } catch(ex) {
      debug("Could not start mDNS:" + ex);

    }
  } else {
    debug("Interface 'mDNS' was disabled in configuration");
  }
}

function startInterfaces(app) {
  debug("Interfaces config:" + JSON.stringify(app.config.settings.interfaces));
  var availableInterfaces = require('./interfaces');
  _.forIn(availableInterfaces, function(interface, name) {
    if(_.isUndefined(app.config.settings.interfaces) ||
      _.isUndefined(app.config.settings.interfaces[name]) ||
      app.config.settings.interfaces[name]) {
      debug("Loading interface '" + name + "'");
      app.interfaces[name] = interface(app);
      if(app.interfaces[name] && _.isFunction(app.interfaces[name].start)) {
        if(_.isUndefined(app.interfaces[name].forceInactive) || !app.interfaces[name].forceInactive) {
          debug("Starting interface '" + name + "'");
          app.interfaces[name].start();
        } else {
          debug("Not starting interface '" + name + "' by forceInactive")
        }
      }
    } else {
      debug("Not loading interface '" + name + "' because of configuration");
    }
  });
}

Server.prototype.reload = function(mixed) {
  var settings, self = this;

  if(typeof mixed === 'string') {
    try {
      settings = require(path.join(process.cwd(), mixed));
    } catch(e) {
      debug("Settings file '" + settings + "' doesn't exist.");
    }
  }

  if(mixed !== null && typeof mixed === 'object') {
    settings = mixed;
  }

  if(settings) {
    this.app.config.settings = settings;
  }

  this.stop();

  setTimeout(function() {
    self.start();
  }, 1000);

  return this;
};

Server.prototype.stop = function(cb) {
  if(this.app.started === true) {
    _.each(this.app.interfaces, function(intf) {
      if(intf !== null && typeof intf === 'object' && typeof intf.stop === 'function') {
        intf.stop();
      }
    });

    debug("Closing server...");

    this.app.server.close(function() {
      debug("Server closed...");
      this.server = null;
      cb();
    });

    for(var id in this.app.connections) {
      if(this.app.connections.hasOwnProperty(id)) {
        debug("Closing connection #" + id);
        this.app.connections[id].destroy();
        delete this.app.connections[id];
      }
    }

    if(this.app.pruneInterval) {
      clearInterval(this.app.pruneInterval);
    }
    this.app.started = false;
  }

  this.app.providers.forEach(function(providerHolder) {
    providerHolder.pipeElements[0].end();
  });

  return this;
};
<commit_msg>fix: ignore null input<commit_after>/*
 * Copyright 2014-2015 Fabian Tollenaar <fabian@starting-point.nl>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

if (typeof [].includes != 'function') {
  console.log("Minimum required Node.js version is 6, please update.")
  process.exit(-1)
}

var express = require('express'),
  _ = require('lodash'),
  debug = require('debug')('signalk-server'),
  path = require('path'),
  http = require('http'),
  https = require('https'),
  httpolyglot = require('httpolyglot'),
  pem = require('pem'),
  fs = require('fs'),
  FullSignalK = require('signalk-schema').FullSignalK,
  StreamBundle = require('./streambundle'),
  SubscriptionManager = require('./subscriptionmanager'),
  Mode = require('stat-mode')

function Server(opts) {
  this.params = opts || {};
  this.app = express();
  this.app.started = false;

  this.app.overrides = {
    settings: this.params.settings || null,
    port: this.params.port || null
  };

  var app = this.app;
  require('./config/config')(app);
  app.selfContext = 'vessels.' + app.selfId;
  app.version = "0.0.1"

  app.use(require('body-parser').json())
  app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html')
  })

  app.get('/apps', (req, res, next) => {
    var html = fs.readFileSync(__dirname + '/appindex.html', {encoding: 'utf8'})
    var insertionIndex = html.indexOf('
');
    var sliceToInsertion = html.slice(0, insertionIndex);
    var sliceToEnd = html.slice(insertionIndex);

    var result = sliceToInsertion;
    result += '
';
    result += app.webapps.reduce(function(result, componentInfo) {
      result += '
';
      result += '' + componentInfo.name + ' ';
      result += componentInfo.description;
      result += '
\n';
      return result;
    }, '');
    result += '
';
    result += sliceToEnd;
    res.send(result);
  })

  app.signalk = new FullSignalK(app.selfId, app.selfType, app.config.defaults);

  app.handleMessage = function(providerId, data) {
    if(data && data.updates) {
      if(typeof data.context === 'undefined') {
        data.context = 'vessels.' + app.selfId;
      }
      data.updates.forEach(function(update) {
        if(typeof update.source != "undefined") {
          update.source.label = providerId;
        }
        if(!update.timestamp) {
          update.timestamp = (new Date()).toISOString();
        }
      })
      app.signalk.addDelta(data);
    }
  }

  app.webapps = []

  app.streambundle = new StreamBundle(app.selfId);
  app.signalk.on('delta', app.streambundle.pushDelta.bind(app.streambundle))
  app.subscriptionmanager = new SubscriptionManager(app);
}

module.exports = Server;

Server.prototype.start = function() {
  var self = this;
  var app = this.app;

  this.app.pruneInterval = setInterval(app.signalk.pruneContexts.bind(app.signalk, 60 * 60), 60 * 1000);
  this.app.providers = [];

  return new Promise((resolve, reject) => {
    createServer(app, function(err, server) {
      if (err) {
        reject(err)
        return
      }
      app.server = server;
      app.interfaces = {};
      app.connections = {};
      app.clients = 0;

      debug("ID type: " + app.selfType);
      debug("ID: " + app.selfId);

      startInterfaces(app);
      startMdns(app);
      app.providers = require('./pipedproviders')(app).start();

      var SD_LISTEN_FDS_START = 3
      var port = process.env.LISTEN_FDS > 0 ?
        {
          fd: SD_LISTEN_FDS_START
        } :
        app.config.port;

      server.listen(port, function() {
        console.log('signalk-server running at 0.0.0.0:' + port + "\n");
        app.started = true;
        resolve(self);
      });
    })
  })
};

function createServer(app, cb) {
  if(typeof app.config.settings.ssl === "undefined" || app.config.settings.ssl) {
    getCertificateOptions(function(err, options) {
      if(err) {
        cb(err)
      } else {
        debug("Starting server to serve both http and https")
        cb(null, httpolyglot.createServer(options, app));
      }
    });
    return;
  };
  var server;
  try {
    debug("Starting server to serve only http")
    server = http.createServer(app);
  } catch(e) {
    cb(e);
    return;
  }
  cb(null, server);
}

function hasStrictPermissions(stat) {
  return new Mode(stat).toString() === '-r--------'
}

function getCertificateOptions(cb) {
  if(fs.existsSync('./settings/ssl-key.pem') && fs.existsSync('./settings/ssl-cert.pem')) {
    if(!hasStrictPermissions(fs.statSync('./settings/ssl-key.pem'))) {
      cb(new Error('./settings/ssl-key.pem must be accessible only by the user that is running the server, refusing to start'))
      return
    }
    if(!hasStrictPermissions(fs.statSync('./settings/ssl-cert.pem'))) {
      cb(new Error('./settings/ssl-cert.pem must be accessible only by the user that is running the server, refusing to start'))
      return
    }
    debug("Using certificate ssl-key.pem and ssl-cert.pem in ./settings/");
    cb(null, {
      key: fs.readFileSync('./settings/ssl-key.pem'),
      cert: fs.readFileSync('./settings/ssl-cert.pem')
    });
    return;
  } else {
    createCertificateOptions(cb);
  }
}

function createCertificateOptions(cb) {
  debug("Creating certificate files in ./settings/");
  pem.createCertificate({
    days: 360,
    selfSigned: true
  }, function(err, keys) {
    if(err) {
      console.error("Could not create SSL certificate:" + err.message)
      throw err
    } else {
      fs.writeFileSync('./settings/ssl-key.pem', keys.serviceKey);
      fs.chmodSync('./settings/ssl-key.pem', '400');
      fs.writeFileSync('./settings/ssl-cert.pem', keys.certificate);
      fs.chmodSync('./settings/ssl-cert.pem', '400');
      cb(null, {
        key: keys.serviceKey,
        cert: keys.certificate
      })
    }
  });
}

function startMdns(app) {
  if(_.isUndefined(app.config.settings.mdns) || app.config.settings.mdns) {
    debug("Starting interface 'mDNS'");
    try {
      app.interfaces['mdns'] = require('./mdns')(app);
    } catch(ex) {
      debug("Could not start mDNS:" + ex);

    }
  } else {
    debug("Interface 'mDNS' was disabled in configuration");
  }
}

function startInterfaces(app) {
  debug("Interfaces config:" + JSON.stringify(app.config.settings.interfaces));
  var availableInterfaces = require('./interfaces');
  _.forIn(availableInterfaces, function(interface, name) {
    if(_.isUndefined(app.config.settings.interfaces) ||
      _.isUndefined(app.config.settings.interfaces[name]) ||
      app.config.settings.interfaces[name]) {
      debug("Loading interface '" + name + "'");
      app.interfaces[name] = interface(app);
      if(app.interfaces[name] && _.isFunction(app.interfaces[name].start)) {
        if(_.isUndefined(app.interfaces[name].forceInactive) || !app.interfaces[name].forceInactive) {
          debug("Starting interface '" + name + "'");
          app.interfaces[name].start();
        } else {
          debug("Not starting interface '" + name + "' by forceInactive")
        }
      }
    } else {
      debug("Not loading interface '" + name + "' because of configuration");
    }
  });
}

Server.prototype.reload = function(mixed) {
  var settings, self = this;

  if(typeof mixed === 'string') {
    try {
      settings = require(path.join(process.cwd(), mixed));
    } catch(e) {
      debug("Settings file '" + settings + "' doesn't exist.");
    }
  }

  if(mixed !== null && typeof mixed === 'object') {
    settings = mixed;
  }

  if(settings) {
    this.app.config.settings = settings;
  }

  this.stop();

  setTimeout(function() {
    self.start();
  }, 1000);

  return this;
};

Server.prototype.stop = function(cb) {
  if(this.app.started === true) {
    _.each(this.app.interfaces, function(intf) {
      if(intf !== null && typeof intf === 'object' && typeof intf.stop === 'function') {
        intf.stop();
      }
    });

    debug("Closing server...");

    this.app.server.close(function() {
      debug("Server closed...");
      this.server = null;
      cb();
    });

    for(var id in this.app.connections) {
      if(this.app.connections.hasOwnProperty(id)) {
        debug("Closing connection #" + id);
        this.app.connections[id].destroy();
        delete this.app.connections[id];
      }
    }

    if(this.app.pruneInterval) {
      clearInterval(this.app.pruneInterval);
    }
    this.app.started = false;
  }

  this.app.providers.forEach(function(providerHolder) {
    providerHolder.pipeElements[0].end();
  });

  return this;
};
<|endoftext|>


<commit_before>
var exec = require('child_process').exec,
    fse = require('fs-extra'),
    nconf = require('nconf'),
    winston = require('winston');

// Logging stuff
nconf.file({ file: './gebo.json' });
var logLevel = nconf.get('logLevel'),
    timeout = nconf.get('pdf2htmlex').timeout;
var logger = new (winston.Logger)({ transports: [ new (winston.transports.Console)({ colorize: true }) ] });

/**
 * Call pdf2htmlEX to convert a PDF to HTML
 *
 * @param string
 * @param function
 *
 * @return string
 */
function _convert(path, destDir, outFile, done) {

    var outputFileName = _getOutputFileName(path, 'html');

    // Set up time limit
    var executionTimer = setTimeout(function() {
          var kill = 'kill $(cat /tmp/' + _getOutputFileName(path, 'html') + '.pid)';
          exec(kill, function(err, stdout, stderr) {
                done('Sorry, that file took too long to process');
                //deferred.resolve({ error: 'Sorry, that file took too long to process' });
            });
        }, timeout);

    fse.mkdirs(destDir, function(err) {
        if (err) {
          if (logLevel === 'trace') logger.error('gebo-pdf2htmlex:', err);
          done(err);
        }

        var command = 'pdf2htmlEX --dest-dir ' + destDir + ' ' + path.replace(/ /g, '\\ ') + ' ' +
                      outFile.replace(/ /g, '\\ ') + ' & echo $! > /tmp/' + outFile + '.pid';

        if (logLevel === 'trace') logger.info('gebo-pdf2htmlex:', command);
        exec(command, function(err, stdout, stderr) {
              clearInterval(executionTimer);
              if (err) {
                if (logLevel === 'trace') logger.error('gebo-pdf2htmlex:', err);
                done(err);
              }
              else {
                if (logLevel === 'trace' && stderr) logger.warn('gebo-pdf2htmlex:', stderr);
                done(null, destDir + '/' + outFile);
              }
          });
      });
  };
exports.convert = _convert;

/**
 * Take the incoming filename and its extension
 * and return the hypothetical output filename
 *
 * @param string
 * @param string
 *
 * @return string
 */
function _getOutputFileName(path, extension) {
    var filename = path.split('/');
    filename = filename[filename.length - 1];
    filename = filename.split('.');

    // No extension found
    if (filename.length === 1) {
      return filename[0] + '.' + extension;
    }

    // Hidden file
    if (filename[0] === '') {
      filename = filename.slice(1);
      filename[0] = '.' + filename[0];
      if (filename.length === 1) {
        return filename[0] + '.' + extension;
      }
    }

    filename = filename.slice(0, -1);

    return filename + '.' + extension;
  };
exports.getOutputFileName = _getOutputFileName;

<commit_msg>Change clearInterval to clearTimeout<commit_after>
var exec = require('child_process').exec,
    fse = require('fs-extra'),
    nconf = require('nconf'),
    winston = require('winston');

// Logging stuff
nconf.file({ file: './gebo.json' });
var logLevel = nconf.get('logLevel'),
    timeout = nconf.get('pdf2htmlex').timeout;
var logger = new (winston.Logger)({ transports: [ new (winston.transports.Console)({ colorize: true }) ] });

/**
 * Call pdf2htmlEX to convert a PDF to HTML
 *
 * @param string
 * @param function
 *
 * @return string
 */
function _convert(path, destDir, outFile, done) {

    var outputFileName = _getOutputFileName(path, 'html');

    // Set up time limit
    var executionTimer = setTimeout(function() {
          var kill = 'kill $(cat /tmp/' + _getOutputFileName(path, 'html') + '.pid)';
          exec(kill, function(err, stdout, stderr) {
                done('Sorry, that file took too long to process');
            });
        }, timeout);

    fse.mkdirs(destDir, function(err) {
        if (err) {
          if (logLevel === 'trace') logger.error('gebo-pdf2htmlex:', err);
          done(err);
        }

        var command = 'pdf2htmlEX --dest-dir ' + destDir + ' ' + path.replace(/ /g, '\\ ') + ' ' +
                      outFile.replace(/ /g, '\\ ') + ' & echo $! > /tmp/' + outFile + '.pid';

        if (logLevel === 'trace') logger.info('gebo-pdf2htmlex:', command);
        exec(command, function(err, stdout, stderr) {
              clearTimeout(executionTimer);
              if (err) {
                if (logLevel === 'trace') logger.error('gebo-pdf2htmlex:', err);
                done(err);
              }
              else {
                if (logLevel === 'trace' && stderr) logger.warn('gebo-pdf2htmlex:', stderr);
                done(null, destDir + '/' + outFile);
              }
          });
      });
  };
exports.convert = _convert;

/**
 * Take the incoming filename and its extension
 * and return the hypothetical output filename
 *
 * @param string
 * @param string
 *
 * @return string
 */
function _getOutputFileName(path, extension) {
    var filename = path.split('/');
    filename = filename[filename.length - 1];
    filename = filename.split('.');

    // No extension found
    if (filename.length === 1) {
      return filename[0] + '.' + extension;
    }

    // Hidden file
    if (filename[0] === '') {
      filename = filename.slice(1);
      filename[0] = '.' + filename[0];
      if (filename.length === 1) {
        return filename[0] + '.' + extension;
      }
    }

    filename = filename.slice(0, -1);

    return filename + '.' + extension;
  };
exports.getOutputFileName = _getOutputFileName;

<|endoftext|>


<commit_before>/*
 * jitsu.js: Top-level include for the jitsu module.
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var path = require('path'),
    util = require('util'),
    colors = require('colors'),
    flatiron = require('flatiron');

var jitsu = module.exports = flatiron.app;

//
// Setup `jitsu` to use `pkginfo` to expose version
//
require('pkginfo')(module, 'name', 'version');

//
// Configure jitsu to use `flatiron.plugins.cli`
//
jitsu.use(flatiron.plugins.cli, {
  version: true,
  usage: require('./jitsu/usage'),
  source: path.join(__dirname, 'jitsu', 'commands'),
  argv: {
    version: {
      alias: 'v',
      description: 'print jitsu version and exit',
      string: true
    },
    localconf: {
      description: 'search for .jitsuconf file in ./ and then parent directories',
      string: true
    },
    jitsuconf: {
      alias: 'j',
      description: 'specify file to load configuration from',
      string: true
    },
    noanalyze: {
      description: 'skip require-analyzer: do not attempt to dynamically detect dependencies',
      boolean: true
    },
    colors: {
      description: '--no-colors will disable output coloring',
      default: true,
      boolean: true
    },
    confirm: {
      alias: 'c',
      description: 'prevents jitsu from asking before overwriting/removing things',
      default: false,
      boolean: true
    },
    release: {
      alias: 'r',
      description: 'specify release version number or semantic increment (build, patch, minor, major)',
      string: true
    },
    raw: {
      description: 'jitsu will only output line-delimited raw JSON (useful for piping)',
      boolean: true
    }
  }
});

jitsu.options.log = {
  console: {
    raw: jitsu.argv.raw
  }
};

//
// Setup config, users, command aliases and prompt settings
//
jitsu.prompt.properties = flatiron.common.mixin(
  jitsu.prompt.properties,
  require('./jitsu/properties')
);
jitsu.prompt.override   = jitsu.argv;
require('./jitsu/config');
require('./jitsu/alias');
require('./jitsu/commands');

//
// Setup other jitsu settings.
//
jitsu.started  = false;
jitsu.common   = require('./jitsu/common');
jitsu.package  = require('./jitsu/package');
jitsu.logFile  = new (require('./jitsu/common/logfile').LogFile)(path.join(process.env.HOME, '.jitsulog'));

//
// Hoist `jitsu.api` from `nodejitsu-api` module.
//
jitsu.api               = {};
jitsu.api.Client        = require('nodejitsu-api').Client;
jitsu.api.Apps          = require('nodejitsu-api').Apps;
jitsu.api.Databases     = require('nodejitsu-api').Databases;
jitsu.api.Logs          = require('nodejitsu-api').Logs;
jitsu.api.Snapshots     = require('nodejitsu-api').Snapshots;
jitsu.api.Users         = require('nodejitsu-api').Users;

//
// ### function welcome ()
// Print welcome message.
//
jitsu.welcome = function () {
  //
  // If a user is logged in, show username
  //
  var username = jitsu.config.get('username') || '';
  jitsu.log.info('Welcome to ' + 'Nodejitsu'.grey + ' ' + username.magenta);
  jitsu.log.info('jitsu v' + jitsu.version + ', node ' + process.version);
  jitsu.log.info('It worked if it ends with ' + 'Nodejitsu'.grey + ' ok'.green.bold);
};

//
// ### function start (command, callback)
// #### @command {string} Command to execute once started
// #### @callback {function} Continuation to pass control to when complete.
// Starts the jitsu CLI and runs the specified command.
//
jitsu.start = function (callback) {
  //
  // Check for --no-colors/--colors option, without hitting the config file
  // yet
  //
  var useColors = (typeof jitsu.argv.colors == 'undefined' || jitsu.argv.colors);

  useColors || (colors.mode = "none");

  //
  // whoami command should not output anything but username
  //
  if (jitsu.argv._[0] === "whoami") {
    console.log(jitsu.config.get('username') || '');
    return;
  }

  jitsu.common.checkVersion(function (err) {
    if (err) {
      return callback();
    }

    jitsu.init(function (err) {
      if (err) {
        jitsu.welcome();
        callback(err);
        return jitsu.showError(jitsu.argv._.join(' '), err);
      }

      //
      // --no-colors option turns off output coloring, and so does setting
      // colors: false in ~/.jitsuconf (see
      // https://github.com/nodejitsu/jitsu/issues/101 )
      //
      if ( !jitsu.config.get('colors') || !useColors ) {
        colors.mode = "none";
        jitsu.log.get('default').stripColors = true;
        jitsu.log.get('default').transports.console.colorize = false;
      }

      jitsu.welcome();

      var username = jitsu.config.get('username');
      if (!username && jitsu.config.get('requiresAuth').indexOf(jitsu.argv._[0]) !== -1) {
        return jitsu.commands.users.login(function (err) {
          if (err) {
            callback(err);
            return jitsu.showError(jitsu.argv._.join(' '), err);
          }

          var username = jitsu.config.get('username');
          jitsu.log.info('Successfully configured user ' + username.magenta);
          return jitsu.exec(jitsu.argv._, callback);
        });
      }
      return jitsu.exec(jitsu.argv._, callback);
    });
  });
};

//
// ### function exec (command, callback)
// #### @command {string} Command to execute
// #### @callback {function} Continuation to pass control to when complete.
// Runs the specified command in the jitsu CLI.
//
jitsu.exec = function (command, callback) {
  function execCommand (err) {
    if (err) {
      return callback(err);
    }

    //
    // Remark: This is a temporary fix for aliasing init=>install,
    // was having a few issues with the alias command on the install resource
    //
    if (command[0] === 'init') {
      command[0] = 'install';
    }

    // Alias db to databases
    if (command[0] === 'db' || command[0] === 'dbs' || command[0] === 'database') {
      command[0] = 'databases';
    }

    // Allow `jitsu logs` as a shortcut for `jitsu logs app`
    if (command[0] === 'logs' && command.length === 1) {
      command[1] = 'app';
    }

    jitsu.log.info('Executing command ' + command.join(' ').magenta);
    jitsu.router.dispatch('on', command.join(' '), jitsu.log, function (err, shallow) {
      if (err) {
        callback(err);
        return jitsu.showError(command.join(' '), err, shallow);
      }

      //
      // TODO (indexzero): Something here
      //
      callback();
    });
  }

  return !jitsu.started ? jitsu.setup(execCommand) : execCommand();
};

//
// ### function setup (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Sets up the instances of the Resource clients for jitsu.
// there is no io here, yet this function is ASYNC.
//
jitsu.setup = function (callback) {
  if (jitsu.started === true) {
    return callback();
  }

  ['Users', 'Apps', 'Snapshots', 'Databases', 'Logs'].forEach(function (key) {
    var k = key.toLowerCase();
    jitsu[k] = new jitsu.api[key](jitsu.config);
    jitsu[k].on('debug::request',  debug);
    jitsu[k].on('debug::response', debug);
    function debug (data) {
      if (jitsu.argv.debug || jitsu.config.get('debug')) {
        util.inspect(data, false, null, true).split('\n').forEach(jitsu.log.debug);
      }
    };
  });

  jitsu.started = true;
  callback();
};

//
// ### function showError (command, err, shallow, skip)
// #### @command {string} Command which has errored.
// #### @err {Error} Error received for the command.
// #### @shallow {boolean} Value indicating if a deep stack should be displayed
// #### @skip {boolean} Value indicating if this error should be forcibly suppressed.
// Displays the `err` to the user for the `command` supplied.
//
jitsu.showError = function (command, err, shallow, skip) {
  var username,
      stack;

  if (err.statusCode === 403) {
    //jitsu.log.error('403 ' + err.result.error);
  }
  else if (err.statusCode === 503) {
    if (err.result && err.result.message) {
      jitsu.log.error(err.result.message);
    }
    else {
      jitsu.log.error('The Nodejitsu cloud is currently at capacity.  Please try again later.');
    }
  }
  else if (!skip) {
    jitsu.log.error('Error running command ' + command.magenta);

    if (!jitsu.config.get('nolog')) {
      jitsu.logFile.log(err);
    }

    if (err.message) {
      jitsu.log.error(err.message);
    }

    if (err.result) {
      if (err.result.error) {
        jitsu.log.error(err.result.error);
      }

      if (err.result.result && err.result.result.error) {
        if (err.result.result.error.stderr || err.result.result.error.stdout) {
          jitsu.log.error('');
          jitsu.log.error('There was an error while attempting to start the app');
          jitsu.log.error(err.result.result.error.message);
          if (err.result.result.error.blame) {
            jitsu.log.error(err.result.result.error.blame.message);
            jitsu.log.error('');
            jitsu.log.error('This type of error is usually a ' + err.result.result.error.blame.type + ' error.');
          }

          jitsu.log.error('Error output from app:');
          jitsu.log.error('');
          if (err.result.result.error.stdout) {
            err.result.result.error.stdout.split('\n').forEach(function (line) {
              jitsu.log.error(line);
            });
          }

          if (err.result.result.error.stderr) {
            err.result.result.error.stderr.split('\n').forEach(function (line) {
              jitsu.log.error(line);
            });
          }
        }
        else if (err.result.result.error.stack) {
          jitsu.log.error('There was an error while attempting to deploy the app');
          jitsu.log.error('');
          jitsu.log.error(err.result.result.error.message);

          if (err.result.result.error.blame) {
            jitsu.log.error(err.result.result.error.blame.message);
            jitsu.log.error('');
            jitsu.log.error('This type of error is usually a ' + err.result.result.error.blame.type + ' error.');
          }

          jitsu.log.error('Error output from Haibu:');
          jitsu.log.error('');
          stack = err.result.result.error.result || err.result.result.error.stack;
          stack.split('\n').forEach(function (line) {
            jitsu.log.error(line);
          });
        }
      }
      else if (err.result.stack) {
        jitsu.log.warn('Error returned from Nodejitsu');
        err.result.stack.split('\n').forEach(function (line) {
          jitsu.log.error(line);
        });
      }
    }
    else {
      if (err.stack && !shallow) {
        if(err.message && err.message === 'socket hang up'){
          if (err.code){
            if (err.code === 'ECONNRESET'){
              jitsu.log.info('\nThis error may indicate that the jitsu client\'s request');
              jitsu.log.info('timed out before the server could respond. If the application');
              jitsu.log.info('normally takes time to load, please wait a few minutes to see if');
              jitsu.log.info('the application deployed. Check the logs to see if there has been');
              jitsu.log.info('an error or logging output with `jitsu logs`.');
            } else {
              jitsu.log.info('\nThis error may indicate that the nodejitsu drone that the');
              jitsu.log.info('application was deployed to may be experiencing difficulties');
              jitsu.log.info('running the application. Check the application logs with `jitsu logs` ');
              jitsu.log.info('to determine if there is an error or logging provided.');
            }

          }
        } else {
          err.stack.split('\n').forEach(function (trace) {
            jitsu.log.error(trace);
          });
        }
      }
    }
  }
  jitsu.log.help("For help with this error contact Nodejitsu Support:");
  jitsu.log.help("  webchat: <http://webchat.nodejitsu.com/>");
  jitsu.log.help("      irc: <irc://chat.freenode.net/#nodejitsu>");
  jitsu.log.help("    email: <support@nodejitsu.com>");
  jitsu.log.help("");
  jitsu.log.help("  Copy and paste this output to a gist (http://gist.github.com/)");
  jitsu.log.info('Nodejitsu '.grey + 'not ok'.red.bold);
};
<commit_msg>[minor] Increased brevity on new error messages.<commit_after>/*
 * jitsu.js: Top-level include for the jitsu module.
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var path = require('path'),
    util = require('util'),
    colors = require('colors'),
    flatiron = require('flatiron');

var jitsu = module.exports = flatiron.app;

//
// Setup `jitsu` to use `pkginfo` to expose version
//
require('pkginfo')(module, 'name', 'version');

//
// Configure jitsu to use `flatiron.plugins.cli`
//
jitsu.use(flatiron.plugins.cli, {
  version: true,
  usage: require('./jitsu/usage'),
  source: path.join(__dirname, 'jitsu', 'commands'),
  argv: {
    version: {
      alias: 'v',
      description: 'print jitsu version and exit',
      string: true
    },
    localconf: {
      description: 'search for .jitsuconf file in ./ and then parent directories',
      string: true
    },
    jitsuconf: {
      alias: 'j',
      description: 'specify file to load configuration from',
      string: true
    },
    noanalyze: {
      description: 'skip require-analyzer: do not attempt to dynamically detect dependencies',
      boolean: true
    },
    colors: {
      description: '--no-colors will disable output coloring',
      default: true,
      boolean: true
    },
    confirm: {
      alias: 'c',
      description: 'prevents jitsu from asking before overwriting/removing things',
      default: false,
      boolean: true
    },
    release: {
      alias: 'r',
      description: 'specify release version number or semantic increment (build, patch, minor, major)',
      string: true
    },
    raw: {
      description: 'jitsu will only output line-delimited raw JSON (useful for piping)',
      boolean: true
    }
  }
});

jitsu.options.log = {
  console: {
    raw: jitsu.argv.raw
  }
};

//
// Setup config, users, command aliases and prompt settings
//
jitsu.prompt.properties = flatiron.common.mixin(
  jitsu.prompt.properties,
  require('./jitsu/properties')
);
jitsu.prompt.override   = jitsu.argv;
require('./jitsu/config');
require('./jitsu/alias');
require('./jitsu/commands');

//
// Setup other jitsu settings.
//
jitsu.started  = false;
jitsu.common   = require('./jitsu/common');
jitsu.package  = require('./jitsu/package');
jitsu.logFile  = new (require('./jitsu/common/logfile').LogFile)(path.join(process.env.HOME, '.jitsulog'));

//
// Hoist `jitsu.api` from `nodejitsu-api` module.
//
jitsu.api               = {};
jitsu.api.Client        = require('nodejitsu-api').Client;
jitsu.api.Apps          = require('nodejitsu-api').Apps;
jitsu.api.Databases     = require('nodejitsu-api').Databases;
jitsu.api.Logs          = require('nodejitsu-api').Logs;
jitsu.api.Snapshots     = require('nodejitsu-api').Snapshots;
jitsu.api.Users         = require('nodejitsu-api').Users;

//
// ### function welcome ()
// Print welcome message.
//
jitsu.welcome = function () {
  //
  // If a user is logged in, show username
  //
  var username = jitsu.config.get('username') || '';
  jitsu.log.info('Welcome to ' + 'Nodejitsu'.grey + ' ' + username.magenta);
  jitsu.log.info('jitsu v' + jitsu.version + ', node ' + process.version);
  jitsu.log.info('It worked if it ends with ' + 'Nodejitsu'.grey + ' ok'.green.bold);
};

//
// ### function start (command, callback)
// #### @command {string} Command to execute once started
// #### @callback {function} Continuation to pass control to when complete.
// Starts the jitsu CLI and runs the specified command.
//
jitsu.start = function (callback) {
  //
  // Check for --no-colors/--colors option, without hitting the config file
  // yet
  //
  var useColors = (typeof jitsu.argv.colors == 'undefined' || jitsu.argv.colors);

  useColors || (colors.mode = "none");

  //
  // whoami command should not output anything but username
  //
  if (jitsu.argv._[0] === "whoami") {
    console.log(jitsu.config.get('username') || '');
    return;
  }

  jitsu.common.checkVersion(function (err) {
    if (err) {
      return callback();
    }

    jitsu.init(function (err) {
      if (err) {
        jitsu.welcome();
        callback(err);
        return jitsu.showError(jitsu.argv._.join(' '), err);
      }

      //
      // --no-colors option turns off output coloring, and so does setting
      // colors: false in ~/.jitsuconf (see
      // https://github.com/nodejitsu/jitsu/issues/101 )
      //
      if ( !jitsu.config.get('colors') || !useColors ) {
        colors.mode = "none";
        jitsu.log.get('default').stripColors = true;
        jitsu.log.get('default').transports.console.colorize = false;
      }

      jitsu.welcome();

      var username = jitsu.config.get('username');
      if (!username && jitsu.config.get('requiresAuth').indexOf(jitsu.argv._[0]) !== -1) {
        return jitsu.commands.users.login(function (err) {
          if (err) {
            callback(err);
            return jitsu.showError(jitsu.argv._.join(' '), err);
          }

          var username = jitsu.config.get('username');
          jitsu.log.info('Successfully configured user ' + username.magenta);
          return jitsu.exec(jitsu.argv._, callback);
        });
      }
      return jitsu.exec(jitsu.argv._, callback);
    });
  });
};

//
// ### function exec (command, callback)
// #### @command {string} Command to execute
// #### @callback {function} Continuation to pass control to when complete.
// Runs the specified command in the jitsu CLI.
//
jitsu.exec = function (command, callback) {
  function execCommand (err) {
    if (err) {
      return callback(err);
    }

    //
    // Remark: This is a temporary fix for aliasing init=>install,
    // was having a few issues with the alias command on the install resource
    //
    if (command[0] === 'init') {
      command[0] = 'install';
    }

    // Alias db to databases
    if (command[0] === 'db' || command[0] === 'dbs' || command[0] === 'database') {
      command[0] = 'databases';
    }

    // Allow `jitsu logs` as a shortcut for `jitsu logs app`
    if (command[0] === 'logs' && command.length === 1) {
      command[1] = 'app';
    }

    jitsu.log.info('Executing command ' + command.join(' ').magenta);
    jitsu.router.dispatch('on', command.join(' '), jitsu.log, function (err, shallow) {
      if (err) {
        callback(err);
        return jitsu.showError(command.join(' '), err, shallow);
      }

      //
      // TODO (indexzero): Something here
      //
      callback();
    });
  }

  return !jitsu.started ? jitsu.setup(execCommand) : execCommand();
};

//
// ### function setup (callback)
// #### @callback {function} Continuation to pass control to when complete.
// Sets up the instances of the Resource clients for jitsu.
// there is no io here, yet this function is ASYNC.
//
jitsu.setup = function (callback) {
  if (jitsu.started === true) {
    return callback();
  }

  ['Users', 'Apps', 'Snapshots', 'Databases', 'Logs'].forEach(function (key) {
    var k = key.toLowerCase();
    jitsu[k] = new jitsu.api[key](jitsu.config);
    jitsu[k].on('debug::request',  debug);
    jitsu[k].on('debug::response', debug);
    function debug (data) {
      if (jitsu.argv.debug || jitsu.config.get('debug')) {
        util.inspect(data, false, null, true).split('\n').forEach(jitsu.log.debug);
      }
    };
  });

  jitsu.started = true;
  callback();
};

//
// ### function showError (command, err, shallow, skip)
// #### @command {string} Command which has errored.
// #### @err {Error} Error received for the command.
// #### @shallow {boolean} Value indicating if a deep stack should be displayed
// #### @skip {boolean} Value indicating if this error should be forcibly suppressed.
// Displays the `err` to the user for the `command` supplied.
//
jitsu.showError = function (command, err, shallow, skip) {
  var username,
      stack;

  if (err.statusCode === 403) {
    //jitsu.log.error('403 ' + err.result.error);
  }
  else if (err.statusCode === 503) {
    if (err.result && err.result.message) {
      jitsu.log.error(err.result.message);
    }
    else {
      jitsu.log.error('The Nodejitsu cloud is currently at capacity.  Please try again later.');
    }
  }
  else if (!skip) {
    jitsu.log.error('Error running command ' + command.magenta);

    if (!jitsu.config.get('nolog')) {
      jitsu.logFile.log(err);
    }

    if (err.message) {
      jitsu.log.error(err.message);
    }

    if (err.result) {
      if (err.result.error) {
        jitsu.log.error(err.result.error);
      }

      if (err.result.result && err.result.result.error) {
        if (err.result.result.error.stderr || err.result.result.error.stdout) {
          jitsu.log.error('');
          jitsu.log.error('There was an error while attempting to start the app');
          jitsu.log.error(err.result.result.error.message);
          if (err.result.result.error.blame) {
            jitsu.log.error(err.result.result.error.blame.message);
            jitsu.log.error('');
            jitsu.log.error('This type of error is usually a ' + err.result.result.error.blame.type + ' error.');
          }

          jitsu.log.error('Error output from app:');
          jitsu.log.error('');
          if (err.result.result.error.stdout) {
            err.result.result.error.stdout.split('\n').forEach(function (line) {
              jitsu.log.error(line);
            });
          }

          if (err.result.result.error.stderr) {
            err.result.result.error.stderr.split('\n').forEach(function (line) {
              jitsu.log.error(line);
            });
          }
        }
        else if (err.result.result.error.stack) {
          jitsu.log.error('There was an error while attempting to deploy the app');
          jitsu.log.error('');
          jitsu.log.error(err.result.result.error.message);

          if (err.result.result.error.blame) {
            jitsu.log.error(err.result.result.error.blame.message);
            jitsu.log.error('');
            jitsu.log.error('This type of error is usually a ' + err.result.result.error.blame.type + ' error.');
          }

          jitsu.log.error('Error output from Haibu:');
          jitsu.log.error('');
          stack = err.result.result.error.result || err.result.result.error.stack;
          stack.split('\n').forEach(function (line) {
            jitsu.log.error(line);
          });
        }
      }
      else if (err.result.stack) {
        jitsu.log.warn('Error returned from Nodejitsu');
        err.result.stack.split('\n').forEach(function (line) {
          jitsu.log.error(line);
        });
      }
    }
    else {
      if (err.stack && !shallow) {
        if(err.message && err.message === 'socket hang up'){
          if (err.code){
            if (err.code === 'ECONNRESET'){
              jitsu.log.info(
                '\njitsu\'s client request timed out before the server ' +
                'could respond'
              );
              jitsu.log.info(
                'This error may be due to network connection problems'
              );
            } else {
              jitsu.log.info('\nThe nodejitsu api reset the connection');
              jitsu.log.info(
                'This error may be due to the application or the drone server'
              );
            }

          }
        } else {
          err.stack.split('\n').forEach(function (trace) {
            jitsu.log.error(trace);
          });
        }
      }
    }
  }
  jitsu.log.help("For help with this error contact Nodejitsu Support:");
  jitsu.log.help("  webchat: <http://webchat.nodejitsu.com/>");
  jitsu.log.help("      irc: <irc://chat.freenode.net/#nodejitsu>");
  jitsu.log.help("    email: <support@nodejitsu.com>");
  jitsu.log.help("");
  jitsu.log.help("  Copy and paste this output to a gist (http://gist.github.com/)");
  jitsu.log.info('Nodejitsu '.grey + 'not ok'.red.bold);
};
<|endoftext|>


<commit_before>(function (window) {
    "use strict";

    var JSON_ = {};

    function snakeToCamelCase(key) {
        // first_name -> firstName
        return key.replace(/(_+[a-z])/g, function (snip) {
            return snip.toUpperCase().replace("_", "");
        });
    }

    function camelToSnakeCase(key) {
        // firstName -> first_name
        return key.replace(/([A-Z])/g, function (snip) {
            return "_" + snip.toLowerCase();
        });
    }

    JSON_.parse = function parse(text, reviver) {
        text = text.replace(/"([^"]*)"\s*:/g, snakeToCamelCase);
        return JSON.parse(text, reviver);
    };

    JSON_.stringify = function stringify(value, replacer, space) {
        var str = JSON.stringify(value, replacer, space);
        return str.replace(/"([^"]*)"\s*:/g, camelToSnakeCase);
    };

    // Export to popular environments boilerplate.
    if (typeof define === "function" && define.amd) {
        define(JSON_);
    } else if (typeof module !== "undefined" && module.exports) {
        module.exports = JSON_;
    } else {
        window["JSON_"] = JSON_;
    }
}(window));
<commit_msg>Clean up and better jshint support.<commit_after>/* jshint unused:vars, sub:true */
/* global window, define, module */
(function () {
    "use strict";

    var JSON_ = {};

    function snakeToCamelCase(key) {
        // first_name -> firstName
        return key.replace(/(_+[a-z])/g, function (snip) {
            return snip.toUpperCase().replace("_", "");
        });
    }

    function camelToSnakeCase(key) {
        // firstName -> first_name
        return key.replace(/([A-Z])/g, function (snip) {
            return "_" + snip.toLowerCase();
        });
    }

    JSON_.parse = function parse(text, reviver) {
        text = text.replace(/"([^"]*)"\s*:/g, snakeToCamelCase);
        return JSON.parse(text, reviver);
    };

    JSON_.stringify = function stringify(value, replacer, space) {
        var str = JSON.stringify(value, replacer, space);
        return str.replace(/"([^"]*)"\s*:/g, camelToSnakeCase);
    };

    // Export to popular environments boilerplate.
    if (typeof define === "function" && define.amd) {
        define(JSON_);
    } else if (typeof module !== "undefined" && module.exports) {
        module.exports = JSON_;
    } else {
        window["JSON_"] = JSON_;
    }
}());
<|endoftext|>


<commit_before>// # JsonML
//
// Various functions for handling
// jsonml in array form.
// For more info on jsonml,
// see [jsonml.org](http://jsonml.org/)
// or [wikipedia](http://en.wikipedia.org/wiki/JsonML)
//
// Implemented to be as portable as possible.
// Not depending on any libraries, and also
// avoid regular expressions to be possible to
// run on javascript-subsets on j2me devices.
//
// ## XML parser
//
// Parse an XML-string.
// Actually this is not a full implementation, but just
// the basic parts to get it up running.
// Nonetheless it is Good Enough(tm) for most uses.
//
// Known deficiencies: CDATA is not supported, will accept even
// non-well-formed documents, <?... > <!... > are not really handled, ...
exports.fromXml = function(xml) {
    if(typeof(xml) !== "string") {
        JsonML_Error(
            "Error: jsonml.parseXML didn't receive a string as parameter");
    }

    // white space definition
    var whitespace = " \n\r\t";
    // the current char in the string that is being parsed
    var c = xml[0];
    // the position in the string
    var pos = 0;
    // stack for handling nested tags
    var stack = [];
    // current tag being parsed
    var tag = [];
    // read the next char from the string
    function next_char() { c = ++pos < xml.length ? xml[pos] : undefined; }
    // check if the current char is one of those in the string parameter
    function is_a(str) { return str.indexOf(c) !== -1; }
    // return the string from the current position to right before the first
    // occurence of any of symb. Translate escaped xml entities to their value
    // on the fly.
    function read_until(symb) {
        var buffer = [];
        while(c && !is_a(symb)) {
            if(c === '&') {
                next_char();
                var entity = read_until(';');
                if(entity[0] === '#') {
                    if(entity[1] === 'x') {
                        c = String.fromCharCode(parseInt(entity.slice(2), 16));
                    } else {
                        c = String.fromCharCode(parseInt(entity.slice(1), 10));
                    }
                } else {
                    c = entities[entity];
                    if(!c) {
                        JsonML_Error("error: unrecognisable xml entity: " + entity);
                    }
                }
            }
            buffer.push(c);
            next_char();
        }
        return buffer.join("");
    }

    // The actual parsing
    while(is_a(whitespace)) { next_char(); }
    while(c) {
        if(is_a("<")) {
            next_char();

            // `<?xml ... >`, `<!-- -->` or similar - skip these
            if(is_a("?!")) {
                if(xml.slice(pos, pos+3) === "!--") {
                    pos += 3;
                    while(xml.slice(pos, pos+2) !== "--") {
                        ++pos;
                    }
                }
                read_until('>');
                next_char();

            // `<sometag ...>` - handle begin tag
            } else if(!is_a("/")) {
                // read tag name
                var newtag = [read_until(whitespace+">/")];

                // read attributes
                var attributes = {};
                var has_attributes = 0;
                while(c && is_a(whitespace)) { next_char(); }
                while(c && !is_a(">/")) {
                    has_attributes = 1;
                    var attr = read_until(whitespace + "=>");
                    if(c === "=") {
                        next_char();
                        var value_terminator = whitespace+">/";
                        if(is_a('"\'')) { value_terminator = c; next_char(); }
                        attributes[attr] = read_until(value_terminator);
                        if(is_a('"\'')) { next_char(); }
                    } else {
                        JsonML_Error("something not attribute in tag");
                    }
                    while(c && is_a(whitespace)) { next_char(); }
                }
                if(has_attributes) { newtag.push(attributes); }

                // end of tag, is it `<.../>` or  `<...>`
                if(is_a("/")) {
                    next_char();
                    if(!is_a(">")) {
                        JsonML_Error('expected ">" after "/" within tag');
                    }
                    tag.push(newtag);
                } else {
                    stack.push(tag);
                    tag = newtag;
                }
                next_char();

            // `</something>` - handle end tag
            } else {
                next_char();
                if(read_until(">") !== tag[0]) {
                    JsonML_Error("end tag not matching: " + tag[0]);
                }
                next_char();
                var parent_tag = stack.pop();
                if(tag.length <= 2 && !isArray(tag[1]) && typeof(tag[1]) !== "string") {
                    tag.push("");
                }
                parent_tag.push(tag);
                tag = parent_tag;

            }

        // actual content / data between tags
        } else {
            tag.push(read_until("<"));
        }
    }
    return tag;
};

// ## XML generation

// Convert jsonml in array form to xml.
exports.toXml = function(jsonml) {
    var acc = [];
    toXmlAcc(jsonml, acc);
    return acc.join('');
};

// The actual implementation. As the XML-string is built by appending to the
// `acc`umulator.
function toXmlAcc(jsonml, acc) {
    if(isArray(jsonml)) {
        acc.push("<");
        acc.push(jsonml[0]);
        var pos = 1;
        var attributes = jsonml[1];
        if(attributes && !isArray(attributes) && typeof(attributes) !== "string") {
            for(var key in attributes) { if(attributes.hasOwnProperty(key)) {
                acc.push(' ');
                acc.push(key);
                acc.push('="');
                xmlEscape(attributes[key], acc);
                acc.push('"');
            } }
            ++pos;
        }
        if(pos < jsonml.length) {
            acc.push(">");
            do {
                toXmlAcc(jsonml[pos], acc);
                ++pos;
            } while(pos < jsonml.length);
            acc.push("</");
            acc.push(jsonml[0]);
            acc.push(">");
        } else {
            acc.push(" />");
        }
    } else {
        //jsonml represents the text within a text tag
        xmlEscape(jsonml, acc, reventitiesTextNodes);
    }
}

// XML escaped entity table for non-text nodes
var entities = {
    "quot": '"',
    "amp": '&',
    "apos": "'",
    "lt": '<',
    "gt": '>'
};

// XML escaped entity table for text nodes
var entitiesTextNodes = {
    "amp": '&',
    "lt": '<'
};

// Generate a reverse xml entity table.
var reventities = (function () {
    var result = {};
    for(var key in entities) { if(entities.hasOwnProperty(key)) {
        result[entities[key]] = key;
    } }
    return result;
})();

var reventitiesTextNodes = (function () {
    var result = {};
    for(var key in entitiesTextNodes) { if(entitiesTextNodes.hasOwnProperty(key)) {
        result[entitiesTextNodes[key]] = key;
    } }
    return result;
})();

// Append the characters of `str`, or the xml-entity they map to, to the `acc`umulator array.
// 'reventities' is optional. Defaults to entities.
function xmlEscape(str, acc, table) {
    //determine which table is to be used for encoding
    if (!table) {
        var table = reventities;
    }
    for(var i = 0; i < str.length; ++i) {
        var c = str[i];
        var code = c.charCodeAt(0);
        var s = table[c];
        if(s) {
            acc.push("&" + s + ";");
        } else if(/*code < 32 ||*/ code >= 128) {
            acc.push("&#" + code + ";");
        } else {
            acc.push(c);
        }
    }
}

// ## Utility functions

// Apply a function to all the child elements of a given jsonml array.
var childReduce = exports.childReduce = function(jsonml, fn, acc) {
    var first = jsonml[1];
    if(typeof(first) !== "object" || isArray(first)) {
        acc = fn(acc, first);
    }
    for(var pos = 2; pos < jsonml.length; ++pos) {
        acc = fn(acc, jsonml[pos]);
    }
    return acc;
};

// - `jsonml.ensureAttributeObject(jsonml_array)` changes an jsonml array such that it has a (possibly empty) attribute object at position 1
exports.ensureAttributeObject = function(jsonml) {
    if(typeof jsonml[1] !== "object" || jsonml[1].constructor === Array) {
        jsonml.unshift(jsonml[0]);
        jsonml[1] = {};
    }
};

exports.getAttr = function(jsonml, attribute) {
    if(typeof jsonml[1] !== "object" || jsonml[1].constructor === Array) {
        return undefined;
    } else {
        return jsonml[1][attribute];
    }
};
// Convert jsonml into an easier subscriptable json structure, not preserving
// the order of the elements
exports.toObject = function(jsonml) {
    var result = {};
    result[jsonml[0]] = toObjectInner(jsonml);
    return result;
};

// Internal function called by toObject. Return an object corresponding to
// the child nodes of the `jsonml`-parameter
function toObjectInner(jsonml) {
    var result = {};
    var attr = jsonml[1];
    var pos;
    if(typeof(attr) === "object" && !isArray(attr)) {
        for(var key in attr) { if(attr.hasOwnProperty(key)) {
            result["@" + key] = attr[key];
        } }
        pos = 2;
    } else {
        pos = 1;
        if(jsonml.length === 2 && !isArray(attr)) {
            return attr;
        }
    }
    while(pos < jsonml.length) {
        var current = jsonml[pos];
        if(isArray(current)) {
            addprop(result, current[0], toObjectInner(current));
        } else {
            addprop(result, "_", current);
        }
        ++pos;
    }
    return result;
}

// Add a property to the object. If the property is already there, append
// the `val`ue to an array at the key instead, possibly putting existing
// object in front of such array, if that is not an array yet.
function addprop(obj, key, val) {
    if(obj[key]) {
        if(isArray(obj[key])) {
            obj[key].push(val);
        } else {
            obj[key] = [obj[key], val];
        }
    } else {
        obj[key] = val;
    }
}
// Error handler
function JsonML_Error(desc) {
    throw desc;
}

// Array check, implemented here to avoid depending on any library
function isArray(a) {
    return a !== null && typeof(a) === "object"  && a.constructor === Array;
}<commit_msg>Added newline at end of file<commit_after>// # JsonML
//
// Various functions for handling
// jsonml in array form.
// For more info on jsonml,
// see [jsonml.org](http://jsonml.org/)
// or [wikipedia](http://en.wikipedia.org/wiki/JsonML)
//
// Implemented to be as portable as possible.
// Not depending on any libraries, and also
// avoid regular expressions to be possible to
// run on javascript-subsets on j2me devices.
//
// ## XML parser
//
// Parse an XML-string.
// Actually this is not a full implementation, but just
// the basic parts to get it up running.
// Nonetheless it is Good Enough(tm) for most uses.
//
// Known deficiencies: CDATA is not supported, will accept even
// non-well-formed documents, <?... > <!... > are not really handled, ...
exports.fromXml = function(xml) {
    if(typeof(xml) !== "string") {
        JsonML_Error(
            "Error: jsonml.parseXML didn't receive a string as parameter");
    }

    // white space definition
    var whitespace = " \n\r\t";
    // the current char in the string that is being parsed
    var c = xml[0];
    // the position in the string
    var pos = 0;
    // stack for handling nested tags
    var stack = [];
    // current tag being parsed
    var tag = [];
    // read the next char from the string
    function next_char() { c = ++pos < xml.length ? xml[pos] : undefined; }
    // check if the current char is one of those in the string parameter
    function is_a(str) { return str.indexOf(c) !== -1; }
    // return the string from the current position to right before the first
    // occurence of any of symb. Translate escaped xml entities to their value
    // on the fly.
    function read_until(symb) {
        var buffer = [];
        while(c && !is_a(symb)) {
            if(c === '&') {
                next_char();
                var entity = read_until(';');
                if(entity[0] === '#') {
                    if(entity[1] === 'x') {
                        c = String.fromCharCode(parseInt(entity.slice(2), 16));
                    } else {
                        c = String.fromCharCode(parseInt(entity.slice(1), 10));
                    }
                } else {
                    c = entities[entity];
                    if(!c) {
                        JsonML_Error("error: unrecognisable xml entity: " + entity);
                    }
                }
            }
            buffer.push(c);
            next_char();
        }
        return buffer.join("");
    }

    // The actual parsing
    while(is_a(whitespace)) { next_char(); }
    while(c) {
        if(is_a("<")) {
            next_char();

            // `<?xml ... >`, `<!-- -->` or similar - skip these
            if(is_a("?!")) {
                if(xml.slice(pos, pos+3) === "!--") {
                    pos += 3;
                    while(xml.slice(pos, pos+2) !== "--") {
                        ++pos;
                    }
                }
                read_until('>');
                next_char();

            // `<sometag ...>` - handle begin tag
            } else if(!is_a("/")) {
                // read tag name
                var newtag = [read_until(whitespace+">/")];

                // read attributes
                var attributes = {};
                var has_attributes = 0;
                while(c && is_a(whitespace)) { next_char(); }
                while(c && !is_a(">/")) {
                    has_attributes = 1;
                    var attr = read_until(whitespace + "=>");
                    if(c === "=") {
                        next_char();
                        var value_terminator = whitespace+">/";
                        if(is_a('"\'')) { value_terminator = c; next_char(); }
                        attributes[attr] = read_until(value_terminator);
                        if(is_a('"\'')) { next_char(); }
                    } else {
                        JsonML_Error("something not attribute in tag");
                    }
                    while(c && is_a(whitespace)) { next_char(); }
                }
                if(has_attributes) { newtag.push(attributes); }

                // end of tag, is it `<.../>` or  `<...>`
                if(is_a("/")) {
                    next_char();
                    if(!is_a(">")) {
                        JsonML_Error('expected ">" after "/" within tag');
                    }
                    tag.push(newtag);
                } else {
                    stack.push(tag);
                    tag = newtag;
                }
                next_char();

            // `</something>` - handle end tag
            } else {
                next_char();
                if(read_until(">") !== tag[0]) {
                    JsonML_Error("end tag not matching: " + tag[0]);
                }
                next_char();
                var parent_tag = stack.pop();
                if(tag.length <= 2 && !isArray(tag[1]) && typeof(tag[1]) !== "string") {
                    tag.push("");
                }
                parent_tag.push(tag);
                tag = parent_tag;

            }

        // actual content / data between tags
        } else {
            tag.push(read_until("<"));
        }
    }
    return tag;
};

// ## XML generation

// Convert jsonml in array form to xml.
exports.toXml = function(jsonml) {
    var acc = [];
    toXmlAcc(jsonml, acc);
    return acc.join('');
};

// The actual implementation. As the XML-string is built by appending to the
// `acc`umulator.
function toXmlAcc(jsonml, acc) {
    if(isArray(jsonml)) {
        acc.push("<");
        acc.push(jsonml[0]);
        var pos = 1;
        var attributes = jsonml[1];
        if(attributes && !isArray(attributes) && typeof(attributes) !== "string") {
            for(var key in attributes) { if(attributes.hasOwnProperty(key)) {
                acc.push(' ');
                acc.push(key);
                acc.push('="');
                xmlEscape(attributes[key], acc);
                acc.push('"');
            } }
            ++pos;
        }
        if(pos < jsonml.length) {
            acc.push(">");
            do {
                toXmlAcc(jsonml[pos], acc);
                ++pos;
            } while(pos < jsonml.length);
            acc.push("</");
            acc.push(jsonml[0]);
            acc.push(">");
        } else {
            acc.push(" />");
        }
    } else {
        //jsonml represents the text within a text tag
        xmlEscape(jsonml, acc, reventitiesTextNodes);
    }
}

// XML escaped entity table for non-text nodes
var entities = {
    "quot": '"',
    "amp": '&',
    "apos": "'",
    "lt": '<',
    "gt": '>'
};

// XML escaped entity table for text nodes
var entitiesTextNodes = {
    "amp": '&',
    "lt": '<'
};

// Generate a reverse xml entity table.
var reventities = (function () {
    var result = {};
    for(var key in entities) { if(entities.hasOwnProperty(key)) {
        result[entities[key]] = key;
    } }
    return result;
})();

var reventitiesTextNodes = (function () {
    var result = {};
    for(var key in entitiesTextNodes) { if(entitiesTextNodes.hasOwnProperty(key)) {
        result[entitiesTextNodes[key]] = key;
    } }
    return result;
})();

// Append the characters of `str`, or the xml-entity they map to, to the `acc`umulator array.
// 'reventities' is optional. Defaults to entities.
function xmlEscape(str, acc, table) {
    //determine which table is to be used for encoding
    if (!table) {
        var table = reventities;
    }
    for(var i = 0; i < str.length; ++i) {
        var c = str[i];
        var code = c.charCodeAt(0);
        var s = table[c];
        if(s) {
            acc.push("&" + s + ";");
        } else if(/*code < 32 ||*/ code >= 128) {
            acc.push("&#" + code + ";");
        } else {
            acc.push(c);
        }
    }
}

// ## Utility functions

// Apply a function to all the child elements of a given jsonml array.
var childReduce = exports.childReduce = function(jsonml, fn, acc) {
    var first = jsonml[1];
    if(typeof(first) !== "object" || isArray(first)) {
        acc = fn(acc, first);
    }
    for(var pos = 2; pos < jsonml.length; ++pos) {
        acc = fn(acc, jsonml[pos]);
    }
    return acc;
};

// - `jsonml.ensureAttributeObject(jsonml_array)` changes an jsonml array such that it has a (possibly empty) attribute object at position 1
exports.ensureAttributeObject = function(jsonml) {
    if(typeof jsonml[1] !== "object" || jsonml[1].constructor === Array) {
        jsonml.unshift(jsonml[0]);
        jsonml[1] = {};
    }
};

exports.getAttr = function(jsonml, attribute) {
    if(typeof jsonml[1] !== "object" || jsonml[1].constructor === Array) {
        return undefined;
    } else {
        return jsonml[1][attribute];
    }
};
// Convert jsonml into an easier subscriptable json structure, not preserving
// the order of the elements
exports.toObject = function(jsonml) {
    var result = {};
    result[jsonml[0]] = toObjectInner(jsonml);
    return result;
};

// Internal function called by toObject. Return an object corresponding to
// the child nodes of the `jsonml`-parameter
function toObjectInner(jsonml) {
    var result = {};
    var attr = jsonml[1];
    var pos;
    if(typeof(attr) === "object" && !isArray(attr)) {
        for(var key in attr) { if(attr.hasOwnProperty(key)) {
            result["@" + key] = attr[key];
        } }
        pos = 2;
    } else {
        pos = 1;
        if(jsonml.length === 2 && !isArray(attr)) {
            return attr;
        }
    }
    while(pos < jsonml.length) {
        var current = jsonml[pos];
        if(isArray(current)) {
            addprop(result, current[0], toObjectInner(current));
        } else {
            addprop(result, "_", current);
        }
        ++pos;
    }
    return result;
}

// Add a property to the object. If the property is already there, append
// the `val`ue to an array at the key instead, possibly putting existing
// object in front of such array, if that is not an array yet.
function addprop(obj, key, val) {
    if(obj[key]) {
        if(isArray(obj[key])) {
            obj[key].push(val);
        } else {
            obj[key] = [obj[key], val];
        }
    } else {
        obj[key] = val;
    }
}
// Error handler
function JsonML_Error(desc) {
    throw desc;
}

// Array check, implemented here to avoid depending on any library
function isArray(a) {
    return a !== null && typeof(a) === "object"  && a.constructor === Array;
}
<|endoftext|>


<commit_before>// Latte supports Mocha syntax for now, but we'd like to evolve.
// It needs to support two different modes: headless and debug
// * headless runs in the background, and is optimized for speed.
// * debug is optimized for an engineer with devtools open.
(function() {
  window.Latte = {}

  // create the root describe block
  var current = Latte.root = {
    children: [], tests: [],
    before: [], beforeEach: [], after: [], afterEach: [], helpers: {}
  }

  var mode // headless or debug
  var currentContext = null // The context of the currently running test.
  var abort = false // stop tests mid-run. Usually when code changes
  var currentTimeout = null
  var otherError = null // stores any other error that occurred during the test run

  // Save some globals in case test code overrides it (ie sinon, bluebird, etc)
  var realSetTimeout = window.setTimeout
  var realClearTimeout = window.clearTimeout
  var RealPromise = window.Promise
  var realReload = window.location.reload.bind(window.location)

  window.describe = function(name, fn) {
    var previous = current
    current = {
      parent: previous, name: name, children: [], tests: [],
      fullName: [previous.fullName, name].filter(s => !!s).join(' '),
      before: [], beforeEach: [], after: [], afterEach: [], helpers: {},
      timeout: (val) => {} // TODO
    }

    // Replace ourselves in the heirarchy. This happens when a test file is hot-reloaded.
    // TODO: if the name changes, this breaks. Maybe use filename instead?
    let oldIndex = previous.children.findIndex(d => d.name == name)
    if (~oldIndex)
      previous.children[oldIndex] = current
    else
      previous.children.push(current)

    fn.call(current)
    current = previous
  }

  window.context = window.describe
  describe.skip = function(name) {}
  describe.only = function(name) {} // TODO
  window.timeout = function(val) {} // TODO
  window.beforeEach = function(fn) { current.beforeEach.push({fn, stack: getStack()}) }
  window.afterEach = function(fn) { current.afterEach.push({fn, stack: getStack()}) }
  window.before = function(fn) { current.before.push({fn, stack: getStack()}) }
  window.after = function(fn) { current.after.push({fn, stack: getStack()}) }

  // Helpers add methods to the current scope that aid writing clean tests.
  // For example, you might have a `login` or `goOffline` helper that you can call at the top of a
  // test to get things in the right state. Helpers are usually preferred over `beforeEach`, because
  // having setup code in the test makes it easier to understand.
  //
  // Helpers can be defined as a single function: `helper('login', function() {...})`
  // Or as a group: `helper('Factory', {user: function() {...}, hats: function() {...}})`
  // Groups are a great way to organize related helpers.
  window.helper = function(name, fnOrGroup) { current.helpers[name || 'default'] = fnOrGroup }

  window.it = function(name, fn) {
    if (!fn) return
    fullName = current.fullName + ' ' + name
    current.tests.push({fn, name, fullName, suite: current, stack: getStack()})
  }
  it.skip = function() {}
  it.only = function() {}

  function getStack() {
    return new Error().stack.split('\n')[3].trim()
  }

  Latte.setup = function latteSetup(md) {
    mode = md

    let oldWarn = console.warn, oldError = console.error
    // console.warn = function() { otherError = arguments; oldWarn.apply(console, arguments) }
    // console.error = function() { otherError = arguments; oldError.apply(console, arguments) }

    // These seem to break Chrome's "pause on uncaught exception", so don't do it in debug
    if (mode == 'headless') {
      window.onunhandledrejection = e => { otherError = e }
      window.onerror = e => { otherError = e }
    }
  }

  // Cancel any remaining tests and clean up.
  // TODO: this should let the current test finish, though that's tricky in debug mode (where uncaught exceptions are the norm)
  Latte.abort = async function latteClean() {
    abort = true

    if (currentContext) {
      await applyCallbacks('afterEach', currentContext)
      await changeSuite(currentContext, null)
      currentContext = null
    }
  }

  // Get all tests (optionally of a specific suite) flattened into an array
  Latte.flatten = function latteFlatten(suite) {
    suite = suite || Latte.root

    var allTests = [], queue = [Latte.root]
    while (step = queue.pop()) {
      allTests = allTests.concat(step.tests)
      queue = queue.concat(step.children)
    }
    return allTests
  }

  Latte.run = async function latteRun(tests) {
    abort = false

    for (test of tests) {
      if (!test.fn) continue
      console.log(`${test.fullName}`)
      otherError = null

      // apply afterEach from the inside out
      if (currentContext && mode == 'debug')
        await applyCallbacks('afterEach', currentContext)

      // each test gets a context that inherits from the suite-level context.
      // This isolates each test but lets us do some setup at the suite-level for speed.
      previousSuiteContext = currentContext && Object.getPrototypeOf(currentContext)
      var suiteContext

      if (mode == 'headless') {
        try { suiteContext = await changeSuite(previousSuiteContext, test.suite) }
        catch(e) { fatalError(e) }
      }

      if (mode == 'debug') {
        suiteContext = await changeSuite(previousSuiteContext, test.suite)
      }

      currentContext = Object.create(suiteContext)

      if (mode == 'headless') {
        try {
          await applyCallbacks('beforeEach', currentContext, {topDown: true})
          await runWithTimeout(test, currentContext)
          Latte.onTest(test, otherError)
        }
        catch (e) {
          realClearTimeout(currentTimeout)
          Latte.onTest(test, e || 'undefined error')
        }

        try { await applyCallbacks('afterEach', currentContext) }
        catch (e) { fatalError(e) }
      }

      if (mode == 'debug') {
        await applyCallbacks('beforeEach', currentContext, {topDown: true})
        await runWithTimeout(test, currentContext)
      }

      if (abort) break
    }

    console.log('Done!')
  }

  function fatalError(e) {
    console.error('Fatal', e)
    realReload()
  }

  // Change our context from one suite to another by running the minimal set of before/after callbacks.
  // Any common ancestors between the two suites don't need to run.
  async function changeSuite(context, nextSuite) {
    let currentSuite = context && context._suite

    // short circuit when the suite doesn't actually change
    if (context && context._suite === nextSuite)
      return context

    // figure out what the common ancestor is between both suites. If either suite is null
    // (happens at beginning and end of run) then commonAncestor will be undefined, and our
    // slice calls below will yield the full lineage.
    let currLineage = lineage(currentSuite)
    let nextLineage = lineage(nextSuite)
    let commonAncestor = currLineage.filter(x => nextLineage.indexOf(x) >= 0)[0]

    // walk the lineage up to (but not including) the common ancestor, running after callbacks
    let currTop = currLineage.indexOf(commonAncestor)
    currTop = currTop == -1 ? undefined : currTop
    let chain = currLineage.slice(0, currTop)
    for (suite of chain) {
      for (cb of suite.after)
        await runWithTimeout(cb, context)
      context = Object.getPrototypeOf(context)
    }

    // now walk down the lineage from right below the common ancestor to the new suite, running before callbacks
    let nextTop = nextLineage.indexOf(commonAncestor)
    nextTop = nextTop == -1 ? undefined : nextTop
    chain = nextLineage.slice(0, nextTop).reverse()
    for (suite of chain) {
      context = Object.create(context)
      context._suite = suite
      context.timeout = function() {}
      attachHelpers(suite, context)
      for (cb of suite.before)
        await runWithTimeout(cb, context)
    }

    return context
  }

  // Run user code with a timeout
  async function runWithTimeout(cbOrTest, context) {
    let timeoutPromise = new RealPromise((res, rej) => {
      currentTimeout = realSetTimeout(() => {
        if (mode == 'debug') {
          abort = true
          console.error('Timeout', cbOrTest.stack)
        }
        else {
          console.error('Timeout', cbOrTest.stack)
          rej(`Timeout ${cbOrTest.stack}`)
        }
      }, 10000)
    })

    return RealPromise.race([timeoutPromise, cbOrTest.fn.call(context)])
      .then(() => realClearTimeout(currentTimeout))
  }

  // run all the callbacks (before, after, beforeEach, or afterEach) for each suite.
  async function applyCallbacks(type, context, {topDown}={}) {
    let suites = lineage(context._suite)
    if (topDown) suites = suites.reverse()

    for (suite of suites)
      for (cb of suite[type])
        await runWithTimeout(cb, context)
  }

  // Get all the suites between the current one and the root.
  function lineage(suite) {
    if (!suite) return []

    let arr = []
    while (suite) {
      arr.push(suite)
      suite = suite.parent
    }
    return arr
  }

  function attachHelpers(suite, context) {
    for (name in suite.helpers) {
      let obj = suite.helpers[name]

      if (typeof(obj) == 'function') {
        context[name] = obj
        continue
      }

      let dest = name == 'default' ? context : (context[name] = {})
      for (prop of Object.keys(obj)) {
        let val = obj[prop]
        let isFunction = typeof(val) == 'function'
        Object.defineProperty(dest, prop, { get: () => isFunction ? val.bind(currentContext) : val})
      }
    }
  }

})()
<commit_msg>Abort should wait for the current test to finish<commit_after>// Latte supports Mocha syntax for now, but we'd like to evolve.
// It needs to support two different modes: headless and debug
// * headless runs in the background, and is optimized for speed.
// * debug is optimized for an engineer with devtools open.
(function() {
  window.Latte = {}

  // create the root describe block
  var current = Latte.root = {
    children: [], tests: [],
    before: [], beforeEach: [], after: [], afterEach: [], helpers: {}
  }

  var mode // headless or debug
  var currentContext = null // The context of the currently running test.
  var abort = false // stop tests mid-run. Usually when code changes
  var currentTimeout = null
  var otherError = null // stores any other error that occurred during the test run

  var whenCurrentTestFinished = null // promise that's resolved when the currently running test finishes
  var currentTestResolve = null

  // Save some globals in case test code overrides it (ie sinon, bluebird, etc)
  var realSetTimeout = window.setTimeout
  var realClearTimeout = window.clearTimeout
  var RealPromise = window.Promise
  var realReload = window.location.reload.bind(window.location)

  window.describe = function(name, fn) {
    var previous = current
    current = {
      parent: previous, name: name, children: [], tests: [],
      fullName: [previous.fullName, name].filter(s => !!s).join(' '),
      before: [], beforeEach: [], after: [], afterEach: [], helpers: {},
      timeout: (val) => {} // TODO
    }

    // Replace ourselves in the heirarchy. This happens when a test file is hot-reloaded.
    // TODO: if the name changes, this breaks. Maybe use filename instead?
    let oldIndex = previous.children.findIndex(d => d.name == name)
    if (~oldIndex)
      previous.children[oldIndex] = current
    else
      previous.children.push(current)

    fn.call(current)
    current = previous
  }

  window.context = window.describe
  describe.skip = function(name) {}
  describe.only = function(name) {} // TODO
  window.timeout = function(val) {} // TODO
  window.beforeEach = function(fn) { current.beforeEach.push({fn, stack: getStack()}) }
  window.afterEach = function(fn) { current.afterEach.push({fn, stack: getStack()}) }
  window.before = function(fn) { current.before.push({fn, stack: getStack()}) }
  window.after = function(fn) { current.after.push({fn, stack: getStack()}) }

  // Helpers add methods to the current scope that aid writing clean tests.
  // For example, you might have a `login` or `goOffline` helper that you can call at the top of a
  // test to get things in the right state. Helpers are usually preferred over `beforeEach`, because
  // having setup code in the test makes it easier to understand.
  //
  // Helpers can be defined as a single function: `helper('login', function() {...})`
  // Or as a group: `helper('Factory', {user: function() {...}, hats: function() {...}})`
  // Groups are a great way to organize related helpers.
  window.helper = function(name, fnOrGroup) { current.helpers[name || 'default'] = fnOrGroup }

  window.it = function(name, fn) {
    if (!fn) return
    fullName = current.fullName + ' ' + name
    current.tests.push({fn, name, fullName, suite: current, stack: getStack()})
  }
  it.skip = function() {}
  it.only = function() {}

  function getStack() {
    return new Error().stack.split('\n')[3].trim()
  }

  Latte.setup = function latteSetup(md) {
    mode = md

    let oldWarn = console.warn, oldError = console.error
    // console.warn = function() { otherError = arguments; oldWarn.apply(console, arguments) }
    // console.error = function() { otherError = arguments; oldError.apply(console, arguments) }

    // These seem to break Chrome's "pause on uncaught exception", so don't do it in debug
    if (mode == 'headless') {
      window.onunhandledrejection = e => { otherError = e }
      window.onerror = e => { otherError = e }
    }
  }

  // Cancel any remaining tests and clean up.
  // TODO: this should let the current test finish, though that's tricky in debug mode (where uncaught exceptions are the norm)
  Latte.abort = async function latteClean() {
    abort = true

    whenCurrentTestFinished && await whenCurrentTestFinished

    if (currentContext) {
      await applyCallbacks('afterEach', currentContext)
      await changeSuite(currentContext, null)
      currentContext = null
    }
  }

  // Get all tests (optionally of a specific suite) flattened into an array
  Latte.flatten = function latteFlatten(suite) {
    suite = suite || Latte.root

    var allTests = [], queue = [Latte.root]
    while (step = queue.pop()) {
      allTests = allTests.concat(step.tests)
      queue = queue.concat(step.children)
    }
    return allTests
  }

  Latte.run = async function latteRun(tests) {
    abort = false

    for (test of tests) {
      if (!test.fn) continue
      console.log(`${test.fullName}`)
      otherError = null
      whenCurrentTestFinished = new RealPromise(res => currentTestResolve = res)

      // apply afterEach from the inside out
      if (currentContext && mode == 'debug')
        await applyCallbacks('afterEach', currentContext)

      // each test gets a context that inherits from the suite-level context.
      // This isolates each test but lets us do some setup at the suite-level for speed.
      previousSuiteContext = currentContext && Object.getPrototypeOf(currentContext)
      var suiteContext

      if (mode == 'headless') {
        try { suiteContext = await changeSuite(previousSuiteContext, test.suite) }
        catch(e) { fatalError(e) }
      }

      if (mode == 'debug') {
        suiteContext = await changeSuite(previousSuiteContext, test.suite)
      }

      currentContext = Object.create(suiteContext)

      if (mode == 'headless') {
        try {
          await applyCallbacks('beforeEach', currentContext, {topDown: true})
          await runWithTimeout(test, currentContext)
          Latte.onTest(test, otherError)
        }
        catch (e) {
          realClearTimeout(currentTimeout)
          Latte.onTest(test, e || 'undefined error')
        }

        try { await applyCallbacks('afterEach', currentContext) }
        catch (e) { fatalError(e) }
      }

      if (mode == 'debug') {
        await applyCallbacks('beforeEach', currentContext, {topDown: true})
        await runWithTimeout(test, currentContext)
      }

      // signal that we've finished this test, in case we're aborting
      currentTestResolve()
      whenCurrentTestFinished = currentTestResolve = null
      if (abort) break
    }

    console.log('Done!')
  }

  function fatalError(e) {
    console.error('Fatal', e)
    realReload()
  }

  // Change our context from one suite to another by running the minimal set of before/after callbacks.
  // Any common ancestors between the two suites don't need to run.
  async function changeSuite(context, nextSuite) {
    let currentSuite = context && context._suite

    // short circuit when the suite doesn't actually change
    if (context && context._suite === nextSuite)
      return context

    // figure out what the common ancestor is between both suites. If either suite is null
    // (happens at beginning and end of run) then commonAncestor will be undefined, and our
    // slice calls below will yield the full lineage.
    let currLineage = lineage(currentSuite)
    let nextLineage = lineage(nextSuite)
    let commonAncestor = currLineage.filter(x => nextLineage.indexOf(x) >= 0)[0]

    // walk the lineage up to (but not including) the common ancestor, running after callbacks
    let currTop = currLineage.indexOf(commonAncestor)
    currTop = currTop == -1 ? undefined : currTop
    let chain = currLineage.slice(0, currTop)
    for (suite of chain) {
      for (cb of suite.after)
        await runWithTimeout(cb, context)
      context = Object.getPrototypeOf(context)
    }

    // now walk down the lineage from right below the common ancestor to the new suite, running before callbacks
    let nextTop = nextLineage.indexOf(commonAncestor)
    nextTop = nextTop == -1 ? undefined : nextTop
    chain = nextLineage.slice(0, nextTop).reverse()
    for (suite of chain) {
      context = Object.create(context)
      context._suite = suite
      context.timeout = function() {}
      attachHelpers(suite, context)
      for (cb of suite.before)
        await runWithTimeout(cb, context)
    }

    return context
  }

  // Run user code with a timeout
  async function runWithTimeout(cbOrTest, context) {
    let timeoutPromise = new RealPromise((res, rej) => {
      currentTimeout = realSetTimeout(() => {
        if (mode == 'debug') {
          abort = true
          console.error('Timeout', cbOrTest.stack)
        }
        else {
          console.error('Timeout', cbOrTest.stack)
          rej(`Timeout ${cbOrTest.stack}`)
        }
      }, 10000)
    })

    return RealPromise.race([timeoutPromise, cbOrTest.fn.call(context)])
      .then(() => realClearTimeout(currentTimeout))
  }

  // run all the callbacks (before, after, beforeEach, or afterEach) for each suite.
  async function applyCallbacks(type, context, {topDown}={}) {
    let suites = lineage(context._suite)
    if (topDown) suites = suites.reverse()

    for (suite of suites)
      for (cb of suite[type])
        await runWithTimeout(cb, context)
  }

  // Get all the suites between the current one and the root.
  function lineage(suite) {
    if (!suite) return []

    let arr = []
    while (suite) {
      arr.push(suite)
      suite = suite.parent
    }
    return arr
  }

  function attachHelpers(suite, context) {
    for (name in suite.helpers) {
      let obj = suite.helpers[name]

      if (typeof(obj) == 'function') {
        context[name] = obj
        continue
      }

      let dest = name == 'default' ? context : (context[name] = {})
      for (prop of Object.keys(obj)) {
        let val = obj[prop]
        let isFunction = typeof(val) == 'function'
        Object.defineProperty(dest, prop, { get: () => isFunction ? val.bind(currentContext) : val})
      }
    }
  }

})()
<|endoftext|>


<commit_before>const Brick = require('./bricks');

 var Levels = [
   [
   '############',
   'OOOOOOOOOOOO',
   '####MMMM####',
   'OOOOOOOOOOOO',
   '############',
   'OOOOOOOOOOOO',
   '############',
   'OOOOOOOOOOOO',
   ]
];

module.exports = Levels;

   var Level1 = [
      '##########',
      'OO###OO###',
      '###OO###OO',
      'UUUUUUUU##'
   ];
<commit_msg>add a level, and brick colors<commit_after>const Brick = require('./bricks');

 var Levels = [
   [
   '############',
   'OOOOOOOOOOOO',
   '####MMMM####',
   'OOOOOOOOOOOO',
   '############',
   'OOOOOOOOOOOO',
   '############',
   'OOOOOOOOOOOO',
 ],
 [
   'MMMMMMMMMMMM',
   'UUUU####UUUU',
   '############',
   'UUUUUU######',
   '############',
   'OOOOOOOOOOOO',
   '############',
   'UUUUUUUU####',
 ]
];

module.exports = Levels;
<|endoftext|>