<commit_before>/************************************************************************
 *  Copyright 2010-2011 Worlize Inc.
 *
 *  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.
 ***********************************************************************/

var crypto = require('crypto');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var WebSocketConnection = require('./WebSocketConnection');

var headerValueSplitRegExp = /,\s*/;
var headerParamSplitRegExp = /;\s*/;
var headerSanitizeRegExp = /[\r\n]/g;
var separators = [
	"(", ")", "<", ">", "@",
	",", ";", ":", "\\", "\"",
	"/", "[", "]", "?", "=",
	"{", "}", " ", String.fromCharCode(9)
];
var controlChars = [String.fromCharCode(127) /* DEL */];
for (var i=0; i < 31; i ++) {
    /* US-ASCII Control Characters */
    controlChars.push(String.fromCharCode(i));
}

var cookieNameValidateRegEx = /([\x00-\x20\x22\x28\x29\x2c\x2f\x3a-\x3f\x40\x5b-\x5e\x7b\x7d\x7f])/;
var cookieValueValidateRegEx = /[^\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]/;

var httpStatusDescriptions = {
    100: "Continue",
    101: "Switching Protocols",
    200: "OK",
    201: "Created",
    203: "Non-Authoritative Information",
    204: "No Content",
    205: "Reset Content",
    206: "Partial Content",
    300: "Multiple Choices",
    301: "Moved Permanently",
    302: "Found",
    303: "See Other",
    304: "Not Modified",
    305: "Use Proxy",
    307: "Temporary Redirect",
    400: "Bad Request",
    401: "Unauthorized",
    402: "Payment Required",
    403: "Forbidden",
    404: "Not Found",
    406: "Not Acceptable",
    407: "Proxy Authorization Required",
    408: "Request Timeout",
    409: "Conflict",
    410: "Gone",
    411: "Length Required",
    412: "Precondition Failed",
    413: "Request Entity Too Long",
    414: "Request-URI Too Long",
    415: "Unsupported Media Type",
    416: "Requested Range Not Satisfiable",
    417: "Expectation Failed",
    426: "Upgrade Required",
    500: "Internal Server Error",
    501: "Not Implemented",
    502: "Bad Gateway",
    503: "Service Unavailable",
    504: "Gateway Timeout",
    505: "HTTP Version Not Supported"
};

function WebSocketRequest(socket, httpRequest, serverConfig) {
    this.socket = socket;
    this.httpRequest = httpRequest;
    this.resource = httpRequest.url;
    this.remoteAddress = socket.remoteAddress;
    this.serverConfig = serverConfig;
};

util.inherits(WebSocketRequest, EventEmitter);

WebSocketRequest.prototype.readHandshake = function() {
    var request = this.httpRequest;
    this.host = request.headers['host'];
    if (!this.host) {
        throw new Error("Client must provide a Host header.");
    }

    this.key = request.headers['sec-websocket-key'];
    if (!this.key) {
        throw new Error("Client must provide a value for Sec-WebSocket-Key.");
    }

    this.websocketVersion = parseInt(request.headers['sec-websocket-version'], 10);

    if (!this.websocketVersion || isNaN(this.websocketVersion)) {
        throw new Error("Client must provide a value for Sec-WebSocket-Version.");
    }

    switch (this.websocketVersion) {
        case 8:
        case 13:
            break;
        default:
            var e = new Error("Unsupported websocket client version: " + this.websocketVersion +
                              "Only versions 8 and 13 are supported.");
            e.httpCode = 426;
            e.headers = {
                "Sec-WebSocket-Version": "13"
            };
            throw e;
    }

    if (this.websocketVersion === 13) {
        this.origin = request.headers['origin'];
    }
    else if (this.websocketVersion === 8) {
        this.origin = request.headers['sec-websocket-origin'];
    }

    // Protocol is optional.
    var protocolString = request.headers['sec-websocket-protocol'];
    if (protocolString) {
        this.requestedProtocols = protocolString.toLocaleLowerCase().split(headerValueSplitRegExp);
    }
    else {
        this.requestedProtocols = [];
    }

    if (request.headers['x-forwarded-for']) {
        this.remoteAddress = request.headers['x-forwarded-for'].split(', ')[0];
    }

    // Extensions are optional.
    var extensionsString = request.headers['sec-websocket-extensions'];
    this.requestedExtensions = this.parseExtensions(extensionsString);
};

WebSocketRequest.prototype.parseExtensions = function(extensionsString) {
    if (!extensionsString || extensionsString.length === 0) {
        return [];
    }
    extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
    extensions.forEach(function(extension, index, array) {
        var params = extension.split(headerParamSplitRegExp);
        var extensionName = params[0];
        var extensionParams = params.slice(1);
        extensionParams.forEach(function(rawParam, index, array) {
            var arr = rawParam.split('=');
            var obj = {
                name: arr[0],
                value: arr[1]
            };
            array.splice(index, 1, obj);
        });
        var obj = {
            name: extensionName,
            params: extensionParams
        };
        array.splice(index, 1, obj);
    });
    return extensions;
},
WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) {
    // TODO: Handle extensions
    var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig);

    connection.websocketVersion = this.websocketVersion;
    connection.remoteAddress = this.remoteAddress;

    // Create key validation hash
    var sha1 = crypto.createHash('sha1');
    sha1.update(this.key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
    var acceptKey = sha1.digest('base64');

    var response = "HTTP/1.1 101 Switching Protocols\r\n" +
                   "Upgrade: websocket\r\n" +
                   "Connection: Upgrade\r\n" +
                   "Sec-WebSocket-Accept: " + acceptKey + "\r\n";

    if (acceptedProtocol) {
        // validate protocol
		for (var i=0; i < acceptedProtocol.length; i++) {
			var charCode = acceptedProtocol.charCodeAt(i);
			var character = acceptedProtocol.charAt(i);
			if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) {
			    this.reject(500);
				throw new Error("Illegal character '" + String.fromCharCode(character) + "' in subprotocol.");
			}
		}
		if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) {
		    this.reject(500);
		    throw new Error("Specified protocol was not requested by the client.");
		}

        acceptedProtocol = acceptedProtocol.replace(headerSanitizeRegExp, '');
        response += "Sec-WebSocket-Protocol: " + acceptedProtocol + "\r\n";
    }
    if (allowedOrigin) {
        allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, '');
        if (this.websocketVersion === 13) {
            response += "Origin: " + allowedOrigin + "\r\n";
        }
        else if (this.websocketVersion === 8) {
            response += "Sec-WebSocket-Origin: " + allowedOrigin + "\r\n";
        }
    }

    if (cookies) {
        if (!Array.isArray(cookies)) {
            this.reject(500);
            throw new Error("Value supplied for 'cookies' argument must be an array.");
        }
        cookies.forEach(function(cookie) {
    		if (!cookie.name || !cookie.value) {
    		    this.reject(500);
    		    throw new Error("Each cookie to set must at least provide a 'name' and 'value'");
    		}

    		// token (RFC 2616, Section 2.2)
    		var invalidChar = cookie.name.match(cookieNameValidateRegEx);
    		if (invalidChar) {
    		    this.reject(500);
    		    throw new Error("Illegal character(s) " + invalidChar[0] + " in cookie name");
    		}

    		// RFC 6265, Section 4.1.1
    		// *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
    		// TODO: add double quote handling
    		invalidChar = cookie.value.match(cookieValueValidateRegEx);
    		if (invalidChar) {
    		    this.reject(500);
    		    throw new Error("Illegal character(s) " + invalidChar[0] + " in cookie value");
    		}

    		var cookieParts = [cookie.name + "=" + cookie.value];

    		if (cookie.expires) {
    		    cookieParts.push("Expires=" + cookie.expires);
    		}
    		// prefer camelcase'd params, but accept all lowercase
    		if (cookie.maxage) {
    		    cookieParts.push("Max-Age=" + cookie.maxage.toString());
    		}
    		if (cookie.domain) {
    		    cookieParts.push("Domain=" + cookie.domain);
    		}
    		if (cookie.path) {
    		    cookieParts.push("Expires=" + cookie.path);
    		}
    		if (cookie.secure) {
    		    cookieParts.push("Secure");
    		}
    		if (cookie.httponly) {
    		    cookieParts.push("HttpOnly");
    		}

    		response += ("Set-Cookie: " + cookieParts.join(';') + "\r\n");
    	}.bind(this));
    }

    // TODO: handle negotiated extensions
    // if (negotiatedExtensions) {
    //     response += "Sec-WebSocket-Extensions: " + negotiatedExtensions.join(", ") + "\r\n";
    // }

    response += "\r\n";

    this.socket.write(response, 'ascii');

    this.emit('requestAccepted', connection);

    return connection;
};

WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) {
    if (typeof(status) !== 'number') {
        status = 403;
    }
    var response = "HTTP/1.1 " + status + " " + httpStatusDescriptions[status] + "\r\n" +
                   "Connection: close\r\n";
    if (reason) {
        reason = reason.replace(headerSanitizeRegExp, '');
        response += "X-WebSocket-Reject-Reason: " + reason + "\r\n";
    }

    if (extraHeaders) {
        for (var key in extraHeaders) {
            var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, '');
            var sanitizedKey = key.replace(headerSanitizeRegExp, '');
            response += (key + ": " + sanitizedValue + "\r\n");
        }
    }

    response += "\r\n";
    this.socket.end(response, 'ascii');

    this.emit('requestRejected', this);
};

module.exports = WebSocketRequest;<commit_msg>Removing an incorrect comment.<commit_after>/************************************************************************
 *  Copyright 2010-2011 Worlize Inc.
 *
 *  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.
 ***********************************************************************/

var crypto = require('crypto');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var WebSocketConnection = require('./WebSocketConnection');

var headerValueSplitRegExp = /,\s*/;
var headerParamSplitRegExp = /;\s*/;
var headerSanitizeRegExp = /[\r\n]/g;
var separators = [
	"(", ")", "<", ">", "@",
	",", ";", ":", "\\", "\"",
	"/", "[", "]", "?", "=",
	"{", "}", " ", String.fromCharCode(9)
];
var controlChars = [String.fromCharCode(127) /* DEL */];
for (var i=0; i < 31; i ++) {
    /* US-ASCII Control Characters */
    controlChars.push(String.fromCharCode(i));
}

var cookieNameValidateRegEx = /([\x00-\x20\x22\x28\x29\x2c\x2f\x3a-\x3f\x40\x5b-\x5e\x7b\x7d\x7f])/;
var cookieValueValidateRegEx = /[^\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]/;

var httpStatusDescriptions = {
    100: "Continue",
    101: "Switching Protocols",
    200: "OK",
    201: "Created",
    203: "Non-Authoritative Information",
    204: "No Content",
    205: "Reset Content",
    206: "Partial Content",
    300: "Multiple Choices",
    301: "Moved Permanently",
    302: "Found",
    303: "See Other",
    304: "Not Modified",
    305: "Use Proxy",
    307: "Temporary Redirect",
    400: "Bad Request",
    401: "Unauthorized",
    402: "Payment Required",
    403: "Forbidden",
    404: "Not Found",
    406: "Not Acceptable",
    407: "Proxy Authorization Required",
    408: "Request Timeout",
    409: "Conflict",
    410: "Gone",
    411: "Length Required",
    412: "Precondition Failed",
    413: "Request Entity Too Long",
    414: "Request-URI Too Long",
    415: "Unsupported Media Type",
    416: "Requested Range Not Satisfiable",
    417: "Expectation Failed",
    426: "Upgrade Required",
    500: "Internal Server Error",
    501: "Not Implemented",
    502: "Bad Gateway",
    503: "Service Unavailable",
    504: "Gateway Timeout",
    505: "HTTP Version Not Supported"
};

function WebSocketRequest(socket, httpRequest, serverConfig) {
    this.socket = socket;
    this.httpRequest = httpRequest;
    this.resource = httpRequest.url;
    this.remoteAddress = socket.remoteAddress;
    this.serverConfig = serverConfig;
};

util.inherits(WebSocketRequest, EventEmitter);

WebSocketRequest.prototype.readHandshake = function() {
    var request = this.httpRequest;
    this.host = request.headers['host'];
    if (!this.host) {
        throw new Error("Client must provide a Host header.");
    }

    this.key = request.headers['sec-websocket-key'];
    if (!this.key) {
        throw new Error("Client must provide a value for Sec-WebSocket-Key.");
    }

    this.websocketVersion = parseInt(request.headers['sec-websocket-version'], 10);

    if (!this.websocketVersion || isNaN(this.websocketVersion)) {
        throw new Error("Client must provide a value for Sec-WebSocket-Version.");
    }

    switch (this.websocketVersion) {
        case 8:
        case 13:
            break;
        default:
            var e = new Error("Unsupported websocket client version: " + this.websocketVersion +
                              "Only versions 8 and 13 are supported.");
            e.httpCode = 426;
            e.headers = {
                "Sec-WebSocket-Version": "13"
            };
            throw e;
    }

    if (this.websocketVersion === 13) {
        this.origin = request.headers['origin'];
    }
    else if (this.websocketVersion === 8) {
        this.origin = request.headers['sec-websocket-origin'];
    }

    // Protocol is optional.
    var protocolString = request.headers['sec-websocket-protocol'];
    if (protocolString) {
        this.requestedProtocols = protocolString.toLocaleLowerCase().split(headerValueSplitRegExp);
    }
    else {
        this.requestedProtocols = [];
    }

    if (request.headers['x-forwarded-for']) {
        this.remoteAddress = request.headers['x-forwarded-for'].split(', ')[0];
    }

    // Extensions are optional.
    var extensionsString = request.headers['sec-websocket-extensions'];
    this.requestedExtensions = this.parseExtensions(extensionsString);
};

WebSocketRequest.prototype.parseExtensions = function(extensionsString) {
    if (!extensionsString || extensionsString.length === 0) {
        return [];
    }
    extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
    extensions.forEach(function(extension, index, array) {
        var params = extension.split(headerParamSplitRegExp);
        var extensionName = params[0];
        var extensionParams = params.slice(1);
        extensionParams.forEach(function(rawParam, index, array) {
            var arr = rawParam.split('=');
            var obj = {
                name: arr[0],
                value: arr[1]
            };
            array.splice(index, 1, obj);
        });
        var obj = {
            name: extensionName,
            params: extensionParams
        };
        array.splice(index, 1, obj);
    });
    return extensions;
},
WebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies) {
    // TODO: Handle extensions
    var connection = new WebSocketConnection(this.socket, [], acceptedProtocol, false, this.serverConfig);

    connection.websocketVersion = this.websocketVersion;
    connection.remoteAddress = this.remoteAddress;

    // Create key validation hash
    var sha1 = crypto.createHash('sha1');
    sha1.update(this.key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
    var acceptKey = sha1.digest('base64');

    var response = "HTTP/1.1 101 Switching Protocols\r\n" +
                   "Upgrade: websocket\r\n" +
                   "Connection: Upgrade\r\n" +
                   "Sec-WebSocket-Accept: " + acceptKey + "\r\n";

    if (acceptedProtocol) {
        // validate protocol
		for (var i=0; i < acceptedProtocol.length; i++) {
			var charCode = acceptedProtocol.charCodeAt(i);
			var character = acceptedProtocol.charAt(i);
			if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) {
			    this.reject(500);
				throw new Error("Illegal character '" + String.fromCharCode(character) + "' in subprotocol.");
			}
		}
		if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) {
		    this.reject(500);
		    throw new Error("Specified protocol was not requested by the client.");
		}

        acceptedProtocol = acceptedProtocol.replace(headerSanitizeRegExp, '');
        response += "Sec-WebSocket-Protocol: " + acceptedProtocol + "\r\n";
    }
    if (allowedOrigin) {
        allowedOrigin = allowedOrigin.replace(headerSanitizeRegExp, '');
        if (this.websocketVersion === 13) {
            response += "Origin: " + allowedOrigin + "\r\n";
        }
        else if (this.websocketVersion === 8) {
            response += "Sec-WebSocket-Origin: " + allowedOrigin + "\r\n";
        }
    }

    if (cookies) {
        if (!Array.isArray(cookies)) {
            this.reject(500);
            throw new Error("Value supplied for 'cookies' argument must be an array.");
        }
        cookies.forEach(function(cookie) {
    		if (!cookie.name || !cookie.value) {
    		    this.reject(500);
    		    throw new Error("Each cookie to set must at least provide a 'name' and 'value'");
    		}

    		// token (RFC 2616, Section 2.2)
    		var invalidChar = cookie.name.match(cookieNameValidateRegEx);
    		if (invalidChar) {
    		    this.reject(500);
    		    throw new Error("Illegal character(s) " + invalidChar[0] + " in cookie name");
    		}

    		// RFC 6265, Section 4.1.1
    		// *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) | %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
    		// TODO: add double quote handling
    		invalidChar = cookie.value.match(cookieValueValidateRegEx);
    		if (invalidChar) {
    		    this.reject(500);
    		    throw new Error("Illegal character(s) " + invalidChar[0] + " in cookie value");
    		}

    		var cookieParts = [cookie.name + "=" + cookie.value];

    		if (cookie.expires) {
    		    cookieParts.push("Expires=" + cookie.expires);
    		}
    		if (cookie.maxage) {
    		    cookieParts.push("Max-Age=" + cookie.maxage.toString());
    		}
    		if (cookie.domain) {
    		    cookieParts.push("Domain=" + cookie.domain);
    		}
    		if (cookie.path) {
    		    cookieParts.push("Expires=" + cookie.path);
    		}
    		if (cookie.secure) {
    		    cookieParts.push("Secure");
    		}
    		if (cookie.httponly) {
    		    cookieParts.push("HttpOnly");
    		}

    		response += ("Set-Cookie: " + cookieParts.join(';') + "\r\n");
    	}.bind(this));
    }

    // TODO: handle negotiated extensions
    // if (negotiatedExtensions) {
    //     response += "Sec-WebSocket-Extensions: " + negotiatedExtensions.join(", ") + "\r\n";
    // }

    response += "\r\n";

    this.socket.write(response, 'ascii');

    this.emit('requestAccepted', connection);

    return connection;
};

WebSocketRequest.prototype.reject = function(status, reason, extraHeaders) {
    if (typeof(status) !== 'number') {
        status = 403;
    }
    var response = "HTTP/1.1 " + status + " " + httpStatusDescriptions[status] + "\r\n" +
                   "Connection: close\r\n";
    if (reason) {
        reason = reason.replace(headerSanitizeRegExp, '');
        response += "X-WebSocket-Reject-Reason: " + reason + "\r\n";
    }

    if (extraHeaders) {
        for (var key in extraHeaders) {
            var sanitizedValue = extraHeaders[key].toString().replace(headerSanitizeRegExp, '');
            var sanitizedKey = key.replace(headerSanitizeRegExp, '');
            response += (key + ": " + sanitizedValue + "\r\n");
        }
    }

    response += "\r\n";
    this.socket.end(response, 'ascii');

    this.emit('requestRejected', this);
};

module.exports = WebSocketRequest;<|endoftext|>


<commit_before>#!/usr/bin/env node

var greasecooper = function() {
  var privy = {};
  privy.mods = {};
  privy.mods.cp = require('child_process');
  privy.mods.url = require('url');

  privy.mods.temp = require('temp');
  privy.mods.eachline = require('eachline');
  privy.mods.nomnom = require('nomnom');

  privy.src = {};
  privy.src.bootstrap = require.resolve('./bootstrap.js');
  privy.src.meta = require.resolve('./meta.xsl');
  privy.src.gmpatch = require.resolve('./greasemonkey.diff');

  privy.defaultout = "greasebarrel.xpi";

  var self = {};

  self.now = new Date();

  self.eq = process;
  self.parser = privy.mods.nomnom();

  self.parser.option("output", {
    abbr: "o",
    default: privy.defaultout,
  });
  self.parser.option("userscript", {
    abbr: "u",
    list: true,
    default: [],
  });
  self.parser.option("scriptdir", {
    abbr: "d",
    list: true,
    default: [],
  });
  self.parser.option("cleanup", {
    abbr: "c",
    flag: true,
    default: true,
  });

  self.procs = {};

  self.eq.on('parse', function(pSpec) {
    var dSpec = Object.create(pSpec || {});

    dSpec.cmd = dSpec.cmd || process.argv.slice(2);

    dSpec.opts = self.parser.parse(dSpec.cmd);

    dSpec.opts.paths = dSpec.opts.userscript.concat(dSpec.opts.scriptdir);
    if (dSpec.opts.paths.length === 0) {
      dSpec.opts.paths = ['./scripts'];
    }

    dSpec.opts.outbase = dSpec.opts.output.split(/\//).slice(-1).join("/").split(/\./).slice(0,-1).join(".");
    dSpec.opts.scriptdir = [dSpec.opts.outbase,'scripts'].join("-");
    dSpec.opts.scriptxpi = [dSpec.opts.scriptdir,'xpi'].join(".");

    console.error("OPTS: %j", dSpec.opts);

    if (dSpec.opts.cleanup) {
      privy.mods.temp.track();
    }

    privy.mods.temp.mkdir("greasecooper", function(err, dirPath) {
      console.error("DIRPATH: %s", dirPath);
      dSpec.tmpdir = dirPath;

      self.eq.emit('prep', dSpec);
    });

  });

  self.eq.on('download_resource', function(rSpec) {
    rSpec.url = privy.mods.url.parse(rSpec.location);
    rSpec.resolved = privy.mods.url.resolve(rSpec.baseurl, rSpec.location);
    rSpec.localfile = [rSpec.tmpdir,rSpec.scriptdir,'content',rSpec.basedir,rSpec.filename].join("/");
    console.error("DOWNLOAD %j", rSpec);

    rSpec.downloads[rSpec.localfile] = rSpec.resolved;
  });

  self.eq.on('prep', function(pSpec) {
    console.time('prep');

    var rSpec = Object.create(pSpec || {});

    self.procs.prep =
      privy.mods.cp.spawn('bash', ['-'],
                          {
                          cwd: rSpec.tmpdir,
                          });

    self.procs.prep.stdout.pipe(process.stdout);
    self.procs.prep.stderr.pipe(process.stderr);

    self.procs.prep.stdout.on('end', function() {
      console.timeEnd('prep');

      self.eq.emit('readscripts', rSpec);
    });

    self.procs.prep.stdin.write(["mkdir -p",
                                 [rSpec.opts.scriptdir,
                                  "content"
                                  ].join("/"),
                                 "\n"
                                 ].join(" "));
    self.procs.prep.stdin.write(["cp",
                                 privy.src.bootstrap,
                                 [rSpec.opts.scriptdir,"bootstrap.js"].join("/"),
                                 "\n"
                                 ].join(" "));
    self.procs.prep.stdin.write(["xsltproc",
                                 "--stringparam target manifest",
                                 ["--stringparam extname",rSpec.opts.scriptdir].join(" "),
                                 privy.src.meta,
                                 privy.src.meta,
                                 ["> ",rSpec.opts.scriptdir,"/chrome.manifest"].join(""),
                                 "\n"
                                 ].join(" "));
    self.procs.prep.stdin.write(["xsltproc",
                                 "--stringparam target userrdf",
                                 ["--stringparam extname",rSpec.opts.scriptdir].join(" "),
                                 privy.src.meta,
                                 privy.src.meta,
                                 ["> ",rSpec.opts.scriptdir,"/install.rdf"].join(""),
                                 "\n"
                                 ].join(" "));
    self.procs.prep.stdin.write(["xsltproc",
                                 privy.src.meta,
                                 privy.src.meta,
                                 "> install.rdf",
                                 "\n"
                                 ].join(" "));
    self.procs.prep.stdin.write(['wget -o /tmp/greasemonkey.log -N "https://addons.mozilla.org/firefox/downloads/latest/748/addon-748-latest.xpi"',
                                 'ln -f addon-748-latest.xpi greasemonkey-orig.xpi',
                                 'mkdir greasemonkey',
                                 'cd greasemonkey',
                                 'unzip ../greasemonkey-orig.xpi',
                                 ['cat',privy.src.gmpatch,'| patch -p1'].join(" "),
                                 'zip -r ../greasemonkey.xpi *',
                                 '\n'
                                 ].join(" ; "));

    self.procs.prep.stdin.end();

  });

  self.eq.on('readscripts', function(rSpec) {
    console.time('readscripts');
    var cSpec = Object.create(rSpec || {});

    cSpec.downloads = {};

    self.procs.find =
      privy.mods.cp.spawn('bash',
                          ['-c',
                           [['find'].concat(
                                           rSpec.opts.paths).concat([
                                                   '-type', 'f',
                                                   '-name', '\\*.user.js',
                                                                     ]).join(" "),
                            'tee /tmp/greasecooper_userscripts_rel.txt',
                            'parallel realpath {}',
                            'tee /tmp/greasecooper_userscripts_abs.txt',
                            'parallel grep -H . {}',
                            'tee /tmp/greasecooper_userscripts_grep.txt',
                            ].join(" | ")]);

    //self.procs.filt = new privy.mods.luigi.filter();
    self.procs.filt = new privy.mods.eachline(function(line) {
      var that = this;
      var info = {};

      var output = [];

      info.parts = line.split(/\.user\.js:/);
      info.filepath = info.parts[0] + ".user.js";
      info.fileline = info.parts.slice(1).join('.user.js:');

      info.filename = info.filepath.split('/').pop();

      info.baseurl = ["file://",info.filepath].join("");

      if (this.meta.state === 'out') {
        if (info.fileline.match(/^\s*\/\/\s*\=\=UserScript\=\=/)) {
          this.meta.state = 'in';
          this.meta.info = {};
          this.meta.multi = {};
          this.meta.multi.grant = [];
          this.meta.multi.include = [];
          this.meta.multi.exclude = [];
          this.meta.multi.require = [];
          this.meta.multi.resource = [];
          output.push("/UserScriptConfig/Script\n");
          output.push("/UserScriptConfig/Script/@enabled=true\n");
          output.push(["/UserScriptConfig/Script/@filename=",
                             info.filename,
                             "\n"].join(""));
          output.push(["/UserScriptConfig/Script/@installTime=",
                             self.now.getTime(),
                             "\n"].join(""));
          output.push(["/UserScriptConfig/Script/@modified=",
                             self.now.getTime(),
                             "\n"].join(""));
          output.push(["/UserScriptConfig/Script/@installurl=",
                             info.baseurl,
                             "\n"].join(""));
          output.push(["/UserScriptConfig/Script/@updateurl=",
                             info.baseurl,
                             "\n"].join(""));
          output.push("/UserScriptConfig/Script/@checkRemoteUpdates=0\n");
        }
      }
      if (this.meta.state === 'in') {
        if (info.fileline.match(/^\s*\/\/\s*\@/)) {
          info.iparts = info.fileline.split(/\@/).slice(1).join("@").split(/\s+/);
          info.ikey = info.iparts[0];
          info.ivals = info.iparts.slice(1);
          info.ival = info.ivals.join(" ");

          this.meta.info[info.ikey] = info.ivals;

          if (this.meta.multi[info.ikey]) {
            this.meta.multi[info.ikey].push(info.ivals);
          }

          switch (info.ikey) {
            case "author":
            case "grant":
            case "include":
            case "exclude":
            case "require":
            case "resource":
              break;
            case "name":
              this.meta.info.basedir = info.ival.replace(/\s/g, '_');
              output.push(["/UserScriptConfig/Script/@",
                                 "basedir",
                                 '=',
                                 this.meta.info.basedir,
                                 "\n"
                                 ].join(""));
              self.eq.emit('download_resource', {
                location: info.filename,
                filename: info.filename,
                basedir: that.meta.info.basedir,
                baseurl: info.baseurl,
                downloads: cSpec.downloads,
                scriptdir: cSpec.opts.scriptdir,
                tmpdir: cSpec.tmpdir,
              });
            default:
              output.push(["/UserScriptConfig/Script/@",
                                 info.ikey,
                                 '=',
                                 info.ival,
                                 "\n"
                                 ].join(""));
          }

          if (info.ikey === 'require') {

          }
        }
        if (info.fileline.match(/^\s*\/\/\s*==\/UserScript==/)) {
          for (var multikey in this.meta.multi) {
            var eltname = [multikey.substring(0,1).toUpperCase(),
                           multikey.substring(1)].join("");

            var ix = 0;
            this.meta.multi[multikey].forEach(function(vals) {
              output.push("/UserScriptConfig/Script/!=sep\n");

              switch (multikey) {
                case "resource":
                  var rezname = vals[0];
                  output.push(["/UserScriptConfig/Script/",
                               eltname,
                               '/@name=',
                               rezname,
                               "\n"
                              ].join(""));
                case "require":
                  var filename = vals.slice(-1)[0].split(/\//).pop();
                  output.push(["/UserScriptConfig/Script/",
                               eltname,
                               '/@filename=',
                               filename,
                               "\n"
                              ].join(""));
                  self.eq.emit('download_resource', {
                    location: vals.slice(-1)[0],
                    filename: filename,
                    basedir: that.meta.info.basedir,
                    baseurl: info.baseurl,
                    downloads: cSpec.downloads,
                    scriptdir: cSpec.opts.scriptdir,
                    tmpdir: cSpec.tmpdir,
                  });
                  break;
                default:
                  output.push(["/UserScriptConfig/Script/",
                               eltname,
                               '=',
                               vals.join(""),
                               "\n"
                              ].join(""));
              }
              ix++;
            });
          }

          this.meta.state = 'out';
        }
      }

      info.meta = this.meta;
      console.error(JSON.stringify(info));

      return output.join("");
    });
    self.procs.filt.meta = {};
    self.procs.filt.meta.state = "out";

    self.procs.writexml =
      privy.mods.cp.spawn('bash', ['-c',
                                   ['tee /tmp/gc.prexml',
                                    '2xml',
                                    'xmllint --format -',
                                    'tee /tmp/gc.xml',
                                    'tee content/config.xml'
                                    ].join(" | ")], {
                          cwd: [cSpec.tmpdir, cSpec.opts.scriptdir].join("/"),
                          });

    self.procs.find.stdout.pipe(self.procs.filt);
    self.procs.find.stderr.pipe(process.stderr);

    self.procs.filt.pipe(self.procs.writexml.stdin);

    self.procs.writexml.stdout.pipe(process.stdout);
    self.procs.writexml.stderr.pipe(process.stderr);

    self.procs.writexml.stdout.on('end', function() {
      console.timeEnd('readscripts');
      self.eq.emit('downloads', cSpec);
    });
  });

  self.eq.on('downloads', function(dSpec) {
    console.time('downloads');

    var oSpec = Object.create(dSpec || {});

    console.error("DOWNLOADS: %j", oSpec.downloads);

    self.procs.downloader =
      privy.mods.cp.spawn('bash', ['-c',
                                   ['tee /tmp/downloads.txt',
                                    'parallel -n2 curl -L --create-dirs --output "{2}" "{1}"',
                                    ].join(" | ")]);

    self.procs.downloader.stdout.pipe(process.stdout);
    self.procs.downloader.stderr.pipe(process.stderr);

    self.procs.downloader.stdout.on('end', function() {
      console.timeEnd('downloads');
      self.eq.emit('package_barrel', oSpec);
    });

    for (var localfile in oSpec.downloads) {
      self.procs.downloader.stdin.write(oSpec.downloads[localfile]);
      self.procs.downloader.stdin.write("\n");

      self.procs.downloader.stdin.write(localfile);
      self.procs.downloader.stdin.write("\n");
    }
    self.procs.downloader.stdin.end();

  });

  self.eq.on('package_barrel', function(pSpec) {
    console.time('package');
    var oSpec = Object.create(pSpec || {});

    self.procs.bpack =
      privy.mods.cp.spawn('bash', ['-c',
                                   [['( cd',
                                     pSpec.opts.scriptdir,
                                     ';',
                                     'ls -d *',
                                     '|',
                                     'parallel -n999 jar cvf',
                                     ['..',pSpec.opts.scriptxpi].join("/"),
                                     ')'
                                     ].join(" "),
                                    ['jar cvf ',
                                     [process.cwd(),pSpec.opts.output].join("/"),
                                     ' greasemonkey.xpi ',
                                     pSpec.opts.scriptxpi,
                                     'install.rdf'
                                     ].join(" "),
                                    ].join(" ; ")], {
                          cwd: pSpec.tmpdir
                              });

    self.procs.bpack.stdout.pipe(process.stdout);
    self.procs.bpack.stderr.pipe(process.stderr);

    self.procs.bpack.stdout.on('end', function() {
      console.timeEnd('package');
    });
  });

  return self;
}();

greasecooper.eq.emit('parse');

<commit_msg>better handling of relative output path<commit_after>#!/usr/bin/env node

var greasecooper = function() {
  var privy = {};
  privy.mods = {};
  privy.mods.cp = require('child_process');
  privy.mods.url = require('url');

  privy.mods.temp = require('temp');
  privy.mods.eachline = require('eachline');
  privy.mods.nomnom = require('nomnom');

  privy.src = {};
  privy.src.bootstrap = require.resolve('./bootstrap.js');
  privy.src.meta = require.resolve('./meta.xsl');
  privy.src.gmpatch = require.resolve('./greasemonkey.diff');

  privy.defaultout = "greasebarrel.xpi";

  var self = {};

  self.now = new Date();

  self.eq = process;
  self.parser = privy.mods.nomnom();

  self.parser.option("output", {
    abbr: "o",
    default: privy.defaultout,
  });
  self.parser.option("userscript", {
    abbr: "u",
    list: true,
    default: [],
  });
  self.parser.option("scriptdir", {
    abbr: "d",
    list: true,
    default: [],
  });
  self.parser.option("cleanup", {
    abbr: "c",
    flag: true,
    default: true,
  });

  self.procs = {};

  self.eq.on('parse', function(pSpec) {
    var dSpec = Object.create(pSpec || {});

    dSpec.cmd = dSpec.cmd || process.argv.slice(2);

    dSpec.opts = self.parser.parse(dSpec.cmd);

    dSpec.opts.paths = dSpec.opts.userscript.concat(dSpec.opts.scriptdir);
    if (dSpec.opts.paths.length === 0) {
      dSpec.opts.paths = ['./scripts'];
    }

    dSpec.opts.outbase = dSpec.opts.output.split(/\//).slice(-1).join("/").split(/\./).slice(0,-1).join(".");
    dSpec.opts.scriptdir = [dSpec.opts.outbase,'scripts'].join("-");
    dSpec.opts.scriptxpi = [dSpec.opts.scriptdir,'xpi'].join(".");

    console.error("OPTS: %j", dSpec.opts);

    if (dSpec.opts.cleanup) {
      privy.mods.temp.track();
    }

    privy.mods.temp.mkdir("greasecooper", function(err, dirPath) {
      console.error("DIRPATH: %s", dirPath);
      dSpec.tmpdir = dirPath;

      self.eq.emit('prep', dSpec);
    });

  });

  self.eq.on('download_resource', function(rSpec) {
    rSpec.url = privy.mods.url.parse(rSpec.location);
    rSpec.resolved = privy.mods.url.resolve(rSpec.baseurl, rSpec.location);
    rSpec.localfile = [rSpec.tmpdir,rSpec.scriptdir,'content',rSpec.basedir,rSpec.filename].join("/");
    console.error("DOWNLOAD %j", rSpec);

    rSpec.downloads[rSpec.localfile] = rSpec.resolved;
  });

  self.eq.on('prep', function(pSpec) {
    console.time('prep');

    var rSpec = Object.create(pSpec || {});

    self.procs.prep =
      privy.mods.cp.spawn('bash', ['-'],
                          {
                          cwd: rSpec.tmpdir,
                          });

    self.procs.prep.stdout.pipe(process.stdout);
    self.procs.prep.stderr.pipe(process.stderr);

    self.procs.prep.stdout.on('end', function() {
      console.timeEnd('prep');

      self.eq.emit('readscripts', rSpec);
    });

    self.procs.prep.stdin.write(["mkdir -p",
                                 [rSpec.opts.scriptdir,
                                  "content"
                                  ].join("/"),
                                 "\n"
                                 ].join(" "));
    self.procs.prep.stdin.write(["cp",
                                 privy.src.bootstrap,
                                 [rSpec.opts.scriptdir,"bootstrap.js"].join("/"),
                                 "\n"
                                 ].join(" "));
    self.procs.prep.stdin.write(["xsltproc",
                                 "--stringparam target manifest",
                                 ["--stringparam extname",rSpec.opts.scriptdir].join(" "),
                                 privy.src.meta,
                                 privy.src.meta,
                                 ["> ",rSpec.opts.scriptdir,"/chrome.manifest"].join(""),
                                 "\n"
                                 ].join(" "));
    self.procs.prep.stdin.write(["xsltproc",
                                 "--stringparam target userrdf",
                                 ["--stringparam extname",rSpec.opts.scriptdir].join(" "),
                                 privy.src.meta,
                                 privy.src.meta,
                                 ["> ",rSpec.opts.scriptdir,"/install.rdf"].join(""),
                                 "\n"
                                 ].join(" "));
    self.procs.prep.stdin.write(["xsltproc",
                                 privy.src.meta,
                                 privy.src.meta,
                                 "> install.rdf",
                                 "\n"
                                 ].join(" "));
    self.procs.prep.stdin.write(['wget -o /tmp/greasemonkey.log -N "https://addons.mozilla.org/firefox/downloads/latest/748/addon-748-latest.xpi"',
                                 'ln -f addon-748-latest.xpi greasemonkey-orig.xpi',
                                 'mkdir greasemonkey',
                                 'cd greasemonkey',
                                 'unzip ../greasemonkey-orig.xpi',
                                 ['cat',privy.src.gmpatch,'| patch -p1'].join(" "),
                                 'zip -r ../greasemonkey.xpi *',
                                 '\n'
                                 ].join(" ; "));

    self.procs.prep.stdin.end();

  });

  self.eq.on('readscripts', function(rSpec) {
    console.time('readscripts');
    var cSpec = Object.create(rSpec || {});

    cSpec.downloads = {};

    self.procs.find =
      privy.mods.cp.spawn('bash',
                          ['-c',
                           [['find'].concat(
                                           rSpec.opts.paths).concat([
                                                   '-type', 'f',
                                                   '-name', '\\*.user.js',
                                                                     ]).join(" "),
                            'tee /tmp/greasecooper_userscripts_rel.txt',
                            'parallel realpath {}',
                            'tee /tmp/greasecooper_userscripts_abs.txt',
                            'parallel grep -H . {}',
                            'tee /tmp/greasecooper_userscripts_grep.txt',
                            ].join(" | ")]);

    //self.procs.filt = new privy.mods.luigi.filter();
    self.procs.filt = new privy.mods.eachline(function(line) {
      var that = this;
      var info = {};

      var output = [];

      info.parts = line.split(/\.user\.js:/);
      info.filepath = info.parts[0] + ".user.js";
      info.fileline = info.parts.slice(1).join('.user.js:');

      info.filename = info.filepath.split('/').pop();

      info.baseurl = ["file://",info.filepath].join("");

      if (this.meta.state === 'out') {
        if (info.fileline.match(/^\s*\/\/\s*\=\=UserScript\=\=/)) {
          this.meta.state = 'in';
          this.meta.info = {};
          this.meta.multi = {};
          this.meta.multi.grant = [];
          this.meta.multi.include = [];
          this.meta.multi.exclude = [];
          this.meta.multi.require = [];
          this.meta.multi.resource = [];
          output.push("/UserScriptConfig/Script\n");
          output.push("/UserScriptConfig/Script/@enabled=true\n");
          output.push(["/UserScriptConfig/Script/@filename=",
                             info.filename,
                             "\n"].join(""));
          output.push(["/UserScriptConfig/Script/@installTime=",
                             self.now.getTime(),
                             "\n"].join(""));
          output.push(["/UserScriptConfig/Script/@modified=",
                             self.now.getTime(),
                             "\n"].join(""));
          output.push(["/UserScriptConfig/Script/@installurl=",
                             info.baseurl,
                             "\n"].join(""));
          output.push(["/UserScriptConfig/Script/@updateurl=",
                             info.baseurl,
                             "\n"].join(""));
          output.push("/UserScriptConfig/Script/@checkRemoteUpdates=0\n");
        }
      }
      if (this.meta.state === 'in') {
        if (info.fileline.match(/^\s*\/\/\s*\@/)) {
          info.iparts = info.fileline.split(/\@/).slice(1).join("@").split(/\s+/);
          info.ikey = info.iparts[0];
          info.ivals = info.iparts.slice(1);
          info.ival = info.ivals.join(" ");

          this.meta.info[info.ikey] = info.ivals;

          if (this.meta.multi[info.ikey]) {
            this.meta.multi[info.ikey].push(info.ivals);
          }

          switch (info.ikey) {
            case "author":
            case "grant":
            case "include":
            case "exclude":
            case "require":
            case "resource":
              break;
            case "name":
              this.meta.info.basedir = info.ival.replace(/\s/g, '_');
              output.push(["/UserScriptConfig/Script/@",
                                 "basedir",
                                 '=',
                                 this.meta.info.basedir,
                                 "\n"
                                 ].join(""));
              self.eq.emit('download_resource', {
                location: info.filename,
                filename: info.filename,
                basedir: that.meta.info.basedir,
                baseurl: info.baseurl,
                downloads: cSpec.downloads,
                scriptdir: cSpec.opts.scriptdir,
                tmpdir: cSpec.tmpdir,
              });
            default:
              output.push(["/UserScriptConfig/Script/@",
                                 info.ikey,
                                 '=',
                                 info.ival,
                                 "\n"
                                 ].join(""));
          }

          if (info.ikey === 'require') {

          }
        }
        if (info.fileline.match(/^\s*\/\/\s*==\/UserScript==/)) {
          for (var multikey in this.meta.multi) {
            var eltname = [multikey.substring(0,1).toUpperCase(),
                           multikey.substring(1)].join("");

            var ix = 0;
            this.meta.multi[multikey].forEach(function(vals) {
              output.push("/UserScriptConfig/Script/!=sep\n");

              switch (multikey) {
                case "resource":
                  var rezname = vals[0];
                  output.push(["/UserScriptConfig/Script/",
                               eltname,
                               '/@name=',
                               rezname,
                               "\n"
                              ].join(""));
                case "require":
                  var filename = vals.slice(-1)[0].split(/\//).pop();
                  output.push(["/UserScriptConfig/Script/",
                               eltname,
                               '/@filename=',
                               filename,
                               "\n"
                              ].join(""));
                  self.eq.emit('download_resource', {
                    location: vals.slice(-1)[0],
                    filename: filename,
                    basedir: that.meta.info.basedir,
                    baseurl: info.baseurl,
                    downloads: cSpec.downloads,
                    scriptdir: cSpec.opts.scriptdir,
                    tmpdir: cSpec.tmpdir,
                  });
                  break;
                default:
                  output.push(["/UserScriptConfig/Script/",
                               eltname,
                               '=',
                               vals.join(""),
                               "\n"
                              ].join(""));
              }
              ix++;
            });
          }

          this.meta.state = 'out';
        }
      }

      info.meta = this.meta;
      console.error(JSON.stringify(info));

      return output.join("");
    });
    self.procs.filt.meta = {};
    self.procs.filt.meta.state = "out";

    self.procs.writexml =
      privy.mods.cp.spawn('bash', ['-c',
                                   ['tee /tmp/gc.prexml',
                                    '2xml',
                                    'xmllint --format -',
                                    'tee /tmp/gc.xml',
                                    'tee content/config.xml'
                                    ].join(" | ")], {
                          cwd: [cSpec.tmpdir, cSpec.opts.scriptdir].join("/"),
                          });

    self.procs.find.stdout.pipe(self.procs.filt);
    self.procs.find.stderr.pipe(process.stderr);

    self.procs.filt.pipe(self.procs.writexml.stdin);

    self.procs.writexml.stdout.pipe(process.stdout);
    self.procs.writexml.stderr.pipe(process.stderr);

    self.procs.writexml.stdout.on('end', function() {
      console.timeEnd('readscripts');
      self.eq.emit('downloads', cSpec);
    });
  });

  self.eq.on('downloads', function(dSpec) {
    console.time('downloads');

    var oSpec = Object.create(dSpec || {});

    console.error("DOWNLOADS: %j", oSpec.downloads);

    self.procs.downloader =
      privy.mods.cp.spawn('bash', ['-c',
                                   ['tee /tmp/downloads.txt',
                                    'parallel -n2 curl -L --create-dirs --output "{2}" "{1}"',
                                    ].join(" | ")]);

    self.procs.downloader.stdout.pipe(process.stdout);
    self.procs.downloader.stderr.pipe(process.stderr);

    self.procs.downloader.stdout.on('end', function() {
      console.timeEnd('downloads');
      self.eq.emit('package_barrel', oSpec);
    });

    for (var localfile in oSpec.downloads) {
      self.procs.downloader.stdin.write(oSpec.downloads[localfile]);
      self.procs.downloader.stdin.write("\n");

      self.procs.downloader.stdin.write(localfile);
      self.procs.downloader.stdin.write("\n");
    }
    self.procs.downloader.stdin.end();

  });

  self.eq.on('package_barrel', function(pSpec) {
    console.time('package');
    var oSpec = Object.create(pSpec || {});

    self.procs.bpack =
      privy.mods.cp.spawn('bash', ['-c',
                                   [['( cd',
                                     pSpec.opts.scriptdir,
                                     ';',
                                     'ls -d *',
                                     '|',
                                     'parallel -n999 jar cvf',
                                     ['..',pSpec.opts.scriptxpi].join("/"),
                                     ')'
                                     ].join(" "),
                                    ['( cd', process.cwd(), ';', 'realpath', pSpec.opts.output, ')', '|',
                                     'parallel jar cvf {}',
                                     ' greasemonkey.xpi ',
                                     pSpec.opts.scriptxpi,
                                     'install.rdf'
                                     ].join(" "),
                                    ].join(" ; ")], {
				      cwd: pSpec.tmpdir
				    });

    self.procs.bpack.stdout.pipe(process.stdout);
    self.procs.bpack.stderr.pipe(process.stderr);

    self.procs.bpack.stdout.on('end', function() {
      console.timeEnd('package');
    });
  });

  return self;
}();

greasecooper.eq.emit('parse');

<|endoftext|>


<commit_before>import FilterModule from "../FilterModule";
import { dependencies as Inject, singleton as Singleton } from "needlepoint";
import ChatApi from "../../util/ChatApi";
import EmojifyCommand from "../commands/EmojifyCommand";

const emojifiablePattern = /\b((?:[A-Z]\s){2,}[A-Z])\b/;

@Singleton
@Inject(ChatApi)
export default class AutoEmojifyFilter extends FilterModule {
    constructor(api) {
        super();

        this.api = api;
    }

    filter(msg) {
        if (AutoEmojifyFilter.shouldEmojify(msg.body)) {
            const response = msg.body.replace(emojifiablePattern, match => EmojifyCommand.emojify(match));

            this.api.sendMessage(response, msg.threadID);
        }

        return msg;
    }

    static shouldEmojify(text) {
        return text.match(emojifiablePattern) !== null;
    }
}<commit_msg>AutoEmojifyFilter: Fix errors on empty body<commit_after>import FilterModule from "../FilterModule";
import { dependencies as Inject, singleton as Singleton } from "needlepoint";
import ChatApi from "../../util/ChatApi";
import EmojifyCommand from "../commands/EmojifyCommand";

const emojifiablePattern = /\b((?:[A-Z]\s){2,}[A-Z])\b/;

@Singleton
@Inject(ChatApi)
export default class AutoEmojifyFilter extends FilterModule {
    constructor(api) {
        super();

        this.api = api;
    }

    filter(msg) {
        if (!msg.body) {
            return msg;
        }

        if (AutoEmojifyFilter.shouldEmojify(msg.body)) {
            const response = msg.body.replace(emojifiablePattern, match => EmojifyCommand.emojify(match));

            this.api.sendMessage(response, msg.threadID);
        }

        return msg;
    }

    static shouldEmojify(text) {
        return text.match(emojifiablePattern) !== null;
    }
}<|endoftext|>


<commit_before>function getCurrentWeather() {
	var timer = new interval(10 * 60000, aux_getCurrentWeather);
	timer.run();
}

function aux_getCurrentWeather() {
	$.getJSON({
		//url: "http://api.openweathermap.org/data/2.5/weather",
		url: "http://localhost/MagicMirror/test/weather.json",
		data: {
			id: weather.cityID,
			lang: locale,
			units: "metric",
			appid: apiKey.openWeatherMap
		},
		success: function (response) {
			var weatherID = response.weather[0].id;
			var iconHtml = "";
			var temp = response.main.temp.toFixed(1) + " °C";

			$(".currentWeatherIcon").html(iconHtml);
			$(".currentWeatherTemp").html(temp);
		}
	});
}

function getWeatherForecast() {
	//var timer = new interval(20 * 60000, aux_getWeatherForecast);
	var timer = new interval(10 * 1000, aux_getWeatherForecast);
	timer.run();
}

function aux_getWeatherForecast() {
	$.getJSON({
		//url: "http://api.openweathermap.org/data/2.5/forecast",
		url: "http://localhost/MagicMirror/test/forecast.json",
		data: {
			id: weather.cityID,
			lang: locale,
			units: "metric",
			appid: apiKey.openWeatherMap
		},
		success: function (response) {
			var forecastsPerDay = extractForecasts(response.list);
			writeForecastsToHtml(forecastsPerDay);
		}
	});

	function extractForecasts(list) {
		var days = {};
		var dayCounter = 0;
		for (let key in list) {
			let singleForecast = list[key];
			let main = singleForecast.main;
			let id = singleForecast.weather[0].id;
			let date = singleForecast.dt_txt.substring(0, 10);

			if (days[date] === undefined) {
				days[date] = {
					"icons": {},
					"min": main.temp_min,
					"max": main.temp_max,
					"counter": dayCounter
				};
				days[date].icons[id] = 1;
				++dayCounter;
			}
			else { //current date already in days
				if (days[date].icons[id] === undefined) {
					days[date].icons[id] = 1;
				}
				else { //id already in icons
					++days[date].icons[id];
				}

				if (main.temp_min < days[date].min) {
					days[date].min = main.temp_min;
				}
				if (main.temp_max > days[date].max) {
					days[date].max = main.temp_max;
				}
			}
		}
		//days now contains an object mapping the icon id to its count, mintemp, maxtemp per day
		for (let key in days) {
			days[key].icons = getMostCommonIconPerDay(days[key]);
		}
		return days;
	}

	//if two have the same number of ocurrences, one is returned at random
	function getMostCommonIconPerDay(day) {
		var sortedDay = sortObjectDescToArray(day.icons);
		return sortedDay[0][0];
		//return (sortedDay[0][1] == sortedDay[1][1]) ? undefined : sortedDay[0][0];
	}

	function sortObjectDescToArray(obj) {
		var sortable = [];
		for (let key in obj) {
			sortable.push([key, obj[key]]);
		}
		sortable.sort(function (a, b) { return b[1] - a[1] });
		return sortable;
	}

	function writeForecastsToHtml(days) {
		var opacity = 1.0;
		$(".forecast").each(function (i) {
			var key = getKeyByCount(days, i);
			var forecastHtml = getForecastHtml(days, key);

			$(this).delay(i * weather.fadeDuration);
			$(this).fadeTo(weather.fadeDuration, 0, "linear", function () {
				$(this).html(forecastHtml);
			});
			$(this).fadeTo(weather.fadeDuration, opacity, "linear");
			opacity -= 0.1;
		});
	}

	function getKeyByCount(days, count) {
		for (let key in days) {
			if (days[key].counter === count) {
				return key;
			}
		}
		return undefined;
	}

	function getForecastHtml(days, key) {
		var day = days[key];
		var dayName = moment(key).format("dd");
		var dayHtml = "" + dayName + "	";
		var iconHtml = "	";
		var maxHtml = "" + putMinusIfNegative(day.max.toFixed(1));
		var minHtml = "" + putMinusIfNegative(day.min.toFixed(1));
		if (weather.showCelciusInForecast) {
			maxHtml += " °C";
			minHtml += " °C";
		}
		return dayHtml + iconHtml + maxHtml + "	" + minHtml + "	";
	}

	function putMinusIfNegative(temp) {
		if (temp < 0.0) {
			return "−" + -(temp);
		}
		return temp;
	}
}
<commit_msg>skip weather icons at night<commit_after>function getCurrentWeather() {
	var timer = new interval(10 * 60000, aux_getCurrentWeather);
	timer.run();
}

function aux_getCurrentWeather() {
	$.getJSON({
		//url: "http://api.openweathermap.org/data/2.5/weather",
		url: "http://localhost/MagicMirror/test/weather.json",
		data: {
			id: weather.cityID,
			lang: locale,
			units: "metric",
			appid: apiKey.openWeatherMap
		},
		success: function (response) {
			var weatherID = response.weather[0].id;
			var iconHtml = "";
			var temp = response.main.temp.toFixed(1) + " °C";

			$(".currentWeatherIcon").html(iconHtml);
			$(".currentWeatherTemp").html(temp);
		}
	});
}

function getWeatherForecast() {
	//var timer = new interval(20 * 60000, aux_getWeatherForecast);
	var timer = new interval(10 * 1000, aux_getWeatherForecast);
	timer.run();
}

function aux_getWeatherForecast() {
	$.getJSON({
		//url: "http://api.openweathermap.org/data/2.5/forecast",
		url: "http://localhost/MagicMirror/test/forecast.json",
		data: {
			id: weather.cityID,
			lang: locale,
			units: "metric",
			appid: apiKey.openWeatherMap
		},
		success: function (response) {
			var forecastsPerDay = extractForecasts(response.list);
			writeForecastsToHtml(forecastsPerDay);
		}
	});

	function extractForecasts(list) {
		var days = {};
		var dayCounter = 0;
		for (let key in list) {
			let singleForecast = list[key];
			let main = singleForecast.main;
			let id = singleForecast.weather[0].id;
			let date = singleForecast.dt_txt.substring(0, 10);
			let hour = singleForecast.dt_txt.substring(12, 13);
			let isDay = (hour === "00" || hour === "03") ? false : true; // skip weather icons at night

			if (days[date] === undefined) {
				days[date] = {
					"icons": {},
					"min": main.temp_min,
					"max": main.temp_max,
					"counter": dayCounter
				};
				if (isDay) {
					days[date].icons[id] = 1;
				}

				++dayCounter;
			}
			else { //current date already in days
				if (isDay) {
					if (days[date].icons[id] === undefined) {
						days[date].icons[id] = 1;
					}
					else { //id already in icons
						++days[date].icons[id];
					}
				}

				if (main.temp_min < days[date].min) {
					days[date].min = main.temp_min;
				}
				if (main.temp_max > days[date].max) {
					days[date].max = main.temp_max;
				}
			}
		}
		//days now contains an object mapping the icon id to its count, mintemp, maxtemp per day
		for (let key in days) {
			days[key].icons = getMostCommonIconPerDay(days[key]);
		}
		return days;
	}

	//if two have the same number of ocurrences, one is returned at random
	function getMostCommonIconPerDay(day) {
		var sortedDay = sortObjectDescToArray(day.icons);
		return sortedDay[0][0];
		//return (sortedDay[0][1] == sortedDay[1][1]) ? undefined : sortedDay[0][0];
	}

	function sortObjectDescToArray(obj) {
		var sortable = [];
		for (let key in obj) {
			sortable.push([key, obj[key]]);
		}
		sortable.sort(function (a, b) { return b[1] - a[1] });
		return sortable;
	}

	function writeForecastsToHtml(days) {
		var opacity = 1.0;
		$(".forecast").each(function (i) {
			var key = getKeyByCount(days, i);
			var forecastHtml = getForecastHtml(days, key);

			$(this).delay(i * weather.fadeDuration);
			$(this).fadeTo(weather.fadeDuration, 0, "linear", function () {
				$(this).html(forecastHtml);
			});
			$(this).fadeTo(weather.fadeDuration, opacity, "linear");
			opacity -= 0.1;
		});
	}

	function getKeyByCount(days, count) {
		for (let key in days) {
			if (days[key].counter === count) {
				return key;
			}
		}
		return undefined;
	}

	function getForecastHtml(days, key) {
		var day = days[key];
		var dayName = moment(key).format("dd");
		var dayHtml = "" + dayName + "	";
		var iconHtml = "	";
		var maxHtml = "" + putMinusIfNegative(day.max.toFixed(1));
		var minHtml = "" + putMinusIfNegative(day.min.toFixed(1));
		if (weather.showCelciusInForecast) {
			maxHtml += " °C";
			minHtml += " °C";
		}
		return dayHtml + iconHtml + maxHtml + "	" + minHtml + "	";
	}

	function putMinusIfNegative(temp) {
		if (temp < 0.0) {
			return "−" + -(temp);
		}
		return temp;
	}
}
<|endoftext|>


<commit_before>var Bullet = require('./bullet');

var Tank = function(gameSize) {
  this.size = { x: 222, y: 100};
  this.x = (gameSize.x - this.size.x)/2;
  this.y = gameSize.y - this.size.y;
  this.image = new Image()
  this.image.src = "/assets/images/tank.png"
};

Tank.prototype.update = function (game) {
  window.onkeydown = function(e) {
    if (e.keyCode === 37){
      this.moveLeft();
    } else if (e.keyCode === 39) {
      this.moveRight();
    } else if (e.keyCode === 32) {
      this.fire(game)
    }
  }.bind(this);
}

Tank.prototype.moveRight = function () {
  this.x+=10;
};

Tank.prototype.moveLeft = function () {
  this.x-=10;
};

Tank.prototype.fire = function (game) {
  var bullet = new Bullet(this.x + (this.size.x / 2),
                          this.y - 10,
                          -3,
                          game)
}

Tank.prototype.draw = function (context) {
  context.drawImage(this.image, this.x, this.y)
}
module.exports = Tank;
<commit_msg>tank now hitten at correct time<commit_after>var Bullet = require('./bullet');

var Tank = function(gameSize) {
  this.size = { x: 222, y: 50};
  this.x = (gameSize.x - this.size.x)/2;
  this.y = gameSize.y - this.size.y;
  this.image = new Image()
  this.image.src = "/assets/images/tank.png"
};

Tank.prototype.update = function (game) {
  window.onkeydown = function(e) {
    if (e.keyCode === 37){
      this.moveLeft();
    } else if (e.keyCode === 39) {
      this.moveRight();
    } else if (e.keyCode === 32) {
      this.fire(game)
    }
  }.bind(this);
}

Tank.prototype.moveRight = function () {
  this.x+=10;
};

Tank.prototype.moveLeft = function () {
  this.x-=10;
};

Tank.prototype.fire = function (game) {
  var bullet = new Bullet(this.x + (this.size.x / 2),
                          this.y - 10,
                          -3,
                          game)
}

Tank.prototype.draw = function (context) {
  context.drawImage(this.image, this.x, this.y-this.size.y)
}
module.exports = Tank;
<|endoftext|>


<commit_before>var async = require('async'),
  fs = require('fs'),
  p = require('path');

/**
 * Load Bob task files in parallel.
 * Only task files in specified taskNames will be loaded.
 *
 * @param {Array} taskNames: an array of task names
 * @param {String} dir: base directory where task files are located
 * @param {Function} cb: standard cb(err, result) callback
 */
function load(taskNames, dir, cb) {

  var jobs = {};

  taskNames.forEach(function (taskName) {
    jobs[taskName] = function (cb) {
      var file = p.join(dir, taskName + '.json');
      fs.readFile(file, cb);
    };
  });

  function parse(err, results) {
    if (!err) {
      Object.keys(results).forEach(function (taskName) {
        results[taskName] = JSON.parse(results[taskName]);
      });
    }
    cb(err, results);
  }

  async.parallel(jobs, parse);
}

exports.load = load;<commit_msg>Handle unknown command due to inexisting task file.<commit_after>var async = require('async'),
  fs = require('fs'),
  p = require('path'),
  util = require('util');

/**
 * Load Bob task files in parallel.
 * Only task files in specified taskNames will be loaded.
 *
 * @param {Array} taskNames: an array of task names
 * @param {String} dir: base directory where task files are located
 * @param {Function} cb: standard cb(err, result) callback
 */
function load(taskNames, dir, cb) {

  var jobs = {};

  taskNames.forEach(function (taskName) {
    jobs[taskName] = function (cb) {
      var file = p.join(dir, taskName + '.json');
      fs.exists(file, function (exists) {
        if (!exists) {
          var err = new Error(util.format('Unknown command: %s, use --help for more info', taskName));
          cb(err);
        } else {
          fs.readFile(file, cb);
        }
      });
    };
  });

  function parse(err, results) {
    if (!err) {
      Object.keys(results).forEach(function (taskName) {
        results[taskName] = JSON.parse(results[taskName]);
      });
    }
    cb(err, results);
  }

  async.parallel(jobs, parse);
}

exports.load = load;<|endoftext|>


<commit_before>"use strict"

var VolTextObject
var map, annotationLayer
var programmaticMapChange = false

var annotationChanges = 0
var selvol = "0"
var annotationClassesAndAppliers
var keyCodeActions

var recentLocations = []
var recentLocationsMaxLength = 10

var lastClickedElement

$(document).ready(function() {
    // This handles selection in dataTable
    var table = $('#vol_table').DataTable();

    $('#vol_table tbody').on( 'click', 'tr', function () {
        if ( $(this).hasClass('selected') ) {
            $(this).removeClass('selected');
        }
        else {
            table.$('tr.selected').removeClass('selected');
            $(this).addClass('selected');
        }
    } );

    $('#button').click( function () {
        table.row('.selected').remove().draw( false );
    } );

    $("#col2text").on("cut copy paste", function(e) {
        e.preventDefault()
    })
    // Prevent changes in a content-editable div
    $("#col2text").on("keydown", function(e) {
        e = e || window.event;
        // Allow arrow keys, home, end, pgup, pgdn
        if (e.keyCode < 33 || e.keyCode > 40)
            e.preventDefault()
        keyCodeActions.forEach(function(action) {
            if (e.keyCode == action.code)
                action.action()
        })
    })
} );

function getSelectionNodes() {
    var selectionRange = getSelectionRange()
    return getRangeNodes(selectionRange, annotationClasses)
}

function setSelectionToNode(node) {
    var range = rangy.createRange()
    range.selectNodeContents(node)
    var sel = rangy.getSelection()
    sel.setSingleRange(range)
}

var DeleteFeature = OpenLayers.Class(OpenLayers.Control, {
    initialize: function(layer, options) {
        OpenLayers.Control.prototype.initialize.apply(this, [options]);
        this.layer = layer;
        this.handler = new OpenLayers.Handler.Feature(
            this, layer, {click: this.clickFeature}
        );
    },
    clickFeature: function(feature) {
        // if feature doesn't have a fid, destroy it
        if(feature.fid == undefined) {
            this.layer.destroyFeatures([feature]);
        } else {
            feature.state = OpenLayers.State.DELETE;
            this.layer.events.triggerEvent("afterfeaturemodified",
                                           {feature: feature});
            feature.renderIntent = "select";
            this.layer.drawFeature(feature);
        }
    },
    setMap: function(map) {
        this.handler.setMap(map);
        OpenLayers.Control.prototype.setMap.apply(this, arguments);
    },
    CLASS_NAME: "OpenLayers.Control.DeleteFeature"
});

function getMapFeatures() {
    //var mLayers = map.layers;
    // var annLayer = map.getLayersByName("Annotations")
    var annLayer = annotationLayer

    var geoJSON = new OpenLayers.Format.GeoJSON()

    var jsonfeatsarr = annLayer.features.map(function(feature) {
        var layer_geom = feature.geometry.transform("EPSG:900913", "EPSG:4326")
        var geoJSONText = geoJSON.write(layer_geom)
        return geoJSONText
    })
    var jsonfeats = jsonfeatsarr.join("@@")
    // window.alert(jsonfeats)
    return jsonfeats
}

function jsonToMapFeatures(jsonstr) {
    var geoJSON = new OpenLayers.Format.GeoJSON()

    var jsonfeats = jsonstr.split("@@")
    // debugger
    var feats = jsonfeats.map(function(jsonfeat) {
        var geom = geoJSON.read(jsonfeat, "Geometry")
        var transformedGeom = geom.transform("EPSG:4326", "EPSG:900913")
        return new OpenLayers.Feature.Vector(transformedGeom)
    })
    return feats
}

function getStoredMapFeatures(node) {
    return $.data(node, "features")
}

function setStoredMapFeatures(node, feats) {
    $.data(node, "features", feats)
}

// Store the specified GeoJSON map features in the ranges associated with the selection.
// Return the range nodes in the selection.
function addMapFeaturesToSelection(jsonfeats) {
    var rangenodes = getSelectionNodes()
    console.log(rangenodes)
    rangenodes.forEach(function(node) {
        setStoredMapFeatures(node, jsonfeats)
    })
    return rangenodes
}

function doProgrammaticMapChange(cb) {
    try {
        programmaticMapChange = true
        cb()
    } finally {
        programmaticMapChange = false
    }
}

function destroyMapFeatures() {
    doProgrammaticMapChange(function() { annotationLayer.destroyFeatures() })
}

function addMapFeatures(feats) {
    doProgrammaticMapChange(function() { annotationLayer.addFeatures(feats) })
}

function displayMapFeatures(jsonfeats) {
    destroyMapFeatures()
    if (jsonfeats)
        addMapFeatures(jsonToMapFeatures(jsonfeats))
}

function populateRecentLocations() {
    var htmlarr = []
    for (var i = recentLocations.length - 1; i >= 0; i--) {
        var recentLoc = recentLocations[i]
        var html = '
' + '⒳	<input value="' +
                     recentLoc.html + '" onchange="locChange(event)" data-index="' + i +
                     '">	Location
'
        console.log(html)
        htmlarr.push(html)
    }
    $('#recentlocs').html(htmlarr.join("\n"))
}

/* Add an element to the recentLocations list with the specified HTML and
 * GeoJSON features. */
function addToRecentLocations(html, jsonfeats) {
    // If the HTML text isn't in the list, add an element to the end.
    // Else replace the current element.
    var curIndex = recentLocations.map(function(obj) { return obj.html }).indexOf(html)
    if (curIndex === -1) {
        if (recentLocations.length >= recentLocationsMaxLength)
            recentLocations = recentLocations.slice(1)
        recentLocations.push({html: html, jsonfeats: jsonfeats})
    } else {
        recentLocations[curIndex] = {html: html, jsonfeats: jsonfeats}
    }
    populateRecentLocations()
}

function locChange(e) {
    console.log(e.target.value)
    recentLocations[$(e.target).attr('data-index')].html = e.target.value
}

function locClick(e) {
    var jsonfeats = unescape($(e.target).attr('data-jsonfeats'))
    console.log(jsonfeats)
    displayMapFeatures(jsonfeats)
    if (lastClickedElement)
        setSelectionToNode(lastClickedElement)
    addMapFeaturesToSelection(jsonfeats)
}

function zoomFeatures() {
    var bounds = annotationLayer.getDataExtent()
    //var bounds = annotationLayer.getDataExtent().transform("EPSG:4326", "EPSG:900913")
    console.log(bounds)
    map.zoomToExtent(bounds)
    //polyboundRefresh.refresh();
}

// Save annotations in a serialized format.
function saveVolumeAnnotations() {
    // Fetch annotations
    var annotations = getAnnotations(annotationClasses)
    // Convert to an array of serialized annotations in the form "CLASS$START$END".
    var serialAnnotations = annotations.map(function(ann) {
        var jsonmapfeats = getStoredMapFeatures(ann.node) || ""
        return ann.node.className + "$" + ann.start + "$" + ann.end + "$" + jsonmapfeats
    })
    // Join to a single serialized string
    var serialString = serialAnnotations.join("|")
    var base64 = utf8ToB64(serialString);
    var parse_file = new Parse.File((annotUser + "-" + selvol +".txt"), { base64: base64 });
    // Save to Parse. First look for an existing entry for the user and volume.
    // If found, update it. Else create a new entry.
    var query = new Parse.Query(SpansObject)
    query.equalTo("user", annotUser)
    query.equalTo("vol", selvol)
    query.first().then(function(existing) {
        if (existing) {
            existing.set("spans", parse_file)
            return existing.save()
        } else {
            var spansObject = new SpansObject()
            return spansObject.save({"user":annotUser, "vol":selvol, "spans":parse_file})
        }
    }, savefailure("finding existing entry")
    ).then(savesuccess(function() {
        annotationChanges = 0
        logMessage("Saved " + annotations.length + " annotations")
    }),
        savefailure("saving new or updating existing entry"))
}

// Called from HTML. Save annotations. If saved successfully, reset list of
// article changes.
function saveAnnotations() {
    if (annotUser != "Default") {
        saveVolumeAnnotations()
    } else {
        logMessage("Please select a non-default Annotator Name prior prior to saving")
    }
}

// Load volume annotations
function loadVolumeAnnotations(results) {
    var textDivNode = getTextNode()
    textDivNode.normalize()
    var textNode = textDivNode.childNodes[0]
    httpGet(results[0].get("spans").url(), function(spansText) {
        var spansSerialized = spansText.split("|")
        var spans = spansSerialized.map(function(span) {
            var splitSpan = span.split("$")
            var className = splitSpan[0]
            var start = splitSpan[1]
            var end = splitSpan[2]
            var jsonmapfeats = splitSpan[3]
            return {start: start, end: end, className: className, jsonmapfeats: jsonmapfeats}
        })
        spans.sort(function(a, b) { return b.start - a.start })
        for (var i = 0; i < spans.length; i++) {
            var span = spans[i]
            if (span.start > textNode.length || span.end > textNode.length) {
                console.log("Skipped span [" + span.start + "," +
                    span.end + "] because > " + textNode.length)
            } else {
                var range = rangy.createRange()
                range.setStartAndEnd(textNode, span.start, span.end)
                annotationClassesAndAppliers.forEach(function(ca) {
                    if (span.className == ca.clazz) {
                        ca.applier.applyToRange(range)
                    }
                })
                getRangeNodes(range, annotationClasses).forEach(function(node) {
                    if (span.jsonmapfeats)
                        setStoredMapFeatures(node, span.jsonmapfeats)
                })
            }
        }
    })
}

function closeDialog(node) {
    $(node).dialog("close")
}

function checkVol(tableSelector) {
    if (annotUser != "Default") {
        var table = $(tableSelector).DataTable()
        var ret = table.$('tr.selected')
        if (ret.length > 0) {
            var newvol = table.$('tr.selected').find('td:first').text()
            if (annotationChanges > 0) {
                $("
Do you want to save the existing annotations?
").dialog({
                    resizable: false,
                    modal: true,
                    buttons: {
                        "Yes": function() {
                            saveAnnotations()
                            loadVolumeText(newvol, SpansObject)
                            closeDialog(this)
                        },
                        "No": function() {
                            loadVolumeText(newvol, SpansObject)
                            closeDialog(this)
                        },
                        "Cancel": function() {
                            console.log("Canceled")
                            window.alert("FIXME: We should set the visibly selected volume to the old one")
                            closeDialog(this)
                        }
                    }
                })
            }
            loadVolumeText(newvol, SpansObject)
        }
    } else {
        logMessage("Please select a non-default Annotator name prior to loading a volume")
    }
}

function nameChangeAnnotator() {
    var el = document.getElementById("selectUserAnnotator");
    annotUser = el.options[el.selectedIndex].innerHTML;
}

function removeAnnotations() {
    destroyMapFeatures()
    annotationClassesAndAppliers.forEach(function(ca) {
        ca.unapplier.undoToRange(makeRange(document.body))
    })
    annotationChanges = 0
}

function removeAnnotation() {
    var selectionRange = getSelectionRange()
    if (overlapsAnnotation(selectionRange, true, annotationClasses))
        logMessage("Selection contains part of an annotation")
    else {
        annotationClassesAndAppliers.forEach(function(ca) {
            ca.unapplier.undoToSelection()
        })
    }
}

function spanClick(element) {
    //window.alert("Clicked inside article")
    setSelectionToNode(element)
    lastClickedElement = element
    var jsonfeats = getStoredMapFeatures(element)
    // alert("GeoJSON: " + jsonfeats)
    displayMapFeatures(jsonfeats)
}

function annotationFeatureChanged(event) {
    if (!programmaticMapChange) {
        var jsonfeats = getMapFeatures()
        var rangenodes = addMapFeaturesToSelection(jsonfeats)
        if (rangenodes.length > 0) {
            var text = rangenodes[0].innerHTML.substring(0, 20)
            addToRecentLocations(text, jsonfeats)
        }
    }
    // var bounds = event.feature.geometry.getBounds();
    // var answer = "bottom: " + bounds.bottom + "\n";
    // answer += "left: " + bounds.left + "\n";
    // answer += "right: " + bounds.right + "\n";
    // answer += "top: " + bounds.top + "\n";
    // alert("Feature modified: " + answer);
}

function annotationFeatureAdded(event) {
    console.log("annotationFeatureAdded " + event)
    annotationFeatureChanged(event)
}

function annotationFeatureModified(event) {
    console.log("annotationFeatureModified " + event)
    annotationFeatureChanged(event)
}

function annotationFeatureRemoved(event) {
    console.log("annotationFeatureRemoved " + event)
    annotationFeatureChanged(event)
}

function commonMapInit() {
    var extent = new OpenLayers.Bounds(
        -12003508, 3009847, -9005759, 6557774
    );

    Parse.initialize("Dxi3BvGT3mHiDC7B1YjeEuiUQKtWIeQNofT5FIIx", "QG352rxcZvLrYeV4jOCsIZvM8mIeQyhvHzDNINAb");

    VolTextObject = Parse.Object.extend("VolumeText");

    rangy.init();

    var table = $('#vol_table').DataTable()
    var rows = getVolTableRows(table)

    map = new OpenLayers.Map('map', {
        projection: new OpenLayers.Projection("EPSG:900913"),
        displayProjection: new OpenLayers.Projection("EPSG:4326"),
        controls: [
            new OpenLayers.Control.PanZoom(),
            new OpenLayers.Control.Navigation(),
            new OpenLayers.Control.MousePosition({prefix: 'Long: ', separator: '  Lat: ', numDigits: 3, emptyString:''})
        ]
    });

    var gphy = new OpenLayers.Layer.Google(
        "Google Streets"
    );

    var saveStrategy = new OpenLayers.Strategy.Save();

    //polyboundRefresh = new OpenLayers.Strategy.Refresh({force: true});

    annotationLayer = new OpenLayers.Layer.Vector("Annotations", {
        projection: new OpenLayers.Projection("EPSG:4326")
    })

    map.addLayers([gphy, annotationLayer]);

    /*annotationLayer.events.on({
        featureadded: annotationFeatureAdded,
        featuremodified: annotationFeatureModified,
        featureremoved: annotationFeatureRemoved
    })*/

    var panel = new OpenLayers.Control.Panel({
        displayClass: 'customEditingToolbar',
        allowDepress: true
    });

    var draw = new OpenLayers.Control.DrawFeature(
        annotationLayer, OpenLayers.Handler.Polygon,
        {
            title: "Draw Feature",
            displayClass: "olControlDrawFeaturePolygon",
            multi: true
        }
    );

    var edit = new OpenLayers.Control.ModifyFeature(annotationLayer, {
        title: "Modify Feature",
        displayClass: "olControlModifyFeature"
    });

    var del = new DeleteFeature(annotationLayer, {title: "Delete Feature"});

    var save = new OpenLayers.Control.Button({
        title: "Save Changes",
        trigger: function() {
            if(edit.feature) {
                edit.selectControl.unselectAll();
            }
            saveStrategy.save();
            logMessage("Done Saving")
        },
        displayClass: "olControlSaveFeatures"
    });

    panel.addControls([save, del, edit, draw]);
    map.addControl(panel);
    map.zoomToExtent(extent, true);
}

// Set 4-space indentation for vi
// vi:sw=4
<commit_msg>Narrowed Problem to featuresadded listener on annotationLayer. Debugging<commit_after>"use strict"

var VolTextObject
var map, annotationLayer
var programmaticMapChange = false

var annotationChanges = 0
var selvol = "0"
var annotationClassesAndAppliers
var keyCodeActions

var recentLocations = []
var recentLocationsMaxLength = 10

var lastClickedElement

$(document).ready(function() {
    // This handles selection in dataTable
    var table = $('#vol_table').DataTable();

    $('#vol_table tbody').on( 'click', 'tr', function () {
        if ( $(this).hasClass('selected') ) {
            $(this).removeClass('selected');
        }
        else {
            table.$('tr.selected').removeClass('selected');
            $(this).addClass('selected');
        }
    } );

    $('#button').click( function () {
        table.row('.selected').remove().draw( false );
    } );

    $("#col2text").on("cut copy paste", function(e) {
        e.preventDefault()
    })
    // Prevent changes in a content-editable div
    $("#col2text").on("keydown", function(e) {
        e = e || window.event;
        // Allow arrow keys, home, end, pgup, pgdn
        if (e.keyCode < 33 || e.keyCode > 40)
            e.preventDefault()
        keyCodeActions.forEach(function(action) {
            if (e.keyCode == action.code)
                action.action()
        })
    })
} );

function getSelectionNodes() {
    var selectionRange = getSelectionRange()
    return getRangeNodes(selectionRange, annotationClasses)
}

function setSelectionToNode(node) {
    var range = rangy.createRange()
    range.selectNodeContents(node)
    var sel = rangy.getSelection()
    sel.setSingleRange(range)
}

var DeleteFeature = OpenLayers.Class(OpenLayers.Control, {
    initialize: function(layer, options) {
        OpenLayers.Control.prototype.initialize.apply(this, [options]);
        this.layer = layer;
        this.handler = new OpenLayers.Handler.Feature(
            this, layer, {click: this.clickFeature}
        );
    },
    clickFeature: function(feature) {
        // if feature doesn't have a fid, destroy it
        if(feature.fid == undefined) {
            this.layer.destroyFeatures([feature]);
        } else {
            feature.state = OpenLayers.State.DELETE;
            this.layer.events.triggerEvent("afterfeaturemodified",
                                           {feature: feature});
            feature.renderIntent = "select";
            this.layer.drawFeature(feature);
        }
    },
    setMap: function(map) {
        this.handler.setMap(map);
        OpenLayers.Control.prototype.setMap.apply(this, arguments);
    },
    CLASS_NAME: "OpenLayers.Control.DeleteFeature"
});

function getMapFeatures() {
    //var mLayers = map.layers;
    // var annLayer = map.getLayersByName("Annotations")
    var annLayer = annotationLayer

    var geoJSON = new OpenLayers.Format.GeoJSON()

    var jsonfeatsarr = annLayer.features.map(function(feature) {
        var layer_geom = feature.geometry.transform("EPSG:900913", "EPSG:4326")
        var geoJSONText = geoJSON.write(layer_geom)
        return geoJSONText
    })
    var jsonfeats = jsonfeatsarr.join("@@")
    // window.alert(jsonfeats)
    return jsonfeats
}

function jsonToMapFeatures(jsonstr) {
    var geoJSON = new OpenLayers.Format.GeoJSON()

    var jsonfeats = jsonstr.split("@@")
    // debugger
    var feats = jsonfeats.map(function(jsonfeat) {
        var geom = geoJSON.read(jsonfeat, "Geometry")
        var transformedGeom = geom.transform("EPSG:4326", "EPSG:900913")
        return new OpenLayers.Feature.Vector(transformedGeom)
    })
    return feats
}

function getStoredMapFeatures(node) {
    return $.data(node, "features")
}

function setStoredMapFeatures(node, feats) {
    $.data(node, "features", feats)
}

// Store the specified GeoJSON map features in the ranges associated with the selection.
// Return the range nodes in the selection.
function addMapFeaturesToSelection(jsonfeats) {
    var rangenodes = getSelectionNodes()
    console.log(rangenodes)
    rangenodes.forEach(function(node) {
        setStoredMapFeatures(node, jsonfeats)
    })
    return rangenodes
}

function doProgrammaticMapChange(cb) {
    try {
        programmaticMapChange = true
        cb()
    } finally {
        programmaticMapChange = false
    }
}

function destroyMapFeatures() {
    doProgrammaticMapChange(function() { annotationLayer.destroyFeatures() })
}

function addMapFeatures(feats) {
    doProgrammaticMapChange(function() { annotationLayer.addFeatures(feats) })
}

function displayMapFeatures(jsonfeats) {
    destroyMapFeatures()
    if (jsonfeats)
        addMapFeatures(jsonToMapFeatures(jsonfeats))
}

function populateRecentLocations() {
    var htmlarr = []
    for (var i = recentLocations.length - 1; i >= 0; i--) {
        var recentLoc = recentLocations[i]
        var html = '
' + '⒳	<input value="' +
                     recentLoc.html + '" onchange="locChange(event)" data-index="' + i +
                     '">	Location
'
        console.log(html)
        htmlarr.push(html)
    }
    $('#recentlocs').html(htmlarr.join("\n"))
}

/* Add an element to the recentLocations list with the specified HTML and
 * GeoJSON features. */
function addToRecentLocations(html, jsonfeats) {
    // If the HTML text isn't in the list, add an element to the end.
    // Else replace the current element.
    var curIndex = recentLocations.map(function(obj) { return obj.html }).indexOf(html)
    if (curIndex === -1) {
        if (recentLocations.length >= recentLocationsMaxLength)
            recentLocations = recentLocations.slice(1)
        recentLocations.push({html: html, jsonfeats: jsonfeats})
    } else {
        recentLocations[curIndex] = {html: html, jsonfeats: jsonfeats}
    }
    populateRecentLocations()
}

function locChange(e) {
    console.log(e.target.value)
    recentLocations[$(e.target).attr('data-index')].html = e.target.value
}

function locClick(e) {
    var jsonfeats = unescape($(e.target).attr('data-jsonfeats'))
    console.log(jsonfeats)
    displayMapFeatures(jsonfeats)
    if (lastClickedElement)
        setSelectionToNode(lastClickedElement)
    addMapFeaturesToSelection(jsonfeats)
}

function zoomFeatures() {
    var bounds = annotationLayer.getDataExtent()
    //var bounds = annotationLayer.getDataExtent().transform("EPSG:4326", "EPSG:900913")
    console.log(bounds)
    map.zoomToExtent(bounds)
    //polyboundRefresh.refresh();
}

// Save annotations in a serialized format.
function saveVolumeAnnotations() {
    // Fetch annotations
    var annotations = getAnnotations(annotationClasses)
    // Convert to an array of serialized annotations in the form "CLASS$START$END".
    var serialAnnotations = annotations.map(function(ann) {
        var jsonmapfeats = getStoredMapFeatures(ann.node) || ""
        return ann.node.className + "$" + ann.start + "$" + ann.end + "$" + jsonmapfeats
    })
    // Join to a single serialized string
    var serialString = serialAnnotations.join("|")
    var base64 = utf8ToB64(serialString);
    var parse_file = new Parse.File((annotUser + "-" + selvol +".txt"), { base64: base64 });
    // Save to Parse. First look for an existing entry for the user and volume.
    // If found, update it. Else create a new entry.
    var query = new Parse.Query(SpansObject)
    query.equalTo("user", annotUser)
    query.equalTo("vol", selvol)
    query.first().then(function(existing) {
        if (existing) {
            existing.set("spans", parse_file)
            return existing.save()
        } else {
            var spansObject = new SpansObject()
            return spansObject.save({"user":annotUser, "vol":selvol, "spans":parse_file})
        }
    }, savefailure("finding existing entry")
    ).then(savesuccess(function() {
        annotationChanges = 0
        logMessage("Saved " + annotations.length + " annotations")
    }),
        savefailure("saving new or updating existing entry"))
}

// Called from HTML. Save annotations. If saved successfully, reset list of
// article changes.
function saveAnnotations() {
    if (annotUser != "Default") {
        saveVolumeAnnotations()
    } else {
        logMessage("Please select a non-default Annotator Name prior prior to saving")
    }
}

// Load volume annotations
function loadVolumeAnnotations(results) {
    var textDivNode = getTextNode()
    textDivNode.normalize()
    var textNode = textDivNode.childNodes[0]
    httpGet(results[0].get("spans").url(), function(spansText) {
        var spansSerialized = spansText.split("|")
        var spans = spansSerialized.map(function(span) {
            var splitSpan = span.split("$")
            var className = splitSpan[0]
            var start = splitSpan[1]
            var end = splitSpan[2]
            var jsonmapfeats = splitSpan[3]
            return {start: start, end: end, className: className, jsonmapfeats: jsonmapfeats}
        })
        spans.sort(function(a, b) { return b.start - a.start })
        for (var i = 0; i < spans.length; i++) {
            var span = spans[i]
            if (span.start > textNode.length || span.end > textNode.length) {
                console.log("Skipped span [" + span.start + "," +
                    span.end + "] because > " + textNode.length)
            } else {
                var range = rangy.createRange()
                range.setStartAndEnd(textNode, span.start, span.end)
                annotationClassesAndAppliers.forEach(function(ca) {
                    if (span.className == ca.clazz) {
                        ca.applier.applyToRange(range)
                    }
                })
                getRangeNodes(range, annotationClasses).forEach(function(node) {
                    if (span.jsonmapfeats)
                        setStoredMapFeatures(node, span.jsonmapfeats)
                })
            }
        }
    })
}

function closeDialog(node) {
    $(node).dialog("close")
}

function checkVol(tableSelector) {
    if (annotUser != "Default") {
        var table = $(tableSelector).DataTable()
        var ret = table.$('tr.selected')
        if (ret.length > 0) {
            var newvol = table.$('tr.selected').find('td:first').text()
            if (annotationChanges > 0) {
                $("
Do you want to save the existing annotations?
").dialog({
                    resizable: false,
                    modal: true,
                    buttons: {
                        "Yes": function() {
                            saveAnnotations()
                            loadVolumeText(newvol, SpansObject)
                            closeDialog(this)
                        },
                        "No": function() {
                            loadVolumeText(newvol, SpansObject)
                            closeDialog(this)
                        },
                        "Cancel": function() {
                            console.log("Canceled")
                            window.alert("FIXME: We should set the visibly selected volume to the old one")
                            closeDialog(this)
                        }
                    }
                })
            }
            loadVolumeText(newvol, SpansObject)
        }
    } else {
        logMessage("Please select a non-default Annotator name prior to loading a volume")
    }
}

function nameChangeAnnotator() {
    var el = document.getElementById("selectUserAnnotator");
    annotUser = el.options[el.selectedIndex].innerHTML;
}

function removeAnnotations() {
    destroyMapFeatures()
    annotationClassesAndAppliers.forEach(function(ca) {
        ca.unapplier.undoToRange(makeRange(document.body))
    })
    annotationChanges = 0
}

function removeAnnotation() {
    var selectionRange = getSelectionRange()
    if (overlapsAnnotation(selectionRange, true, annotationClasses))
        logMessage("Selection contains part of an annotation")
    else {
        annotationClassesAndAppliers.forEach(function(ca) {
            ca.unapplier.undoToSelection()
        })
    }
}

function spanClick(element) {
    //window.alert("Clicked inside article")
    setSelectionToNode(element)
    lastClickedElement = element
    var jsonfeats = getStoredMapFeatures(element)
    // alert("GeoJSON: " + jsonfeats)
    displayMapFeatures(jsonfeats)
}

function annotationFeatureChanged(event) {
    console.log("Programmatic Change:" + programmaticMapChange)
    if (!programmaticMapChange) {
        var jsonfeats = getMapFeatures()
        var rangenodes = addMapFeaturesToSelection(jsonfeats)
        if (rangenodes.length > 0) {
            var text = rangenodes[0].innerHTML.substring(0, 20)
            addToRecentLocations(text, jsonfeats)
        }
    }
    // var bounds = event.feature.geometry.getBounds();
    // var answer = "bottom: " + bounds.bottom + "\n";
    // answer += "left: " + bounds.left + "\n";
    // answer += "right: " + bounds.right + "\n";
    // answer += "top: " + bounds.top + "\n";
    // alert("Feature modified: " + answer);
}

function annotationFeatureAdded(event) {
    console.log("annotationFeatureAdded " + event)
    annotationFeatureChanged(event)
}

function annotationFeatureModified(event) {
    console.log("annotationFeatureModified " + event)
    annotationFeatureChanged(event)
}

function annotationFeatureRemoved(event) {
    console.log("annotationFeatureRemoved " + event)
    annotationFeatureChanged(event)
}

function commonMapInit() {
    var extent = new OpenLayers.Bounds(
        -12003508, 3009847, -9005759, 6557774
    );

    Parse.initialize("Dxi3BvGT3mHiDC7B1YjeEuiUQKtWIeQNofT5FIIx", "QG352rxcZvLrYeV4jOCsIZvM8mIeQyhvHzDNINAb");

    VolTextObject = Parse.Object.extend("VolumeText");

    rangy.init();

    var table = $('#vol_table').DataTable()
    var rows = getVolTableRows(table)

    map = new OpenLayers.Map('map', {
        projection: new OpenLayers.Projection("EPSG:900913"),
        displayProjection: new OpenLayers.Projection("EPSG:4326"),
        controls: [
            new OpenLayers.Control.PanZoom(),
            new OpenLayers.Control.Navigation(),
            new OpenLayers.Control.MousePosition({prefix: 'Long: ', separator: '  Lat: ', numDigits: 3, emptyString:''})
        ]
    });

    var gphy = new OpenLayers.Layer.Google(
        "Google Streets"
    );

    var saveStrategy = new OpenLayers.Strategy.Save();

    //polyboundRefresh = new OpenLayers.Strategy.Refresh({force: true});

    annotationLayer = new OpenLayers.Layer.Vector("Annotations", {
        projection: new OpenLayers.Projection("EPSG:4326")
    })

    map.addLayers([gphy, annotationLayer]);

    annotationLayer.events.on({
        /*featureadded: annotationFeatureAdded,*/
        featuremodified: annotationFeatureModified,
        featureremoved: annotationFeatureRemoved
    })

    var panel = new OpenLayers.Control.Panel({
        displayClass: 'customEditingToolbar',
        allowDepress: true
    });

    var draw = new OpenLayers.Control.DrawFeature(
        annotationLayer, OpenLayers.Handler.Polygon,
        {
            title: "Draw Feature",
            displayClass: "olControlDrawFeaturePolygon",
            multi: true
        }
    );

    var edit = new OpenLayers.Control.ModifyFeature(annotationLayer, {
        title: "Modify Feature",
        displayClass: "olControlModifyFeature"
    });

    var del = new DeleteFeature(annotationLayer, {title: "Delete Feature"});

    var save = new OpenLayers.Control.Button({
        title: "Save Changes",
        trigger: function() {
            if(edit.feature) {
                edit.selectControl.unselectAll();
            }
            saveStrategy.save();
            logMessage("Done Saving")
        },
        displayClass: "olControlSaveFeatures"
    });

    panel.addControls([save, del, edit, draw]);
    map.addControl(panel);
    map.zoomToExtent(extent, true);
}

// Set 4-space indentation for vi
// vi:sw=4
<|endoftext|>