Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit 685028eead11c8e2261319e0e08b9a8e7ea43476 0 parents
@waratuman authored
917 lib/redis-client.js
@@ -0,0 +1,917 @@
+/*
+
+© 2010 by Fictorial LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+// To add support for new commands, edit the array called "commands" at the
+// bottom of this file.
+
+// Set this to true to aid in debugging wire protocol input/output,
+// parsing methods, etc.
+
+exports.debugMode = false;
+
+var net = require("net"),
+ sys = require("sys"),
+ Buffer = require('buffer').Buffer,
+ events = require('events'),
+
+ CRLF = "\r\n",
+ CRLF_LEN = 2,
+
+ PLUS = exports.PLUS = 0x2B, // +
+ MINUS = exports.MINUS = 0x2D, // -
+ DOLLAR = exports.DOLLAR = 0x24, // $
+ STAR = exports.STAR = 0x2A, // *
+ COLON = exports.COLON = 0x3A, // :
+ CR = exports.CR = 0x0D, // \r
+ LF = exports.LF = 0x0A, // \n
+
+ NONE = exports.NONE = "NONE",
+ BULK = exports.BULK = "BULK",
+ MULTIBULK = exports.MULTIBULK = "MULTIBULK",
+ INLINE = exports.INLINE = "INLINE",
+ INTEGER = exports.INTEGER = "INTEGER",
+ ERROR = exports.ERROR = "ERROR";
+
+exports.DEFAULT_HOST = '127.0.0.1';
+exports.DEFAULT_PORT = 6379;
+
+exports.COMMAND_ORPHANED_ERROR = "connection lost before reply received";
+exports.NO_CONNECTION_ERROR = "failed to establish a connection to Redis";
+
+function debugFilter(buffer, len) {
+ // Redis is binary-safe but assume for debug display that
+ // the encoding of textual data is UTF-8.
+
+ var filtered = buffer.utf8Slice(0, len || buffer.length);
+
+ filtered = filtered.replace(/\r\n/g, '<CRLF>');
+ filtered = filtered.replace(/\r/g, '<CR>');
+ filtered = filtered.replace(/\n/g, '<LF>');
+
+ return filtered;
+}
+
+// A fully interruptable, binary-safe Redis reply parser.
+// 'callback' is called with each reply parsed in 'feed'.
+// 'thisArg' is the "thisArg" for the callback "call".
+
+function ReplyParser(callback, thisArg) {
+ this.onReply = callback;
+ this.thisArg = thisArg;
+ this.clearState();
+ this.clearMultiBulkState();
+}
+
+exports.ReplyParser = ReplyParser;
+
+ReplyParser.prototype.clearState = function () {
+ this.type = NONE;
+ this.bulkLengthExpected = null;
+ this.valueBufferLen = 0;
+ this.skip = 0;
+ this.valueBuffer = new Buffer(4096);
+};
+
+ReplyParser.prototype.clearMultiBulkState = function () {
+ this.multibulkReplies = null;
+ this.multibulkRepliesExpected = null;
+};
+
+ReplyParser.prototype.feed = function (inbound) {
+ for (var i=0; i < inbound.length; ++i) {
+ if (this.skip > 0) {
+ this.skip--;
+ continue;
+ }
+
+ var typeBefore = this.type;
+
+ if (this.type === NONE) {
+ switch (inbound[i]) {
+ case DOLLAR: this.type = BULK; break;
+ case STAR: this.type = MULTIBULK; break;
+ case COLON: this.type = INTEGER; break;
+ case PLUS: this.type = INLINE; break;
+ case MINUS: this.type = ERROR; break;
+ }
+ }
+
+ // Just a state transition on '*', '+', etc.?
+
+ if (typeBefore != this.type)
+ continue;
+
+ // If the reply is a part of a multi-bulk reply. Save it. If we have
+ // received all the expected replies of a multi-bulk reply, then
+ // callback. If the reply is not part of a multi-bulk. Call back
+ // immediately.
+
+ var self = this;
+
+ var maybeCallbackWithReply = function (reply) {
+ if (self.multibulkReplies != null) {
+ self.multibulkReplies.push(reply);
+ if (--self.multibulkRepliesExpected == 0) {
+ self.onReply.call(self.thisArg, {
+ type: MULTIBULK,
+ value: self.multibulkReplies
+ });
+ self.clearMultiBulkState();
+ }
+ } else {
+ self.onReply.call(self.thisArg, reply);
+ }
+ self.clearState();
+ self.skip = 1; // Skip LF
+ };
+
+ switch (inbound[i]) {
+ case CR:
+ switch (this.type) {
+ case INLINE:
+ case ERROR:
+ // CR denotes end of the inline/error value.
+ // +OK\r\n
+ // ^
+
+ var inlineBuf = new Buffer(this.valueBufferLen);
+ this.valueBuffer.copy(inlineBuf, 0, 0, this.valueBufferLen);
+ maybeCallbackWithReply({ type:this.type, value:inlineBuf });
+ break;
+
+ case INTEGER:
+ // CR denotes the end of the integer value.
+ // :42\r\n
+ // ^
+
+ var n = parseInt(this.valueBuffer.asciiSlice(0, this.valueBufferLen), 10);
+ maybeCallbackWithReply({ type:INTEGER, value:n });
+ break;
+
+ case BULK:
+ if (this.bulkLengthExpected == null) {
+ // CR denotes end of first line of a bulk reply,
+ // which is the length of the bulk reply value.
+ // $5\r\nhello\r\n
+ // ^
+
+ var bulkLengthExpected =
+ parseInt(this.valueBuffer.asciiSlice(0, this.valueBufferLen), 10);
+
+ if (bulkLengthExpected <= 0) {
+ maybeCallbackWithReply({ type:BULK, value:null });
+ } else {
+ this.clearState();
+
+ this.bulkLengthExpected = bulkLengthExpected;
+ this.type = BULK;
+ this.skip = 1; // skip LF
+ }
+ } else if (this.valueBufferLen == this.bulkLengthExpected) {
+ // CR denotes end of the bulk reply value.
+ // $5\r\nhello\r\n
+ // ^
+
+ var bulkBuf = new Buffer(this.valueBufferLen);
+ this.valueBuffer.copy(bulkBuf, 0, 0, this.valueBufferLen);
+ maybeCallbackWithReply({ type:BULK, value:bulkBuf });
+ } else {
+ // CR is just an embedded CR and has nothing to do
+ // with the reply specification.
+ // $11\r\nhello\rworld\r\n
+ // ^
+
+ this.valueBuffer[this.valueBufferLen++] = inbound[i];
+ }
+ break;
+
+ case MULTIBULK:
+ // Parse the count which is the number of expected replies
+ // in the multi-bulk reply.
+ // *2\r\n$5\r\nhello\r\n$5\r\nworld\r\n
+ // ^
+
+ var multibulkRepliesExpected =
+ parseInt(this.valueBuffer.asciiSlice(0, this.valueBufferLen), 10);
+
+ if (multibulkRepliesExpected <= 0) {
+ maybeCallbackWithReply({ type:MULTIBULK, value:null });
+ } else {
+ this.clearState();
+ this.skip = 1; // skip LF
+ this.multibulkReplies = [];
+ this.multibulkRepliesExpected = multibulkRepliesExpected;
+ }
+ break;
+ }
+ break;
+
+ default:
+ this.valueBuffer[this.valueBufferLen++] = inbound[i];
+ break;
+ }
+
+ // If the current value buffer is too big, create a new buffer, copy in
+ // the old buffer, and replace the old buffer with the new buffer.
+
+ if (this.valueBufferLen === this.valueBuffer.length) {
+ var newBuffer = new Buffer(this.valueBuffer.length * 2);
+ this.valueBuffer.copy(newBuffer, 0, 0);
+ this.valueBuffer = newBuffer;
+ }
+ }
+};
+
+/**
+ * Emits:
+ *
+ * - 'connected' when connected (or on a reconnection, reconnected).
+ * - 'reconnecting' when about to retry to connect to Redis.
+ * - 'reconnected' when connected after a reconnection was established.
+ * - 'noconnection' when a connection (or reconnection) cannot be established.
+ * - 'drained' when no submitted commands are expecting a reply from Redis.
+ *
+ * Options:
+ *
+ * - maxReconnectionAttempts (default: 10)
+ */
+
+function Client(stream, options) {
+ events.EventEmitter.call(this);
+
+ this.stream = stream;
+ this.originalCommands = [];
+ this.queuedOriginalCommands = [];
+ this.queuedRequestBuffers = [];
+ this.channelCallbacks = {};
+ this.requestBuffer = new Buffer(512);
+ this.replyParser = new ReplyParser(this.onReply_, this);
+ this.reconnectionTimer = null;
+ this.maxReconnectionAttempts = 10;
+ this.reconnectionAttempts = 0;
+ this.reconnectionDelay = 500; // doubles, so starts at 1s delay
+ this.connectionsMade = 0;
+
+ if (options !== undefined)
+ this.maxReconnectionAttempts = Math.abs(options.maxReconnectionAttempts || 10);
+
+ var client = this;
+
+ stream.addListener("connect", function () {
+ if (exports.debugMode)
+ sys.debug("[CONNECT]");
+
+ stream.setNoDelay();
+ stream.setTimeout(0);
+
+ client.reconnectionAttempts = 0;
+ client.reconnectionDelay = 500;
+ if (client.reconnectionTimer) {
+ clearTimeout(client.reconnectionTimer);
+ client.reconnectionTimer = null;
+ }
+
+ var eventName = client.connectionsMade == 0
+ ? 'connected'
+ : 'reconnected';
+
+ client.connectionsMade++;
+ client.expectingClose = false;
+
+ // If this a reconnection and there were commands submitted, then they
+ // are gone! We cannot say with any confidence which were processed by
+ // Redis; perhaps some were processed but we never got the reply, or
+ // perhaps all were processed but Redis is configured with less than
+ // 100% durable writes, etc.
+ //
+ // We punt to the user by calling their callback with an I/O error.
+ // However, we provide enough information to allow the user to retry
+ // the interrupted operation. We are certainly not retrying anything
+ // for them as it is too dangerous and application-specific.
+
+ if (client.connectionsMade > 1 && client.originalCommands.length > 0) {
+ if (exports.debug) {
+ sys.debug("[RECONNECTION] some commands orphaned (" +
+ client.originalCommands.length + "). notifying...");
+ }
+
+ client.callbackOrphanedCommandsWithError();
+ }
+
+ client.originalCommands = [];
+ client.flushQueuedCommands();
+
+ client.emit(eventName, client);
+ });
+
+ stream.addListener('error', function (e) {
+ if (exports.debugMode)
+ sys.debug("[ERROR] Connection to redis encountered an error: " + e);
+ });
+
+ stream.addListener("data", function (buffer) {
+ if (exports.debugMode)
+ sys.debug("[RECV] " + debugFilter(buffer));
+
+ client.replyParser.feed(buffer);
+ });
+
+ stream.addListener("error", function (e) {
+ if (exports.debugMode)
+ sys.debug('[ERROR] ' + e);
+ client.replyParser.clearState();
+ client.maybeReconnect();
+ throw e;
+ });
+
+ stream.addListener("end", function () {
+ if (exports.debugMode && client.originalCommands.length > 0) {
+ sys.debug("Connection to redis closed with " +
+ client.originalCommands.length +
+ " commands pending replies that will never arrive!");
+ }
+
+ stream.end();
+ });
+
+ stream.addListener("close", function (hadError) {
+ if (exports.debugMode)
+ sys.debug("[NO CONNECTION]");
+
+ client.maybeReconnect();
+ });
+}
+
+sys.inherits(Client, events.EventEmitter);
+
+exports.Client = Client;
+
+exports.createClient = function (port, host, options) {
+ var port = port || exports.DEFAULT_PORT;
+ var host = host || exports.DEFAULT_HOST;
+
+ var client = new Client(net.createConnection(port, host), options);
+
+ client.port = port;
+ client.host = host;
+
+ return client;
+};
+
+Client.prototype.close = function () {
+ this.expectingClose = true;
+ this.stream.end();
+};
+
+Client.prototype.onReply_ = function (reply) {
+ this.flushQueuedCommands();
+
+ if (this.handlePublishedMessage_(reply))
+ return;
+
+ var originalCommand = this.originalCommands.shift();
+ var callback = originalCommand[originalCommand.length - 1];
+
+ // Callbacks expect (err, reply) as args.
+
+ if (typeof callback == "function") {
+ if (reply.type == ERROR) {
+ callback(reply.value.utf8Slice(0, reply.value.length), null);
+ } else {
+ callback(null, maybeConvertReplyValue(originalCommand[0], reply));
+ }
+ }
+
+ if (this.originalCommands.length == 0)
+ this.emit('drained', this);
+};
+
+Client.prototype.handlePublishedMessage_ = function (reply) {
+ // We're looking for a multibulk resembling
+ // ["message", "channelName", messageBuffer]; or
+ // ["pmessage", "matchingPattern", "channelName", messageBuffer]
+ // The latter is sent when the client subscribed to a channel by a pattern;
+ // the former when subscribed to a channel by name.
+ // If the client subscribes by name -and- by pattern and there's some
+ // overlap, the client -will- receive multiple p/message notifications.
+
+ if (reply.type != MULTIBULK || !(reply.value instanceof Array))
+ return false;
+
+ var isMessage = (reply.value.length == 3 &&
+ reply.value[0].value.length == 7 &&
+ reply.value[0].value.asciiSlice(0, 7) == 'message');
+
+ var isPMessage = (reply.value.length == 4 &&
+ reply.value[0].value.length == 8 &&
+ reply.value[0].value.asciiSlice(0, 8) == 'pmessage');
+
+ if (!isMessage && !isPMessage)
+ return false;
+
+ // This is tricky. We are returning true even though there
+ // might not be any callback called! This may happen when a
+ // caller subscribes then unsubscribes while a published
+ // message is in transit to us. When the message arrives, no
+ // one is there to consume it. In essence, as long as the
+ // reply type is a published message (see above), then we've
+ // "handled" the reply.
+
+ if (Object.getOwnPropertyNames(this.channelCallbacks).length == 0)
+ return true;
+
+ var channelName, channelPattern, channelCallback, payload;
+
+ if (isMessage) {
+ channelName = reply.value[1].value;
+ channelCallback = this.channelCallbacks[channelName];
+ payload = reply.value[2].value;
+ } else if (isPMessage) {
+ channelPattern = reply.value[1].value;
+ channelName = reply.value[2].value;
+ channelCallback = this.channelCallbacks[channelPattern];
+ payload = reply.value[3].value;
+ } else {
+ return false;
+ }
+
+ if (typeof channelCallback == "function") {
+ channelCallback(channelName, payload, channelPattern);
+ return true;
+ }
+
+ return false;
+}
+
+function maybeAsNumber(str) {
+ var value = parseInt(str, 10);
+
+ if (isNaN(value))
+ value = parseFloat(str);
+
+ if (isNaN(value))
+ return str;
+
+ return value;
+}
+
+function maybeConvertReplyValue(commandName, reply) {
+ if (reply.value === null)
+ return null;
+
+ // Redis' INFO command returns a BULK reply of the form:
+ // "redis_version:1.3.8
+ // arch_bits:64
+ // multiplexing_api:kqueue
+ // process_id:11604
+ // ..."
+ //
+ // We convert that to a JS object like:
+ // { redis_version: '1.3.8'
+ // , arch_bits: '64'
+ // , multiplexing_api: 'kqueue'
+ // , process_id: '11604'
+ // , ... }
+
+ if (commandName === 'info' && reply.type === BULK) {
+ var info = {};
+ reply.value.asciiSlice(0, reply.value.length).split(/\r\n/g)
+ .forEach(function (line) {
+ var parts = line.split(':');
+ if (parts.length === 2)
+ info[parts[0]] = parts[1];
+ });
+ return info;
+ }
+
+ // HGETALL returns a MULTIBULK where each consecutive reply-pair
+ // is a key and value for the Redis HASH. We convert this into
+ // a JS object.
+
+ if (commandName === 'hgetall' &&
+ reply.type === MULTIBULK &&
+ reply.value.length % 2 === 0) {
+
+ var hash = {};
+ for (var i=0; i<reply.value.length; i += 2)
+ hash[reply.value[i].value] = reply.value[i + 1].value;
+ return hash;
+ }
+
+ // Redis returns "+OK\r\n" to signify success.
+ // We convert this into a JS boolean with value true.
+
+ if (reply.type === INLINE && reply.value.asciiSlice(0,2) === 'OK')
+ return true;
+
+ // ZSCORE returns a string representation of a floating point number.
+ // We convert this into a JS number.
+
+ if (commandName === "zscore")
+ return maybeAsNumber(reply.value);
+
+ // Multibulk replies are returned from our reply parser as an
+ // array like: [ {type:BULK, value:"foo"}, {type:BULK, value:"bar"} ]
+ // But, end-users want the value and don't care about the
+ // Redis protocol reply types. We here extract the value from each
+ // object in the multi-bulk array.
+
+ if (reply.type === MULTIBULK)
+ return reply.value.map(function (element) { return element.value; });
+
+ // Otherwise, we have no conversions to offer.
+
+ return reply.value;
+}
+
+exports.maybeConvertReplyValue_ = maybeConvertReplyValue;
+
+var commands = [
+ "append",
+ "auth",
+ "bgsave",
+ "blpop",
+ "brpop",
+ "dbsize",
+ "decr",
+ "decrby",
+ "del",
+ "exists",
+ "expire",
+ "expireat",
+ "flushall",
+ "flushdb",
+ "get",
+ "getset",
+ "hdel",
+ "hexists",
+ "hget",
+ "hgetall",
+ "hincrby",
+ "hkeys",
+ "hlen",
+ "hmget",
+ "hmset",
+ "hset",
+ "hvals",
+ "incr",
+ "incrby",
+ "info",
+ "keys",
+ "lastsave",
+ "len",
+ "lindex",
+ "llen",
+ "lpop",
+ "lpush",
+ "lrange",
+ "lrem",
+ "lset",
+ "ltrim",
+ "mget",
+ "move",
+ "mset",
+ "msetnx",
+ "psubscribe",
+ "publish",
+ "punsubscribe",
+ "randomkey",
+ "rename",
+ "renamenx",
+ "rpop",
+ "rpoplpush",
+ "rpush",
+ "sadd",
+ "save",
+ "scard",
+ "sdiff",
+ "sdiffstore",
+ "select",
+ "set",
+ "setex",
+ "setnx",
+ "shutdown",
+ "sinter",
+ "sinterstore",
+ "sismember",
+ "smembers",
+ "smove",
+ "sort",
+ "spop",
+ "srandmember",
+ "srem",
+ "subscribe",
+ "sunion",
+ "sunionstore",
+ "ttl",
+ "type",
+ "unsubscribe",
+ "zadd",
+ "zcard",
+ "zcount",
+ "zincrby",
+ "zinter",
+ "zrange",
+ "zrangebyscore",
+ "zrank",
+ "zrem",
+ "zrembyrank",
+ "zremrangebyrank",
+ "zremrangebyscore",
+ "zrevrange",
+ "zrevrank",
+ "zscore",
+ "zunion",
+];
+
+// For internal use but maybe useful in rare cases or when the client command
+// set is not 100% up to date with Redis' latest commands.
+// client.sendCommand('GET', 'foo', function (err, value) {...});
+//
+// arguments[0] = commandName
+// arguments[1..N-2] = Redis command arguments
+// arguments[N-1] = callback function
+
+Client.prototype.sendCommand = function () {
+ var originalCommand = Array.prototype.slice.call(arguments);
+
+ // If this client has given up trying to connect/reconnect to Redis,
+ // just call the errback (if any). Regardless, don't enqueue the command.
+
+ if (this.noConnection) {
+ if (arguments.length > 0 && typeof arguments[arguments.length - 1] == 'function')
+ arguments[arguments.length - 1](this.makeErrorForCommand(originalCommand, exports.NO_CONNECTION_ERROR));
+ return;
+ }
+
+ this.flushQueuedCommands();
+
+ var commandName = arguments[0].toLowerCase();
+
+ // Invariant: number of queued callbacks == number of commands sent to
+ // Redis whose replies have not yet been received and processed. Thus,
+ // if no callback was given, we create a dummy callback.
+
+ var argCount = arguments.length;
+ if (typeof arguments[argCount - 1] == 'function')
+ --argCount;
+
+ // All requests are formatted as multi-bulk.
+ // The first line of a multi-bulk request is "*<number of parts to follow>\r\n".
+ // Next is: "$<length of the command name>\r\n<command name>\r\n".
+
+ // Write the request as we go into a request Buffer. Recall that buffers
+ // are fixed length. We thus guess at how much space is needed. If we
+ // need to grow beyond this, we create a new buffer, copy the old one, and
+ // continue. Once we're ready to write the buffer, we use a 0-copy slice
+ // to send just that which we've written to the buffer.
+ //
+ // We reuse the buffer after each request. When the buffer "grows" to
+ // accomodate a request, it stays that size until it needs to grown again,
+ // which may of course be never.
+
+ var offset = this.requestBuffer.utf8Write('*' + argCount.toString() + CRLF +
+ '$' + commandName.length + CRLF +
+ commandName + CRLF, 0);
+
+ var self = this;
+
+ function ensureSpaceFor(atLeast) {
+ var currentLength = self.requestBuffer.length;
+
+ if (offset + atLeast > currentLength) {
+ // If we know how much space we need, use that + 10%.
+ // Else double the size of the buffer.
+
+ var bufferLength = Math.max(currentLength * 2, atLeast * 1.1);
+ var newBuffer = new Buffer(Math.round(bufferLength));
+ self.requestBuffer.copy(newBuffer, 0, 0, offset); // target, targetStart, srcStart, srcEnd
+ self.requestBuffer = newBuffer;
+ }
+ }
+
+ // Serialize the arguments into the request buffer
+ // If the request is a Buffer, just copy. Else if
+ // the arg has a .toString() method, call it and write
+ // it to the request buffer as UTF8.
+
+ var extrasLength = 5; // '$', '\r\n', '\r\n'
+
+ for (var i=1; i < argCount; ++i) {
+ var arg = arguments[i];
+ if (arg instanceof Buffer) {
+ ensureSpaceFor(arg.length + arg.length.toString().length + extrasLength);
+ offset += this.requestBuffer.asciiWrite('$' + arg.length + CRLF, offset);
+ offset += arg.copy(this.requestBuffer, offset, 0); // target, targetStart, srcStart
+ offset += this.requestBuffer.asciiWrite(CRLF, offset);
+ } else if (arg.toString) {
+ var asString = arg.toString();
+ var serialized = '$' + Buffer.byteLength(asString, "binary") + CRLF + asString + CRLF;
+ ensureSpaceFor(Buffer.byteLength(serialized, "binary"));
+ offset += this.requestBuffer.binaryWrite(serialized, offset);
+ }
+ }
+
+ // If the stream is writable, write the command. Else enqueue the command
+ // for when we first establish a connection or reconnect.
+
+ if (this.stream.writable) {
+ this.originalCommands.push(originalCommand);
+ var outBuffer = new Buffer(offset);
+ this.requestBuffer.copy(outBuffer, 0, 0, offset);
+ this.stream.write(outBuffer, 'binary');
+
+ if (exports.debugMode)
+ sys.debug("[SEND] " + debugFilter(this.requestBuffer, offset) +
+ " originalCommands = " + this.originalCommands.length);
+ } else {
+ var toEnqueue = new Buffer(offset);
+ this.requestBuffer.copy(toEnqueue, 0, 0, offset); // dst, dstStart, srcStart, srcEnd
+ this.queuedRequestBuffers.push(toEnqueue);
+ this.queuedOriginalCommands.push(originalCommand);
+
+ if (exports.debugMode) {
+ sys.debug("[ENQUEUE] Not connected. Request queued. There are " +
+ this.queuedRequestBuffers.length + " requests queued.");
+ }
+ }
+};
+
+commands.forEach(function (commandName) {
+ Client.prototype[commandName] = function () {
+ var args = Array.prototype.slice.call(arguments);
+ // [[1,2,3],function(){}] => [1,2,3,function(){}]
+ if (args.length > 0 && Array.isArray(args[0]))
+ args = args.shift().concat(args);
+ args.unshift(commandName);
+ this.sendCommand.apply(this, args);
+ };
+});
+
+// Send any commands that were queued while we were not connected.
+
+Client.prototype.flushQueuedCommands = function () {
+ if (exports.debugMode && this.queuedRequestBuffers.length > 0)
+ sys.debug("[FLUSH QUEUE] " + this.queuedRequestBuffers.length +
+ " queued request buffers.");
+
+ for (var i=0; i<this.queuedRequestBuffers.length && this.stream.writable; ++i) {
+ var buffer = this.queuedRequestBuffers.shift();
+ this.stream.write(buffer, 'binary');
+ this.originalCommands.push(this.queuedOriginalCommands.shift());
+
+ if (exports.debugMode)
+ sys.debug("[DEQUEUE/SEND] " + debugFilter(buffer) +
+ ". queued buffers remaining = " +
+ this.queuedRequestBuffers.length);
+ }
+};
+
+Client.prototype.makeErrorForCommand = function (command, errorMessage) {
+ var err = new Error(errorMessage);
+ err.originalCommand = command;
+ return err;
+};
+
+Client.prototype.callbackCommandWithError = function (command, errorMessage) {
+ var callback = command[command.length - 1];
+ if (typeof callback == "function")
+ callback(this.makeErrorForCommand(command, errorMessage));
+};
+
+Client.prototype.callbackOrphanedCommandsWithError = function () {
+ for (var i=0, n=this.originalCommands.length; i<n; ++i)
+ this.callbackCommandWithError(this.originalCommands[i], exports.COMMAND_ORPHANED_ERROR);
+ this.originalCommands = [];
+};
+
+Client.prototype.callbackQueuedCommandsWithError = function () {
+ for (var i=0, n=this.queuedOriginalCommands.length; i<n; ++i)
+ this.callbackCommandWithError(this.queuedOriginalCommands[i], exports.NO_CONNECTION_ERROR);
+ this.queuedOriginalCommands = [];
+ this.queuedRequestBuffers = [];
+};
+
+Client.prototype.giveupConnectionAttempts = function () {
+ this.callbackOrphanedCommandsWithError();
+ this.callbackQueuedCommandsWithError();
+ this.noConnection = true;
+ this.emit('noconnection', this);
+};
+
+Client.prototype.maybeReconnect = function () {
+ if (this.stream.writable && this.stream.readable)
+ return;
+
+ if (this.expectingClose)
+ return;
+
+ // Do not reconnect on first connection failure.
+ // Else try to reconnect if we're asked to.
+
+ if (this.connectionsMade == 0) {
+ this.giveupConnectionAttempts();
+ } else if (this.maxReconnectionAttempts > 0) {
+ if (this.reconnectionAttempts++ >= this.maxReconnectionAttempts) {
+ this.giveupConnectionAttempts();
+ } else {
+ this.reconnectionDelay *= 2;
+
+ if (exports.debugMode) {
+ sys.debug("[RECONNECTING " + this.reconnectionAttempts + "/" +
+ this.maxReconnectionAttempts + "]");
+
+ sys.debug("[WAIT " + this.reconnectionDelay + " ms]");
+ }
+
+ var self = this;
+
+ this.reconnectionTimer = setTimeout(function () {
+ self.emit('reconnecting', self);
+ self.stream.connect(self.port, self.host);
+ }, this.reconnectionDelay);
+ }
+ }
+};
+
+// Wraps 'subscribe' and 'psubscribe' methods to manage a single
+// callback function per subscribed channel name/pattern.
+//
+// 'nameOrPattern' is a channel name like "hello" or a pattern like
+// "h*llo", "h?llo", or "h[ae]llo".
+//
+// 'callback' is a function that is called back with 2 args:
+// channel name/pattern and message payload.
+//
+// Note: You are not permitted to do anything but subscribe to
+// additional channels or unsubscribe from subscribed channels
+// when there are >= 1 subscriptions active. Should you need to
+// issue other commands, use a second client instance.
+
+Client.prototype.subscribeTo = function (nameOrPattern, callback) {
+ if (typeof this.channelCallbacks[nameOrPattern] === 'function')
+ return;
+
+ if (typeof(callback) !== 'function')
+ throw new Error("requires a callback function");
+
+ this.channelCallbacks[nameOrPattern] = callback;
+
+ var method = nameOrPattern.match(/[\*\?\[]/)
+ ? "psubscribe"
+ : "subscribe";
+
+ this[method](nameOrPattern);
+};
+
+Client.prototype.unsubscribeFrom = function (nameOrPattern) {
+ if (typeof this.channelCallbacks[nameOrPattern] === 'undefined')
+ return;
+
+ delete this.channelCallbacks[nameOrPattern];
+
+ var method = nameOrPattern.match(/[\*\?\[]/)
+ ? "punsubscribe"
+ : "unsubscribe";
+
+ this[method](nameOrPattern);
+};
+
+// Multi-bulk replies return an array of other replies. Perhaps all you care
+// about is the representation of such buffers as UTF-8 encoded strings? Use
+// this to convert each such Buffer to a (UTF-8 encoded) String in-place.
+
+exports.convertMultiBulkBuffersToUTF8Strings = function (o) {
+ if (o instanceof Array) {
+ for (var i=0; i<o.length; ++i)
+ if (o[i] instanceof Buffer)
+ o[i] = o[i].utf8Slice(0, o[i].length);
+ } else if (o instanceof Object) {
+ var props = Object.getOwnPropertyNames(o);
+ for (var i=0; i<props.length; ++i)
+ if (o[props[i]] instanceof Buffer)
+ o[props[i]] = o[props[i]].utf8Slice(0, o[props[i]].length);
+ }
+};
+
133 lib/ws.js
@@ -0,0 +1,133 @@
+
+
+/*-----------------------------------------------
+ Requirements:
+-----------------------------------------------*/
+
+// System
+var sys = require("sys")
+ , http = require("http")
+ , events = require("events")
+ , path = require("path");
+
+// Local
+require.paths.unshift(__dirname);
+
+var Manager = require("ws/manager")
+ , Connection = require("ws/connection");
+
+
+/*-----------------------------------------------
+ Mixin:
+-----------------------------------------------*/
+var mixin = function(target, source) {
+ for(var i = 0, keys = Object.keys(source), l = keys.length; i < l; ++i) {
+ var key = keys[i];
+ target[key] = source[key];
+ }
+ return target;
+};
+
+/*-----------------------------------------------
+ WebSocket Server Exports:
+-----------------------------------------------*/
+exports.Server = Server;
+exports.createServer = function(options, server){
+ return new Server(options || {}, server);
+};
+
+/*-----------------------------------------------
+ WebSocket Server Implementation:
+-----------------------------------------------*/
+// Server(options, [externalServer])
+function Server(options){
+ this.options = mixin({
+ debug: false, // Boolean: Show debug information.
+ version: "auto", // String: Value must be either: draft75, draft76, auto
+ origin: "*", // String, Array: A match for a valid connection origin
+ subprotocol: null, // String, Array: A match for a valid connection subprotocol.
+ }, options || {});
+
+ var ws = this
+ , externalServer = arguments.length > 1 && arguments[1] instanceof http.Server;
+
+ this.debug = !!this.options.debug;
+ this.server = externalServer ? arguments[1] : new http.Server();
+ this.manager = new Manager(this.debug);
+
+ events.EventEmitter.call(this);
+
+ this.server.addListener("upgrade", function(req, socket, upgradeHead){
+ if( req.method == "GET" && ( "upgrade" in req.headers && "connection" in req.headers) &&
+ req.headers.upgrade.toLowerCase() == "websocket" && req.headers.connection.toLowerCase() == "upgrade"
+ ){
+ // create a new connection, it'll handle everything else.
+ new Connection(ws, req, socket, upgradeHead);
+ } else {
+ // Close the socket, it wasn't a valid connection.
+ socket.end();
+ socket.destroy();
+ }
+ });
+
+ this.server.addListener("listening", function(req, res){
+ ws.emit("listening");
+ });
+
+ this.server.addListener("connection", function(socket){
+ socket.setTimeout(0);
+ socket.setNoDelay(true);
+ socket.setKeepAlive(true, 0);
+ });
+
+ if(externalServer && ! this.server._events.hasOwnProperty("request")){
+ this.server.addListener("request", function(req, res){
+ ws.emit("request", req, res);
+ });
+ }
+
+ if(externalServer && ! this.server._events.hasOwnProperty("stream")){
+ this.server.addListener("stream", function(stream){
+ ws.emit("stream", stream);
+ });
+ }
+
+ this.server.addListener("close", function(errno){
+ ws.emit("shutdown", errno);
+ });
+
+ if(externalServer && ! this.server._events.hasOwnProperty("clientError")){
+ this.server.addListener("clientError", function(e){
+ ws.emit("clientError", e);
+ });
+ }
+};
+
+sys.inherits(Server, events.EventEmitter);
+
+/*-----------------------------------------------
+ Public API
+-----------------------------------------------*/
+Server.prototype.listen = function(){
+ this.server.listen.apply(this.server, arguments);
+};
+
+Server.prototype.close = function(){
+ this.server.close();
+};
+
+Server.prototype.send = function(id, data){
+ this.manager.find(id, function(client){
+ if(client && client._state === 4){
+ client.write(data);
+ }
+ });
+};
+
+Server.prototype.broadcast = function(data){
+ this.manager.forEach(function(client){
+ if(client && client._state === 4){
+ client.write(data);
+ }
+ });
+};
439 lib/ws/connection.js
@@ -0,0 +1,439 @@
+
+var sys = require("sys")
+ , events = require("events")
+ , Buffer = require("buffer").Buffer
+ , Crypto = require("crypto");
+
+
+/*-----------------------------------------------
+ Debugged
+-----------------------------------------------*/
+var debug;
+
+/*-----------------------------------------------
+ The Connection:
+-----------------------------------------------*/
+module.exports = Connection;
+
+function Connection(server, req, socket, upgradeHead){
+ this.debug = server.debug;
+
+ if (this.debug) {
+ debug = function () { sys.error('\033[90mWS: ' + Array.prototype.join.call(arguments, ", ") + "\033[39m"); };
+ } else {
+ debug = function () { };
+ }
+
+ this._req = req;
+ this._server = server;
+ this._upgradeHead = upgradeHead;
+ this.id = this._req.socket.remotePort;
+
+ events.EventEmitter.call(this);
+
+ this.version = this.getVersion();
+
+ if( !checkVersion(this)) {
+ this.reject("Invalid version.");
+ } else {
+ debug(this.id, this.version+" connection");
+
+ // Set the initial connecting state.
+ this.state(1);
+
+ var connection = this;
+ // Handle incoming data:
+ var parser = new Parser(this);
+ socket.addListener("data", function(data){
+ if(connection._state == 2){
+ connection._upgradeHead = data;
+ } else {
+ parser.write(data);
+ }
+ });
+
+ // Handle the end of the stream, and set the state
+ // appropriately to notify the correct events.
+ socket.addListener("end", function(){
+ connection.state(5);
+ });
+
+ // Setup the connection manager's state change listeners:
+ this.addListener("stateChange", function(state, laststate){
+ debug(connection.id, "Change state: "+laststate+" => "+state);
+ if(state === 4){
+ server.manager.attach(connection.id, connection);
+ server.emit("connection", connection);
+ } else if(state === 5){
+ connection.close();
+ } else if(state === 6 && laststate === 5){
+ server.manager.detach(connection.id, function(){
+ server.emit("close", connection);
+ connection.emit("close");
+ });
+ }
+ });
+
+ // Let us see the messages when in debug mode.
+ if(this.debug){
+ this.addListener("message", function(msg){
+ debug(connection.id, "recv: " + msg);
+ });
+ }
+
+ // Carry out the handshaking.
+ // - Draft75: There's no upgradeHead, goto Then.
+ // Draft76: If there's an upgradeHead of the right length, goto Then.
+ // Then: carry out the handshake.
+ //
+ // - Currently no browsers to my knowledge split the upgradeHead off the request,
+ // but in the case it does happen, then the state is set to waiting for
+ // the upgradeHead.
+ //
+ // HANDLING FOR THIS EDGE CASE IS NOT IMPLEMENTED.
+ //
+ if((this.version == "draft75") || (this.version == "draft76" && this._upgradeHead && this._upgradeHead.length == 8)){
+ this.handshake();
+ } else {
+ this.state(2);
+ debug(this.id, "waiting.");
+ }
+ }
+};
+
+sys.inherits(Connection, events.EventEmitter);
+
+// <<DEPRECATED
+
+var warned = false;
+Object.defineProperty(Connection.prototype, "_id", {
+ get: function(){
+ if(!warned){
+ sys.error('`Connection._id` will be removed in future versions of Node, please use `Connection.id`.');
+ warned = true;
+ }
+
+ return this.id;
+ }
+});
+
+// DEPRECATED>>
+
+/*-----------------------------------------------
+ Various utility style functions:
+-----------------------------------------------*/
+var writeSocket = function(socket, data, encoding) {
+ if(socket.writable){
+ try {
+ socket.write(data, encoding);
+ return true;
+ } catch(e){
+ debug(null, "Error on write: "+e.toString());
+ return false;
+ }
+ }
+ return false;
+};
+
+var closeClient = function(client){
+ client._req.socket.flush();
+ client._req.socket.end();
+ client._req.socket.destroy();
+ debug(client.id, "socket closed");
+ client.state(6);
+};
+
+function checkVersion(client){
+ var server_version = client._server.options.version.toLowerCase()
+ , client_version = client.version = client.version || client.getVersion();
+
+ return (server_version == "auto" || server_version == client_version);
+};
+
+
+function pack(num) {
+ var result = '';
+ result += String.fromCharCode(num >> 24 & 0xFF);
+ result += String.fromCharCode(num >> 16 & 0xFF);
+ result += String.fromCharCode(num >> 8 & 0xFF);
+ result += String.fromCharCode(num & 0xFF);
+ return result;
+};
+
+
+/*-----------------------------------------------
+ Formatters for the urls
+-----------------------------------------------*/
+function websocket_origin(){
+ var origin = this._server.options.origin || "*";
+ if(origin == "*" || Array.isArray(origin)){
+ origin = this._req.headers.origin;
+ }
+ return origin;
+};
+
+function websocket_location(){
+ var location = "",
+ secure = this._req.socket.secure,
+ request_host = this._req.headers.host.split(":"),
+ port = request_host[1];
+
+ if(secure){
+ location += "wss://";
+ } else {
+ location += "ws://";
+ }
+
+ location += request_host[0]
+
+ if(!secure && port != 80 || secure && port != 443){
+ location += ":"+port;
+ }
+
+ location += this._req.url;
+
+ return location;
+};
+
+
+/*-----------------------------------------------
+ 0. unknown
+ 1. opening
+ 2. waiting
+ 3. handshaking
+ 4, connected
+ 5. closed
+-----------------------------------------------*/
+Connection.prototype._state = 0;
+
+
+/*-----------------------------------------------
+ Connection Public API
+-----------------------------------------------*/
+Connection.prototype.state = function(state){
+ if(state !== undefined && typeof state === "number"){
+ var oldstate = this._state;
+ this._state = state;
+ this.emit("stateChange", this._state, oldstate);
+ }
+};
+
+Connection.prototype.getVersion = function(){
+ if(this._req.headers["sec-websocket-key1"] && this._req.headers["sec-websocket-key2"]){
+ return "draft76";
+ } else {
+ return "draft75";
+ }
+};
+
+
+Connection.prototype.write = function(data){
+ var socket = this._req.socket;
+
+ if(this._state == 4){
+ debug(this.id, "write: "+data);
+
+ if(
+ writeSocket(socket, "\x00", "binary") &&
+ writeSocket(socket, data, "utf8") &&
+ writeSocket(socket, "\xff", "binary")
+ ){
+ return true;
+ } else {
+ debug(this.id, "\033[31mERROR: write: "+data);
+ }
+ } else {
+ debug(this.id, "\033[31mCouldn't send.");
+ }
+ return false;
+};
+
+Connection.prototype.broadcast = function(data){
+ var conn = this;
+
+ this._server.manager.forEach(function(client){
+ if(client && client._state === 4 && client.id != conn.id){
+ client.write(data);
+ }
+ });
+};
+
+Connection.prototype.close = function(){
+ var socket = this._req.socket;
+
+ if(this._state == 4 && socket.writable){
+ writeSocket(socket, "\x00", "binary");
+ writeSocket(socket, "\xff", "binary");
+ }
+ closeClient(this);
+};
+
+
+Connection.prototype.reject = function(reason){
+ debug(this.id, "rejected. Reason: "+reason);
+
+ this.emit("rejected");
+ closeClient(this);
+};
+
+
+Connection.prototype.handshake = function(){
+ if(this._state < 3){
+ debug(this.id, this.version+" handshake");
+
+ this.state(3);
+
+ doHandshake[this.version].call(this);
+ } else {
+ debug(this.id, "Already handshaked.");
+ }
+};
+
+/*-----------------------------------------------
+ Do the handshake.
+-----------------------------------------------*/
+var doHandshake = {
+ /* Using draft75, work out and send the handshake. */
+ draft75: function(){
+ var res = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
+ + "Upgrade: WebSocket\r\n"
+ + "Connection: Upgrade\r\n"
+ + "WebSocket-Origin: "+websocket_origin.call(this)+"\r\n"
+ + "WebSocket-Location: "+websocket_location.call(this);
+
+ if(this._server.options.subprotocol && typeof this._server.options.subprotocol == "string") {
+ res += "\r\nWebSocket-Protocol: "+this._server.options.subprotocol;
+ }
+
+ writeSocket(this._req.socket, res+"\r\n\r\n", "ascii");
+ this.state(4);
+ },
+
+ /* Using draft76 (security model), work out and send the handshake. */
+ draft76: function(){
+ var data = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
+ + "Upgrade: WebSocket\r\n"
+ + "Connection: Upgrade\r\n"
+ + "Sec-WebSocket-Origin: "+websocket_origin.call(this)+"\r\n"
+ + "Sec-WebSocket-Location: "+websocket_location.call(this);
+
+ if(this._server.options.subprotocol && typeof this._server.options.subprotocol == "string") {
+ data += "\r\nSec-WebSocket-Protocol: "+this._server.options.subprotocol;
+ }
+
+ var strkey1 = this._req.headers['sec-websocket-key1']
+ , strkey2 = this._req.headers['sec-websocket-key2']
+
+ , numkey1 = parseInt(strkey1.replace(/[^\d]/g, ""), 10)
+ , numkey2 = parseInt(strkey2.replace(/[^\d]/g, ""), 10)
+
+ , spaces1 = strkey1.replace(/[^\ ]/g, "").length
+ , spaces2 = strkey2.replace(/[^\ ]/g, "").length;
+
+
+ if (spaces1 == 0 || spaces2 == 0 || numkey1 % spaces1 != 0 || numkey2 % spaces2 != 0) {
+ this.reject("WebSocket contained an invalid key -- closing connection.");
+ } else {
+ var hash = Crypto.createHash("md5")
+ , key1 = pack(parseInt(numkey1/spaces1))
+ , key2 = pack(parseInt(numkey2/spaces2));
+
+ hash.update(key1);
+ hash.update(key2);
+ hash.update(this._upgradeHead.toString("binary"));
+
+ data += "\r\n\r\n";
+ data += hash.digest("binary");
+
+ writeSocket(this._req.socket, data, "binary");
+ this.state(4);
+ }
+ }
+};
+
+/*-----------------------------------------------
+ The new onData callback for
+ http.Server IncomingMessage
+-----------------------------------------------*/
+var Parser = function(client){
+ this.frameData = [];
+ this.order = 0;
+ this.client = client;
+};
+
+Parser.prototype.write = function(data){
+ var pkt, msg;
+ for(var i = 0, len = data.length; i<len; i++){
+ if(this.order == 0){
+ if(data[i] & 0x80 == 0x80){
+ this.order = 1;
+ } else {
+ this.order = -1;
+ }
+ } else if(this.order == -1){
+ if(data[i] === 0xFF){
+ pkt = new Buffer(this.frameData);
+ this.order = 0;
+ this.frameData = [];
+
+ this.client.emit("message", pkt.toString("utf8", 0, pkt.length));
+ } else {
+ this.frameData.push(data[i]);
+ }
+ } else if(this.order == 1){
+ debug(this.client.id, "High Order packet handling is not yet implemented.");
+ this.order = 0;
+ }
+ }
+};
+
+/*
+function ondata(data, start, end){
+ if(this.state == 2 && this.version == "draft76"){
+ // TODO: I need to figure out an alternative here.
+ // data.copy(this._req.upgradeHead, 0, start, end);
+ debug.call(this, "Using draft76 & upgrade body not sent with request.");
+ this.reject("missing upgrade body");
+ // Assume the data is now a message:
+ } else if(this.state == 4){
+ data = data.slice(start, end);
+
+ var frame_type = null, length, b;
+ var parser_offset = -1;
+ var raw_data = [];
+
+ while(parser_offset < data.length-2){
+ frame_type = data[parser_offset++];
+
+ if(frame_type & 0x80 == 0x80){
+ debug.call(this, "high");
+ b = null;
+ length = 1;
+ while(length--){
+ b = data[parser_offset++];
+ length = length * 128 + (b & 0x7F);
+ if(b & 0x80 == 0){
+ break;
+ }
+ }
+ parser_offset += length;
+ if(frame_type == 0xFF && length == 0){
+ this.close();
+ }
+ } else {
+ raw_data = [];
+
+ while(parser_offset <= data.length){
+ b = data[parser_offset++];
+ if(b == 0xFF){
+ var buf = new Buffer(raw_data);
+ this.emit("message", buf.toString("utf8", 0, buf.length));
+ break;
+ }
+ raw_data.push(b);
+ }
+ }
+ }
+ }
+};
+*/
101 lib/ws/manager.js
@@ -0,0 +1,101 @@
+var Events = require("events")
+ , sys = require("sys");
+
+var debug;
+
+/*-----------------------------------------------
+ Connection Manager
+-----------------------------------------------*/
+module.exports = Manager;
+
+function Manager(showDebug){
+ if(showDebug) {
+ debug = function(){sys.error('\033[31mManager: ' + Array.prototype.join.call(arguments, ", ") + "\033[39m"); };
+ } else {
+ debug = function(){};
+ }
+
+ this._head = null;
+ this._tail = null;
+ this._length = 0;
+};
+
+Object.defineProperty(Manager.prototype, "length", {
+ get: function(){
+ return this._length;
+ }
+});
+
+
+Manager.prototype.attach = function(id, client){
+ var connection = {
+ _prev: null,
+ _next: null,
+ id: id,
+ client: client
+ };
+
+ if(this._length == 0) {
+ this._head = connection;
+ this._tail = connection;
+ } else {
+ this._head._prev = connection;
+ connection._next = this._head;
+ this._head = connection;
+ }
+
+ ++this._length;
+ debug("Attached: "+id);
+};
+
+Manager.prototype.detach = function(id, callback){
+ var current = this._tail;
+
+ if(this._length == 1 && current.id == id){
+ this._head = {
+ _prev: null,
+ _next: null
+ };
+ this._tail = null;
+ } else {
+ while(current && current.id !== id){
+ current = current._prev;
+ }
+ if(current !== null){
+ if(current._prev !== null){
+ current._prev._next = current._next;
+ }
+
+ if(current._next !== null){
+ current._next._prev = current._prev;
+ }
+ }
+ }
+
+ this._length--;
+
+ debug("Detached: "+id);
+ callback();
+};
+
+Manager.prototype.find = function(id, callback){
+ var current = this._head;
+
+ while(current && current.id !== id){
+ current = current._next;
+ }
+
+ if(current !== null && current.id === id && current.client){
+ callback(current.client);
+ }
+};
+
+Manager.prototype.forEach = function(callback, thisArg){
+ var context = (typeof thisArg !== "undefined" && thisArg !== null) ? thisArg : this;
+ var current = this._head;
+
+ while(current && current.client){
+ callback.call(context, current.client);
+ current = current._next;
+ }
+};
131 public/application.css
@@ -0,0 +1,131 @@
+body { background: url(/resources/images/background.png); }
+
+#header { width: 800px; margin: 1em auto; }
+#header h1 { color: #003153; display: inline; font-size: 2em; }
+#header h1 a, header h1 a:link, header h1 a:hover, header h1 a:visited {
+ color: inherit;
+ text-decoration: none;
+}
+#header p { color: #666; font-style: italic; font-size: 0.9em; }
+
+#content {
+ width: 800px;
+ margin: 1em auto 0 auto;
+ position: relative;
+ top: -35px; }
+
+#footer { color: #666; width: 800px; margin: 0 auto; font-size: 0.8em; }
+
+#airport-tabs {
+ float: right;
+ margin: 0;
+ padding: 0;
+ border-left: 1px solid #D0D0D0;
+}
+#airport-tabs li {
+ border: 1px solid #D0D0D0;
+ list-style: none;
+ float: left;
+ height: 2em;
+ line-height: 2em;
+ border-left: none;
+ padding: 0;
+ margin: 0;
+}
+#airport-tabs li a {
+ color: #003153;
+ font-weight: bold;
+ text-decoration: none;
+ margin: 0;
+ padding: 0 20px;
+}
+
+.airport-feed
+{
+ position: relative;
+ top: -1px;
+ padding: 1em;
+ background: #FFF;
+ clear: both;
+ border: 1px solid #D0D0D0;
+}
+
+.airport-feed header { overflow: auto; }
+.airport-feed header h2 { margin: 0; padding: 0; float: left; }
+.airport-feed header p { float: right; font-style: italic; color: #444; }
+
+.airport-feed .weather-bar {
+ padding: 5px 0;
+ margin-bottom: 1em;
+ text-align: center;
+ border-bottom: 1px solid #D0D0D0;
+ border-top: 1px solid #D0D0D0;
+ width: 100%;
+}
+
+.flight-updates { width: 100%; }
+.flight-updates thead { border-bottom: 1px solid #333; }
+.flight-updates td { text-align: center; vertical-align: middle; padding: 10px; height: 27px;}
+.flight-updates tr { border-bottom: 1px dashed black; }
+.flight-updates tr:nth-child(even) { background: #F0F0F0; }
+
+/*.tag {
+ margin-right: 10px;
+ margin-bottom: 10px;
+}*/
+
+.tag {
+ float: right;
+ text-decoration:none;
+ height: 27px;
+ line-height: 26px;
+ color: #fffeff;
+ font-size: 12px;
+ text-shadow: rgba(35,69,90,0.51) 0 -1px 0;
+ padding: 0 4px 0 10px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 2px;
+ -o-border-radius: 2px;
+ -webkit-box-shadow:0px 2px 2px #cddbe2;
+ -moz-box-shadow:0px 2px 2px #cddbe2;
+ -o-box-shadow:0px 2px 2px #cddbe2;
+}
+
+.green {
+ background-image: url(/resources/images/green.png);
+}
+.red {
+ background-image: url(/resources/images/red.png);
+}
+.blue {
+ background-image: url(/resources/images/blue.png);
+}
+.orange {
+ background-image: url(/resources/images/orange.png);
+}
+
+.tag {
+ padding-right: 10px;
+}
+
+.tag a:hover {
+ text-decoration: none;
+}
+
+.tag a:active {
+ position: relative;
+ top: 1px;
+}
+
+.tag span {
+ margin-left: 10px;
+ background-color: rgba(255,254,255,0.3);
+ height: 18px;
+ padding: 0 7px;
+ margin-top: 4px;
+ line-height: 18px;
+ -webkit-border-radius: 25px;
+ -moz-border-radius: 25px;
+ -o-border-radius: 25px;
+ display: inline-block;
+}
63 public/index.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+
+<html lang="en">
+
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <title>Flight Stream</title>
+ <meta name="author" content="Jonathan Bracy">
+
+ <link href="/reset.css" media="screen" rel="stylesheet" type="text/css" />
+ <link href="/text.css" media="screen" rel="stylesheet" type="text/css" />
+ <link href="/application.css" media="screen" rel="stylesheet" type="text/css" />
+
+ <script src="/jquery.js" type="text/javascript"></script>
+ <script src="/application.js" type="text/javascript"></script>
+ </head>
+
+ <body>
+
+ <header id="header">
+ <h1><a href="/">Flight Stream</a></h1>
+ <p>Real Time Flight Updates</p>
+ </header>
+
+ <div id="content">
+ <ul id="airport-tabs">
+ <li><a href="#BOS">BOS</a></li>
+ </ul>
+
+ <section id="BOS" class="airport-feed">
+ <header>
+ <h2>General Edward Lawrence Logan International Airport</h2>
+ <p>No Delays</p>
+ </header>
+
+ <div class="weather-bar">
+ KBOS 110154Z 12004KT 10SM CLR 23/18 A2984 RMK AO2 SLP105 T02280178
+ </div>
+
+ <table class="flight-updates" summary="Flight Updates" cellspacing="0" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Flight</th>
+ <th>Origin</th>
+ <th>Gate</th>
+ <th>Departure Status</th>
+ <th>Destination</th>
+ <th>Gate</th>
+ <th>Arrival Status</th>
+ </tr>
+ </thead>
+ <tbody>
+ </tbody>
+ </table>
+ </section>
+ </div>
+
+ <footer id="footer">Copyright &copy; 2010 Jonathan Bracy</footer>
+
+ </body>
+
+</html>
154 public/jquery.js
@@ -0,0 +1,154 @@
+/*!
+ * jQuery JavaScript Library v1.4.2
+ * http://jquery.com/
+ *
+ * Copyright 2010, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2010, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Sat Feb 13 22:33:48 2010 -0500
+ */
+(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o<i;o++)e(a[o],b,f?d.call(a[o],o,e(a[o],b)):d,j);return a}return i?
+e(a[0],b):w}function J(){return(new Date).getTime()}function Y(){return false}function Z(){return true}function na(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function oa(a){var b,d=[],f=[],e=arguments,j,i,o,k,n,r;i=c.data(this,"events");if(!(a.liveFired===this||!i||!i.live||a.button&&a.type==="click")){a.liveFired=this;var u=i.live.slice(0);for(k=0;k<u.length;k++){i=u[k];i.origType.replace(O,"")===a.type?f.push(i.selector):u.splice(k--,1)}j=c(a.target).closest(f,a.currentTarget);n=0;for(r=
+j.length;n<r;n++)for(k=0;k<u.length;k++){i=u[k];if(j[n].selector===i.selector){o=j[n].elem;f=null;if(i.preType==="mouseenter"||i.preType==="mouseleave")f=c(a.relatedTarget).closest(i.selector)[0];if(!f||f!==o)d.push({elem:o,handleObj:i})}}n=0;for(r=d.length;n<r;n++){j=d[n];a.currentTarget=j.elem;a.data=j.handleObj.data;a.handleObj=j.handleObj;if(j.handleObj.origHandler.apply(j.elem,e)===false){b=false;break}}return b}}function pa(a,b){return"live."+(a&&a!=="*"?a+".":"")+b.replace(/\./g,"`").replace(/ /g,
+"&")}function qa(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function ra(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var f=c.data(a[d++]),e=c.data(this,f);if(f=f&&f.events){delete e.handle;e.events={};for(var j in f)for(var i in f[j])c.event.add(this,j,f[j][i],f[j][i].data)}}})}function sa(a,b,d){var f,e,j;b=b&&b[0]?b[0].ownerDocument||b[0]:s;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===s&&!ta.test(a[0])&&(c.support.checkClone||!ua.test(a[0]))){e=
+true;if(j=c.fragments[a[0]])if(j!==1)f=j}if(!f){f=b.createDocumentFragment();c.clean(a,b,f,d)}if(e)c.fragments[a[0]]=j?f:1;return{fragment:f,cacheable:e}}function K(a,b){var d={};c.each(va.concat.apply([],va.slice(0,b)),function(){d[this]=a});return d}function wa(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var c=function(a,b){return new c.fn.init(a,b)},Ra=A.jQuery,Sa=A.$,s=A.document,T,Ta=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/,
+Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&&
+(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this,
+a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b===
+"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,
+function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(j in e){i=a[j];o=e[j];if(a!==o)if(f&&o&&(c.isPlainObject(o)||c.isArray(o))){i=i&&(c.isPlainObject(i)||
+c.isArray(i))?i:c.isArray(o)?[]:{};a[j]=c.extend(f,i,o)}else if(o!==w)a[j]=o}return a};c.extend({noConflict:function(a){A.$=Sa;if(a)A.jQuery=Ra;return c},isReady:false,ready:function(){if(!c.isReady){if(!s.body)return setTimeout(c.ready,13);c.isReady=true;if(Q){for(var a,b=0;a=Q[b++];)a.call(s,c);Q=null}c.fn.triggerHandler&&c(s).triggerHandler("ready")}},bindReady:function(){if(!xa){xa=true;if(s.readyState==="complete")return c.ready();if(s.addEventListener){s.addEventListener("DOMContentLoaded",
+L,false);A.addEventListener("load",c.ready,false)}else if(s.attachEvent){s.attachEvent("onreadystatechange",L);A.attachEvent("onload",c.ready);var a=false;try{a=A.frameElement==null}catch(b){}s.documentElement.doScroll&&a&&ma()}}},isFunction:function(a){return $.call(a)==="[object Function]"},isArray:function(a){return $.call(a)==="[object Array]"},isPlainObject:function(a){if(!a||$.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!aa.call(a,"constructor")&&!aa.call(a.constructor.prototype,
+"isPrototypeOf"))return false;var b;for(b in a);return b===w||aa.call(a,b)},isEmptyObject:function(a){for(var b in a)return false;return true},error:function(a){throw a;},parseJSON:function(a){if(typeof a!=="string"||!a)return null;a=c.trim(a);if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return A.JSON&&A.JSON.parse?A.JSON.parse(a):(new Function("return "+
+a))();else c.error("Invalid JSON: "+a)},noop:function(){},globalEval:function(a){if(a&&Va.test(a)){var b=s.getElementsByTagName("head")[0]||s.documentElement,d=s.createElement("script");d.type="text/javascript";if(c.support.scriptEval)d.appendChild(s.createTextNode(a));else d.text=a;b.insertBefore(d,b.firstChild);b.removeChild(d)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,b,d){var f,e=0,j=a.length,i=j===w||c.isFunction(a);if(d)if(i)for(f in a){if(b.apply(a[f],
+d)===false)break}else for(;e<j;){if(b.apply(a[e++],d)===false)break}else if(i)for(f in a){if(b.call(a[f],f,a[f])===false)break}else for(d=a[0];e<j&&b.call(d,e,d)!==false;d=a[++e]);return a},trim:function(a){return(a||"").replace(Wa,"")},makeArray:function(a,b){b=b||[];if(a!=null)a.length==null||typeof a==="string"||c.isFunction(a)||typeof a!=="function"&&a.setInterval?ba.call(b,a):c.merge(b,a);return b},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var d=0,f=b.length;d<f;d++)if(b[d]===
+a)return d;return-1},merge:function(a,b){var d=a.length,f=0;if(typeof b.length==="number")for(var e=b.length;f<e;f++)a[d++]=b[f];else for(;b[f]!==w;)a[d++]=b[f++];a.length=d;return a},grep:function(a,b,d){for(var f=[],e=0,j=a.length;e<j;e++)!d!==!b(a[e],e)&&f.push(a[e]);return f},map:function(a,b,d){for(var f=[],e,j=0,i=a.length;j<i;j++){e=b(a[j],j,d);if(e!=null)f[f.length]=e}return f.concat.apply([],f)},guid:1,proxy:function(a,b,d){if(arguments.length===2)if(typeof b==="string"){d=a;a=d[b];b=w}else if(b&&
+!c.isFunction(b)){d=b;b=w}if(!b&&a)b=function(){return a.apply(d||this,arguments)};if(a)b.guid=a.guid=a.guid||b.guid||c.guid++;return b},uaMatch:function(a){a=a.toLowerCase();a=/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||!/compatible/.test(a)&&/(mozilla)(?:.*? rv:([\w.]+))?/.exec(a)||[];return{browser:a[1]||"",version:a[2]||"0"}},browser:{}});P=c.uaMatch(P);if(P.browser){c.browser[P.browser]=true;c.browser.version=P.version}if(c.browser.webkit)c.browser.safari=
+true;if(ya)c.inArray=function(a,b){return ya.call(b,a)};T=c(s);if(s.addEventListener)L=function(){s.removeEventListener("DOMContentLoaded",L,false);c.ready()};else if(s.attachEvent)L=function(){if(s.readyState==="complete"){s.detachEvent("onreadystatechange",L);c.ready()}};(function(){c.support={};var a=s.documentElement,b=s.createElement("script"),d=s.createElement("div"),f="script"+J();d.style.display="none";d.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
+var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected,
+parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent=
+false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n=
+s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true,
+applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando];
+else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,
+a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===
+w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i,
+cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1)if(e.className){for(var j=" "+e.className+" ",
+i=e.className,o=0,k=b.length;o<k;o++)if(j.indexOf(" "+b[o]+" ")<0)i+=" "+b[o];e.className=c.trim(i)}else e.className=a}return this},removeClass:function(a){if(c.isFunction(a))return this.each(function(k){var n=c(this);n.removeClass(a.call(this,k,n.attr("class")))});if(a&&typeof a==="string"||a===w)for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1&&e.className)if(a){for(var j=(" "+e.className+" ").replace(Aa," "),i=0,o=b.length;i<o;i++)j=j.replace(" "+b[i]+" ",
+" ");e.className=c.trim(j)}else e.className=""}return this},toggleClass:function(a,b){var d=typeof a,f=typeof b==="boolean";if(c.isFunction(a))return this.each(function(e){var j=c(this);j.toggleClass(a.call(this,e,j.attr("class"),b),b)});return this.each(function(){if(d==="string")for(var e,j=0,i=c(this),o=b,k=a.split(ca);e=k[j++];){o=f?o:!i.hasClass(e);i[o?"addClass":"removeClass"](e)}else if(d==="undefined"||d==="boolean"){this.className&&c.data(this,"__className__",this.className);this.className=
+this.className||a===false?"":c.data(this,"__className__")||""}})},hasClass:function(a){a=" "+a+" ";for(var b=0,d=this.length;b<d;b++)if((" "+this[b].className+" ").replace(Aa," ").indexOf(a)>-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j<d;j++){var i=
+e[j];if(i.selected){a=c(i).val();if(b)return a;f.push(a)}}return f}if(Ba.test(b.type)&&!c.support.checkOn)return b.getAttribute("value")===null?"on":b.value;return(b.value||"").replace(Za,"")}return w}var o=c.isFunction(a);return this.each(function(k){var n=c(this),r=a;if(this.nodeType===1){if(o)r=a.call(this,k,n.val());if(typeof r==="number")r+="";if(c.isArray(r)&&Ba.test(this.type))this.checked=c.inArray(n.val(),r)>=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected=
+c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");
+a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g,
+function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split(".");
+k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a),
+C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B<r.length;B++){u=r[B];if(d.guid===u.guid){if(i||k.test(u.namespace)){f==null&&r.splice(B--,1);n.remove&&n.remove.call(a,u)}if(f!=
+null)break}}if(r.length===0||f!=null&&r.length===1){if(!n.teardown||n.teardown.call(a,o)===false)Ca(a,e,z.handle);delete C[e]}}else for(var B=0;B<r.length;B++){u=r[B];if(i||k.test(u.namespace)){c.event.remove(a,n,u.handler,B);r.splice(B--,1)}}}if(c.isEmptyObject(C)){if(b=z.handle)b.elem=null;delete z.events;delete z.handle;c.isEmptyObject(z)&&c.removeData(a)}}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[G]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type=
+e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&&
+f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;
+if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e<j;e++){var i=d[e];if(b||f.test(i.namespace)){a.handler=i.handler;a.data=i.data;a.handleObj=i;i=i.handler.apply(this,arguments);if(i!==w){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}}return a.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+fix:function(a){if(a[G])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement||s;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=s.documentElement;d=s.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop||
+d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&&a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==w)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a){c.event.add(this,a.origType,c.extend({},a,{handler:oa}))},remove:function(a){var b=true,d=a.origType.replace(O,"");c.each(c.data(this,
+"events").live||[],function(){if(d===this.origType.replace(O,""))return b=false});b&&c.event.remove(this,a.origType,oa)}},beforeunload:{setup:function(a,b,d){if(this.setInterval)this.onbeforeunload=d;return false},teardown:function(a,b){if(this.onbeforeunload===b)this.onbeforeunload=null}}}};var Ca=s.removeEventListener?function(a,b,d){a.removeEventListener(b,d,false)}:function(a,b,d){a.detachEvent("on"+b,d)};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent=
+a;this.type=a.type}else this.type=a;this.timeStamp=J();this[G]=true};c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=Z;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped=Z;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=Z;this.stopPropagation()},isDefaultPrevented:Y,isPropagationStopped:Y,
+isImmediatePropagationStopped:Y};var Da=function(a){var b=a.relatedTarget;try{for(;b&&b!==this;)b=b.parentNode;if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}}catch(d){}},Ea=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ea:Da,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ea:Da)}}});if(!c.support.submitBubbles)c.event.special.submit=
+{setup:function(){if(this.nodeName.toLowerCase()!=="form"){c.event.add(this,"click.specialSubmit",function(a){var b=a.target,d=b.type;if((d==="submit"||d==="image")&&c(b).closest("form").length)return na("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,d=b.type;if((d==="text"||d==="password")&&c(b).closest("form").length&&a.keyCode===13)return na("submit",this,arguments)})}else return false},teardown:function(){c.event.remove(this,".specialSubmit")}};
+if(!c.support.changeBubbles){var da=/textarea|input|select/i,ea,Fa=function(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex>-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",
+e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,
+"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a,
+d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j<o;j++)c.event.add(this[j],d,i,f)}return this}});c.fn.extend({unbind:function(a,b){if(typeof a==="object"&&
+!a.preventDefault)for(var d in a)this.unbind(d,a[d]);else{d=0;for(var f=this.length;d<f;d++)c.event.remove(this[d],a,b)}return this},delegate:function(a,b,d,f){return this.live(b,d,f,a)},undelegate:function(a,b,d){return arguments.length===0?this.unbind("live"):this.die(b,null,d,a)},trigger:function(a,b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}},
+toggle:function(a){for(var b=arguments,d=1;d<b.length;)c.proxy(a,b[d++]);return this.click(c.proxy(a,function(f){var e=(c.data(this,"lastToggle"+a.guid)||0)%d;c.data(this,"lastToggle"+a.guid,e+1);f.preventDefault();return b[e].apply(this,arguments)||false}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var Ga={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};c.each(["live","die"],function(a,b){c.fn[b]=function(d,f,e,j){var i,o=0,k,n,r=j||this.selector,
+u=j?this:c(this.context);if(c.isFunction(f)){e=f;f=w}for(d=(d||"").split(" ");(i=d[o++])!=null;){j=O.exec(i);k="";if(j){k=j[0];i=i.replace(O,"")}if(i==="hover")d.push("mouseenter"+k,"mouseleave"+k);else{n=i;if(i==="focus"||i==="blur"){d.push(Ga[i]+k);i+=k}else i=(Ga[i]||i)+k;b==="live"?u.each(function(){c.event.add(this,pa(i,r),{data:f,selector:r,handler:e,origType:i,origHandler:e,preType:n})}):u.unbind(pa(i,r),e)}}return this}});c.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),
+function(a,b){c.fn[b]=function(d){return d?this.bind(b,d):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});A.attachEvent&&!A.addEventListener&&A.attachEvent("onunload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}});(function(){function a(g){for(var h="",l,m=0;g[m];m++){l=g[m];if(l.nodeType===3||l.nodeType===4)h+=l.nodeValue;else if(l.nodeType!==8)h+=a(l.childNodes)}return h}function b(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];
+if(t){t=t[g];for(var y=false;t;){if(t.sizcache===l){y=m[t.sizset];break}if(t.nodeType===1&&!p){t.sizcache=l;t.sizset=q}if(t.nodeName.toLowerCase()===h){y=t;break}t=t[g]}m[q]=y}}}function d(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===l){y=m[t.sizset];break}if(t.nodeType===1){if(!p){t.sizcache=l;t.sizset=q}if(typeof h!=="string"){if(t===h){y=true;break}}else if(k.filter(h,[t]).length>0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
+e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift();
+t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D||
+g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h<g.length;h++)g[h]===g[h-1]&&g.splice(h--,1)}return g};k.matches=function(g,h){return k(g,null,null,h)};k.find=function(g,h,l){var m,q;if(!g)return[];
+for(var p=0,v=n.order.length;p<v;p++){var t=n.order[p];if(q=n.leftMatch[t].exec(g)){var y=q[1];q.splice(1,1);if(y.substr(y.length-1)!=="\\"){q[1]=(q[1]||"").replace(/\\/g,"");m=n.find[t](q,h,l);if(m!=null){g=g.replace(n.match[t],"");break}}}}m||(m=h.getElementsByTagName("*"));return{set:m,expr:g}};k.filter=function(g,h,l,m){for(var q=g,p=[],v=h,t,y,S=h&&h[0]&&x(h[0]);g&&h.length;){for(var H in n.filter)if((t=n.leftMatch[H].exec(g))!=null&&t[2]){var M=n.filter[H],I,D;D=t[1];y=false;t.splice(1,1);if(D.substr(D.length-
+1)!=="\\"){if(v===p)p=[];if(n.preFilter[H])if(t=n.preFilter[H](t,v,l,p,m,S)){if(t===true)continue}else y=I=true;if(t)for(var U=0;(D=v[U])!=null;U++)if(D){I=M(D,t,U,v);var Ha=m^!!I;if(l&&I!=null)if(Ha)y=true;else v[U]=false;else if(Ha){p.push(D);y=true}}if(I!==w){l||(v=p);g=g.replace(n.match[H],"");if(!y)return[];break}}}if(g===q)if(y==null)k.error(g);else break;q=g}return v};k.error=function(g){throw"Syntax error, unrecognized expression: "+g;};var n=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
+CLASS:/\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(g){return g.getAttribute("href")}},
+relative:{"+":function(g,h){var l=typeof h==="string",m=l&&!/\W/.test(h);l=l&&!m;if(m)h=h.toLowerCase();m=0;for(var q=g.length,p;m<q;m++)if(p=g[m]){for(;(p=p.previousSibling)&&p.nodeType!==1;);g[m]=l||p&&p.nodeName.toLowerCase()===h?p||false:p===h}l&&k.filter(h,g,true)},">":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m<q;m++){var p=g[m];if(p){l=p.parentNode;g[m]=l.nodeName.toLowerCase()===h?l:false}}}else{m=0;for(q=g.length;m<q;m++)if(p=g[m])g[m]=
+l?p.parentNode:p.parentNode===h;l&&k.filter(h,g,true)}},"":function(g,h,l){var m=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("parentNode",h,m,g,p,l)},"~":function(g,h,l){var m=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("previousSibling",h,m,g,p,l)}},find:{ID:function(g,h,l){if(typeof h.getElementById!=="undefined"&&!l)return(g=h.getElementById(g[1]))?[g]:[]},NAME:function(g,h){if(typeof h.getElementsByName!=="undefined"){var l=[];
+h=h.getElementsByName(g[1]);for(var m=0,q=h.length;m<q;m++)h[m].getAttribute("name")===g[1]&&l.push(h[m]);return l.length===0?null:l}},TAG:function(g,h){return h.getElementsByTagName(g[1])}},preFilter:{CLASS:function(g,h,l,m,q,p){g=" "+g[1].replace(/\\/g,"")+" ";if(p)return g;p=0;for(var v;(v=h[p])!=null;p++)if(v)if(q^(v.className&&(" "+v.className+" ").replace(/[\t\n]/g," ").indexOf(g)>=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},
+CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m,
+g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},
+text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},
+setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return h<l[3]-0},gt:function(g,h,l){return h>l[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h=
+h[3];l=0;for(m=h.length;l<m;l++)if(h[l]===g)return false;return true}else k.error("Syntax error, unrecognized expression: "+q)},CHILD:function(g,h){var l=h[1],m=g;switch(l){case "only":case "first":for(;m=m.previousSibling;)if(m.nodeType===1)return false;if(l==="first")return true;m=g;case "last":for(;m=m.nextSibling;)if(m.nodeType===1)return false;return true;case "nth":l=h[2];var q=h[3];if(l===1&&q===0)return true;h=h[0];var p=g.parentNode;if(p&&(p.sizcache!==h||!g.nodeIndex)){var v=0;for(m=p.firstChild;m;m=
+m.nextSibling)if(m.nodeType===1)m.nodeIndex=++v;p.sizcache=h}g=g.nodeIndex-q;return l===0?g===0:g%l===0&&g/l>=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m===
+"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g,
+h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l<m;l++)h.push(g[l]);else for(l=0;g[l];l++)h.push(g[l]);return h}}var B;if(s.documentElement.compareDocumentPosition)B=function(g,h){if(!g.compareDocumentPosition||
+!h.compareDocumentPosition){if(g==h)i=true;return g.compareDocumentPosition?-1:1}g=g.compareDocumentPosition(h)&4?-1:g===h?0:1;if(g===0)i=true;return g};else if("sourceIndex"in s.documentElement)B=function(g,h){if(!g.sourceIndex||!h.sourceIndex){if(g==h)i=true;return g.sourceIndex?-1:1}g=g.sourceIndex-h.sourceIndex;if(g===0)i=true;return g};else if(s.createRange)B=function(g,h){if(!g.ownerDocument||!h.ownerDocument){if(g==h)i=true;return g.ownerDocument?-1:1}var l=g.ownerDocument.createRange(),m=
+h.ownerDocument.createRange();l.setStart(g,0);l.setEnd(g,0);m.setStart(h,0);m.setEnd(h,0);g=l.compareBoundaryPoints(Range.START_TO_END,m);if(g===0)i=true;return g};(function(){var g=s.createElement("div"),h="script"+(new Date).getTime();g.innerHTML="<a name='"+h+"'/>";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&&
+q.getAttributeNode("id").nodeValue===m[1]?[q]:w