From 0ce9c6f550e842b89d5534215a5f1f6831fb7b1c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 21:43:08 +0000 Subject: [PATCH 1/5] Modernize the build and convert the client to ES modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client was ~10k lines of ES5 AMD modules loaded by a vendored RequireJS 2.1.8, built by Grunt 0.4 against node ~0.12.7, styled with LESS 1.3, and tested by doctest.js under PhantomJS. `npm install` no longer resolved on a current Node at all. Toolchain: - esbuild replaces Grunt's copylib/maybeless/substitute/config-requirejs chain. One build/build.mjs produces dist/togetherjs.js (IIFE, still a drop-in + + + diff --git a/togetherjs/tests/manual/index.html b/examples/manual/index.html similarity index 100% rename from togetherjs/tests/manual/index.html rename to examples/manual/index.html diff --git a/togetherjs/tests/manual/multi-textarea-focus.html b/examples/manual/multi-textarea-focus.html similarity index 100% rename from togetherjs/tests/manual/multi-textarea-focus.html rename to examples/manual/multi-textarea-focus.html diff --git a/togetherjs/tests/manual/youtube-video.html b/examples/manual/youtube-video.html similarity index 100% rename from togetherjs/tests/manual/youtube-video.html rename to examples/manual/youtube-video.html diff --git a/togetherjs/recorder.html b/examples/recorder.html similarity index 100% rename from togetherjs/recorder.html rename to examples/recorder.html diff --git a/togetherjs/walkabout.html b/examples/walkabout.html similarity index 100% rename from togetherjs/walkabout.html rename to examples/walkabout.html diff --git a/hub/server.js b/hub/server.js deleted file mode 100644 index 58f7448f9..000000000 --- a/hub/server.js +++ /dev/null @@ -1,433 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// New Relic Server monitoring support -if ( process.env.NEW_RELIC_HOME ) { - require("newrelic"); -} - -var SAMPLE_STATS_INTERVAL = 60*1000; // 1 minute -var SAMPLE_LOAD_INTERVAL = 5*60*1000; // 5 minutes -var EMPTY_ROOM_LOG_TIMEOUT = 3*60*1000; // 3 minutes -var WEBSOCKET_COMPAT = true; - -var WebSocketServer = WEBSOCKET_COMPAT ? - require("./websocket-compat").server : - require("websocket").server; -var http = require('http'); -var parseUrl = require('url').parse; -var fs = require('fs'); - -// FIXME: not sure what logger to use -//var logger = require('../../lib/logger'); - -// LOG_LEVEL values: -// 0: show everything (including debug) -// 1: don't show debug, do show logger.log -// 2: don't show logger.log and debug, do show logger.info (and STATS) -// 3: don't show info, do show warn -// 4: don't show warn, do show error -// 5: don't show anything -// Stats are at level 2 - -var thisSource = "// What follows is the source for the server.\n" + - "// Obviously we can't prove this is the actual source, but if it isn't then we're \n" + - "// a bunch of lying liars, so at least you have us on record.\n\n" + - fs.readFileSync(__filename); - -var Logger = function (level, filename, stdout) { - this.level = level; - this.filename = filename; - this.stdout = !!stdout; - this._open(); - process.on("SIGUSR2", (function () { - this._open(); - }).bind(this)); -}; - -Logger.prototype = { - - write: function () { - if (this.stdout) { - console.log.apply(console, arguments); - } - if (this.file) { - var s = []; - for (var i=0; i EMPTY_ROOM_LOG_TIMEOUT) { - logStats(id, connectionStats[id]); - delete connectionStats[id]; - continue; - } - var totalClients = countClients(connectionStats[id].clients); - var connections = 0; - if (allConnections[id]) { - connections = allConnections[id].length; - } - connectionStats[id].sample.push({ - time: Date.now(), - totalClients: totalClients, - connections: connections - }); - } -}, SAMPLE_STATS_INTERVAL); - -setInterval(function () { - var load = getLoad(); - load.time = Date.now(); - logger.info("LOAD", JSON.stringify(load)); -}, SAMPLE_LOAD_INTERVAL); - -function getLoad() { - var sessions = 0; - var connections = 0; - var empty = 0; - var solo = 0; - for (var id in allConnections) { - if (allConnections[id].length) { - sessions++; - connections += allConnections[id].length; - if (allConnections[id].length == 1) { - solo++; - } - } else { - empty++; - } - } - return { - sessions: sessions, - connections: connections, - empty: empty, - solo: solo - }; -} - -function countClients(clients) { - var n = 0; - for (var clientId in clients) { - n++; - } - return n; -} - -function logStats(id, stats) { - logger.info("STATS", JSON.stringify({ - id: id, - created: stats.created, - sample: stats.sample, - totalClients: countClients(stats.clients), - totalMessageChars: stats.totalMessageChars, - totalMessages: stats.totalMessages, - domain: stats.firstDomain || null, - domainCount: countClients(stats.domains), - urls: countClients(stats.urls) - })); -} - -if (require.main == module) { - var ops = require('optimist') - .usage("Usage: $0 [--port 8080] [--host=localhost] [--log=filename] [--log-level=N]") - .describe("port", "The port to server on (default $HUB_SERVER_PORT, $PORT, $VCAP_APP_PORT, or 8080") - .describe("host", "The interface to serve on (default $HUB_SERVER_HOST, $HOST, $VCAP_APP_HOST, 127.0.0.1). Use 0.0.0.0 to make it public") - .describe("log-level", "The level of logging to do, from 0 (very verbose) to 5 (nothing) (default $LOG_LEVEL or 0)") - .describe("log", "A file to log to (default $LOG_FILE or stdout)") - .describe("stdout", "Log to both stdout and the log file"); - var port = ops.argv.port || process.env.HUB_SERVER_PORT || process.env.VCAP_APP_PORT || - process.env.PORT || 8080; - var host = ops.argv.host || process.env.HUB_SERVER_HOST || process.env.VCAP_APP_HOST || - process.env.HOST || '127.0.0.1'; - var logLevel = process.env.LOG_LEVEL || 0; - var logFile = process.env.LOG_FILE || ops.argv.log; - var stdout = ops.argv.stdout || !logFile; - if (ops.argv['log-level']) { - logLevel = parseInt(ops.argv['log-level'], 10); - } - logger = new Logger(logLevel, logFile, stdout); - if (ops.argv.h || ops.argv.help) { - console.log(ops.help()); - process.exit(); - } else { - startServer(port, host); - } -} - -exports.startServer = startServer; diff --git a/hub/websocket-compat.js b/hub/websocket-compat.js deleted file mode 100644 index 6a85209e8..000000000 --- a/hub/websocket-compat.js +++ /dev/null @@ -1,150 +0,0 @@ -/* - * A hacked websocket module which retains compatibility with the old - * Hixie-76 version of the standard, needed for phantom JS (and, - * presumably, very old browsers). - * - * This file released into the public domain - * by C. Scott Ananian 2014-08-26 - * - * Based on https://gist.github.com/toshirot/1428579 - */ -var events = require("events"); -var util = require("util"); - -var WebSocketRequest = require('websocket').request; -var WebSocketServer = require('websocket').server; - -// Copy helpers from WebSocketServer to WebSocketRequest - -WebSocketRequest.prototype.connections = []; -WebSocketRequest.prototype.handleRequestAccepted = - WebSocketServer.prototype.handleRequestAccepted; -WebSocketRequest.prototype.handleConnectionClose = - WebSocketServer.prototype.handleConnectionClose; -WebSocketRequest.prototype.broadcastUTF = - WebSocketServer.prototype.broadcastUTF; - -var miksagoServerFactory = require('websocket-server'); -var miksagoConnection = require('../node_modules/websocket-server/lib/ws/connection'); - -var CompatWebSocketServer = function(options) { - events.EventEmitter.call(this); // superclass constructor - var self = this; - var handleConnection; - - // node-websocket-server (hixie-75 and hixie-76 support) - var miksagoServer = miksagoServerFactory.createServer(); - miksagoServer.server = options.httpServer; - miksagoServer.addListener('connection', function(connection) { - // Add remoteAddress property - connection.remoteAddress = connection._socket.remoteAddress; - - // We want to use "sendUTF" regardless of the server implementation - connection.sendUTF = connection.send; - handleConnection(connection); - }); - - // WebSocket-Node config (modern websocket support) - var wsServerConfig = { - // All options *except* 'httpServer' are required when bypassing - // WebSocketServer. - maxReceivedFrameSize: options.maxReceivedFrameSize || 0x10000, - maxReceivedMessageSize: options.maxReceivedMessageSize || 0x100000, - fragmentOutgoingMessages: true, - fragmentationThreshold: 0x4000, - keepalive: true, - keepaliveInterval: 20000, - assembleFragments: true, - // autoAcceptConnections is not applicable when bypassing WebSocketServer - // autoAcceptConnections: false, - disableNagleAlgorithm: true, - closeTimeout: 5000 - }; - - // Handle the upgrade event ourselves instead of using WebSocketServer - var wsRequest={}; - options.httpServer.on('upgrade', function(req, socket, head) { - if (typeof req.headers['sec-websocket-version'] !== 'undefined') { - - // WebSocket hybi-08/-09/-10 connection (WebSocket-Node) - wsRequest = new WebSocketRequest(socket, req, wsServerConfig); - try { - wsRequest.readHandshake(); - } catch (e) { - wsRequest.reject( - e.httpCode ? e.httpCode : 400, - e.message, - e.headers - ); - return; - } - wsRequest.once('requestAccepted', function(connection) { - wsRequest.handleRequestAccepted(connection); - }); - self.emit('request', wsRequest); - - } else { - - // WebSocket hixie-75/-76/hybi-00 connection (node-websocket-server) - if (req.method === 'GET' && - (req.headers.upgrade && req.headers.connection) && - req.headers.upgrade.toLowerCase() === 'websocket' && - req.headers.connection.toLowerCase() === 'upgrade') { - new miksagoConnection( - miksagoServer.manager, miksagoServer.options, req, socket, head - ); - } - } - }); - - // A connection handler for old-style websockets - handleConnection = function(connection) { - // fake a request - self.emit('request', new CompatRequest(self, connection)); - }; -}; -util.inherits(CompatWebSocketServer, events.EventEmitter); - -var CompatRequest = function(server, connection) { - this._server = server; - this._connection = connection; - this.origin = connection._options.origin || '*'; - this.httpRequest = connection._req; - // create wrapper right away in order to install event handlers promptly - this._connectionWrapper = new CompatConnection(server, connection); -}; -CompatRequest.prototype.reject = function(code, message) { - this._connection.reject(message || "no reason"); -}; -CompatRequest.prototype.accept = function(proto, origin) { - // this is faked: we've already accepted the connection - return this._connectionWrapper; -}; - -var CompatConnection = function(server, connection) { - var self = this; - events.EventEmitter.call(this); // superclass constructor - - this._server = server; - this._connection = connection; - this.remoteAddress = connection.remoteAddress; - - connection.addListener('message', function(wsMessage) { - // make the argument compatible with WebSocket-Node - self.emit('message', { - type: 'utf8', - utf8Data: wsMessage - }); - }); - - connection.addListener('close', function() { - self.emit('close'); - }); -}; -util.inherits(CompatConnection, events.EventEmitter); - -CompatConnection.prototype.sendUTF = function(message) { - return this._connection.sendUTF(message); -}; - -module.exports.server = CompatWebSocketServer; diff --git a/package-lock.json b/package-lock.json index 60c8cdcb4..f6c7aa4af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2014 +1,4085 @@ { "name": "togetherjs", - "version": "0.4.0a", - "lockfileVersion": 1, + "version": "0.5.0", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "ajv": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.0.tgz", - "integrity": "sha1-6yhAdG6dxIvV4GOjbj/UAMXqtak=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "amdefine": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-0.0.8.tgz", - "integrity": "sha1-NNyMmB5qyzvhhTvvjw7JSjnVW6A=" - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "packages": { + "": { + "name": "togetherjs", + "version": "0.5.0", + "license": "MPL-2.0", + "dependencies": { + "jquery": "^3.7.1", + "tinycolor2": "^1.6.0" + }, + "devDependencies": { + "@playwright/test": "^1.49.0", + "esbuild": "^0.25.0", + "eslint": "^9.17.0", + "globals": "^15.14.0", + "jsdom": "^25.0.1", + "prettier": "^3.4.2", + "vitest": "^3.2.4", + "ws": "^8.21.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", "dev": true }, - "argparse": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", - "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", - "requires": { - "underscore": "1.7.0", - "underscore.string": "2.4.0" + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" }, - "dependencies": { - "underscore": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" - }, - "underscore.string": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", - "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true } } }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "async": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", - "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=" + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", "optional": true, - "requires": { - "tweetnacl": "0.14.5" + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "bl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", - "integrity": "sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4=", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "requires": { - "readable-stream": "2.0.6" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.0" + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "buffer-crc32": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.1.1.tgz", - "integrity": "sha1-fhENyZU5CKt8MqzccMn5RbHLxSY=" - }, - "bunyan": { - "version": "0.14.6", - "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.14.6.tgz", - "integrity": "sha1-zNnPCu2Og/hFPW3XLJ69jY3kVQs=" - }, - "bytes": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-0.1.0.tgz", - "integrity": "sha1-xXSBIigSbWNp0VdpJahXnbP45aI=" + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "clean-css": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-2.0.8.tgz", - "integrity": "sha1-6TfN/cxXgaAIF67EB56Fs+wVeiA=", - "optional": true, - "requires": { - "commander": "2.0.0" + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" }, - "dependencies": { - "commander": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.0.0.tgz", - "integrity": "sha1-0bhvkB+LZL2UG96tr5JFMDk76Sg=", - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "cli": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/cli/-/cli-0.4.5.tgz", - "integrity": "sha1-ePlIXNFhtWbppsctcXDEJw6B22E=", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "requires": { - "glob": "3.1.21" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "coffee-script": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", - "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=" - }, - "colors": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", - "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=" - }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "commander": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", - "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=" - }, - "concat-stream": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz", - "integrity": "sha1-U/fUPFHF5D+ByP3QMyHGMb5o1hE=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "readable-stream": "2.0.6", - "typedarray": "0.0.6" - } - }, - "connect": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/connect/-/connect-2.7.2.tgz", - "integrity": "sha1-EXmUY72qr5nV+b7xM78kILJuJoA=", - "requires": { - "buffer-crc32": "0.1.1", - "bytes": "0.1.0", - "cookie": "0.0.5", - "cookie-signature": "0.0.1", - "debug": "3.1.0", - "formidable": "1.0.11", - "fresh": "0.1.0", - "pause": "0.0.1", - "qs": "0.5.1", - "send": "0.1.0" - } - }, - "cookie": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.0.5.tgz", - "integrity": "sha1-+az521frdWjJ/MWWJWt7si4wfIE=" - }, - "cookie-signature": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-0.0.1.tgz", - "integrity": "sha1-E9NgO1z2O++/haiAHjeqkA20aYU=" + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "core-util-is": { + "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.0" - } - } + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "csslint": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/csslint/-/csslint-0.9.10.tgz", - "integrity": "sha1-xBuptrn+x3vKhxEuces6Ig71m8Q=", - "dev": true + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "cycle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" } }, - "dateformat": { - "version": "1.0.2-1.2.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", - "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=" + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "docco": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/docco/-/docco-0.6.3.tgz", - "integrity": "sha1-xHtYI9eVY9b8Or1J895ImG5VIu4=", - "dev": true, - "requires": { - "commander": "0.6.1", - "fs-extra": "4.0.2", - "highlight.js": "9.12.0", - "marked": "0.3.6", - "underscore": "1.4.4" - }, - "dependencies": { - "highlight.js": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", - "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=", - "dev": true - } + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" } }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "ecstatic": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-0.4.13.tgz", - "integrity": "sha1-nLbq/+IRuchO+z9VPN4sMAJxeyk=", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "requires": { - "ent": "0.0.7", - "mime": "1.2.6", - "optimist": "0.3.7" - }, + "license": "MIT", "dependencies": { - "optimist": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", - "dev": true, - "requires": { - "wordwrap": "0.0.3" - } - } + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "ejs": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.5.7.tgz", - "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=" - }, - "ejs-locals": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ejs-locals/-/ejs-locals-1.0.2.tgz", - "integrity": "sha1-ubMg/2kzFUEF+g7taD6mTWeAiM4=" + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" }, - "ent": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ent/-/ent-0.0.7.tgz", - "integrity": "sha1-g11Of556jUkhxpLpAQ7JdtpemUk=", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" }, - "esprima": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" - }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=" - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" - }, - "express": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/express/-/express-3.0.6.tgz", - "integrity": "sha1-0nT8uGi5V4i/SvYhaNddE/132LQ=", - "requires": { - "buffer-crc32": "0.1.1", - "commander": "0.6.1", - "connect": "2.7.2", - "cookie": "0.0.5", - "cookie-signature": "0.0.1", - "debug": "3.1.0", - "fresh": "0.1.0", - "methods": "0.0.1", - "mkdirp": "0.3.3", - "range-parser": "0.0.4", - "send": "0.1.0" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extract-zip": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.5.0.tgz", - "integrity": "sha1-ksz22B73Cp+kwXRxFMzvbYaIpsQ=", - "dev": true, - "requires": { - "concat-stream": "1.5.0", - "debug": "0.7.4", - "mkdirp": "0.5.0", - "yauzl": "2.4.1" - }, - "dependencies": { - "debug": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", - "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", - "dev": true - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", - "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true } } }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "eyes": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" - }, - "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + "node_modules/jsdom/node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } }, - "faye-websocket": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.4.4.tgz", - "integrity": "sha1-wUxbO/FNdBf/v9mQwKdJXNnzN7w=", - "dev": true + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" }, - "fd-slicer": { + "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, - "requires": { - "pend": "1.2.0" - } + "license": "MIT" }, - "fileset": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-0.1.8.tgz", - "integrity": "sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E=", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "requires": { - "glob": "3.1.21", - "minimatch": "0.2.14" + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" } }, - "findup-sync": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", - "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", - "requires": { - "glob": "3.2.11", - "lodash": "2.4.2" - }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", "dependencies": { - "glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", - "requires": { - "inherits": "2.0.3", - "minimatch": "0.3.0" - } - }, - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" - }, - "minimatch": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - } + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "formidable": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.11.tgz", - "integrity": "sha1-aPYzJaA15kS297s9ESQ7l2HeGzA=" - }, - "freeport": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/freeport/-/freeport-1.0.5.tgz", - "integrity": "sha1-JV6KuEFwwzuoXZkOghrl9KGpvF0=", - "dev": true + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" }, - "fresh": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.1.0.tgz", - "integrity": "sha1-A+SwF4Qk5MLV0ZpU2IFM3JeTSFA=" + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" }, - "fs-extra": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.2.tgz", - "integrity": "sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s=", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "4.0.0", - "universalify": "0.1.1" - }, + "license": "MIT", "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - } + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "gaze": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.3.4.tgz", - "integrity": "sha1-X5S92gr+U7xxCWm81vKCVI1gwnk=", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, - "requires": { - "fileset": "0.1.8", - "minimatch": "0.2.14" + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "requires": { - "is-property": "1.0.2" + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "getobject": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", - "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - } - }, - "glob": { - "version": "3.1.21", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", - "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", - "requires": { - "graceful-fs": "1.2.3", - "inherits": "1.0.2", - "minimatch": "0.2.14" - }, - "dependencies": { - "inherits": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", - "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=" + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "graceful-fs": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=" - }, - "grunt": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", - "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", - "requires": { - "async": "0.1.22", - "coffee-script": "1.3.3", - "colors": "0.6.2", - "dateformat": "1.0.2-1.2.3", - "eventemitter2": "0.4.14", - "exit": "0.1.2", - "findup-sync": "0.1.3", - "getobject": "0.1.0", - "glob": "3.1.21", - "grunt-legacy-log": "0.1.3", - "grunt-legacy-util": "0.2.0", - "hooker": "0.2.3", - "iconv-lite": "0.2.11", - "js-yaml": "2.0.5", - "lodash": "0.9.2", - "minimatch": "0.2.14", - "nopt": "1.0.10", - "rimraf": "2.2.8", - "underscore.string": "2.2.1", - "which": "1.0.9" - } - }, - "grunt-amd-check": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/grunt-amd-check/-/grunt-amd-check-0.5.2.tgz", - "integrity": "sha1-UjwyoeZaUWI4sLeOj+0txUypJj0=", - "requires": { - "grunt": "0.4.5", - "grunt-lib-amd": "0.1.3", - "underscore": "1.4.4" - } - }, - "grunt-contrib-copy": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-0.4.1.tgz", - "integrity": "sha1-8HU7QK4hu3BtrvsLKZ4DzfX6nW4=", - "dev": true - }, - "grunt-contrib-csslint": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/grunt-contrib-csslint/-/grunt-contrib-csslint-0.1.2.tgz", - "integrity": "sha1-UFo/YW1MV5AUrkmgiHIU1dCZmrY=", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, - "requires": { - "csslint": "0.9.10" - } + "license": "MIT" }, - "grunt-contrib-jshint": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-0.4.3.tgz", - "integrity": "sha1-79bO/oT8rBQ+QlE82uc2eMUiB5M=", + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", "dev": true, - "requires": { - "jshint": "1.1.0" - } + "license": "MIT" }, - "grunt-contrib-less": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-0.5.2.tgz", - "integrity": "sha1-XoFCpB97nj8OCpd0P9WwkYm7oW8=", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, - "requires": { - "less": "1.3.3" + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" } }, - "grunt-contrib-requirejs": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/grunt-contrib-requirejs/-/grunt-contrib-requirejs-0.4.4.tgz", - "integrity": "sha1-h/IWWpgeSKRdIvjMUpnQk0AxuXI=", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "requires": { - "requirejs": "2.1.22" + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "grunt-contrib-watch": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-0.4.4.tgz", - "integrity": "sha1-Mg/HfFzTO3PlRGzZ7ZGWEhFqpjw=", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "requires": { - "gaze": "0.3.4", - "tiny-lr": "0.0.4" + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "grunt-http-server": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/grunt-http-server/-/grunt-http-server-0.0.5.tgz", - "integrity": "sha1-p4kKitivHdnlbLh24egjbyA9/RY=", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "requires": { - "http-server": "0.6.0", - "lodash": "4.17.4" - }, + "license": "MIT", "dependencies": { - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true - } + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "grunt-legacy-log": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", - "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", - "requires": { - "colors": "0.6.2", - "grunt-legacy-log-utils": "0.1.1", - "hooker": "0.2.3", - "lodash": "2.4.2", - "underscore.string": "2.3.3" - }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" - }, - "underscore.string": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=" - } + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "grunt-legacy-log-utils": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", - "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", - "requires": { - "colors": "0.6.2", - "lodash": "2.4.2", - "underscore.string": "2.3.3" - }, - "dependencies": { - "lodash": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", - "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" - }, - "underscore.string": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", - "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=" - } + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "grunt-legacy-util": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", - "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", - "requires": { - "async": "0.1.22", - "exit": "0.1.2", - "getobject": "0.1.0", - "hooker": "0.2.3", - "lodash": "0.9.2", - "underscore.string": "2.2.1", - "which": "1.0.9" - } - }, - "grunt-lib-amd": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/grunt-lib-amd/-/grunt-lib-amd-0.1.3.tgz", - "integrity": "sha1-78mznvE5lri9Il2BU/cAkzuQNX8=", - "requires": { - "amdefine": "0.0.8", - "grunt": "0.4.5", - "mout": "0.3.0", - "underscore": "1.4.4" - } - }, - "grunt-lib-phantomjs": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/grunt-lib-phantomjs/-/grunt-lib-phantomjs-0.6.0.tgz", - "integrity": "sha1-rR9/IS/EojJfMvzUnGoNo2h8H7Q=", - "dev": true, - "requires": { - "eventemitter2": "0.4.14", - "phantomjs": "1.9.20", - "semver": "1.0.14", - "temporary": "0.0.8" - } - }, - "habitat": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/habitat/-/habitat-0.4.2.tgz", - "integrity": "sha1-0I7NHLF07jMS4Icrs5hSks3VqqM=" + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.5.0", - "har-schema": "2.0.0" + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" } }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, - "requires": { - "ansi-regex": "2.1.1" + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "hasha": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", - "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=", + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", "dev": true, - "requires": { - "is-stream": "1.1.0", - "pinkie-promise": "2.0.1" + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" } }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" } }, - "highlight.js": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-7.3.0.tgz", - "integrity": "sha1-bF8PZOcHj2ZAK82/yJEQw/0bqZ8=", - "dev": true + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" - }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=" - }, - "http-server": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/http-server/-/http-server-0.6.0.tgz", - "integrity": "sha1-uze++MEqSYOWwNcaC1rI36l41R4=", - "dev": true, - "requires": { - "colors": "0.6.2", - "ecstatic": "0.4.13", - "opener": "1.3.0", - "optimist": "0.5.2", - "portfinder": "0.2.1", - "union": "0.3.8" - }, - "dependencies": { - "optimist": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.5.2.tgz", - "integrity": "sha1-hcjBRUszFeSniUfoV7HfAzRQv7w=", - "dev": true, - "requires": { - "wordwrap": "0.0.3" - } + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" } }, - "iconv-lite": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", - "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=" + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" }, - "is": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is/-/is-0.3.0.tgz", - "integrity": "sha1-qPcd/IpuKDcWJ/JskpCYxvTV1dc=" + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" }, - "is-my-json-valid": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.1.tgz", - "integrity": "sha512-ochPsqWS1WXj8ZnMIV0vnNXooaMhp7cyL4FMSIPKTtnV0Ha/T19G2b9kkhcNsabV9bxYkze7/aLZJb/bYuFduQ==", + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, - "requires": { - "generate-function": "2.0.0", - "generate-object-property": "1.2.0", - "jsonpointer": "4.0.1", - "xtend": "4.0.1" + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" } }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { + "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, - "js-yaml": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", - "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", - "requires": { - "argparse": "0.1.16", - "esprima": "1.0.4" + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" }, - "jshint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-1.1.0.tgz", - "integrity": "sha1-mYe5C4YFVsvH84DVdVoj1QQhNRM=", - "dev": true, - "requires": { - "cli": "0.4.5", - "esprima": "https://github.com/ariya/esprima/tarball/master", - "minimatch": "0.2.14", - "peakle": "0.0.1", - "shelljs": "0.1.4", - "underscore": "1.4.4" - }, - "dependencies": { - "esprima": { - "version": "https://github.com/ariya/esprima/tarball/master", - "integrity": "sha1-C0XMQgDkwwAPPkY1H9aa+FeCIPo=", - "dev": true - } + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, - "requires": { - "graceful-fs": "4.1.11" + "license": "MIT", + "engines": { + "node": ">=8" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true, - "optional": true - } + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kew": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", - "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", - "dev": true + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" }, - "klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true, - "requires": { - "graceful-fs": "4.1.11" - }, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", "dependencies": { - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true, - "optional": true - } + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "less": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/less/-/less-1.3.3.tgz", - "integrity": "sha1-fujzAKQQgPNUTIDHpwzfamEoDPk=", - "requires": { - "ycssmin": "1.0.1" + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" } }, - "less-middleware": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/less-middleware/-/less-middleware-0.1.15.tgz", - "integrity": "sha1-hQoUWaWWmego6zoH4ItKNQ7aC0Y=", - "requires": { - "less": "1.6.3", - "mkdirp": "0.3.5", - "node.extend": "1.0.10" - }, - "dependencies": { - "less": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/less/-/less-1.6.3.tgz", - "integrity": "sha1-cc6J7DC3dLNWfyVMZ5WPLywZO94=", - "requires": { - "clean-css": "2.0.8", - "mime": "1.2.6", - "mkdirp": "0.3.5", - "request": "2.83.0", - "source-map": "0.1.43" - } - }, - "mkdirp": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" - } + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" } }, - "lodash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", - "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=" + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } }, - "marked": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", - "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=", - "dev": true + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" }, - "methods": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/methods/-/methods-0.0.1.tgz", - "integrity": "sha1-J3yQ+L7zlwlkWoNxxRw7bGSOBow=" - }, - "mime": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.6.tgz", - "integrity": "sha1-sfhsdowCX6h7SAdfFwnyiuryA2U=" - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } - }, - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" - }, - "mkdirp": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.3.tgz", - "integrity": "sha1-WV4lHBNww6aLqyE20ONIuBBa3xM=" - }, - "mout": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mout/-/mout-0.3.0.tgz", - "integrity": "sha1-C281MgPlopKen1K0oTprwkWyieg=" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" - }, - "newrelic": { - "version": "0.9.20", - "resolved": "https://registry.npmjs.org/newrelic/-/newrelic-0.9.20.tgz", - "integrity": "sha1-YTuSzT9zUsUIurZ+GKfHebEMX38=", - "requires": { - "bunyan": "0.14.6" - } - }, - "node-static": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.6.9.tgz", - "integrity": "sha1-GZYe3TVwPIzJLCikNtP13Rodkdo=", - "requires": { - "colors": "0.6.2", - "mime": "2.0.3", - "optimist": "0.6.1" - }, - "dependencies": { - "mime": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.0.3.tgz", - "integrity": "sha512-TrpAd/vX3xaLPDgVRm6JkZwLR0KHfukMdU2wTEbqMDdCnY6Yo3mE+mjs9YE6oMNw2QRfXVeBEYpmpO94BIqiug==" - } + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" } }, - "node.extend": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.0.10.tgz", - "integrity": "sha1-Mmm934HFRTX0CKvHhMMrDSvVX28=", - "requires": { - "is": "0.3.0" + "node_modules/tr46/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "requires": { - "abbrev": "1.1.1" + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "noptify": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/noptify/-/noptify-0.0.3.tgz", - "integrity": "sha1-WPZUpz2XU98MUdlobckhBKZ/S7s=", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "requires": { - "nopt": "2.0.0" - }, + "license": "BSD-2-Clause", "dependencies": { - "nopt": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.0.0.tgz", - "integrity": "sha1-ynQW8gpeP5w7hhgPlilfo9C1Lg0=", - "dev": true, - "requires": { - "abbrev": "1.1.1" - } - } + "punycode": "^2.1.0" } }, - "nunjucks": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-0.1.10.tgz", - "integrity": "sha1-jZYIBQM+hX0D0BoiuUOpTnl/MOo=", - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "opener": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.3.0.tgz", - "integrity": "sha1-EwumYiE/qELttM0DYdMaFTAaQ+I=", - "dev": true - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, - "package": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package/-/package-1.0.1.tgz", - "integrity": "sha1-0lofmeJQbcsn1nBLg9yooxLk7cw=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "pause": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" - }, - "peakle": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/peakle/-/peakle-0.0.1.tgz", - "integrity": "sha1-KGRF1qdzPxfcJzUB4uA5n0G4kOA=", - "dev": true - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "phantomjs": { - "version": "1.9.20", - "resolved": "https://registry.npmjs.org/phantomjs/-/phantomjs-1.9.20.tgz", - "integrity": "sha1-RCSsog4U0lXAsIia9va4lz2hDg0=", - "dev": true, - "requires": { - "extract-zip": "1.5.0", - "fs-extra": "0.26.7", - "hasha": "2.2.0", - "kew": "0.7.0", - "progress": "1.1.8", - "request": "2.67.0", - "request-progress": "2.0.1", - "which": "1.2.14" - }, - "dependencies": { - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "dev": true - }, - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", - "dev": true, - "requires": { - "lodash": "4.17.4" - } - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "dev": true - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", - "dev": true - }, - "commander": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", - "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "requires": { - "boom": "2.10.1" - } - }, - "form-data": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", - "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", - "dev": true, - "requires": { - "async": "2.6.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "fs-extra": { - "version": "0.26.7", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", - "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "jsonfile": "2.4.0", - "klaw": "1.3.1", - "path-is-absolute": "1.0.1", - "rimraf": "2.2.8" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "har-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "commander": "2.12.2", - "is-my-json-valid": "2.16.1", - "pinkie-promise": "2.0.1" - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "dev": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } + "jiti": { + "optional": true }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", - "dev": true, - "requires": { - "graceful-fs": "4.1.11" - } + "less": { + "optional": true }, - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true + "lightningcss": { + "optional": true }, - "node-uuid": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", - "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=", - "dev": true + "sass": { + "optional": true }, - "qs": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.1.tgz", - "integrity": "sha1-gB/uAw4LlFDWOFrcSKTMVbRK7fw=", - "dev": true + "sass-embedded": { + "optional": true }, - "request": { - "version": "2.67.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.67.0.tgz", - "integrity": "sha1-ivdHgOK/EeoK6aqWXBHxGv0nJ0I=", - "dev": true, - "requires": { - "aws-sign2": "0.6.0", - "bl": "1.0.3", - "caseless": "0.11.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "1.0.1", - "har-validator": "2.0.6", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "node-uuid": "1.4.8", - "oauth-sign": "0.8.2", - "qs": "5.2.1", - "stringstream": "0.0.5", - "tough-cookie": "2.2.2", - "tunnel-agent": "0.4.3" - } + "stylus": { + "optional": true }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } + "sugarss": { + "optional": true }, - "tough-cookie": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz", - "integrity": "sha1-yDoYMPTl7wuT7yo0iOck+N4Basc=", - "dev": true + "terser": { + "optional": true }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", - "dev": true + "tsx": { + "optional": true }, - "which": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", - "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", - "dev": true, - "requires": { - "isexe": "2.0.0" - } + "yaml": { + "optional": true } } }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, - "requires": { - "pinkie": "2.0.4" + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "pkginfo": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.2.3.tgz", - "integrity": "sha1-cjnEKl72wwuPMoQ52bn/cQQkkPg=" + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "portfinder": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-0.2.1.tgz", - "integrity": "sha1-srmwFk+eF/o6nH2yME0KdRQMca0=", + "node_modules/vite/node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, - "requires": { - "mkdirp": "0.0.7" + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", "dependencies": { - "mkdirp": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.0.7.tgz", - "integrity": "sha1-2JtPDkw+XlylQjWTFnXglP4aUHI=", - "dev": true + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true } } }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-0.5.1.tgz", - "integrity": "sha1-n2v12axsdjhOldNtFbSJgOXkrdA=" - }, - "range-parser": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz", - "integrity": "sha1-wEJ//vUcEKy6B4KkbJYC50T/Ygs=" - }, - "readable-stream": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", - "dev": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "0.10.31", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - }, - "dependencies": { - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - } + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" } }, - "request-progress": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", - "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=", + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, - "requires": { - "throttleit": "1.0.0" + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" } }, - "requirejs": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.1.22.tgz", - "integrity": "sha1-3Xj9LTQYDA1ixyS1uK68BmTgNm8=", - "dev": true - }, - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=" - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "semver": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/semver/-/semver-1.0.14.tgz", - "integrity": "sha1-ysXi1Vpvv5WMsiCuhEBFBxx49nY=", - "dev": true - }, - "send": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.1.0.tgz", - "integrity": "sha1-z7COvTzsm3/Bo32f+eh1qXHPRkA=", - "requires": { - "debug": "3.1.0", - "fresh": "0.1.0", - "mime": "1.2.6", - "range-parser": "0.0.4" + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" } }, - "shelljs": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.1.4.tgz", - "integrity": "sha1-37vnjVbDwBaNL7eeEOzR28sH7A4=", - "dev": true - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=" - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", - "requires": { - "hoek": "4.2.0" - } - }, - "source-map": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "optional": true, - "requires": { - "amdefine": "0.0.8" - } - }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - } - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, - "requires": { - "ansi-regex": "2.1.1" + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "temporary": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/temporary/-/temporary-0.0.8.tgz", - "integrity": "sha1-oYqYHSi6jKNgJ/s8MFOMPst0CsA=", + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, - "requires": { - "package": "1.0.1" + "license": "MIT", + "engines": { + "node": ">=18" } }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", - "dev": true - }, - "tiny-lr": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-0.0.4.tgz", - "integrity": "sha1-gGGFR/Y/aX0Fy0DEwsSwg1Ia77Y=", + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, - "requires": { - "debug": "0.7.4", - "faye-websocket": "0.4.4", - "noptify": "0.0.3", - "qs": "0.5.6" - }, + "license": "MIT", "dependencies": { - "debug": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", - "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", - "dev": true - }, - "qs": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/qs/-/qs-0.5.6.tgz", - "integrity": "sha1-MbGtBYVnZRxSaSFQa5qHk5EaA4Q=", - "dev": true - } + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" } }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.2.tgz", - "integrity": "sha1-EBezLZhP9VbroQD1AViauhrOLgQ=", - "requires": { - "is-typedarray": "1.0.0" - } - }, - "underscore": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", - "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=" - }, - "underscore.string": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", - "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=" - }, - "union": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/union/-/union-0.3.8.tgz", - "integrity": "sha1-JqcCpNNSi0NYyXEcir/2zJHSQlc=", - "dev": true, - "requires": { - "pkginfo": "0.2.3", - "qs": "0.5.1" - } - }, - "universal-analytics": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.1.3.tgz", - "integrity": "sha1-ltPF/v3cMs+6ohB5k7TuGIblruI=", - "requires": { - "async": "0.2.10", - "node-uuid": "1.4.8", - "request": "2.83.0", - "underscore": "1.4.4" - }, - "dependencies": { - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - }, - "node-uuid": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", - "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" - } + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } }, - "universalify": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", - "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - } - }, - "websocket": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.25.tgz", - "integrity": "sha512-M58njvi6ZxVb5k7kpnHh2BvNKuBWiwIYvsToErBzWhvBZYwlEiLcyLrG41T1jRcrY9ettqPYEqduLI7ul54CVQ==", - "requires": { - "debug": "2.6.9", - "nan": "2.8.0", - "typedarray-to-buffer": "3.1.2", - "yaeti": "0.0.6" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - } + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "websocket-server": { - "version": "github:miksago/node-websocket-server#dae6bed226ccfccf3939973155570b39dc8b3df0" - }, - "which": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", - "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=" - }, - "winston": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-0.6.2.tgz", - "integrity": "sha1-QUT+JYbNwZphK/jANVkBMskGS9I=", - "requires": { - "async": "0.1.22", - "colors": "0.6.2", - "cycle": "1.0.3", - "eyes": "0.1.8", - "pkginfo": "0.2.3", - "request": "2.9.203", - "stack-trace": "0.0.10" - }, - "dependencies": { - "request": { - "version": "2.9.203", - "resolved": "https://registry.npmjs.org/request/-/request-2.9.203.tgz", - "integrity": "sha1-bBcRpUB/uUoRQhlWPkQUW8v0cjo=" + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true } } }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } }, - "yaeti": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", - "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" }, - "yauzl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", - "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, - "requires": { - "fd-slicer": "1.0.1" + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } - }, - "ycssmin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ycssmin/-/ycssmin-1.0.1.tgz", - "integrity": "sha1-fN3o23jPqwDSkBw7IwHjBPr03xY=", - "optional": true } } } diff --git a/package.json b/package.json index c52f2bebc..b90a74ec6 100644 --- a/package.json +++ b/package.json @@ -1,53 +1,50 @@ { "name": "togetherjs", - "version": "0.4.0a", - "main": "hub/server.js", + "version": "0.5.0", "description": "Collaborative help system", "keywords": [], + "license": "MPL-2.0", "repository": { "type": "git", - "url": "https://github.com/mozilla/togetherjs.git" + "url": "https://github.com/jsfiddle/togetherjs.git" }, + "type": "module", + "main": "./dist/togetherjs.js", + "module": "./dist/togetherjs.esm.js", + "exports": { + ".": { + "import": "./dist/togetherjs.esm.js", + "default": "./dist/togetherjs.js" + }, + "./dist/*": "./dist/*" + }, + "files": [ + "dist" + ], "dependencies": { - "ejs": "~2.5.5", - "ejs-locals": "~1.0.2", - "express": "~3.0.6", - "grunt-amd-check": "~0.5.1", - "habitat": "~0.4.0", - "less": "~1.3.1", - "less-middleware": "~0.1.9", - "newrelic": "0.9.20", - "node-static": "~0.6.5", - "optimist": "~0.6.0", - "universal-analytics": "~0.1.3", - "websocket": "~1.0.7", - "websocket-server": "github:miksago/node-websocket-server#master", - "winston": "~0.6.2" + "jquery": "^3.7.1", + "tinycolor2": "^1.6.0" }, "devDependencies": { - "grunt-contrib-less": "~0.5.1", - "grunt-contrib-csslint": "~0.1.2", - "grunt-contrib-jshint": "~0.4.3", - "grunt-contrib-requirejs": "~0.4.1", - "grunt-contrib-watch": "~0.4.3", - "grunt": "~0.4.1", - "grunt-contrib-copy": "~0.4.1", - "grunt-http-server": "~0.0.5", - "nunjucks": "~0.1.8a", - "marked": "~0.3.4", - "docco": "~0.6.2", - "highlight.js": "~7.3.0", - "optimist": "~0.6.0", - "freeport": "~1.0.3", - "grunt-lib-phantomjs": "~0.6.0" + "@playwright/test": "^1.49.0", + "esbuild": "^0.25.0", + "eslint": "^9.17.0", + "globals": "^15.14.0", + "jsdom": "^25.0.1", + "prettier": "^3.4.2", + "vitest": "^3.2.4", + "ws": "^8.21.3" }, "engines": { - "node": "~0.12.7", - "npm": "^2.11.3" + "node": ">=20" }, "scripts": { - "start": "node hub/server.js", - "test": "grunt test", - "build": "grunt build" + "build": "node build/build.mjs", + "dev": "node build/build.mjs --watch --serve", + "lint": "eslint .", + "format": "prettier --write \"src/**/*.js\" \"build/**/*.mjs\" \"tests/**/*.js\"", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test" } } diff --git a/phantomjs/bridge.js b/phantomjs/bridge.js deleted file mode 100644 index a4af2a618..000000000 --- a/phantomjs/bridge.js +++ /dev/null @@ -1,82 +0,0 @@ -(function (doctest) { - 'use strict'; - - // Function.bind is not defined in phantomjs (!) so polyfill it - if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 - // internal IsCallable function - throw new TypeError("can't bind"); - } - - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis - ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; - - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); - - return fBound; - }; - } - - // default phantomjs background is transparent - document.body.bgColor = 'white'; - - // Send messages to the parent PhantomJS process via alert! Good times!! - function sendMessage() { - var args = [].slice.call(arguments); - alert(JSON.stringify(args)); - } - - // doctestjs-specific stuff. First, be sure we don't autostart: - document.body.className = document.body.className.replace(/autodoctest/,''); - - // Now define a custom reporter which will pass the results up to grunt - var PhantomReporter = function(runner) { - this.runner = runner; - }; - PhantomReporter.prototype.logSuccess = function(example, got) { - this._send('doctestjs.pass', example, got); - }; - PhantomReporter.prototype.logFailure = function(example, got) { - this._send('doctestjs.fail', example, got); - }; - PhantomReporter.prototype._send = function(msg, example, got) { - sendMessage(msg, { - example: { - expr: example.expr, - summary: example.textSummary(), - expected: example.expected - }, - got: got - }); - }; - - // Start/finish the doctest runner. - window.doctestReporterHook = { - finish: function() { - sendMessage('doctestjs.end'); - } - }; - window.addEventListener('load', function() { - var runner = new doctest.Runner({ - Reporter: PhantomReporter - }); - var parser = new doctest.HTMLParser(runner); - parser.loadRemotes(function() { - runner.init(); - parser.parse(); - sendMessage('doctestjs.start'); - runner.run(); - }); - }); - -})(window.doctest); diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 000000000..ad6487de8 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,37 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/e2e", + timeout: 30000, + expect: { timeout: 10000 }, + fullyParallel: false, + workers: 1, + reporter: process.env.CI ? "list" : [["list"]], + use: { + baseURL: "http://localhost:8099", + trace: "retain-on-failure", + }, + projects: [ + { + name: "chromium", + use: { + // Chromium is preinstalled in this environment; never run + // `playwright install`. + launchOptions: { + executablePath: + process.env.CHROMIUM_PATH || "/opt/pw-browsers/chromium-1194/chrome-linux/chrome", + args: [ + // The sandbox may route through an HTTPS proxy that would + // intercept the loopback hub connection. + "--no-proxy-server", + // Synthetic camera/mic so getUserMedia resolves headlessly, and + // no permission prompt to click through. + "--use-fake-device-for-media-capture", + "--use-fake-ui-for-media-stream", + "--autoplay-policy=no-user-gesture-required", + ], + }, + }, + }, + ], +}); diff --git a/togetherjs/README.md b/src/README.md similarity index 100% rename from togetherjs/README.md rename to src/README.md diff --git a/src/core/channels.js b/src/core/channels.js new file mode 100644 index 000000000..945cd5304 --- /dev/null +++ b/src/core/channels.js @@ -0,0 +1,471 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Channel abstraction. Supported channels: + +- WebSocket to an address +- postMessage between windows + +In the future: + +- XMLHttpRequest to a server (with some form of queuing) + +The interface: + +channel = new ChannelName(parameters) + +The instantiation is specific to the kind of channel + +Methods: + +onmessage: set to function (jsonData) +rawdata: set to true if you want onmessage to receive raw string data +onclose: set to function () +send: function (string or jsonData) +close: function () + +.send() will encode the data if it is not a string. + +(should I include readyState as an attribute?) + +Channels must accept messages immediately, caching if the connection +is not fully established yet. + +*/ + +import util from "./util.js"; + + +var channels = util.Module("channels"); +/* Subclasses must define: + +- ._send(string) +- ._setupConnection() +- ._ready() +- .close() (and must set this.closed to true) + +And must call: + +- ._flush() on open +- ._incoming(string) on incoming message +- onclose() (not onmessage - instead _incoming) +- emit("close") +*/ + +var AbstractChannel = util.mixinEvents({ +onmessage: null, +rawdata: false, +onclose: null, +closed: false, + +baseConstructor: function () { + this._buffer = []; + this._setupConnection(); +}, + +send: function (data) { + if (this.closed) { + throw 'Cannot send to a closed connection'; + } + if (typeof data != "string") { + data = JSON.stringify(data); + } + if (! this._ready()) { + this._buffer.push(data); + return; + } + this._send(data); +}, + +_flush: function () { + for (var i=0; i this.backoffDetection) { + this._backoff = 0; + } else { + this._backoff++; + } + var time = Math.min(this._backoff * this.backoffTime, this.maxBackoffTime); + setTimeout((function () { + this._setupConnection(); + }).bind(this), time); + } + }).bind(this); + this.socket.onmessage = (function (event) { + this._incoming(event.data); + }).bind(this); + this.socket.onerror = (function (event) { + console.error('WebSocket error:', event.data); + }).bind(this); +} + +}); + + +/* Sends TO a window or iframe */ +channels.PostMessageChannel = util.Class(AbstractChannel, { +_pingPollPeriod: 100, // milliseconds +_pingPollIncrease: 100, // +100 milliseconds for each failure +_pingMax: 2000, // up to a max of 2000 milliseconds + +constructor: function (win, expectedOrigin) { + this.expectedOrigin = expectedOrigin; + this._pingReceived = false; + this._receiveMessage = this._receiveMessage.bind(this); + if (win) { + this.bindWindow(win, true); + } + this._pingFailures = 0; + this.baseConstructor(); +}, + +toString: function () { + var s = '[PostMessageChannel'; + if (this.window) { + s += ' to window ' + this.window; + } else { + s += ' not bound to a window'; + } + if (this.window && ! this._pingReceived) { + s += ' still establishing'; + } + return s + ']'; +}, + +bindWindow: function (win, noSetup) { + if (this.window) { + this.close(); + // Though we deinitialized everything, we aren't exactly closed: + this.closed = false; + } + if (win && win.contentWindow) { + win = win.contentWindow; + } + this.window = win; + // FIXME: The distinction between this.window and window seems unimportant + // in the case of postMessage + var w = this.window; + // In a Content context we add the listener to the local window + // object, but in the addon context we add the listener to some + // other window, like the one we were given: + if (typeof window != "undefined") { + w = window; + } + w.addEventListener("message", this._receiveMessage, false); + if (! noSetup) { + this._setupConnection(); + } +}, + +_send: function (data) { + this.window.postMessage(data, this.expectedOrigin || "*"); +}, + +_ready: function () { + return this.window && this._pingReceived; +}, + +_setupConnection: function () { + if (this.closed || this._pingReceived || (! this.window)) { + return; + } + this._pingFailures++; + this._send("hello"); + // We'll keep sending ping messages until we get a reply + var time = this._pingPollPeriod + (this._pingPollIncrease * this._pingFailures); + time = time > this._pingPollMax ? this._pingPollMax : time; + this._pingTimeout = setTimeout(this._setupConnection.bind(this), time); +}, + +_receiveMessage: function (event) { + if (event.source !== this.window) { + return; + } + if (this.expectedOrigin && event.origin != this.expectedOrigin) { + console.info("Expected message from", this.expectedOrigin, + "but got message from", event.origin); + return; + } + if (! this.expectedOrigin) { + this.expectedOrigin = event.origin; + } + if (event.data == "hello") { + this._pingReceived = true; + if (this._pingTimeout) { + clearTimeout(this._pingTimeout); + this._pingTimeout = null; + } + this._flush(); + return; + } + this._incoming(event.data); +}, + +close: function () { + this.closed = true; + this._pingReceived = false; + if (this._pingTimeout) { + clearTimeout(this._pingTimeout); + } + window.removeEventListener("message", this._receiveMessage, false); + if (this.onclose) { + this.onclose(); + } + this.emit("close"); +} + +}); + + +/* Handles message FROM an exterior window/parent */ +channels.PostMessageIncomingChannel = util.Class(AbstractChannel, { + +constructor: function (expectedOrigin) { + this.source = null; + this.expectedOrigin = expectedOrigin; + this._receiveMessage = this._receiveMessage.bind(this); + window.addEventListener("message", this._receiveMessage, false); + this.baseConstructor(); +}, + +toString: function () { + var s = '[PostMessageIncomingChannel'; + if (this.source) { + s += ' bound to source ' + s; + } else { + s += ' awaiting source'; + } + return s + ']'; +}, + +_send: function (data) { + this.source.postMessage(data, this.expectedOrigin); +}, + +_ready: function () { + return !!this.source; +}, + +_setupConnection: function () { +}, + +_receiveMessage: function (event) { + if (this.expectedOrigin && this.expectedOrigin != "*" && + event.origin != this.expectedOrigin) { + // FIXME: Maybe not worth mentioning? + console.info("Expected message from", this.expectedOrigin, + "but got message from", event.origin); + return; + } + if (! this.expectedOrigin) { + this.expectedOrigin = event.origin; + } + if (! this.source) { + this.source = event.source; + } + if (event.data == "hello") { + // Just a ping + this.source.postMessage("hello", this.expectedOrigin); + return; + } + this._incoming(event.data); +}, + +close: function () { + this.closed = true; + window.removeEventListener("message", this._receiveMessage, false); + if (this._pingTimeout) { + clearTimeout(this._pingTimeout); + } + if (this.onclose) { + this.onclose(); + } + this.emit("close"); +} + +}); + +channels.Router = util.Class(util.mixinEvents({ + +constructor: function (channel) { + this._channelMessage = this._channelMessage.bind(this); + this._channelClosed = this._channelClosed.bind(this); + this._routes = Object.create(null); + if (channel) { + this.bindChannel(channel); + } +}, + +bindChannel: function (channel) { + if (this.channel) { + this.channel.removeListener("message", this._channelMessage); + this.channel.removeListener("close", this._channelClosed); + } + this.channel = channel; + this.channel.on("message", this._channelMessage.bind(this)); + this.channel.on("close", this._channelClosed.bind(this)); +}, + +_channelMessage: function (msg) { + if (msg.type == "route") { + var id = msg.routeId; + var route = this._routes[id]; + if (! route) { + console.warn("No route with the id", id); + return; + } + if (msg.close) { + this._closeRoute(route.id); + } else { + if (route.onmessage) { + route.onmessage(msg.message); + } + route.emit("message", msg.message); + } + } +}, + +_channelClosed: function () { + for (var id in this._routes) { + this._closeRoute(id); + } +}, + +_closeRoute: function (id) { + var route = this._routes[id]; + if (route.onclose) { + route.onclose(); + } + route.emit("close"); + delete this._routes[id]; +}, + +makeRoute: function (id) { + id = id || util.generateId(); + var route = Route(this, id); + this._routes[id] = route; + return route; +} +})); + +var Route = util.Class(util.mixinEvents({ +constructor: function (router, id) { + this.router = router; + this.id = id; +}, + +send: function (msg) { + this.router.channel.send({ + type: "route", + routeId: this.id, + message: msg + }); +}, + +close: function () { + if (this.router._routes[this.id] !== this) { + // This route instance has been overwritten, so ignore + return; + } + delete this.router._routes[this.id]; +} + +})); + +export default channels; diff --git a/src/core/console.js b/src/core/console.js new file mode 100644 index 000000000..dd97155d5 --- /dev/null +++ b/src/core/console.js @@ -0,0 +1,287 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +import TogetherJS from "./togetherjs.js"; +import util from "./util.js"; + + +var console = window.console || {log: function () {}}; + +var Console = util.Class({ + constructor: function () { + this.messages = []; + this.level = this.levels.log; + }, + + messageLimit: 100, + + levels: { + debug: 1, + // FIXME: I'm considering *not* wrapping console.log, and strictly keeping + // it as a debugging tool; also line numbers would be preserved + log: 2, + info: 3, + notify: 4, + warn: 5, + error: 6, + fatal: 7 + }, + + // Gets set below: + maxLevel: 0, + + consoleLevels: [ + [], + console.debug || [], + console.log || [], + console.info || [], + console.notify || [], + console.warn || [], + console.error || [], + console.fatal || [] + ], + + levelNames: {}, + + setLevel: function (l) { + var number; + if (typeof l == "string") { + number = this.levels[l]; + if (number === undefined) { + throw new Error("Tried to set Console level to unknown level string: " + l); + } + l = number; + } + if (typeof l == "function") { + number = this.consoleLevels.indexOf(l); + if (number == -1) { + throw new Error("Tried to set Console level based on unknown console function: " + l); + } + l = number; + } + if (typeof l == "number") { + if (l < 0) { + throw new Error("Console level must be 0 or larger: " + l); + } else if (l > this.maxLevel) { + throw new Error("Console level must be " + this.maxLevel + " or smaller: " + l); + } + } + this.level = l; + }, + + write: function (level) { + try { + this.messages.push([ + Date.now(), + level, + this._stringify(Array.prototype.slice.call(arguments, 1)) + ]); + } catch (e) { + console.warn("Error stringifying argument:", e); + } + if (level != "suppress" && this.level <= level) { + var method = console[this.levelNames[level]]; + if (! method) { + method = console.log; + } + method.apply(console, Array.prototype.slice.call(arguments, 1)); + } + }, + + suppressedWrite: function () { + this.write.apply(this, ["suppress"].concat(Array.prototype.slice.call(arguments))); + }, + + trace: function (level) { + level = level || 'log'; + if (console.trace) { + level = "suppressedWrite"; + } + try { + throw new Error(); + } catch (e) { + // FIXME: trim this frame + var stack = e.stack; + stack = stack.replace(/^[^\n]*\n/, ""); + this[level](stack); + } + if (console.trace) { + console.trace(); + } + }, + + _browserInfo: function () { + // FIXME: add TogetherJS version and + return [ + "TogetherJS base URL: " + TogetherJS.baseUrl, + "User Agent: " + navigator.userAgent, + "Page loaded: " + this._formatDate(TogetherJS.pageLoaded), + "Age: " + this._formatMinutes(Date.now() - TogetherJS.pageLoaded) + " minutes", + // FIXME: make this right: + //"Window: height: " + window.screen.height + " width: " + window.screen.width + "URL: " + location.href, + "------+------+----------------------------------------------" + ]; + }, + + _stringify: function (args) { + var s = ""; + for (var i=0; i 10) { + // Over 10 minutes, just ignore the seconds + return m; + } + var seconds = Math.floor(remaining / 1000) + ""; + m += ":"; + seconds = lpad(seconds, 2, "0"); + m += seconds; + if (m == "0:00") { + m += ((remaining / 1000).toFixed(3) + "").substr(1); + } + return m; + }, + + _formatLevel: function (l) { + if (l === "suppress") { + return ""; + } + return this.levelNames[l]; + }, + + toString: function () { + try { + var lines = this._browserInfo(); + this.messages.forEach(function (m) { + lines.push(lpad(this._formatTime(m[0]), 6) + " " + rpad(this._formatLevel(m[1]), 6) + " " + lpadLines(m[2], 14)); + }, this); + return lines.join("\n"); + } catch (e) { + // toString errors can otherwise be swallowed: + console.warn("Error running console.toString():", e); + throw e; + } + }, + + submit: function (options) { + // FIXME: friendpaste is broken for this + // (and other pastebin sites aren't really Browser-accessible) + return util.Deferred(function (def) { + options = options || {}; + var site = options.site || TogetherJS.config.get("pasteSite") || "https://www.friendpaste.com/"; + var req = new XMLHttpRequest(); + req.open("POST", site); + req.setRequestHeader("Content-Type", "application/json"); + req.send(JSON.stringify({ + "title": options.title || "TogetherJS log file", + "snippet": this.toString(), + "language": "text" + })); + req.onreadystatechange = function () { + if (req.readyState === 4) { + var data = JSON.parse(req.responseText); + } + }; + }); + } + +}); + +function rpad(s, len, pad) { + s = s + ""; + pad = pad || " "; + while (s.length < len) { + s += pad; + } + return s; +} + +function lpad(s, len, pad) { + s = s + ""; + pad = pad || " "; + while (s.length < len) { + s = pad + s; + } + return s; +} + +function lpadLines(s, len, pad) { + var i; + s = s + ""; + if (s.indexOf("\n") == -1) { + return s; + } + pad = pad || " "; + var fullPad = ""; + for (i=0; i's are probably a sign something is wrong. + console.warn("Error in getUserName(): should return a string (got", name, ")"); + name = null; + } + } + if (getUserColor) { + if (typeof getUserColor == "string") { + color = getUserColor; + } else { + color = getUserColor(); + } + if (color && typeof color != "string") { + // FIXME: would be nice to test for color-ness here. + console.warn("Error in getUserColor(): should return a string (got", color, ")"); + color = null; + } + } + if (getUserAvatar) { + if (typeof getUserAvatar == "string") { + avatar = getUserAvatar; + } else { + avatar = getUserAvatar(); + } + if (avatar && typeof avatar != "string") { + console.warn("Error in getUserAvatar(): should return a string (got", avatar, ")"); + avatar = null; + } + } + if (name || color || avatar) { + this.update({ + name: name, + color: color, + avatar: avatar + }); + } + } + }); + + peers.Self.view = need("ui").PeerView(peers.Self); + storage.tab.get("peerCache").then(deserialize); + peers.Self._loadFromSettings().then(function() { + peers.Self._loadFromApp(); + peers.Self.view.update(); + session.emit("self-updated"); + }); +}); + +session.on("refresh-user-data", function () { + if (peers.Self) { + peers.Self._loadFromApp(); + } +}); + +TogetherJS.config.track( + "getUserName", + TogetherJS.config.track( + "getUserColor", + TogetherJS.config.track( + "getUserAvatar", + function () { + if (peers.Self) { + peers.Self._loadFromApp(); + } + } + ) + ) +); + +peers._SelfLoaded = util.Deferred(); + +function serialize() { + var peers = []; + util.forEachAttr(Peer.peers, function (peer) { + peers.push(peer.serialize()); + }); + return { + peers: peers + }; +} + +function deserialize(obj) { + if (! obj) { + return; + } + obj.peers.forEach(function (peer) { + Peer.deserialize(peer); + }); +} + +peers.getPeer = function getPeer(id, message, ignoreMissing) { + assert(id); + var peer = Peer.peers[id]; + if (id === session.clientId) { + return peers.Self; + } + if (message && ! peer) { + peer = Peer(id, {fromHelloMessage: message}); + return peer; + } + if (ignoreMissing && !peer) { + return null; + } + assert(peer, "No peer with id:", id); + if (message && + (message.type == "hello" || message.type == "hello-back" || + message.type == "peer-update")) { + peer.updateFromHello(message); + peer.view.update(); + } + return Peer.peers[id]; +}; + +peers.getAllPeers = function (liveOnly) { + var result = []; + util.forEachAttr(Peer.peers, function (peer) { + if (liveOnly && peer.status != "live") { + return; + } + result.push(peer); + }); + return result; +}; + +function checkActivity() { + var ps = peers.getAllPeers(); + var now = Date.now(); + ps.forEach(function (p) { + if (p.idle == "active" && now - p.lastMessageDate > IDLE_TIME) { + p.update({idle: "inactive"}); + } + if (p.status != "bye" && now - p.lastMessageDate > BYE_TIME) { + p.bye(); + } + }); +} + +session.hub.on("bye", function (msg) { + var peer = peers.getPeer(msg.clientId); + peer.bye(); +}); + +var checkActivityTask = null; + +session.on("start", function () { + if (checkActivityTask) { + console.warn("Old peers checkActivityTask left over?"); + clearTimeout(checkActivityTask); + } + checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL); +}); + +session.on("close", function () { + util.forEachAttr(Peer.peers, function (peer) { + peer.destroy(); + }); + storage.tab.set("peerCache", undefined); + clearTimeout(checkActivityTask); + checkActivityTask = null; +}); + +var tabIdleTimeout = null; + +session.on("visibility-change", function (hidden) { + if (hidden) { + if (tabIdleTimeout) { + clearTimeout(tabIdleTimeout); + } + tabIdleTimeout = setTimeout(function () { + peers.Self.update({idle: "inactive"}); + }, TAB_IDLE_TIME); + } else { + if (tabIdleTimeout) { + clearTimeout(tabIdleTimeout); + } + if (peers.Self.idle == "inactive") { + peers.Self.update({idle: "active"}); + } + } +}); + +session.hub.on("idle-status", function (msg) { + msg.peer.update({idle: msg.idle}); +}); + +// Pings are a straight alive check, and contain no more information: +session.hub.on("ping", function () { + session.send({type: "ping-back"}); +}); + +window.addEventListener("pagehide", function () { + // FIXME: not certain if this should be tab local or not: + storeSerialization(); +}, false); + +function storeSerialization() { + storage.tab.set("peerCache", serialize()); +} + +util.mixinEvents(peers); + +util.testExpose({ + setIdleTime: function (time) { + IDLE_TIME = time; + CHECK_ACTIVITY_INTERVAL = time / 2; + if (TogetherJS.running) { + clearTimeout(checkActivityTask); + checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL); + } + } +}); + +util.testExpose({ + setByeTime: function (time) { + BYE_TIME = time; + CHECK_ACTIVITY_INTERVAL = Math.min(CHECK_ACTIVITY_INTERVAL, time / 2); + if (TogetherJS.running) { + clearTimeout(checkActivityTask); + checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL); + } + } +}); + +provide("peers", peers); + +export default peers; diff --git a/src/core/registry.js b/src/core/registry.js new file mode 100644 index 000000000..515305a6f --- /dev/null +++ b/src/core/registry.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* A tiny lazy-module registry. + * + * The client has genuine import cycles: session needs ui, ui needs session, + * peers needs ui, and so on. Under RequireJS these were expressed as deferred + * `require("ui")` calls made long after load. Plain ESM cannot express that — + * a static `import ui from "../ui/ui.js"` inside session.js would evaluate + * ui.js *before* session.js's own body, so ui.js's top-level `session.on(...)` + * would run against an uninitialized binding. + * + * So the modules on a cycle publish themselves here as they evaluate, and + * their dependents look them up at call time. Same semantics as the old + * `require("ui")`, minus the module loader. + */ + +var modules = {}; + +/** Publish a module under a name. Called at the bottom of each cyclic module. */ +export function provide(name, mod) { + modules[name] = mod; + return mod; +} + +/** Look a module up. Throws if it has not been loaded yet — a programming + error, since ../index.js imports the whole graph before anything runs. */ +export function need(name) { + var mod = modules[name]; + if (!mod) { + throw new Error( + "TogetherJS module '" + name + "' was used before it loaded (check src/index.js imports)", + ); + } + return mod; +} + +/** Non-throwing variant, for genuinely optional modules. */ +export function maybe(name) { + return modules[name] || null; +} diff --git a/src/core/session.js b/src/core/session.js new file mode 100644 index 000000000..900ba0124 --- /dev/null +++ b/src/core/session.js @@ -0,0 +1,491 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import TogetherJS from "./togetherjs.js"; +import { provide, need } from "./registry.js"; +import util from "./util.js"; +import channels from "./channels.js"; +import $ from "jquery"; +import storage from "./storage.js"; + + +var DEBUG = true; + +// This is the amount of time in which a hello-back must be received after a hello +// for us to respect a URL change: +var HELLO_BACK_CUTOFF = 1500; + +var session = util.mixinEvents(util.Module("session")); +var assert = util.assert; + +// We will load this module later (there's a circular import): +var peers; + +// This is the hub we connect to: +session.shareId = null; +// This is the ID that identifies this client: +session.clientId = null; +session.router = channels.Router(); +// Indicates if TogetherJS has just started (not continuing from a saved session): +session.firstRun = false; + +// This is the key we use for localStorage: +var localStoragePrefix = "togetherjs."; +// This is the channel to the hub: +var channel = null; + +// Setting, essentially global: +session.AVATAR_SIZE = 90; + +var MAX_SESSION_AGE = 30*24*60*60*1000; // 30 days + +/**************************************** + * URLs + */ +/* Config is read lazily throughout this module: it is evaluated as soon as the + bundle loads, which is before the host page's configuration is applied. */ +function includeHashInUrl() { + return TogetherJS.config.close("includeHashInUrl"); +} + +session.hubUrl = function (id) { + id = id || session.shareId; + assert(id, "URL cannot be resolved before TogetherJS.shareId has been initialized"); + TogetherJS.config.close("hubBase"); + var hubBase = TogetherJS.config.get("hubBase"); + return hubBase.replace(/\/*$/, "") + "/hub/" + id; +}; + +session.shareUrl = function () { + assert(session.shareId, "Attempted to access shareUrl() before shareId is set"); + var hash = location.hash; + var m = /\?[^#]*/.exec(location.href); + var query = ""; + if (m) { + query = m[0]; + } + hash = hash.replace(/&?togetherjs-[a-zA-Z0-9]+/, ""); + hash = hash || "#"; + return location.protocol + "//" + location.host + location.pathname + query + + hash + "&togetherjs=" + session.shareId; +}; + +session.recordUrl = function () { + assert(session.shareId); + var url = TogetherJS.baseUrl.replace(/\/*$/, "") + "/recorder.html"; + url += "#&togetherjs=" + session.shareId + "&hubBase=" + TogetherJS.config.get("hubBase"); + return url; +}; + +/* location.href without the hash */ +session.currentUrl = function () { + if (includeHashInUrl()) { + return location.href; + } else { + return location.href.replace(/#.*/, ""); + } +}; + +/**************************************** + * Message handling/dispatching + */ + +session.hub = util.mixinEvents({}); + +var _ignoreMessages = null; +function ignoreMessage(type) { + if (_ignoreMessages === null) { + var configured = TogetherJS.config.get("ignoreMessages"); + if (configured === true) { + DEBUG = false; + _ignoreMessages = []; + return true; + } + _ignoreMessages = configured || []; + } + return _ignoreMessages.indexOf(type) != -1; +} +// These are messages sent by clients who aren't "part" of the TogetherJS session: +var MESSAGES_WITHOUT_CLIENTID = ["who", "invite", "init-connection"]; + +// We ignore incoming messages from the channel until this is true: +var readyForMessages = false; + +function openChannel() { + assert(! channel, "Attempt to re-open channel"); + console.info("Connecting to", session.hubUrl(), location.href); + var c = channels.WebSocketChannel(session.hubUrl()); + c.onmessage = function (msg) { + if (! readyForMessages) { + if (DEBUG) { + console.info("In (but ignored for being early):", msg); + } + return; + } + if (DEBUG && ! ignoreMessage(msg.type)) { + console.info("In:", msg); + } + if (! peers) { + // We're getting messages before everything is fully initialized + console.warn("Message received before all modules loaded (ignoring):", msg); + return; + } + if ((! msg.clientId) && MESSAGES_WITHOUT_CLIENTID.indexOf(msg.type) == -1) { + console.warn("Got message without clientId, where clientId is required", msg); + return; + } + if (msg.clientId) { + msg.peer = peers.getPeer(msg.clientId, msg); + } + if (msg.type == "hello" || msg.type == "hello-back" || msg.type == "peer-update") { + // We do this here to make sure this is run before any other + // hello handlers: + msg.peer.updateFromHello(msg); + } + if (msg.peer) { + msg.sameUrl = msg.peer.url == session.currentUrl(); + if (!msg.peer.isSelf) { + msg.peer.updateMessageDate(msg); + } + } + session.hub.emit(msg.type, msg); + TogetherJS._onmessage(msg); + }; + channel = c; + session.router.bindChannel(channel); +} + +session.send = function (msg) { + if (DEBUG && ! ignoreMessage(msg.type)) { + console.info("Send:", msg); + } + msg.clientId = session.clientId; + channel.send(msg); +}; + +session.appSend = function (msg) { + var type = msg.type; + if (type.search(/^togetherjs\./) === 0) { + type = type.substr("togetherjs.".length); + } else if (type.search(/^app\./) === -1) { + type = "app." + type; + } + msg.type = type; + session.send(msg); +}; + +/**************************************** + * Standard message responses + */ + +/* Always say hello back, and keep track of peers: */ +session.hub.on("hello hello-back", function (msg) { + if (msg.type == "hello") { + sendHello(true); + } + if (session.isClient && (! msg.isClient) && + session.firstRun && session.timeHelloSent && + Date.now() - session.timeHelloSent < HELLO_BACK_CUTOFF) { + processFirstHello(msg); + } +}); + +session.hub.on("who", function (msg) { + sendHello(true); +}); + +function processFirstHello(msg) { + if (! msg.sameUrl) { + var url = msg.url; + if (msg.urlHash) { + url += msg.urlHash; + } + need("ui").showUrlChangeMessage(msg.peer, url); + location.href = url; + } +} + +session.timeHelloSent = null; + +function sendHello(helloBack) { + var msg = session.makeHelloMessage(helloBack); + if (! helloBack) { + session.timeHelloSent = Date.now(); + peers.Self.url = msg.url; + } + session.send(msg); +} + +session.makeHelloMessage = function (helloBack) { + var msg = { + name: peers.Self.name || peers.Self.defaultName, + avatar: peers.Self.avatar, + color: peers.Self.color, + url: session.currentUrl(), + urlHash: location.hash, + // FIXME: titles update, we should track those changes: + title: document.title, + rtcSupported: session.RTCSupported, + isClient: session.isClient + }; + if (helloBack) { + msg.type = "hello-back"; + } else { + msg.type = "hello"; + msg.clientVersion = TogetherJS.version; + } + if (! TogetherJS.startup.continued) { + msg.starting = true; + } + // This is a chance for other modules to effect the hello message: + session.emit("prepare-hello", msg); + return msg; +}; +/**************************************** + * Lifecycle (start and end) + */ + +/* Feature modules used to be injected at runtime by RequireJS. They are all + part of the bundle now (see ../index.js), so by the time start() runs they + have registered their session handlers already. */ + +function getRoomName(prefix, maxSize) { + var findRoom = TogetherJS.config.get("hubBase").replace(/\/*$/, "") + "/findroom"; + var url = new URL(findRoom); + url.searchParams.set("prefix", prefix); + url.searchParams.set("max", maxSize); + return fetch(url, {headers: {Accept: "application/json"}}).then(function (resp) { + if (! resp.ok) { + throw new Error("findroom failed: " + resp.status + " " + resp.statusText); + } + return resp.json(); + }).then(function (body) { + return body.name; + }); +} + +function initIdentityId() { + return util.Deferred(function (def) { + if (session.identityId) { + def.resolve(); + return; + } + storage.get("identityId").then(function (identityId) { + if (! identityId) { + identityId = util.generateId(); + storage.set("identityId", identityId); + } + session.identityId = identityId; + // We don't actually have to wait for the set to succede, so + // long as session.identityId is set + def.resolve(); + }); + }); +} + +function initShareId() { + return util.Deferred(function (def) { + var hash = location.hash; + var shareId = session.shareId; + var isClient = true; + var set = true; + var sessionId; + session.firstRun = ! TogetherJS.startup.continued; + if (! shareId) { + if (TogetherJS.startup._joinShareId) { + // Like, below, this *also* means we got the shareId from the hash + // (in togetherjs.js): + shareId = TogetherJS.startup._joinShareId; + } + } + if (! shareId) { + // FIXME: I'm not sure if this will ever happen, because togetherjs.js should + // handle it + var m = /&?togetherjs=([^&]*)/.exec(hash); + if (m) { + isClient = ! m[1]; + shareId = m[2]; + var newHash = hash.substr(0, m.index) + hash.substr(m.index + m[0].length); + location.hash = newHash; + } + } + return storage.tab.get("status").then(function (saved) { + var findRoom = TogetherJS.config.get("findRoom"); + TogetherJS.config.close("findRoom"); + if (findRoom && saved && findRoom != saved.shareId) { + console.info("Ignoring findRoom in lieu of continued session"); + } else if (findRoom && TogetherJS.startup._joinShareId) { + console.info("Ignoring findRoom in lieu of explicit invite to session"); + } + if (findRoom && typeof findRoom == "string" && (! saved) && (! TogetherJS.startup._joinShareId)) { + isClient = true; + shareId = findRoom; + sessionId = util.generateId(); + } else if (findRoom && (! saved) && (! TogetherJS.startup._joinShareId)) { + assert(findRoom.prefix && typeof findRoom.prefix == "string", "Bad findRoom.prefix", findRoom); + assert(findRoom.max && typeof findRoom.max == "number" && findRoom.max > 0, + "Bad findRoom.max", findRoom); + sessionId = util.generateId(); + if (findRoom.prefix.search(/[^a-zA-Z0-9]/) != -1) { + console.warn("Bad value for findRoom.prefix:", JSON.stringify(findRoom.prefix)); + } + getRoomName(findRoom.prefix, findRoom.max).then(function (shareId) { + // FIXME: duplicates code below: + session.clientId = session.identityId + "." + sessionId; + storage.tab.set("status", {reason: "joined", shareId: shareId, running: true, date: Date.now(), sessionId: sessionId}); + session.isClient = true; + session.shareId = shareId; + session.emit("shareId"); + def.resolve(session.shareId); + }); + return; + } else if (TogetherJS.startup._launch) { + if (saved) { + isClient = saved.reason == "joined"; + if (! shareId) { + shareId = saved.shareId; + } + sessionId = saved.sessionId; + } else { + isClient = TogetherJS.startup.reason == "joined"; + assert(! sessionId); + sessionId = util.generateId(); + } + if (! shareId) { + shareId = util.generateId(); + } + } else if (saved) { + isClient = saved.reason == "joined"; + TogetherJS.startup.reason = saved.reason; + TogetherJS.startup.continued = true; + shareId = saved.shareId; + sessionId = saved.sessionId; + // The only case when we don't need to set the storage status again is when + // we're already set to be running + set = ! saved.running; + } else { + throw new util.AssertionError("No saved status, and no startup._launch request; why did TogetherJS start?"); + } + assert(session.identityId); + session.clientId = session.identityId + "." + sessionId; + if (set) { + storage.tab.set("status", {reason: TogetherJS.startup.reason, shareId: shareId, running: true, date: Date.now(), sessionId: sessionId}); + } + session.isClient = isClient; + session.shareId = shareId; + session.emit("shareId"); + def.resolve(session.shareId); + }); + }); +} + +function initStartTarget() { + var id; + if (TogetherJS.startup.button) { + id = TogetherJS.startup.button.id; + if (id) { + storage.set("startTarget", id); + } + return; + } + storage.get("startTarget").then(function (id) { + var el = document.getElementById(id); + if (el) { + TogetherJS.startup.button = el; + } + }); +} +session.start = function () { + initStartTarget(); + // Deferreds swallow exceptions thrown inside .then() callbacks, so without + // this an error anywhere in the start chain just leaves TogetherJS silently + // half-started. Surface it instead. + function startupFailed(error) { + console.error("TogetherJS failed to start:", error); + TogetherJS.running = false; + } + initIdentityId().then(function () { + initShareId().then(function () { + readyForMessages = false; + openChannel(); + var ui = need("ui"); + TogetherJS.running = true; + ui.prepareUI(); + $(function () { + peers = need("peers"); + var startup = need("startup"); + session.emit("start"); + session.once("ui-ready", function () { + readyForMessages = true; + startup.start(); + }); + ui.activateUI(); + peers._SelfLoaded.then(function () { + sendHello(false); + }); + TogetherJS.emit("ready"); + }); + }, startupFailed); + }, startupFailed); +}; + +session.close = function (reason) { + TogetherJS.running = false; + var msg = {type: "bye"}; + if (reason) { + msg.reason = reason; + } + session.send(msg); + session.emit("close"); + var name = window.name; + storage.tab.get("status").then(function (saved) { + if (! saved) { + console.warn("No session information saved in", "status." + name); + } else { + saved.running = false; + saved.date = Date.now(); + storage.tab.set("status", saved); + } + channel.close(); + channel = null; + session.shareId = null; + session.emit("shareId"); + TogetherJS.emit("close"); + TogetherJS._teardown(); + }); +}; + +session.on("start", function () { + $(window).on("resize", resizeEvent); + if (includeHashInUrl()) { + $(window).on("hashchange", hashchangeEvent); + } +}); + +session.on("close", function () { + $(window).off("resize", resizeEvent); + if (includeHashInUrl()) { + $(window).off("hashchange", hashchangeEvent); + } +}); + +function hashchangeEvent() { + // needed because when message arives from peer this variable will be checked to + // decide weather to show actions or not + sendHello(false); +} + +function resizeEvent() { + session.emit("resize"); +} + +util.testExpose({ + getChannel: function () { + return channel; + } +}); + +provide("session", session); + +export default session; diff --git a/src/core/startup.js b/src/core/startup.js new file mode 100644 index 000000000..c2f768bba --- /dev/null +++ b/src/core/startup.js @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This module handles all the different UI that happens (sometimes in order) when + TogetherJS is started: + + - Introduce the session when you've been invited + - Show any browser compatibility indicators + - Show the walkthrough the first time + - Show the share link window + + When everything is done it fires session.emit("startup-ready") + +*/ +import TogetherJS from "./togetherjs.js"; +import { provide, need } from "./registry.js"; +import util from "./util.js"; +import $ from "jquery"; +import windowing from "../ui/windowing.js"; +import storage from "./storage.js"; + +var assert = util.assert; +var startup = util.Module("startup"); +// Avoid circular import: +var session = null; + +var STEPS = [ + "browserBroken", + "browserUnsupported", + "sessionIntro", + "walkthrough", + // Look in the share() below if you add anything after here: + "share" + ]; + +var currentStep = null; + +startup.start = function () { + if (! session) { + session = need("session"); + } + var index = -1; + if (currentStep) { + index = STEPS.indexOf(currentStep); + } + index++; + if (index >= STEPS.length) { + session.emit("startup-ready"); + return; + } + currentStep = STEPS[index]; + handlers[currentStep](startup.start); +}; + +var handlers = { + + browserBroken: function (next) { + if (window.WebSocket) { + next(); + return; + } + windowing.show("#togetherjs-browser-broken", { + onClose: function () { + session.close(); + } + }); + if ($.browser.msie) { + $("#togetherjs-browser-broken-is-ie").show(); + } + }, + + browserUnsupported: function (next) { + next(); + }, + + sessionIntro: function (next) { + if ((! session.isClient) || ! session.firstRun) { + next(); + return; + } + TogetherJS.config.close("suppressJoinConfirmation"); + if (TogetherJS.config.get("suppressJoinConfirmation")) { + next(); + return; + } + var cancelled = false; + windowing.show("#togetherjs-intro", { + onClose: function () { + if (! cancelled) { + next(); + } + } + }); + $("#togetherjs-intro .togetherjs-modal-dont-join").click(function () { + cancelled = true; + windowing.hide(); + session.close("declined-join"); + }); + }, + + walkthrough: function (next) { + storage.settings.get("seenIntroDialog").then(function (seenIntroDialog) { + if (seenIntroDialog) { + next(); + return; + } + var walkthrough = need("walkthrough"); + walkthrough.start(true, function () { + storage.settings.set("seenIntroDialog", true); + next(); + }); + }); + }, + + share: function (next) { + TogetherJS.config.close("suppressInvite"); + if (session.isClient || (! session.firstRun) || + TogetherJS.config.get("suppressInvite")) { + next(); + return; + } + need("windowing").show("#togetherjs-share"); + // FIXME: no way to detect when the window is closed + // If there was a next() step then it would not work + } + +}; + +provide("startup", startup); + +export default startup; diff --git a/src/core/storage.js b/src/core/storage.js new file mode 100644 index 000000000..d3b67c62f --- /dev/null +++ b/src/core/storage.js @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import TogetherJS from "./togetherjs.js"; +import util from "./util.js"; + +var assert = util.assert; +var Deferred = util.Deferred; +var DEFAULT_SETTINGS = { + name: "", + defaultName: "", + avatar: null, + stickyShare: null, + color: null, + seenIntroDialog: false, + seenWalkthrough: false, + dontShowRtcInfo: false +}; + +var DEBUG_STORAGE = false; + +var Storage = util.Class({ + // `suffix` is appended to the configured storagePrefix. The prefix is + // resolved lazily (see .prefix below) because this module is evaluated as + // soon as the bundle loads, which is before the host page's configuration + // has been applied. + constructor: function (name, storage, suffix) { + this.name = name; + this.storage = storage; + this.suffix = suffix; + }, + + get prefix() { + if (this._prefix === undefined) { + this._prefix = TogetherJS.config.close("storagePrefix") + this.suffix; + } + return this._prefix; + }, + + get: function (key, defaultValue) { + var self = this; + return Deferred(function (def) { + // Strictly this isn't necessary, but eventually I want to move to something more + // async for the storage, and this simulates that much better. + setTimeout(util.resolver(def, function () { + key = self.prefix + key; + var value = self.storage.getItem(key); + if (! value) { + value = defaultValue; + if (DEBUG_STORAGE) { + console.debug("Get storage", key, "defaults to", value); + } + } else { + value = JSON.parse(value); + if (DEBUG_STORAGE) { + console.debug("Get storage", key, "=", value); + } + } + return value; + })); + }); + }, + + set: function (key, value) { + var self = this; + if (value !== undefined) { + value = JSON.stringify(value); + } + return Deferred(function (def) { + key = self.prefix + key; + if (value === undefined) { + self.storage.removeItem(key); + if (DEBUG_STORAGE) { + console.debug("Delete storage", key); + } + } else { + self.storage.setItem(key, value); + if (DEBUG_STORAGE) { + console.debug("Set storage", key, value); + } + } + setTimeout(def.resolve); + }); + }, + + clear: function () { + var self = this; + var promises = []; + return Deferred((function (def) { + this.keys().then(function (keys) { + keys.forEach(function (key) { + // FIXME: technically we're ignoring the promise returned by all + // these sets: + promises.push(self.set(key, undefined)); + }); + util.resolveMany(promises).then(function () { + def.resolve(); + }); + }); + }).bind(this)); + }, + + keys: function (prefix, excludePrefix) { + // Returns a list of keys, potentially with the given prefix + var self = this; + return Deferred(function (def) { + setTimeout(util.resolver(def, function () { + prefix = prefix || ""; + var result = []; + for (var i = 0; i < self.storage.length; i++) { + var key = self.storage.key(i); + if (key.indexOf(self.prefix + prefix) === 0) { + var shortKey = key.substr(self.prefix.length); + if (excludePrefix) { + shortKey = shortKey.substr(prefix.length); + } + result.push(shortKey); + } + } + return result; + })); + }); + }, + + toString: function () { + return '[storage for ' + this.name + ']'; + } + +}); + +var storage = Storage('localStorage', localStorage, "."); + +storage.settings = util.mixinEvents({ + defaults: DEFAULT_SETTINGS, + + get: function (name) { + assert(storage.settings.defaults.hasOwnProperty(name), "Unknown setting:", name); + return storage.get("settings." + name, storage.settings.defaults[name]); + }, + + set: function (name, value) { + assert(storage.settings.defaults.hasOwnProperty(name), "Unknown setting:", name); + return storage.set("settings." + name, value); + } + +}); + +storage.tab = Storage('sessionStorage', sessionStorage, "-session."); + +export default storage; diff --git a/src/core/togetherjs.js b/src/core/togetherjs.js new file mode 100644 index 000000000..3efce45d9 --- /dev/null +++ b/src/core/togetherjs.js @@ -0,0 +1,531 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* The TogetherJS object itself: configuration, the event mixin, startup state, + * and the public API surface. + * + * This is deliberately free of any dependency on the rest of the client, so + * that it evaluates first and every other module can `import TogetherJS from + * "…/core/togetherjs.js"` instead of reaching for a global that may not exist + * yet. Under RequireJS the load order made the global safe; in a single bundle + * it would not be. + * + * The boot logic that actually starts a session lives in ../index.js. + */ + +var defaultConfiguration = { + // Disables clicks for a certain element. + // (e.g., 'canvas' would not show clicks on canvas elements.) + // Setting this to true will disable clicks globally. + dontShowClicks: false, + // Experimental feature to echo clicks to certain elements across clients: + cloneClicks: false, + // The base URL of the hub (gets filled in below): + hubBase: null, + // A function that will return the name of the user: + getUserName: null, + // A function that will return the color of the user: + getUserColor: null, + // A function that will return the avatar of the user: + getUserAvatar: null, + // The siteName is used in the walkthrough (defaults to document.title): + siteName: null, + // Any events to bind to + on: {}, + // Hub events to bind to + hub_on: {}, + // Enables the alt-T alt-T TogetherJS shortcut; however, this setting + // must be enabled early as TogetherJSConfig_enableShortcut = true; + enableShortcut: false, + // The name of this tool as provided to users. The UI is updated to use this. + // Because of how it is used in text it should be a proper noun, e.g., + // "MySite's Collaboration Tool" + toolName: null, + // Used to auto-start TogetherJS with a {prefix: pageName, max: participants} + // Also with findRoom: "roomName" it will connect to the given room name + findRoom: null, + // If true, starts TogetherJS automatically (of course!) + autoStart: false, + // If true, then the "Join TogetherJS Session?" confirmation dialog + // won't come up + suppressJoinConfirmation: false, + // If true, then the "Invite a friend" window won't automatically come up + suppressInvite: false, + // A room in which to find people to invite to this session, + inviteFromRoom: null, + // This is used to keep sessions from crossing over on the same + // domain, if for some reason you want sessions that are limited + // to only a portion of the domain: + storagePrefix: "togetherjs", + // When true, we treat the entire URL, including the hash, as the identifier + // of the page; i.e., if you one person is on `http://example.com/#view1` + // and another person is at `http://example.com/#view2` then these two people + // are considered to be at completely different URLs + includeHashInUrl: false, + // When true, the WebRTC-based mic/chat will be disabled + disableWebRTC: false, + // When true, the camera button is offered alongside the microphone. + // Off by default: camera access is a bigger ask than the mic, and an + // existing embed should not sprout a camera button on upgrade. + enableVideo: false, + // ICE servers for WebRTC. null means "just the default STUN server". + // Supply TURN servers here if your users sit behind symmetric NATs. + iceServers: null, + // async () => RTCIceServer[]. Preferred over `iceServers` when TURN + // credentials are short-lived and have to be minted per session. + getIceServers: null, + // The mesh is full-mesh, so uplink cost grows with the square of the + // participant count. Past this many peers we refuse new connections + // rather than degrading silently. + maxRtcPeers: 6, + // When true, youTube videos will synchronize + youtube: true, + // Ignores the following console messages, disables all messages if set to true + ignoreMessages: ["cursor-update", "keydown", "scroll-update"], + // Ignores the following forms (will ignore all forms if set to true): + ignoreForms: [":password"], + // When undefined, attempts to use the browser's language + lang: undefined, + fallbackLang: "en-US", + // Overrides the UI's font-family; accepts any CSS font-family value, + // including a reference to a CSS custom property already defined on + // the host page (e.g. "var(--font-base)") + baseFont: null, +}; + +// Substituted by build/build.mjs. +var BUILD_HUB_URL = __HUB_URL__; +var BUILD_GIT_COMMIT = __GIT_COMMIT__; +var BUILD_BASE_URL = __BASE_URL__; + +defaultConfiguration.hubBase = BUILD_HUB_URL; + +/* Resolve the base URL the client was served from; images and the stylesheet + are fetched relative to it. */ +function resolveBaseUrl() { + if (window.TogetherJSConfig && window.TogetherJSConfig.baseUrl) { + return window.TogetherJSConfig.baseUrl; + } + if (window.TogetherJSConfig_baseUrl) { + return window.TogetherJSConfig_baseUrl; + } + if (BUILD_BASE_URL) { + return BUILD_BASE_URL; + } + // import.meta.url is not available in an IIFE bundle, so fall back to + // locating our own - - - - diff --git a/togetherjs/tests/doctestjs/.resources/boilerplate/css/main.css b/togetherjs/tests/doctestjs/.resources/boilerplate/css/main.css deleted file mode 100755 index 16a90cab7..000000000 --- a/togetherjs/tests/doctestjs/.resources/boilerplate/css/main.css +++ /dev/null @@ -1,397 +0,0 @@ -/* ========================================================================== - HTML5 Boilerplate styles - h5bp.com (generated via initializr.com) - ========================================================================== */ - -html, -button, -input, -select, -textarea { - color: #222; -} - -body { - font-size: 1em; - line-height: 1.4; -} - -::-moz-selection { - background: #b3d4fc; - text-shadow: none; -} - -::selection { - background: #b3d4fc; - text-shadow: none; -} - -hr { - display: block; - height: 1px; - border: 0; - border-top: 1px solid #ccc; - margin: 1em 0; - padding: 0; -} - -img { - vertical-align: middle; -} - -fieldset { - border: 0; - margin: 0; - padding: 0; -} - -textarea { - resize: vertical; -} - -.chromeframe { - margin: 0.2em 0; - background: #ccc; - color: #000; - padding: 0.2em 0; -} - - -/* ===== Initializr Styles ================================================== - Author: Jonathan Verrecchia - verekia.com/initializr/responsive-template - ========================================================================== */ - -body { - font: 16px/26px Helvetica, Helvetica Neue, Arial; -} - -.wrapper { - width: 90%; - margin: 0 5%; -} - -/* =================== - ALL: Orange Theme - =================== */ - -.header-container { - border-bottom: 20px solid #e44d26; -} - -.footer-container, -.main aside { - border-top: 20px solid #e44d26; -} - -.header-container, -.footer-container, -.main aside { - background: #f16529; -} - -.title { - color: white; -} - -/* ============== - MOBILE: Menu - ============== */ - -nav ul { - margin: 0; - padding: 0; -} - -nav a { - display: block; - margin-bottom: 10px; - padding: 15px 0; - - text-align: center; - text-decoration: none; - font-weight: bold; - - color: white; - background: #e44d26; -} - -nav a:hover, -nav a:visited { - color: white; -} - -nav a:hover { - text-decoration: underline; -} - -/* ============== - MOBILE: Main - ============== */ - -.main { - padding: 30px 0; -} - -.main article h1 { - font-size: 2em; -} - -.main aside { - color: white; - padding: 0px 5% 10px; -} - -.footer-container footer { - color: white; - padding: 20px 0; -} - -/* =============== - ALL: IE Fixes - =============== */ - -.ie7 .title { - padding-top: 20px; -} - -/* ========================================================================== - Author's custom styles - ========================================================================== */ - - - - - - - - - - - - - - - -/* ========================================================================== - Media Queries - ========================================================================== */ - -@media only screen and (min-width: 480px) { - -/* ==================== - INTERMEDIATE: Menu - ==================== */ - - nav a { - float: left; - width: 27%; - margin: 0 1.7%; - padding: 25px 2%; - margin-bottom: 0; - } - - nav li:first-child a { - margin-left: 0; - } - - nav li:last-child a { - margin-right: 0; - } - -/* ======================== - INTERMEDIATE: IE Fixes - ======================== */ - - nav ul li { - display: inline; - } - - .oldie nav a { - margin: 0 0.7%; - } -} - -@media only screen and (min-width: 768px) { - -/* ==================== - WIDE: CSS3 Effects - ==================== */ - - .header-container, - .main aside { - -webkit-box-shadow: 0 5px 10px #aaa; - -moz-box-shadow: 0 5px 10px #aaa; - box-shadow: 0 5px 10px #aaa; - } - -/* ============ - WIDE: Menu - ============ */ - - .title { - float: left; - } - - nav { - float: right; - width: 38%; - } - -/* ============ - WIDE: Main - ============ */ - - .main article { - float: left; - width: 57%; - } - - .main aside { - float: right; - width: 28%; - } -} - -@media only screen and (min-width: 1140px) { - -/* =============== - Maximal Width - =============== */ - - .wrapper { - width: 1026px; /* 1140px - 10% for margins */ - margin: 0 auto; - } -} - -/* ========================================================================== - Helper classes - ========================================================================== */ - -.ir { - background-color: transparent; - border: 0; - overflow: hidden; - *text-indent: -9999px; -} - -.ir:before { - content: ""; - display: block; - width: 0; - height: 100%; -} - -.hidden { - display: none !important; - visibility: hidden; -} - -.visuallyhidden { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - -.visuallyhidden.focusable:active, -.visuallyhidden.focusable:focus { - clip: auto; - height: auto; - margin: 0; - overflow: visible; - position: static; - width: auto; -} - -.invisible { - visibility: hidden; -} - -.clearfix:before, -.clearfix:after { - content: " "; - display: table; -} - -.clearfix:after { - clear: both; -} - -.clearfix { - *zoom: 1; -} - -/* ========================================================================== - Print styles - ========================================================================== */ - -@media print { - * { - background: transparent !important; - color: #000 !important; /* Black prints faster: h5bp.com/s */ - box-shadow:none !important; - text-shadow: none !important; - } - - a, - a:visited { - text-decoration: underline; - } - - a[href]:after { - content: " (" attr(href) ")"; - } - - abbr[title]:after { - content: " (" attr(title) ")"; - } - - /* - * Don't show links for images, or javascript/internal links - */ - - .ir a:after, - a[href^="javascript:"]:after, - a[href^="#"]:after { - content: ""; - } - - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - - thead { - display: table-header-group; /* h5bp.com/t */ - } - - tr, - img { - page-break-inside: avoid; - } - - img { - max-width: 100% !important; - } - - @page { - margin: 0.5cm; - } - - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - - h2, - h3 { - page-break-after: avoid; - } -} \ No newline at end of file diff --git a/togetherjs/tests/doctestjs/.resources/boilerplate/css/normalize.css b/togetherjs/tests/doctestjs/.resources/boilerplate/css/normalize.css deleted file mode 100755 index d4210aac2..000000000 --- a/togetherjs/tests/doctestjs/.resources/boilerplate/css/normalize.css +++ /dev/null @@ -1,504 +0,0 @@ -/*! normalize.css v1.0.1 | MIT License | git.io/normalize */ - -/* ========================================================================== - HTML5 display definitions - ========================================================================== */ - -/* - * Corrects `block` display not defined in IE 6/7/8/9 and Firefox 3. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section, -summary { - display: block; -} - -/* - * Corrects `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. - */ - -audio, -canvas, -video { - display: inline-block; - *display: inline; - *zoom: 1; -} - -/* - * Prevents modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/* - * Addresses styling for `hidden` attribute not present in IE 7/8/9, Firefox 3, - * and Safari 4. - * Known issue: no IE 6 support. - */ - -[hidden] { - display: none; -} - -/* ========================================================================== - Base - ========================================================================== */ - -/* - * 1. Corrects text resizing oddly in IE 6/7 when body `font-size` is set using - * `em` units. - * 2. Prevents iOS text size adjust after orientation change, without disabling - * user zoom. - */ - -html { - font-size: 100%; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ - -ms-text-size-adjust: 100%; /* 2 */ -} - -/* - * Addresses `font-family` inconsistency between `textarea` and other form - * elements. - */ - -html, -button, -input, -select, -textarea { - font-family: sans-serif; -} - -/* - * Addresses margins handled incorrectly in IE 6/7. - */ - -body { - margin: 0; -} - -/* ========================================================================== - Links - ========================================================================== */ - -/* - * Addresses `outline` inconsistency between Chrome and other browsers. - */ - -a:focus { - outline: thin dotted; -} - -/* - * Improves readability when focused and also mouse hovered in all browsers. - */ - -a:active, -a:hover { - outline: 0; -} - -/* ========================================================================== - Typography - ========================================================================== */ - -/* - * Addresses font sizes and margins set differently in IE 6/7. - * Addresses font sizes within `section` and `article` in Firefox 4+, Safari 5, - * and Chrome. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -h2 { - font-size: 1.5em; - margin: 0.83em 0; -} - -h3 { - font-size: 1.17em; - margin: 1em 0; -} - -h4 { - font-size: 1em; - margin: 1.33em 0; -} - -h5 { - font-size: 0.83em; - margin: 1.67em 0; -} - -h6 { - font-size: 0.75em; - margin: 2.33em 0; -} - -/* - * Addresses styling not present in IE 7/8/9, Safari 5, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/* - * Addresses style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -blockquote { - margin: 1em 40px; -} - -/* - * Addresses styling not present in Safari 5 and Chrome. - */ - -dfn { - font-style: italic; -} - -/* - * Addresses styling not present in IE 6/7/8/9. - */ - -mark { - background: #ff0; - color: #000; -} - -/* - * Addresses margins set differently in IE 6/7. - */ - -p, -pre { - margin: 1em 0; -} - -/* - * Corrects font family set oddly in IE 6, Safari 4/5, and Chrome. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, serif; - _font-family: 'courier new', monospace; - font-size: 1em; -} - -/* - * Improves readability of pre-formatted text in all browsers. - */ - -pre { - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; -} - -/* - * Addresses CSS quotes not supported in IE 6/7. - */ - -q { - quotes: none; -} - -/* - * Addresses `quotes` property not supported in Safari 4. - */ - -q:before, -q:after { - content: ''; - content: none; -} - -/* - * Addresses inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/* - * Prevents `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* ========================================================================== - Lists - ========================================================================== */ - -/* - * Addresses margins set differently in IE 6/7. - */ - -dl, -menu, -ol, -ul { - margin: 1em 0; -} - -dd { - margin: 0 0 0 40px; -} - -/* - * Addresses paddings set differently in IE 6/7. - */ - -menu, -ol, -ul { - padding: 0 0 0 40px; -} - -/* - * Corrects list images handled incorrectly in IE 7. - */ - -nav ul, -nav ol { - list-style: none; - list-style-image: none; -} - -/* ========================================================================== - Embedded content - ========================================================================== */ - -/* - * 1. Removes border when inside `a` element in IE 6/7/8/9 and Firefox 3. - * 2. Improves image quality when scaled in IE 7. - */ - -img { - border: 0; /* 1 */ - -ms-interpolation-mode: bicubic; /* 2 */ -} - -/* - * Corrects overflow displayed oddly in IE 9. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* ========================================================================== - Figures - ========================================================================== */ - -/* - * Addresses margin not present in IE 6/7/8/9, Safari 5, and Opera 11. - */ - -figure { - margin: 0; -} - -/* ========================================================================== - Forms - ========================================================================== */ - -/* - * Corrects margin displayed oddly in IE 6/7. - */ - -form { - margin: 0; -} - -/* - * Define consistent border, margin, and padding. - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/* - * 1. Corrects color not being inherited in IE 6/7/8/9. - * 2. Corrects text not wrapping in Firefox 3. - * 3. Corrects alignment displayed oddly in IE 6/7. - */ - -legend { - border: 0; /* 1 */ - padding: 0; - white-space: normal; /* 2 */ - *margin-left: -7px; /* 3 */ -} - -/* - * 1. Corrects font size not being inherited in all browsers. - * 2. Addresses margins set differently in IE 6/7, Firefox 3+, Safari 5, - * and Chrome. - * 3. Improves appearance and consistency in all browsers. - */ - -button, -input, -select, -textarea { - font-size: 100%; /* 1 */ - margin: 0; /* 2 */ - vertical-align: baseline; /* 3 */ - *vertical-align: middle; /* 3 */ -} - -/* - * Addresses Firefox 3+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -button, -input { - line-height: normal; -} - -/* - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Corrects inability to style clickable `input` types in iOS. - * 3. Improves usability and consistency of cursor style between image-type - * `input` and others. - * 4. Removes inner spacing in IE 7 without affecting normal text inputs. - * Known issue: inner spacing remains in IE 6. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ - *overflow: visible; /* 4 */ -} - -/* - * Re-set default cursor for disabled elements. - */ - -button[disabled], -input[disabled] { - cursor: default; -} - -/* - * 1. Addresses box sizing set to content-box in IE 8/9. - * 2. Removes excess padding in IE 8/9. - * 3. Removes excess padding in IE 7. - * Known issue: excess padding remains in IE 6. - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ - *height: 13px; /* 3 */ - *width: 13px; /* 3 */ -} - -/* - * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. - * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome - * (include `-moz` to future-proof). - */ - -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; /* 2 */ - box-sizing: content-box; -} - -/* - * Removes inner padding and search cancel button in Safari 5 and Chrome - * on OS X. - */ - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* - * Removes inner padding and border in Firefox 3+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/* - * 1. Removes default vertical scrollbar in IE 6/7/8/9. - * 2. Improves readability and alignment in all browsers. - */ - -textarea { - overflow: auto; /* 1 */ - vertical-align: top; /* 2 */ -} - -/* ========================================================================== - Tables - ========================================================================== */ - -/* - * Remove most spacing between table cells. - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} diff --git a/togetherjs/tests/doctestjs/.resources/boilerplate/css/normalize.min.css b/togetherjs/tests/doctestjs/.resources/boilerplate/css/normalize.min.css deleted file mode 100755 index a783c5307..000000000 --- a/togetherjs/tests/doctestjs/.resources/boilerplate/css/normalize.min.css +++ /dev/null @@ -1,50 +0,0 @@ -/*! normalize.css v1.0.1 | MIT License | git.io/normalize */ -article,aside,details,figcaption,figure,footer,header,hgroup,nav,section,summary{display:block} -audio,canvas,video{display:inline-block;*display:inline;*zoom:1} -audio:not([controls]){display:none;height:0} -[hidden]{display:none} -html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%} -html,button,input,select,textarea{font-family:sans-serif} -body{margin:0} -a:focus{outline:thin dotted} -a:active,a:hover{outline:0} -h1{font-size:2em;margin:.67em 0} -h2{font-size:1.5em;margin:.83em 0} -h3{font-size:1.17em;margin:1em 0} -h4{font-size:1em;margin:1.33em 0} -h5{font-size:.83em;margin:1.67em 0} -h6{font-size:.75em;margin:2.33em 0} -abbr[title]{border-bottom:1px dotted} -b,strong{font-weight:bold} -blockquote{margin:1em 40px} -dfn{font-style:italic} -mark{background:#ff0;color:#000} -p,pre{margin:1em 0} -code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em} -pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word} -q{quotes:none} -q:before,q:after{content:'';content:none} -small{font-size:80%} -sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} -sup{top:-0.5em} -sub{bottom:-0.25em} -dl,menu,ol,ul{margin:1em 0} -dd{margin:0 0 0 40px} -menu,ol,ul{padding:0 0 0 40px} -nav ul,nav ol{list-style:none;list-style-image:none} -img{border:0;-ms-interpolation-mode:bicubic} -svg:not(:root){overflow:hidden} -figure{margin:0} -form{margin:0} -fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} -legend{border:0;padding:0;white-space:normal;*margin-left:-7px} -button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle} -button,input{line-height:normal} -button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible} -button[disabled],input[disabled]{cursor:default} -input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px} -input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box} -input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none} -button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0} -textarea{overflow:auto;vertical-align:top} -table{border-collapse:collapse;border-spacing:0} \ No newline at end of file diff --git a/togetherjs/tests/doctestjs/.resources/boilerplate/favicon.ico b/togetherjs/tests/doctestjs/.resources/boilerplate/favicon.ico deleted file mode 100755 index 1f2a998670d70716df63fe473f4990f6dd7e0bc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 318 zcmZQzU<5(|0RbS%!l1#(z#zuJz@P!d0zj+)#2|58Kv7wdK}lJcK}pS-K}%1ALCK(u zLC?XNL0P|(LEUySgK=a%gNny71`UsM48}2Y81xdiGZ>{@XE08E1U3?A9S{I*Vq^p{ z83jOWCLm@$!oau<#P2JN0V|DtcXt6$5u^3HdkG+y#lCwt38;(X-Q9QZ92gk(0;#*p f1%Q5udjvL#!FD208fYAM0gw;WD5?nZB$WXGagiqp diff --git a/togetherjs/tests/doctestjs/.resources/boilerplate/index.html b/togetherjs/tests/doctestjs/.resources/boilerplate/index.html deleted file mode 100755 index 05268f657..000000000 --- a/togetherjs/tests/doctestjs/.resources/boilerplate/index.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - -
-
-

h1.title

- -
-
- -
-
- -
-
-

article header h1

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sodales urna non odio egestas tempor. Nunc vel vehicula ante. Etiam bibendum iaculis libero, eget molestie nisl pharetra in. In semper consequat est, eu porta velit mollis nec.

-
-
-

article section h2

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sodales urna non odio egestas tempor. Nunc vel vehicula ante. Etiam bibendum iaculis libero, eget molestie nisl pharetra in. In semper consequat est, eu porta velit mollis nec. Curabitur posuere enim eget turpis feugiat tempor. Etiam ullamcorper lorem dapibus velit suscipit ultrices. Proin in est sed erat facilisis pharetra.

-
-
-

article section h2

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sodales urna non odio egestas tempor. Nunc vel vehicula ante. Etiam bibendum iaculis libero, eget molestie nisl pharetra in. In semper consequat est, eu porta velit mollis nec. Curabitur posuere enim eget turpis feugiat tempor. Etiam ullamcorper lorem dapibus velit suscipit ultrices. Proin in est sed erat facilisis pharetra.

-
-
-

article footer h3

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sodales urna non odio egestas tempor. Nunc vel vehicula ante. Etiam bibendum iaculis libero, eget molestie nisl pharetra in. In semper consequat est, eu porta velit mollis nec. Curabitur posuere enim eget turpis feugiat tempor.

-
-
- - - -
-
- - - - - - - - - - - diff --git a/togetherjs/tests/doctestjs/.resources/boilerplate/js/main.js b/togetherjs/tests/doctestjs/.resources/boilerplate/js/main.js deleted file mode 100755 index 8b1378917..000000000 --- a/togetherjs/tests/doctestjs/.resources/boilerplate/js/main.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/togetherjs/tests/doctestjs/.resources/boilerplate/js/vendor/jquery-1.8.1.min.js b/togetherjs/tests/doctestjs/.resources/boilerplate/js/vendor/jquery-1.8.1.min.js deleted file mode 100755 index e7f2a292b..000000000 --- a/togetherjs/tests/doctestjs/.resources/boilerplate/js/vendor/jquery-1.8.1.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v@1.8.1 jquery.com | jquery.org/license */ -(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write(""),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bS[a]=c,c}function ci(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||ce.test(a)?d(a,e):ci(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ci(a+"["+e+"]",b[e],c,d);else d(a,b)}function cz(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.1",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,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(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return typeof a=="object"?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b
a",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length||!d)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="
t
",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="
",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||++p.uuid:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.length,e=c.shift(),f=p._queueHooks(a,b),g=function(){p.dequeue(a,b)};e==="inprogress"&&(e=c.shift(),d--),e&&(b==="fx"&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c-1)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c-1)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,""+d),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;j=0),h[l]&&j.push(k);j.length&&t.push({elem:f,matches:j})}n.length>o&&t.push({elem:this,matches:n.slice(o)});for(d=0;d0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function $(a,b,c,d){c=c||[],b=b||q;var e,f,g,j,k=b.nodeType;if(k!==1&&k!==9)return[];if(!a||typeof a!="string")return c;g=h(b);if(!g&&!d)if(e=L.exec(a))if(j=e[1]){if(k===9){f=b.getElementById(j);if(!f||!f.parentNode)return c;if(f.id===j)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(j))&&i(b,f)&&f.id===j)return c.push(f),c}else{if(e[2])return u.apply(c,t.call(b.getElementsByTagName(a),0)),c;if((j=e[3])&&X&&b.getElementsByClassName)return u.apply(c,t.call(b.getElementsByClassName(j),0)),c}return bk(a,b,c,d,g)}function _(a){return function(b){var c=b.nodeName.toLowerCase();return c==="input"&&b.type===a}}function ba(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}}function bb(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}function bc(a,b,c,d){var e,g,h,i,j,k,l,m,n,p,r=!c&&b!==q,s=(r?"":"")+a.replace(H,"$1"),u=y[o][s];if(u)return d?0:t.call(u,0);j=a,k=[],m=0,n=f.preFilter,p=f.filter;while(j){if(!e||(g=I.exec(j)))g&&(j=j.slice(g[0].length),h.selector=l),k.push(h=[]),l="",r&&(j=" "+j);e=!1;if(g=J.exec(j))l+=g[0],j=j.slice(g[0].length),e=h.push({part:g.pop().replace(H," "),string:g[0],captures:g});for(i in p)(g=S[i].exec(j))&&(!n[i]||(g=n[i](g,b,c)))&&(l+=g[0],j=j.slice(g[0].length),e=h.push({part:i,string:g.shift(),captures:g}));if(!e)break}return l&&(h.selector=l),d?j.length:j?$.error(a):t.call(y(s,k),0)}function bd(a,b,e,f){var g=b.dir,h=s++;return a||(a=function(a){return a===e}),b.first?function(b){while(b=b[g])if(b.nodeType===1)return a(b)&&b}:f?function(b){while(b=b[g])if(b.nodeType===1&&a(b))return b}:function(b){var e,f=h+"."+c,i=f+"."+d;while(b=b[g])if(b.nodeType===1){if((e=b[o])===i)return b.sizset;if(typeof e=="string"&&e.indexOf(f)===0){if(b.sizset)return b}else{b[o]=i;if(a(b))return b.sizset=!0,b;b.sizset=!1}}}}function be(a,b){return a?function(c){var d=b(c);return d&&a(d===!0?c:d)}:b}function bf(a,b,c){var d,e,g=0;for(;d=a[g];g++)f.relative[d.part]?e=bd(e,f.relative[d.part],b,c):e=be(e,f.filter[d.part].apply(null,d.captures.concat(b,c)));return e}function bg(a){return function(b){var c,d=0;for(;c=a[d];d++)if(c(b))return!0;return!1}}function bh(a,b,c,d){var e=0,f=b.length;for(;e0?i(h,c,g):[]}function bj(a,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q,r,s=0,t=a.length,v=S.POS,w=new RegExp("^"+v.source+"(?!"+A+")","i"),x=function(){var a=1,c=arguments.length-2;for(;al){g+=k.slice(l,n.index),l=p,q=[c],J.test(g)&&(m&&(q=m),m=e);if(r=O.test(g))g=g.slice(0,-5).replace(J,"$&*"),l++;n.length>1&&n[0].replace(w,x),m=bi(g,n[1],n[2],q,m,r)}g=""}}o||(g+=k),o=!1}g?J.test(g)?bh(g,m||[c],d,e):$(g,c,d,e?e.concat(m):m):u.apply(d,m)}return t===1?d:$.uniqueSort(d)}function bk(a,b,e,g,h){a=a.replace(H,"$1");var i,k,l,m,n,o,p,q,r,s,v=bc(a,b,h),w=b.nodeType;if(S.POS.test(a))return bj(v,b,e,g);if(g)i=t.call(g,0);else if(v.length===1){if((o=t.call(v[0],0)).length>2&&(p=o[0]).part==="ID"&&w===9&&!h&&f.relative[o[1].part]){b=f.find.ID(p.captures[0].replace(R,""),b,h)[0];if(!b)return e;a=a.slice(o.shift().string.length)}r=(v=N.exec(o[0].string))&&!v.index&&b.parentNode||b,q="";for(n=o.length-1;n>=0;n--){p=o[n],s=p.part,q=p.string+q;if(f.relative[s])break;if(f.order.test(s)){i=f.find[s](p.captures[0].replace(R,""),r,h);if(i==null)continue;a=a.slice(0,a.length-q.length)+q.replace(S[s],""),a||u.apply(e,t.call(i,0));break}}}if(a){k=j(a,b,h),c=k.dirruns++,i==null&&(i=f.find.TAG("*",N.test(a)&&b.parentNode||b));for(n=0;m=i[n];n++)d=k.runs++,k(m)&&e.push(m)}return e}var c,d,e,f,g,h,i,j,k,l,m=!0,n="undefined",o=("sizcache"+Math.random()).replace(".",""),q=a.document,r=q.documentElement,s=0,t=[].slice,u=[].push,v=function(a,b){return a[o]=b||!0,a},w=function(){var a={},b=[];return v(function(c,d){return b.push(c)>f.cacheLength&&delete a[b.shift()],a[c]=d},a)},x=w(),y=w(),z=w(),A="[\\x20\\t\\r\\n\\f]",B="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",C=B.replace("w","w#"),D="([*^$|!~]?=)",E="\\["+A+"*("+B+")"+A+"*(?:"+D+A+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+C+")|)|)"+A+"*\\]",F=":("+B+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+E+")|[^:]|\\\\.)*|.*))\\)|)",G=":(nth|eq|gt|lt|first|last|even|odd)(?:\\(((?:-\\d)?\\d*)\\)|)(?=[^-]|$)",H=new RegExp("^"+A+"+|((?:^|[^\\\\])(?:\\\\.)*)"+A+"+$","g"),I=new RegExp("^"+A+"*,"+A+"*"),J=new RegExp("^"+A+"*([\\x20\\t\\r\\n\\f>+~])"+A+"*"),K=new RegExp(F),L=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,M=/^:not/,N=/[\x20\t\r\n\f]*[+~]/,O=/:not\($/,P=/h\d/i,Q=/input|select|textarea|button/i,R=/\\(?!\\)/g,S={ID:new RegExp("^#("+B+")"),CLASS:new RegExp("^\\.("+B+")"),NAME:new RegExp("^\\[name=['\"]?("+B+")['\"]?\\]"),TAG:new RegExp("^("+B.replace("w","w*")+")"),ATTR:new RegExp("^"+E),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|nth|last|first)-child(?:\\("+A+"*(even|odd|(([+-]|)(\\d*)n|)"+A+"*(?:([+-]|)"+A+"*(\\d+)|))"+A+"*\\)|)","i"),POS:new RegExp(G,"ig"),needsContext:new RegExp("^"+A+"*[>+~]|"+G,"i")},T=function(a){var b=q.createElement("div");try{return a(b)}catch(c){return!1}finally{b=null}},U=T(function(a){return a.appendChild(q.createComment("")),!a.getElementsByTagName("*").length}),V=T(function(a){return a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!==n&&a.firstChild.getAttribute("href")==="#"}),W=T(function(a){a.innerHTML="";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),X=T(function(a){return a.innerHTML="",!a.getElementsByClassName||!a.getElementsByClassName("e").length?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length===2)}),Y=T(function(a){a.id=o+0,a.innerHTML="
",r.insertBefore(a,r.firstChild);var b=q.getElementsByName&&q.getElementsByName(o).length===2+q.getElementsByName(o+0).length;return e=!q.getElementById(o),r.removeChild(a),b});try{t.call(r.childNodes,0)[0].nodeType}catch(Z){t=function(a){var b,c=[];for(;b=this[a];a++)c.push(b);return c}}$.matches=function(a,b){return $(a,null,null,b)},$.matchesSelector=function(a,b){return $(b,null,null,[a]).length>0},g=$.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(e===1||e===9||e===11){if(typeof a.textContent=="string")return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=g(a)}else if(e===3||e===4)return a.nodeValue}else for(;b=a[d];d++)c+=g(b);return c},h=$.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?b.nodeName!=="HTML":!1},i=$.contains=r.contains?function(a,b){var c=a.nodeType===9?a.documentElement:a,d=b&&b.parentNode;return a===d||!!(d&&d.nodeType===1&&c.contains&&c.contains(d))}:r.compareDocumentPosition?function(a,b){return b&&!!(a.compareDocumentPosition(b)&16)}:function(a,b){while(b=b.parentNode)if(b===a)return!0;return!1},$.attr=function(a,b){var c,d=h(a);return d||(b=b.toLowerCase()),f.attrHandle[b]?f.attrHandle[b](a):W||d?a.getAttribute(b):(c=a.getAttributeNode(b),c?typeof a[b]=="boolean"?a[b]?b:null:c.specified?c.value:null:null)},f=$.selectors={cacheLength:50,createPseudo:v,match:S,order:new RegExp("ID|TAG"+(Y?"|NAME":"")+(X?"|CLASS":"")),attrHandle:V?{}:{href:function(a){return a.getAttribute("href",2)},type:function(a){return a.getAttribute("type")}},find:{ID:e?function(a,b,c){if(typeof b.getElementById!==n&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==n&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==n&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:U?function(a,b){if(typeof b.getElementsByTagName!==n)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c},NAME:function(a,b){if(typeof b.getElementsByName!==n)return b.getElementsByName(name)},CLASS:function(a,b,c){if(typeof b.getElementsByClassName!==n&&!c)return b.getElementsByClassName(a)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(R,""),a[3]=(a[4]||a[5]||"").replace(R,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||$.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&$.error(a[0]),a},PSEUDO:function(a,b,c){var d,e;if(S.CHILD.test(a[0]))return null;if(a[3])a[2]=a[3];else if(d=a[4])K.test(d)&&(e=bc(d,b,c,!0))&&(e=d.indexOf(")",d.length-e)-d.length)&&(d=d.slice(0,e),a[0]=a[0].slice(0,e)),a[2]=d;return a.slice(0,3)}},filter:{ID:e?function(a){return a=a.replace(R,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(R,""),function(b){var c=typeof b.getAttributeNode!==n&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(R,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=x[o][a];return b||(b=x(a,new RegExp("(^|"+A+")"+a+"("+A+"|$)"))),function(a){return b.test(a.className||typeof a.getAttribute!==n&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return b?function(d){var e=$.attr(d,a),f=e+"";if(e==null)return b==="!=";switch(b){case"=":return f===c;case"!=":return f!==c;case"^=":return c&&f.indexOf(c)===0;case"*=":return c&&f.indexOf(c)>-1;case"$=":return c&&f.substr(f.length-c.length)===c;case"~=":return(" "+f+" ").indexOf(c)>-1;case"|=":return f===c||f.substr(0,c.length+1)===c+"-"}}:function(b){return $.attr(b,a)!=null}},CHILD:function(a,b,c,d){if(a==="nth"){var e=s++;return function(a){var b,f,g=0,h=a;if(c===1&&d===0)return!0;b=a.parentNode;if(b&&(b[o]!==e||!a.sizset)){for(h=b.firstChild;h;h=h.nextSibling)if(h.nodeType===1){h.sizset=++g;if(h===a)break}b[o]=e}return f=a.sizset-d,c===0?f===0:f%c===0&&f/c>=0}}return function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b,c,d){var e,g=f.pseudos[a]||f.pseudos[a.toLowerCase()];return g||$.error("unsupported pseudo: "+a),g[o]?g(b,c,d):g.length>1?(e=[a,a,"",b],function(a){return g(a,0,e)}):g}},pseudos:{not:v(function(a,b,c){var d=j(a.replace(H,"$1"),b,c);return function(a){return!d(a)}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!f.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},contains:v(function(a){return function(b){return(b.textContent||b.innerText||g(b)).indexOf(a)>-1}}),has:v(function(a){return function(b){return $(a,b).length>0}}),header:function(a){return P.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:_("radio"),checkbox:_("checkbox"),file:_("file"),password:_("password"),image:_("image"),submit:ba("submit"),reset:ba("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return Q.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b,c){return c?a.slice(1):[a[0]]},last:function(a,b,c){var d=a.pop();return c?a:[d]},even:function(a,b,c){var d=[],e=c?1:0,f=a.length;for(;e",a.querySelectorAll("[selected]").length||e.push("\\["+A+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),T(function(a){a.innerHTML="

",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+A+"*(?:\"\"|'')"),a.innerHTML="",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=e.length&&new RegExp(e.join("|")),bk=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a)))if(d.nodeType===9)try{return u.apply(f,t.call(d.querySelectorAll(a),0)),f}catch(i){}else if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){var j,k,l,m=d.getAttribute("id"),n=m||o,p=N.test(a)&&d.parentNode||d;m?n=n.replace(c,"\\$&"):d.setAttribute("id",n),j=bc(a,d,h),n="[id='"+n+"']";for(k=0,l=j.length;k0})}(),f.setFilters.nth=f.setFilters.eq,f.filters=f.pseudos,$.attr=p.attr,p.find=$,p.expr=$.selectors,p.expr[":"]=p.expr.pseudos,p.unique=$.uniqueSort,p.text=$.getText,p.isXMLDoc=$.isXML,p.contains=$.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b0)for(e=d;e=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*\s*$/g,bz={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X
","
"]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1>");try{for(;d1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=b===e&&bA,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(f=0;(h=a[f])!=null;f++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{s=s||bk(b),l=b.createElement("div"),s.appendChild(l),h=h.replace(bo,"<$1>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]===""&&!m?l.childNodes:[];for(g=n.length-1;g>=0;--g)p.nodeName(n[g],"tbody")&&!n[g].childNodes.length&&n[g].parentNode.removeChild(n[g])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l.parentNode.removeChild(l)}h.nodeType?t.push(h):p.merge(t,h)}l&&(h=l=s=null);if(!p.support.appendChecked)for(f=0;(h=t[f])!=null;f++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(f=0;(h=t[f])!=null;f++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[f+1,0].concat(r)),f+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.chrome?b.webkit=!0:b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^(none|table(?!-c[ea]).+)/,bO=/^margin/,bP=new RegExp("^("+q+")(.*)$","i"),bQ=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bR=new RegExp("^([-+])=("+q+")","i"),bS={},bT={position:"absolute",visibility:"hidden",display:"block"},bU={letterSpacing:0,fontWeight:400},bV=["Top","Right","Bottom","Left"],bW=["Webkit","O","Moz","ms"],bX=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return b$(this,!0)},hide:function(){return b$(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bX.apply(this,arguments):this.each(function(){(c?a:bZ(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bY(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bR.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bY(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bU&&(f=bU[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(b,c){var d,e,f,g,h=a.getComputedStyle(b,null),i=b.style;return h&&(d=h[c],d===""&&!p.contains(b.ownerDocument,b)&&(d=p.style(b,c)),bQ.test(d)&&bO.test(c)&&(e=i.width,f=i.minWidth,g=i.maxWidth,i.minWidth=i.maxWidth=i.width=d,d=h.width,i.width=e,i.minWidth=f,i.maxWidth=g)),d}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bQ.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth===0&&bN.test(bH(a,"display"))?p.swap(a,bT,function(){return cb(a,b,d)}):cb(a,b,d)},set:function(a,c,d){return b_(a,c,d?ca(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bQ.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bV[d]+b]=e[d]||e[d-2]||e[0];return f}},bO.test(a)||(p.cssHooks[a+b].set=b_)});var cd=/%20/g,ce=/\[\]$/,cf=/\r?\n/g,cg=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ch=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ch.test(this.nodeName)||cg.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(cf,"\r\n")}}):{name:b.name,value:c.replace(cf,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ci(d,a[d],c,f);return e.join("&").replace(cd,"+")};var cj,ck,cl=/#.*$/,cm=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,co=/^(?:GET|HEAD)$/,cp=/^\/\//,cq=/\?/,cr=/)<[^<]*)*<\/script>/gi,cs=/([?&])_=[^&]*/,ct=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,cu=p.fn.load,cv={},cw={},cx=["*/"]+["*"];try{cj=f.href}catch(cy){cj=e.createElement("a"),cj.href="",cj=cj.href}ck=ct.exec(cj.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&cu)return cu.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):c&&typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("
").append(a.replace(cr,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cB(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cB(a,b),a},ajaxSettings:{url:cj,isLocal:cn.test(ck[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cx},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cz(cv),ajaxTransport:cz(cw),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cC(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cD(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=""+(c||y),k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cm.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(cl,"").replace(cp,ck[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=ct.exec(l.url.toLowerCase()),l.crossDomain=!(!i||i[1]==ck[1]&&i[2]==ck[2]&&(i[3]||(i[1]==="http:"?80:443))==(ck[3]||(ck[1]==="http:"?80:443)))),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cA(cv,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!co.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cq.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cs,"$1_="+z);l.url=A+(A===l.url?(cq.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cx+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cA(cw,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cE=[],cF=/\?/,cG=/(=)\?(?=&|$)|\?\?/,cH=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cE.pop()||p.expando+"_"+cH++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cG.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cG.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cG,"$1"+f):m?c.data=i.replace(cG,"$1"+f):k&&(c.url+=(cF.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cE.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cI,cJ=a.ActiveXObject?function(){for(var a in cI)cI[a](0,1)}:!1,cK=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cL()||cM()}:cL,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cJ&&delete cI[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cK,cJ&&(cI||(cI={},p(a).unload(cJ)),cI[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cN,cO,cP=/^(?:toggle|show|hide)$/,cQ=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cR=/queueHooks$/,cS=[cY],cT={"*":[function(a,b){var c,d,e,f=this.createTween(a,b),g=cQ.exec(b),h=f.cur(),i=+h||0,j=1;if(g){c=+g[2],d=g[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&i){i=p.css(f.elem,a,!0)||c||1;do e=j=j||".5",i=i/j,p.style(f.elem,a,i+d),j=f.cur()/h;while(j!==1&&j!==e)}f.unit=d,f.start=i,f.end=g[1]?i+(g[1]+1)*c:c}return f}]};p.Animation=p.extend(cW,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c_.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c_.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=da(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g,null)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window); \ No newline at end of file diff --git a/togetherjs/tests/doctestjs/.resources/boilerplate/js/vendor/modernizr-2.6.1.min.js b/togetherjs/tests/doctestjs/.resources/boilerplate/js/vendor/modernizr-2.6.1.min.js deleted file mode 100755 index 52b523fa2..000000000 --- a/togetherjs/tests/doctestjs/.resources/boilerplate/js/vendor/modernizr-2.6.1.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/* Modernizr 2.6.1 (Custom Build) | MIT & BSD - * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load - */ -;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),k.id=h,(l?k:m).innerHTML+=f,m.appendChild(k),l||(m.style.background="",g.appendChild(m)),i=c(k,a),l?k.parentNode.removeChild(k):m.parentNode.removeChild(m),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(['#modernizr:after{content:"',l,'";visibility:hidden}'].join(""),function(b){a=b.offsetHeight>=1}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var L in s)C(s,L)&&(x=L.toLowerCase(),e[x]=s[L](),v.push((e[x]?"":"no-")+x));return e.input||K(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},D(""),i=k=null,function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return o.call(a)=="[object Function]"}function e(a){return typeof a=="string"}function f(){}function g(a){return!a||a=="loaded"||a=="complete"||a=="uninitialized"}function h(){var a=p.shift();q=1,a?a.t?m(function(){(a.t=="c"?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){a!="img"&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l={},o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};y[c]===1&&(r=1,y[c]=[],l=b.createElement(a)),a=="object"?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),a!="img"&&(r||y[c]===2?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i(b=="c"?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),p.length==1&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&o.call(a.opera)=="[object Opera]",l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return o.call(a)=="[object Array]"},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f ul { - padding-left: 0; -} - -#contents li { - list-style: none; -} - -#contents { - border-bottom: 1px solid #999; - padding-left: 1em; -} - -.header-container header { - width: 98%; -} - -code { - color: #288; -} - -aside code { - color: #9ff; -} - -section:target h1, section:target h2, section:target h3, section:target h4, -section:target h5, section:target h6, h3:target, h4:target { - border-bottom: 3px solid #f90; -} - -footer a:link, footer a:visited, -aside a:link, aside a:visited { - text-decoration: none; - color: #fd1; -} - -section header h1, section header h2, section header h3, section header h4, -section header h5, section header h6 { - margin-left: -1em; - margin-top: 1.75em; - border-bottom: 1px solid #000; -} - -pre { - font-size: 90%; - line-height: 1.3; - border: 1px solid #999; - border-radius: 4px; -} - -@media only screen and (min-width: 768px) { - - .main aside { - position: absolute; - right: 1em; - } - -} - -.header-container header { - margin-left: 1em; -} - -h1.title a:link, h1.title a:visited { - text-decoration: none; - color: #fff; -} - -h1.title a:hover { - text-decoration: underline; -} - - -dd p { - margin-top: 0; -} - -dd p:last-child { - margin-bottom: 0; -} \ No newline at end of file diff --git a/togetherjs/tests/doctestjs/.resources/example.xml b/togetherjs/tests/doctestjs/.resources/example.xml deleted file mode 100644 index 5c9a987b2..000000000 --- a/togetherjs/tests/doctestjs/.resources/example.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - Example Feed - A subtitle. - - - urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6 - 2003-12-13T18:30:02Z - - John Doe - johndoe@example.com - - - - Atom-Powered Robots Run Amok - - - - urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a - 2003-12-13T18:30:02Z - Some text. - - - diff --git a/togetherjs/tests/doctestjs/.resources/footer.html b/togetherjs/tests/doctestjs/.resources/footer.html deleted file mode 100644 index acd0e2785..000000000 --- a/togetherjs/tests/doctestjs/.resources/footer.html +++ /dev/null @@ -1,30 +0,0 @@ -
- -

Download

-

- You can download this project in either - zip or - tar formats. -

- -

You can also clone the project with Git - by running: -

$ git clone git://github.com/ianb/doctestjs
-

- - - - - - - diff --git a/togetherjs/tests/doctestjs/.resources/header.html b/togetherjs/tests/doctestjs/.resources/header.html deleted file mode 100644 index 149857f9c..000000000 --- a/togetherjs/tests/doctestjs/.resources/header.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - __TITLE__ - - - - - - - - - - - - -
-
-

Doctest.js:

- - - -
-
- -
-
- - - - - __BODY__ - - - -
- -
- - - - - - - - - - - - diff --git a/togetherjs/tests/doctestjs/.resources/include-scripts.sh b/togetherjs/tests/doctestjs/.resources/include-scripts.sh deleted file mode 100755 index 44cd544c8..000000000 --- a/togetherjs/tests/doctestjs/.resources/include-scripts.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash - -set -e - -base="$(python -c 'import sys, os; print os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[1])))' "$BASH_SOURCE")" - -if [ ! -f $base/doctest.js ] ; then - echo "Could not find $base/doctest.js" - exit 1 -fi - -if [ ! -d $base/.resources/jshint ] ; then - echo "Could not find $base/.resources/jshint" - echo "Try:" - echo " git clone https://github.com/jshint/jshint.git .resources/jshint" - exit 2 -fi - -if [ ! -d $base/.resources/esprima ] ; then - echo "Could not find $base/.resources/esprima" - echo "Try:" - echo " git clone https://github.com/ariya/esprima.git .resources/esprima" - exit 3 -fi - -echo "Substituting $base/doctest.js" - -python -c ' -import os, sys, re, subprocess -os.chdir(sys.argv[1]) -with open("doctest.js", "rb") as fp: - content = fp.read() -names = {} -for arg in sys.argv[2:]: - name, rest = arg.split("=", 1) - names[name] = rest -regex = re.compile(r"\/\*\s+INSERT\s+(.*?)\s+\*\/\s*\n(.*?)\/*\s+END\s+INSERT\s+\*\/", re.S) -def repl(match): - print "Replacing %s" % match.group(1) - filename = names.get(match.group(1)) - # I do not understand why --ascii needs an option - output = subprocess.check_output( - ["uglifyjs", "--no-copyright", "--max-line-len", "200", "-b", "max-line-len=200,ascii-only=true,beautify=false", filename]) - return "/* INSERT %s */\n%s\n/* END INSERT */" % ( - match.group(1), output) -new_content = regex.sub(repl, content) -with open("doctest.js", "wb") as fp: - fp.write(new_content) -print "wrote doctest.js" -' "$base" esprima.js=$base/.resources/esprima/esprima.js jshint.js=$base/.resources/jshint/jshint.js diff --git a/togetherjs/tests/doctestjs/.resources/retemplate.py b/togetherjs/tests/doctestjs/.resources/retemplate.py deleted file mode 100755 index f4a3f58ce..000000000 --- a/togetherjs/tests/doctestjs/.resources/retemplate.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python -import re - -section_re = re.compile(r''' -(?P) - (?P[^\000]*) -(?P) -''', re.VERBOSE | re.MULTILINE) - -value_sub_re = re.compile(r'(?P<[^>]*")(?:__)(?P[A-Z_]+)(?:__)(?P"[^>]*>)') - -title_sub_re = re.compile(r'(.*?)', re.I) - - -def get_variables(content, value_matches): - vars = {} - for match in section_re.finditer(content): - vars[match.group('name')] = match.group('value') - for name, value_match_start, value_match_end in value_matches: - regex = re.escape(value_match_start) + '([^"]*)' + re.escape(value_match_end) - regex = re.compile(regex) - for match in regex.finditer(content): - vars[name] = match.group(1) - match = title_sub_re.search(content) - if match: - vars['PAGE_TITLE'] = match.group(1) - else: - print 'No found' - return vars - - -def get_value_matches(template): - matches = [] - for match in value_sub_re.finditer(template): - matches.append(( - match.group('name'), - match.group('front'), - match.group('back'))) - return matches - - -def sub_template(template, content): - matches = get_value_matches(template) - content_vars = get_variables(content, matches) - - def sub_section(match): - if match.group('name') not in content_vars: - # Failure, needs to be fixed - raise Exception('Must have section <!-- %s -->' % match.group('name')) - return ( - match.group('front1') + match.group('name') + match.group('front2') - + content_vars.get(match.group('name'), '') - + match.group('back1') + '/' + match.group('name') + match.group('back2') - ) - - new_content = section_re.sub(sub_section, template) - - def sub_variable(match): - if match.group('name') not in content_vars: - print 'Missing tag: __%s__' % match.group('name') - return '<!-- ' + match.group(0) + ' -->' - return ( - match.group('front') - + content_vars[match.group('name')] - + match.group('back')) - - new_content = value_sub_re.sub(sub_variable, new_content) - - def sub_title(match): - if 'PAGE_TITLE' in content_vars: - return '<title>' + content_vars['PAGE_TITLE'] + '' - else: - return match.group(0) - - new_content = title_sub_re.sub(sub_title, new_content) - - return new_content - - -def rewrite_page(page_name, template_name): - with open(template_name) as fp: - template = fp.read() - with open(page_name) as fp: - content = fp.read() - try: - new_content = sub_template(template, content) - except: - print 'Error in page:', page_name - raise - with open(page_name, 'w') as fp: - fp.write(new_content) - -if __name__ == '__main__': - import sys - if len(sys.argv) < 3: - print 'Usage: retemplate.py TEMPLATE_FILE CONTENT_FILE [...CONTENT_FILE2...]' - sys.exit(2) - template_name = sys.argv[1] - for filename in sys.argv[2:]: - rewrite_page(filename, template_name) diff --git a/togetherjs/tests/doctestjs/.resources/template.html b/togetherjs/tests/doctestjs/.resources/template.html deleted file mode 100644 index 954bbf7dc..000000000 --- a/togetherjs/tests/doctestjs/.resources/template.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
-
-

Doctest.js:

- - - -
-
- -
-
- - - - - __BODY__ - - - -
- -
- - - - - - - - - - - - diff --git a/togetherjs/tests/doctestjs/.resources/toc.js b/togetherjs/tests/doctestjs/.resources/toc.js deleted file mode 100644 index a58aeec03..000000000 --- a/togetherjs/tests/doctestjs/.resources/toc.js +++ /dev/null @@ -1,52 +0,0 @@ -function contentsOnLoad() { - if (contentsOnLoad.hasRun) { - return; - } - contentsOnLoad.hasRun = true; - var dest = document.getElementById('contents'); - var toc = [document.createElement('ul')]; - var generatedIds = []; - dest.appendChild(toc[0]); - var els = document.querySelectorAll('h3, h4, h5, h6'); - for (var i=0; i= toc.length) { - var ul = document.createElement('ul'); - var container = document.createElement('li'); - container.appendChild(ul); - toc[toc.length-1].appendChild(container); - toc.push(ul); - } - var name = el.getAttribute('id'); - if (! name) { - name = 'header-'+(i+1); - generatedIds.push(name); - el.setAttribute('id', name); - } - var li = document.createElement('li'); - var anchor = document.createElement('a'); - if (el.getAttribute('href')) { - anchor.setAttribute('href', el.getAttribute('href')); - el.style.display = 'none'; - } else { - anchor.setAttribute('href', '#'+name); - } - li.appendChild(anchor); - anchor.innerHTML = el.innerHTML; - toc[toc.length-1].appendChild(li); - } - // Re-scroll: - if (location.hash && generatedIds.indexOf(location.hash.substr(1)) != -1) { - location.hash = location.hash; - } -} - -document.addEventListener("DOMContentLoaded", contentsOnLoad, false); -window.addEventListener("load", contentsOnLoad, false); diff --git a/togetherjs/tests/doctestjs/.resources/try.js b/togetherjs/tests/doctestjs/.resources/try.js deleted file mode 100644 index 8592ba94a..000000000 --- a/togetherjs/tests/doctestjs/.resources/try.js +++ /dev/null @@ -1,25 +0,0 @@ -window.addEventListener('load', function () { - var innerHTML = $('#display').html(); - if (localStorage.editText) { - $('#editor').val(localStorage.editText); - } - $('#editor').change(function () { - localStorage.editText = $('#editor').val(); - }); - $('#testit').click(function () { - $('#display').html(innerHTML); - $('#editit').click(function () { - $('#edit').show(); - $('#display').hide(); - $('#editor').focus(); - $('#doctest-output').hide(); - }); - $('#test-location').addClass('test').text($('#editor').val()); - console.log($('#test-location').text()); - var runner = new doctest.Runner(); - var parser = new doctest.HTMLParser(runner, $('#display')[0], 'pre#test-location'); - runner.init(); - parser.parse(); - runner.run(); - }); -}, false); diff --git a/togetherjs/tests/doctestjs/.syncignore b/togetherjs/tests/doctestjs/.syncignore deleted file mode 100644 index 8b1378917..000000000 --- a/togetherjs/tests/doctestjs/.syncignore +++ /dev/null @@ -1 +0,0 @@ - diff --git a/togetherjs/tests/doctestjs/README.md b/togetherjs/tests/doctestjs/README.md deleted file mode 100644 index a6b824180..000000000 --- a/togetherjs/tests/doctestjs/README.md +++ /dev/null @@ -1,39 +0,0 @@ -## doctest.js - -For a more complete description please [read the main -page](http://doctestjs.org). - -`doctest.js` is a test runner for Javascript, organized around *examples* and *expected result*. Tests look like this: - -```javascript -// Simple stuff: -print(3 * 4); -// => 12 - -// Or complicated stuff: -var complete = false; -var savedResult = null; -$.ajax({ - url: "/test", - dataType: "json", - success: function (result) { - complete = true; - savedResult = result; - } -}); -wait(function () {return complete;}); -print(savedResult); -// => {value1: "something", value2: true} -``` - -And a bunch more features: check out the [tutorial](http://doctestjs.org/tutorial.html) to get started, or read the [reference](http://doctestjs.org/reference.html) for more detail. - -## License - -Doctest.js is released under an MIT-style license. - -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. diff --git a/togetherjs/tests/doctestjs/bin/doctest b/togetherjs/tests/doctestjs/bin/doctest deleted file mode 100755 index ac6d474c1..000000000 --- a/togetherjs/tests/doctestjs/bin/doctest +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env node - -var path = require('path'); -var fs = require('fs'); -var root = path.join(path.dirname(fs.realpathSync(__filename)), '..'); - -// FIXME: Not sure this is the best way to go about this, or if we should -// just require("doctest")? -var doctest = require(root + '/doctest'); - -function main(filename) { - var runner = new doctest.Runner({Reporter: doctest.ConsoleReporter}); - runner.globs = runner.evalInit(); - var parser = new doctest.TextParser.fromFile(runner, process.argv[2]); - parser.parse(); - runner.run(); - - var reporter = runner.reporter; - return reporter; -} - -function showReport(reporter) { - console.log('Successes:', reporter.successes); - console.log('Failures:', reporter.failures); - if ((! reporter.successes) || reporter.failures) { - // Set exit code to number of failures (if any). It gives a nice failures - // summary when running under make. E.g.: `make: *** [doctest] Error 2` - process.exit(reporter.failures || 1); - } -} - -var filename = process.argv[2]; -if (! filename) { - console.log("Error: you did not give a filename"); - console.log("Usage:", process.argv[1], "FILENAME"); - process.exit(500); -} -showReport(main(filename)); diff --git a/togetherjs/tests/doctestjs/doctest.css b/togetherjs/tests/doctestjs/doctest.css deleted file mode 100644 index c5e4e6a0a..000000000 --- a/togetherjs/tests/doctestjs/doctest.css +++ /dev/null @@ -1,140 +0,0 @@ -/* Basic styling: */ - -body { - font-family: sans-serif; -} - -pre { - padding: 0.3em; -} - -/* Test block formatting: */ - -pre.doctest, pre.commenttest, pre.test { - border: 1px solid #999; - border-radius: 4px; -} - -pre.doctest.doctest-some-failure, pre.commenttest.doctest-some-failure, pre.test.doctest-some-failure { - border: 1px solid #f00; -} - -/* FIXME: it would be nice if this was more obviously styled to show that there - was more content */ -pre.doctest.expand-on-failure, pre.commenttest.expand-on-failure, pre.test.expand-on-failure { - max-height: 3em; - overflow-y: auto; -} - -pre.doctest.expand-on-failure.doctest-some-failure, pre.commenttest.expand-on-failure.doctest-some-failure, pre.test.expand-on-failure.doctest-some-failure { - max-height: none; -} - -/* Individual example formatting: */ - -.doctest-example:target { - border-left: 4px solid #f00; - padding-left: 4px; -} - -.doctest-example { -} - -.doctest-example.doctest-success { - color: #060; -} - -.doctest-example.doctest-failure { - color: #900; -} - -.doctest-example .doctest-actual-output { - color: #066; - /*padding-left: 1em;*/ -} - -.doctest-example.doctest-failure .doctest-output { - padding-left: 1em; -} - -.doctest-example .doctest-output { - font-weight: bold; -} - -.doctest-example .doctest-description { - color: #000; - font-weight: bold; -} - -.doctest-example .doctest-console { - color: #009; - padding-left: 1em; -} - -/* Reporter formatting: */ - -#doctest-success-count.doctest-nonzero { - color: #0f0; -} - -#doctest-failure-count.doctest-nonzero { - color: #f00; -} - -#doctest-aborted { - background-color: #900; - color: #fff; -} - -.doctest-report-table th { - font-weight: normal; - text-align: left; -} - -.doctest-report-table td { - padding: 0 1em; -} - -a.doctest-failure-link { - color: #00f; - text-decoration: none; - padding: 0 1em 0 0; -} - -a.doctest-failure-link:visited { - color: #00f; -} - -a.doctest-failure-link:hover { - text-decoration: underline; -} - -/* Comparison table */ - -.doctest-comparison-table { - border: 1px solid #000; - /* FIXME: not sure why this doesn't keep the table limited to 100% */ - width: 100%; -} - -.doctest-comparison-table td { - overflow: auto; -} - -.doctest-comparison-header th { - background-color: #000; - color: #fff; -} - -.doctest-comparison-error td { - background-color: #fdd; -} - -td.doctest-comparison-got { - padding-right: 1em; - color: #060; -} - -td.doctest-comparison-expected { - color: #900; -} diff --git a/togetherjs/tests/doctestjs/doctest.js b/togetherjs/tests/doctestjs/doctest.js deleted file mode 100644 index cc42a68ca..000000000 --- a/togetherjs/tests/doctestjs/doctest.js +++ /dev/null @@ -1,2410 +0,0 @@ -(function (exports) { - -// Some Node.js globals: -/*global global, require, exports */ -// Some browser globals: -/*global console */ -// Some doctest.js globals: -/*global writeln, wait, doctest:true, doctestReporterHook, esprima:true, JSHINT:true */ - -var globalObject; -if (typeof window == 'undefined') { - if (typeof global == 'undefined') { - globalObject = (function () {return this;})(); - } else { - globalObject = global; - } -} else { - globalObject = window; -} - -var doc; -if (typeof document != 'undefined') { - doc = document; -} else { - doc = null; -} - -exports.setDocument = function (newDocument) { - doc = newDocument; -}; - -var Example = exports.Example = function (runner, expr, expected, attrs) { - this.runner = runner; - this.expr = expr; - if (typeof expected != "string") { - throw "Bad value for expected: " + expected; - } - this.expected = expected; - if (attrs) { - for (var i in attrs) { - if (attrs.hasOwnProperty(i)) { - this[i] = attrs[i]; - } - } - } -}; - -Example.prototype = { - run: function () { - this.output = []; - this.consoleOutput = []; - var globs = this.runner.evalInit(); - try { - this.result = this.runner.evaller(this.expr, globs, this.filename); - } catch (e) { - if (e && e['doctest.abort']) { - return; - } - this.write('Error: ' + e + '\n'); - // FIXME: doesn't format nicely: - if (e && e.stack) { - console.log('Exception Stack:'); - console.log(e.stack); - } - } - }, - check: function () { - var output = this.output.join(''); - // FIXME: consider using this.result - this.runner.matcher.match(this, output, this.expected); - }, - write: function (text) { - this.output.push(text); - }, - writeConsole: function (message) { - this.consoleOutput.push(message); - }, - clearConsole: function () { - this.consoleOutput = []; - }, - timeout: function (passed) { - this.runner.reporter.logFailure(this, "Error: wait timed out after " + passed + " milliseconds"); - }, - textSummary: function () { - return strip(strip(this.expr).substr(0, 20)) + '...'; - } -}; - -var Matcher = exports.Matcher = function (runner) { - this.runner = runner; -}; - -Matcher.prototype = { - match: function (example, got, expected) { - var cleanGot = this.clean(got); - var cleanExpected = this.clean(expected); - var regexp = this.makeRegex(cleanExpected); - if (cleanGot.search(regexp) != -1) { - this.runner.reporter.logSuccess(example, got); - return; - } - var comparisonTable = this.makeComparisonTable(cleanGot, cleanExpected); - this.runner.reporter.logFailure(example, got, comparisonTable); - }, - - makeComparisonTable: function (cleanGot, cleanExpected) { - var gotLines = this.splitLines(cleanGot); - var expectedLines = this.splitLines(cleanExpected); - if (gotLines.length <= 1 || expectedLines.length <= 1) { - return null; - } - var comparisonTable = []; - comparisonTable.push({header: 'Details of mismatch:'}); - var shownTrailing = false; - var matching = 0; - for (var i=0; i= expectedLines.length) { - if (! shownTrailing) { - comparisonTable.push({header: 'Trailing lines in got:'}); - shownTrailing = true; - } - comparisonTable.push({got: gotLines[i], error: true}); - } else { - var regexp = this.makeRegex(expectedLines[i]); - var error = gotLines[i].search(regexp) == -1; - comparisonTable.push({got: gotLines[i], expected: expectedLines[i], error: error}); - if (! error) { - matching++; - } - } - } - if (matching <= 1) { - return null; - } - if (expectedLines.length > gotLines.length) { - comparisonTable.push({header: 'Trailing expected line(s):'}); - for (i=gotLines.length; i' + - '
' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '
Passed:0
Failures:0
' - ); - this.successEl = doc.getElementById('doctest-success-count'); - this.failureEl = doc.getElementById('doctest-failure-count'); - this.failureLinksEl = doc.getElementById('doctest-failure-links'); - var button = doc.getElementById('doctest-reload'); - // Sometimes this is sticky: - button.disabled = false; - button.addEventListener('click', function (ev) { - button.innerHTML = 'reloading...'; - button.disabled = true; - location.reload(); - }, false); -}; - -HTMLReporter.prototype = { - - logSuccess: function (example, got) { - var num = parseInt(this.successEl.innerHTML.split('/')[0], 10); - num++; - this.successEl.innerHTML = num+' / '+this.runner.examples.length; - addClass(this.successEl, 'doctest-nonzero'); - if (example.htmlSpan) { - addClass(example.htmlSpan, 'doctest-success'); - if (example.expected.indexOf('...') != -1 || - example.expected.indexOf('?') != -1) { - this.addExampleNote(example, 'Output:', 'doctest-actual-output', got || '(none)'); - } - } - this.showConsoleOutput(example, false); - this.runner._hook('reportSuccess', example, got); - }, - - logFailure: function (example, got, comparisonTable) { - this.addFailure(); - if (example.htmlSpan) { - addClass(example.htmlSpan, 'doctest-failure'); - var showGot = got || '(nothing output)'; - var expectedSpan = makeElement('span', {className: 'doctest-description'}, ['Expected:\n']); - example.htmlSpan.insertBefore(expectedSpan, example.htmlSpan.querySelector('.doctest-output')); - if (! example.expected) { - example.htmlSpan.querySelector('.doctest-output').innerHTML = '(nothing expected)\n'; - } - this.addExampleNote(example, 'Got:', 'doctest-actual-output', showGot); - } - if (comparisonTable) { - this.addComparisonTable(example, comparisonTable); - } - if (example.blockEl) { - addClass(example.blockEl, 'doctest-some-failure'); - } - if (example.htmlID) { - var anchor = makeElement('a', {href: '#' + example.htmlID, className: 'doctest-failure-link'}, [example.textSummary()]); - this.failureLinksEl.appendChild(anchor); - if (example.htmlID == positionOnFailure) { - location.hash = '#' + example.htmlID; - } - } - this.showConsoleOutput(example, true); - this.runner._hook('reportFailure', example, got); - }, - - logAbort: function (example, abortMessage) { - this.addFailure(); - this.addAborted(abortMessage); - if (example.htmlSpan) { - addClass(example.htmlSpan, 'doctest-failure'); - } - if (example.blockEl) { - addClass(example.blockEl, 'doctest-some-failure'); - } - this.addExampleNote(example, 'Aborted:', 'doctest-actual-output', abortMessage); - this.runner._hook('reportAbort', example, abortMessage); - }, - - addFailure: function () { - var num = parseInt(this.failureEl.innerHTML, 10); - num++; - this.failureEl.innerHTML = num+''; - addClass(this.failureEl, 'doctest-nonzero'); - }, - - addAborted: function (message) { - doc.getElementById('doctest-abort-row').style.display = ''; - var td = doc.getElementById('doctest-aborted'); - td.appendChild(doc.createTextNode(message)); - }, - - showConsoleOutput: function (example, error) { - if (! example.consoleOutput.length) { - return; - } - if (! example.htmlSpan) { - return; - } - var text = example.consoleOutput.join('\n'); - this.addExampleNote(example, 'Console:', 'doctest-console', text); - }, - - addExampleNote: function (example, description, className, text) { - if (! example.htmlSpan) { - return; - } - example.htmlSpan.appendChild(makeElement('span', {className: 'doctest-description'}, [description + '\n'])); - example.htmlSpan.appendChild(makeElement('span', {className: className}, [text + '\n'])); - }, - - addComparisonTable: function (example, comparisonTable) { - if (! example.htmlSpan) { - // FIXME; should display text table - return; - } - var table = makeElement('table', {className: 'doctest-comparison-table'}); - for (var i=0; i (this.maxLen - indentString.length)) { - this.popSeen(seenPosition); - ostring = this.multilineObjRepr(obj, indentString); - } - this.popSeen(seenPosition); - return ostring; - }, - - multilineObjRepr: function (obj, indentString) { - var keys = sortedKeys(obj); - var ostring = '{\n'; - for (var i=0; i (this.maxLen + indentString.length)) { - this.popSeen(seenPosition); - s = this.multilineArrayRepr(obj, indentString); - } - this.popSeen(seenPosition); - return s; - }, - - multilineArrayRepr: function (obj, indentString) { - var s = "[\n"; - for (var i=0; i"; - } - if (el.nodeType == el.DOCUMENT_TYPE_NODE) { - return ""; - } - var tagName = el.tagName || "(no tag)"; - var s = '<' + tagName.toLowerCase(); - var attrs = []; - if (el.attributes && el.attributes.length) { - for (i=0; i'; - return s; - }, - - xhrRepr: function (req, indentString) { - var s = '[XMLHttpRequest '; - var states = { - 0: 'UNSENT', - 1: 'OPENED', - 2: 'HEADERS_RECEIVED', - 3: 'LOADING', - 4: 'DONE' - }; - s += states[req.readyState]; - if (req.readyState == 4) { - s += ' ' + req.status + ' ' + req.statusText; - } - return s + ']'; - }, - - registry: [ - [function (o) { - return typeof o == 'string'; - }, - function (o) { - o = '"' + o.replace(/([\"\\])/g, '\\$1') + '"'; - o = o.replace(/[\f]/g, "\\f") - .replace(/[\b]/g, "\\b") - .replace(/[\n]/g, "\\n") - .replace(/[\t]/g, "\\t") - .replace(/[\r]/g, "\\r"); - return o; - } - ], - [function (o) { - return typeof o == 'number'; - }, - function (o) { - return o + ""; - } - ], - [function (o) { - return typeof o == 'object' && o.nodeType; - }, - "xmlRepr" - ], - [function (o) { - var typ = typeof o; - if ((typ != 'object' && ! (typ == 'function' && typeof o.item == 'function')) || - o === null || - typeof o.length != 'number' || - o.nodeType === 3) { - return false; - } - return true; - }, - "arrayRepr" - ], - [function (o) { - return typeof XMLHttpRequest !== 'undefined' && o instanceof XMLHttpRequest; - - }, - 'xhrRepr' - ] - ] - -}; - -repr.register = function (condition, reprFunc) { - repr.ReprClass.prototype.registry.push([condition, reprFunc]); -}; - -var Runner = exports.Runner = function (options) { - this.examples = []; - options = options || {}; - for (var i in options) { - if (options.hasOwnProperty(i)) { - if (this[i] === undefined) { - throw 'Unexpected option: ' + i; - } - this[i] = options[i]; - } - } -}; - -Runner.prototype = { - - init: function () { - if (this.matcher === null) { - this.matcher = this.makeMatcher(); - } - if (this.reporter === null) { - this.reporter = this.makeReporter(); - } - if (this.repr === null) { - this.repr = this.makeRepr(); - } - this._hook('init', this); - }, - - run: function () { - this.init(); - if (! this.examples.length) { - throw 'No examples have been added'; - } - this._exampleIndex = 0; - this._runExample(); - }, - - evalInit: function () { - if (typeof this.globs != "undefined") { - return this.globs; - } - this.logGrouped = false; - this._abortCalled = false; - var globs = { - write: this.write.bind(this), - writeln: this.writeln.bind(this), - printResolved: this.printResolved.bind(this), - wait: this.wait.bind(this), - Abort: this.Abort.bind(this), - repr: repr, - Spy: Spy, - jshint: jshint - }; - globs.print = globs.writeln; - var consoleOverwrites = { - log: this.logFactory(null, console.log), - warn: this.logFactory(null, console.warn), - error: this.logFactory(null, console.error), - info: this.logFactory(null, console.info), - clear: this.clearLogs.bind(this) - }; - if (typeof window == 'undefined') { - // Can't just overwrite the console object - globs.console = consoleOverwrites; - for (var i in console) { - if (console.hasOwnProperty(i) && (! globs.console.hasOwnProperty(i))) { - if (console[i].bind) { - globs.console[i] = console[i].bind(console); - } else { - globs.console[i] = console[i]; - } - } - } - var context = require('vm').Script.createContext(); - extend(context, globs); - return context; - } else { - extend(console, consoleOverwrites); - window.onerror = this.windowOnerror; - extend(window, globs); - return null; - } - }, - - write: function (text) { - this._currentExample.write(text); - }, - - writeln: function () { - for (var i=0; i= this.examples.length) { - this._finish(); - break; - } - this._currentExample = this.examples[this._exampleIndex]; - this._exampleIndex++; - this._currentExample.run(); - if (this._exampleWait && ! this._abortCalled) { - this._runWait(); - break; - } - this.evalUninit(); - this._currentExample.check(); - if (this._abortCalled) { - // FIXME: this should show that while finished, and maybe successful, - // the tests were aborted - this.reporter.logAbort(this._currentExample, this._abortCalled); - this._finish(); - break; - } - this._currentExample = null; - } - }, - - _runWait: function () { - var start = Date.now(); - var waitTimeout = this._waitTimeout || this._defaultWaitTimeout; - this._waitTimeout = null; - var self = this; - function poll() { - var now = Date.now(); - var cond = self._waitCondition; - if (typeof cond == "number") { - if (now - start >= cond) { - self._exampleWait = false; - } - } else if (cond) { - if (cond()) { - self._exampleWait = false; - } - } - if (self._exampleWait) { - if (now - start > waitTimeout) { - self._currentExample.timeout(now - start); - } else { - setTimeout(poll, self._waitPollTime); - return; - } - } - self.evalUninit(); - self._currentExample.check(); - self._currentExample = null; - self._runExample(); - } - // FIXME: instead of the poll time, cond could be used if it is a number - setTimeout(poll, this._waitPollTime); - }, - - _hook: function (method) { - if (typeof doctestReporterHook == "undefined") { - return null; - } else if (method && arguments.length > 1 && doctestReporterHook[method]) { - var args = argsToArray(arguments).slice(1); - return doctestReporterHook[method].apply(doctestReporterHook, args); - } else if (method) { - return doctestReporterHook[method]; - } else { - return doctestReporterHook; - } - }, - - _finish: function () { - if (attemptedHash && location.hash == attemptedHash) { - // This fixes up the anchor position after tests have run. - // FIXME: would be nice to detect if the user has scrolled between - // page load and the current moment - location.hash = ''; - location.hash = attemptedHash; - } - this._hook('finish', this); - }, - - _waitPollTime: 100, - _waitTimeout: null, - _waitCondition: null, - _defaultWaitTimeout: 5000, - - /* Dependency Injection, yay! */ - examples: null, - Example: Example, - exampleOptions: null, - makeExample: function (text, expected, filename) { - var options = {filename: filename}; - extend(options, this.exampleOptions); - return new this.Example(this, text, expected, options); - }, - matcher: null, - Matcher: Matcher, - matcherOptions: null, - makeMatcher: function () { - return new this.Matcher(this, this.matcherOptions); - }, - reporter: null, - Reporter: HTMLReporter, - reporterOptions: null, - makeReporter: function () { - return new this.Reporter(this, this.reporterOptions); - }, - repr: repr -}; - -var HTMLParser = exports.HTMLParser = function (runner, containerEl, selector) { - this.runner = runner; - containerEl = containerEl || doc.body; - if (typeof containerEl == 'string') { - containerEl = doc.getElementById(containerEl); - } - if (! containerEl) { - throw 'Bad/null/missing containerEl'; - } - this.containerEl = containerEl; - this.selector = selector || 'pre.doctest, pre.commenttest, pre.test'; -}; - -HTMLParser.prototype = { - parse: function () { - var els = this.findEls(); - for (var i=0; i/.test(line)) { - if (! exampleLines.length) { - throw ('Bad example: ' + this.runner.repr(line) + '\n' + - '> line not preceded by $'); - } - rawExample.push(line); - line = line.replace(/^ *> ?/, ''); - exampleLines.push(line); - } else { - rawOutput.push(line); - outputLines.push(line); - } - } - return result; - }, - - parseCommentEl: function (el) { - if (typeof esprima == "undefined") { - if (typeof require != "undefined") { - esprima = require("./esprima/esprima.js"); - } else { - throw 'You must install or include esprima.js'; - } - } - var contents = getElementText(el); - var ast = esprima.parse(contents, { - range: true, - comment: true - }); - var pos = 0; - var result = []; - for (var i=0; i/) == -1) { - // Not a comment we care about - continue; - } - var start = comment.range[0]; - var end = comment.range[1]; - var example = contents.substr(pos, start-pos); - var output = comment.value.replace(/^\s*=> ?/, ''); - var orig = comment.type == 'Block' ? '/*' + comment.value + '*/' : '//' + comment.value; - if (example === '') { - result[result.length-1][1] += '\n'+output; - result[result.length-1][3] += '\n'+orig; - } - else { - result.push([example, output, example, orig]); - } - pos = end; - } - var last = contents.substr(pos, contents.length-pos); - if (strip(last)) { - result.push([last, '', last, '']); - } - return result; - }, - - loadRemotes: function (callback, selector) { - var els; - if (! selector) { - els = this.findEls(); - } else { - els = document.querySelectorAll(selector); - } - var pending = 0; - argsToArray(els).forEach(function (el) { - var href = el.getAttribute('data-href-pattern'); - if (href) { - try { - href = this.fillPattern(href); - } catch (e) { - var text = '// Error resolving data-href-pattern "' + href + '":\n'; - text += '// ' + e; - el.innerHTML = ''; - el.appendChild(document.createTextNode(text)); - return; - } - } - if (! href) { - href = el.getAttribute('href'); - } - if (! href) { - href = el.getAttribute('src'); - } - if (! href) { - return; - } - pending++; - var req = new XMLHttpRequest(); - if (href.indexOf('?') == -1) { - // Try to stop some caching: - href += '?nocache=' + Date.now(); - } - req.open('GET', href); - req.setRequestHeader('Cache-Control', 'no-cache, max-age=0'); - req.onreadystatechange = (function () { - if (req.readyState != 4) { - return; - } - if (req.status != 200 && !(req.status === 0 && document.location.protocol == "file:")) { - el.appendChild(doc.createTextNode('\n// Error fetching ' + href + ' status: ' + req.status)); - } else { - this.fillElement(el, req.responseText); - } - pending--; - if (! pending) { - callback(); - } - }).bind(this); - req.send(); - }, this); - if (! pending) { - callback(); - } - }, - - fillPattern: function (pattern) { - var regex = /\{([^\}]+)\}/; - var result = ''; - while (true) { - var match = regex.exec(pattern); - if (! match) { - result += pattern; - break; - } - result += pattern.substr(0, match.index); - pattern = pattern.substr(match.index + match[0].length); - var name = match[1]; - var restriction = "^[\\w_\\-\\.]+$"; - var defaultValue = ''; - if (name.lastIndexOf('|') != -1) { - defaultValue = name.substr(name.lastIndexOf('|')+1); - name = name.substr(0, name.lastIndexOf('|')); - } - if (name.indexOf(':') != -1) { - restriction = name.substr(name.indexOf(':')+1); - name = name.substr(0, name.indexOf(':')); - } - var value = params[name]; - if (! value) { - value = defaultValue; - } - if (restriction && value.search(new RegExp(restriction)) == -1) { - throw 'Bad substitution for {' + name + ':' + restriction + '}: "' + value + '"'; - } - result += value; - } - return result; - }, - - fillElement: function (el, text) { - el.innerHTML = ''; - if (hasClass(el, 'commenttest') || hasClass(el, 'test')) { - var texts = this.splitText(text); - console.log("filling in tests", texts, el); - if (texts && texts.length == 1 && ! texts[0].header) { - el.appendChild(document.createTextNode(texts[0].body)); - } else if (texts && texts.length) { - for (var i=0; i/) == -1) { - // Not a comment we care about - continue; - } - var start = comment.range[0]; - var end = comment.range[1]; - var example = this.text.substr(pos, start-pos); - var output = comment.value.replace(/^\s*=>\s*/, ''); - var ex = this.runner.makeExample(example, output, this.filename); - this.runner.examples.push(ex); - pos = end; - } - var last = this.text.substr(pos, this.text.length-pos); - if (strip(last)) { - this.runner.examples.push(this.runner.makeExample(last, '', this.filename)); - } - } -}; - -var strip = exports.strip = function (str) { - str = str + ""; - return str.replace(/\s+$/, "").replace(/^\s+/, ""); -}; - -var rstrip = exports.rstrip = function (str) { - str = str + ""; - return str.replace(/\s+$/, ""); -}; - -var argsToArray = exports.argToArray = function (args) { - var array = []; - for (var i=0; i ' + repr(current, indentString)); - return; - } - indentString = indentString || ''; - var diff = objDiff(orig, current); - var i, keys; - var any = false; - keys = sortedKeys(diff.added); - for (i=0; i ' - + repr(diff.changed[keys[i]][1], indentString)); - } - if (! any) { - print(indentString + '(no changes)'); - } -}; - -var sortedKeys = exports.sortedKeys = function (obj) { - var keys = []; - for (var i in obj) { - if (obj.hasOwnProperty(i)) { - keys.push(i); - } - } - keys.sort(); - return keys; -}; - -var Spy = exports.Spy = function (name, options, extraOptions) { - var self; - name = name || 'spy'; - if (Spy.spies[name]) { - self = Spy.spies[name]; - if ((! options) && ! extraOptions) { - return self; - } - } else { - self = function () { - return self.func.apply(this, arguments); - }; - } - options = options || {}; - if (typeof options == 'function') { - options = {applies: options}; - } - if (extraOptions) { - extendDefault(options, extraOptions); - } - extendDefault(options, Spy.defaultOptions); - self._name = name; - self.options = options; - self.called = false; - self.calledWait = false; - self.args = null; - self.self = null; - self.argList = []; - self.selfList = []; - self.writes = options.writes || false; - self.returns = options.returns || undefined; - self.applies = options.applies || null; - self.throwError = options.throwError || null; - self.ignoreThis = options.ignoreThis || false; - self.wrapArgs = options.wrapArgs || false; - self.func = function () { - self.called = true; - self.calledWait = true; - self.args = argsToArray(arguments); - self.self = this; - self.argList.push(self.args); - self.selfList.push(this); - // It might be possible to get the caller? - if (self.writes) { - if (typeof writeln == "undefined") { - console.warn("Spy writing outside of test:", self.formatCall()); - } else { - writeln(self.formatCall()); - } - } - if (self.throwError) { - var throwError = self.throwError; - if (typeof throwError == "function") { - throwError = self.throwError.apply(this, arguments); - } - throw throwError; - } - if (self.applies) { - try { - return self.applies.apply(this, arguments); - } catch (e) { - console.error('Error in ' + self.repr() + '.applies:', e); - throw e; - } - } - return self.returns; - }; - self.func.toString = function () { - return "Spy('" + self._name + "').func"; - }; - - // Method definitions: - self.formatCall = function () { - var s = ''; - if ((! self.ignoreThis) && self.self !== globalObject && self.self !== self) { - s += repr(self.self) + '.'; - } - s += self._name; - if (self.args === null) { - return s + ':never called'; - } - s += '('; - // This eliminates trailing undefined arguments: - var length = self.args.length; - while (length && self.args[length-1] === undefined) { - length--; - } - for (var i=0; i(<(div|p)[^>]*>)?/g, '\n'); - report = report.replace(/<(div|p)[^>]*>/g, '\n'); - report = report.replace(/<[^>]*>/g, ' '); - report = report.replace(/ +/g, ' '); - console.log('Report:', report); - */ - } - done = true; - }; - req.send(); - wait(function () {return done;}); -} - -function _removeJshintSections(text) { - /* Removes anything surrounded with a comment like: - // jshint-ignore - ... - // jshint-endignore - - It replaces these with whitespace so character and line counts still work. - */ - var result = ''; - var start = /(\/\/|\/\*)\s*jshint-ignore/i; - var end = /jshint-endignore\s*(\*\/)?/i; - while (true) { - var match = text.search(start); - if (match == -1) { - result += text; - break; - } - result += text.substr(0, match); - text = text.substr(match); - match = end.exec(text); - if (! match) { - // throw everything left away. Warn? - break; - } - var endPos = match.index + match[0].length; - var skipped = text.substr(0, endPos); - text = text.substr(endPos); - // Maintain line numbers: - skipped = skipped.replace(/[^\n]/g, ' '); - result += skipped; - } - return result; -} - - -exports.jshint = jshint; - -function NosyXMLHttpRequest(name, req) { - if (this === globalObject) { - throw 'You forgot *new* NosyXMLHttpRequest(' + repr(name) + ')'; - } - if (! name) { - throw 'The name argument is required'; - } - if (typeof name != "string") { - throw 'Wrong type of argument for name: ' + name; - } - if (! req) { - req = new NosyXMLHttpRequest.realXMLHttpRequest(); - } - this._name = name; - this._req = req; - this._method = null; - this._data = null; - this._url = null; - this._headers = {}; - this.abort = printWrap(this._req, 'abort', this._name); - this.getAllResponseHeaders = this._req.getAllResponseHeaders.bind(this._req); - this.getResponseHeader = this._req.getResponseHeader.bind(this._req); - this.open = printWrap(this._req, 'open', this._name, (function (method, url) { - this._method = method; - this._url = url; - }).bind(this)); - this.overrideMimeType = printWrap(this._req, 'overrideMimeType', this._name); - this.send = printWrap(this._req, 'send', this._name, (function (data) { - if (this.timeout !== undefined) { - this._req.timeout = this.timeout; - } - if (this.withCredentials !== undefined) { - this._req.withCredentials = this.withCredentials; - } - this._data = data; - }).bind(this)); - this.setRequestHeader = printWrap(this._req, 'setRequestHeader', this._name, (function (name, value) { - this._headers[name] = value; - }).bind(this)); - this.onreadystatechange = null; - this._req.onreadystatechange = (function () { - this.readyState = this._req.readyState; - if (this.readyState >= this.HEADERS_RECEIVED) { - var props = ['response', 'responseText', 'responseType', - 'responseXML', 'status', 'statusText', 'upload']; - - for (var i=0; i";TokenName[Token.Identifier]="Identifier";TokenName[Token.Keyword]="Keyword";TokenName[Token.NullLiteral]="Null";TokenName[Token.NumericLiteral]="Numeric"; -TokenName[Token.Punctuator]="Punctuator";TokenName[Token.StringLiteral]="String";TokenName[Token.RegularExpression]="RegularExpression";FnExprTokens=["(","{","[","in","typeof","instanceof","new","return","case","delete","throw","void","=","+=","-=","*=","/=","%=","<<=",">>=",">>>=","&=","|=","^=",",","+","-","*","/","%","++","--","<<",">>",">>>","&","|","^","!","~","&&","||","?",":","===","==",">=","<=","<",">","!=","!=="]; -Syntax={AssignmentExpression:"AssignmentExpression",ArrayExpression:"ArrayExpression",BlockStatement:"BlockStatement",BinaryExpression:"BinaryExpression",BreakStatement:"BreakStatement",CallExpression:"CallExpression",CatchClause:"CatchClause",ConditionalExpression:"ConditionalExpression",ContinueStatement:"ContinueStatement",DoWhileStatement:"DoWhileStatement",DebuggerStatement:"DebuggerStatement",EmptyStatement:"EmptyStatement",ExpressionStatement:"ExpressionStatement",ForStatement:"ForStatement",ForInStatement:"ForInStatement",FunctionDeclaration:"FunctionDeclaration",FunctionExpression:"FunctionExpression",Identifier:"Identifier",IfStatement:"IfStatement",Literal:"Literal",LabeledStatement:"LabeledStatement",LogicalExpression:"LogicalExpression",MemberExpression:"MemberExpression",NewExpression:"NewExpression",ObjectExpression:"ObjectExpression",Program:"Program",Property:"Property",ReturnStatement:"ReturnStatement",SequenceExpression:"SequenceExpression",SwitchStatement:"SwitchStatement",SwitchCase:"SwitchCase",ThisExpression:"ThisExpression",ThrowStatement:"ThrowStatement",TryStatement:"TryStatement",UnaryExpression:"UnaryExpression",UpdateExpression:"UpdateExpression",VariableDeclaration:"VariableDeclaration",VariableDeclarator:"VariableDeclarator",WhileStatement:"WhileStatement",WithStatement:"WithStatement"}; -PropertyKind={Data:1,Get:2,Set:4};Messages={UnexpectedToken:"Unexpected token %0",UnexpectedNumber:"Unexpected number",UnexpectedString:"Unexpected string",UnexpectedIdentifier:"Unexpected identifier",UnexpectedReserved:"Unexpected reserved word",UnexpectedEOS:"Unexpected end of input",NewlineAfterThrow:"Illegal newline after throw",InvalidRegExp:"Invalid regular expression",UnterminatedRegExp:"Invalid regular expression: missing /",InvalidLHSInAssignment:"Invalid left-hand side in assignment",InvalidLHSInForIn:"Invalid left-hand side in for-in",MultipleDefaultsInSwitch:"More than one default clause in switch statement",NoCatchOrFinally:"Missing catch or finally after try",UnknownLabel:"Undefined label '%0'",Redeclaration:"%0 '%1' has already been declared",IllegalContinue:"Illegal continue statement",IllegalBreak:"Illegal break statement",IllegalReturn:"Illegal return statement",StrictModeWith:"Strict mode code may not include a with statement",StrictCatchVariable:"Catch variable may not be eval or arguments in strict mode",StrictVarName:"Variable name may not be eval or arguments in strict mode",StrictParamName:"Parameter name eval or arguments is not allowed in strict mode",StrictParamDupe:"Strict mode function may not have duplicate parameter names",StrictFunctionName:"Function name may not be eval or arguments in strict mode",StrictOctalLiteral:"Octal literals are not allowed in strict mode.",StrictDelete:"Delete of an unqualified identifier in strict mode.",StrictDuplicateProperty:"Duplicate data property in object literal not allowed in strict mode",AccessorDataProperty:"Object literal may not have data and accessor property with the same name",AccessorGetSet:"Object literal may not have multiple get/set accessors with the same name",StrictLHSAssignment:"Assignment to eval or arguments is not allowed in strict mode",StrictLHSPostfix:"Postfix increment/decrement may not have eval or arguments operand in strict mode",StrictLHSPrefix:"Prefix increment/decrement may not have eval or arguments operand in strict mode",StrictReservedWord:"Use of future reserved word in strict mode"}; -Regex={NonAsciiIdentifierStart:new RegExp("[\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0\u08a2-\u08ac\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19c1-\u19c7\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa80-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]"),NonAsciiIdentifierPart:new RegExp("[\u00aa\u00b5\u00ba\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0300-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u0483-\u0487\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u05d0-\u05ea\u05f0-\u05f2\u0610-\u061a\u0620-\u0669\u066e-\u06d3\u06d5-\u06dc\u06df-\u06e8\u06ea-\u06fc\u06ff\u0710-\u074a\u074d-\u07b1\u07c0-\u07f5\u07fa\u0800-\u082d\u0840-\u085b\u08a0\u08a2-\u08ac\u08e4-\u08fe\u0900-\u0963\u0966-\u096f\u0971-\u0977\u0979-\u097f\u0981-\u0983\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bc-\u09c4\u09c7\u09c8\u09cb-\u09ce\u09d7\u09dc\u09dd\u09df-\u09e3\u09e6-\u09f1\u0a01-\u0a03\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a59-\u0a5c\u0a5e\u0a66-\u0a75\u0a81-\u0a83\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abc-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ad0\u0ae0-\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3c-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b5c\u0b5d\u0b5f-\u0b63\u0b66-\u0b6f\u0b71\u0b82\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd0\u0bd7\u0be6-\u0bef\u0c01-\u0c03\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c58\u0c59\u0c60-\u0c63\u0c66-\u0c6f\u0c82\u0c83\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbc-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0cde\u0ce0-\u0ce3\u0ce6-\u0cef\u0cf1\u0cf2\u0d02\u0d03\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d-\u0d44\u0d46-\u0d48\u0d4a-\u0d4e\u0d57\u0d60-\u0d63\u0d66-\u0d6f\u0d7a-\u0d7f\u0d82\u0d83\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e01-\u0e3a\u0e40-\u0e4e\u0e50-\u0e59\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb9\u0ebb-\u0ebd\u0ec0-\u0ec4\u0ec6\u0ec8-\u0ecd\u0ed0-\u0ed9\u0edc-\u0edf\u0f00\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e-\u0f47\u0f49-\u0f6c\u0f71-\u0f84\u0f86-\u0f97\u0f99-\u0fbc\u0fc6\u1000-\u1049\u1050-\u109d\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u135d-\u135f\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176c\u176e-\u1770\u1772\u1773\u1780-\u17d3\u17d7\u17dc\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u1820-\u1877\u1880-\u18aa\u18b0-\u18f5\u1900-\u191c\u1920-\u192b\u1930-\u193b\u1946-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u19d0-\u19d9\u1a00-\u1a1b\u1a20-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1aa7\u1b00-\u1b4b\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1bf3\u1c00-\u1c37\u1c40-\u1c49\u1c4d-\u1c7d\u1cd0-\u1cd2\u1cd4-\u1cf6\u1d00-\u1de6\u1dfc-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u200c\u200d\u203f\u2040\u2054\u2071\u207f\u2090-\u209c\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d7f-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2de0-\u2dff\u2e2f\u3005-\u3007\u3021-\u302f\u3031-\u3035\u3038-\u303c\u3041-\u3096\u3099\u309a\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua62b\ua640-\ua66f\ua674-\ua67d\ua67f-\ua697\ua69f-\ua6f1\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua827\ua840-\ua873\ua880-\ua8c4\ua8d0-\ua8d9\ua8e0-\ua8f7\ua8fb\ua900-\ua92d\ua930-\ua953\ua960-\ua97c\ua980-\ua9c0\ua9cf-\ua9d9\uaa00-\uaa36\uaa40-\uaa4d\uaa50-\uaa59\uaa60-\uaa76\uaa7a\uaa7b\uaa80-\uaac2\uaadb-\uaadd\uaae0-\uaaef\uaaf2-\uaaf6\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabea\uabec\uabed\uabf0-\uabf9\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe00-\ufe0f\ufe20-\ufe26\ufe33\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]")}; -function assert(condition,message){if(!condition){throw new Error("ASSERT: "+message)}}function isDecimalDigit(ch){return ch>=48&&ch<=57}function isHexDigit(ch){return"0123456789abcdefABCDEF".indexOf(ch)>=0 -}function isOctalDigit(ch){return"01234567".indexOf(ch)>=0}function isWhiteSpace(ch){return ch===32||ch===9||ch===11||ch===12||ch===160||ch>=5760&&"\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff".indexOf(String.fromCharCode(ch))>0 -}function isLineTerminator(ch){return ch===10||ch===13||ch===8232||ch===8233}function isIdentifierStart(ch){return ch===36||ch===95||ch>=65&&ch<=90||ch>=97&&ch<=122||ch===92||ch>=128&&Regex.NonAsciiIdentifierStart.test(String.fromCharCode(ch)) -}function isIdentifierPart(ch){return ch===36||ch===95||ch>=65&&ch<=90||ch>=97&&ch<=122||ch>=48&&ch<=57||ch===92||ch>=128&&Regex.NonAsciiIdentifierPart.test(String.fromCharCode(ch))}function isFutureReservedWord(id){switch(id){case"class":case"enum":case"export":case"extends":case"import":case"super":return true; -default:return false}}function isStrictModeReservedWord(id){switch(id){case"implements":case"interface":case"package":case"private":case"protected":case"public":case"static":case"yield":case"let":return true; -default:return false}}function isRestrictedWord(id){return id==="eval"||id==="arguments"}function isKeyword(id){if(strict&&isStrictModeReservedWord(id)){return true}switch(id.length){case 2:return id==="if"||id==="in"||id==="do"; -case 3:return id==="var"||id==="for"||id==="new"||id==="try"||id==="let";case 4:return id==="this"||id==="else"||id==="case"||id==="void"||id==="with"||id==="enum";case 5:return id==="while"||id==="break"||id==="catch"||id==="throw"||id==="const"||id==="yield"||id==="class"||id==="super"; -case 6:return id==="return"||id==="typeof"||id==="delete"||id==="switch"||id==="export"||id==="import";case 7:return id==="default"||id==="finally"||id==="extends";case 8:return id==="function"||id==="continue"||id==="debugger"; -case 10:return id==="instanceof";default:return false}}function skipComment(){var ch,blockComment,lineComment;blockComment=false;lineComment=false;while(index=length){throwError({},Messages.UnexpectedToken,"ILLEGAL")}}else{ch=source.charCodeAt(index++);if(index>=length){throwError({},Messages.UnexpectedToken,"ILLEGAL") -}if(ch===42){ch=source.charCodeAt(index);if(ch===47){++index;blockComment=false}}}}else if(ch===47){ch=source.charCodeAt(index+1);if(ch===47){index+=2;lineComment=true}else if(ch===42){index+=2;blockComment=true; -if(index>=length){throwError({},Messages.UnexpectedToken,"ILLEGAL")}}else{break}}else if(isWhiteSpace(ch)){++index}else if(isLineTerminator(ch)){++index;if(ch===13&&source.charCodeAt(index)===10){++index -}++lineNumber;lineStart=index}else{break}}}function scanHexEscape(prefix){var i,len,ch,code=0;len=prefix==="u"?4:2;for(i=0;i"&&ch2===">"&&ch3===">"){if(ch4==="="){index+=4;return{type:Token.Punctuator,value:">>>=",lineNumber:lineNumber,lineStart:lineStart,range:[start,index]} -}}if(ch1===">"&&ch2===">"&&ch3===">"){index+=3;return{type:Token.Punctuator,value:">>>",lineNumber:lineNumber,lineStart:lineStart,range:[start,index]}}if(ch1==="<"&&ch2==="<"&&ch3==="="){index+=3;return{type:Token.Punctuator,value:"<<=",lineNumber:lineNumber,lineStart:lineStart,range:[start,index]} -}if(ch1===">"&&ch2===">"&&ch3==="="){index+=3;return{type:Token.Punctuator,value:">>=",lineNumber:lineNumber,lineStart:lineStart,range:[start,index]}}if(ch1===ch2&&"+-<>&|".indexOf(ch1)>=0){index+=2;return{type:Token.Punctuator,value:ch1+ch2,lineNumber:lineNumber,lineStart:lineStart,range:[start,index]} -}if("<>=!+-*%&|^/".indexOf(ch1)>=0){++index;return{type:Token.Punctuator,value:ch1,lineNumber:lineNumber,lineStart:lineStart,range:[start,index]}}throwError({},Messages.UnexpectedToken,"ILLEGAL")}function scanHexLiteral(start){var number=""; -while(index=0&&index=0){return scanPunctuator()}return scanRegExp()}return scanRegExp()}if(prevToken.type==="Keyword"){return scanRegExp()}return scanPunctuator() -}function advance(){var ch;skipComment();if(index>=length){return{type:Token.EOF,lineNumber:lineNumber,lineStart:lineStart,range:[index,index]}}ch=source.charCodeAt(index);if(ch===40||ch===41||ch===58){return scanPunctuator() -}if(ch===39||ch===34){return scanStringLiteral()}if(isIdentifierStart(ch)){return scanIdentifier()}if(ch===46){if(isDecimalDigit(source.charCodeAt(index+1))){return scanNumericLiteral()}return scanPunctuator() -}if(isDecimalDigit(ch)){return scanNumericLiteral()}if(extra.tokenize&&ch===47){return advanceSlash()}return scanPunctuator()}function lex(){var token;token=lookahead;index=token.range[1];lineNumber=token.lineNumber; -lineStart=token.lineStart;lookahead=advance();index=token.range[1];lineNumber=token.lineNumber;lineStart=token.lineStart;return token}function peek(){var pos,line,start;pos=index;line=lineNumber;start=lineStart; -lookahead=advance();index=pos;lineNumber=line;lineStart=start}SyntaxTreeDelegate={name:"SyntaxTree",markStart:function(){},markEnd:function(node){return node},markGroupEnd:function(node){return node},postProcess:function(node){return node -},createArrayExpression:function(elements){return{type:Syntax.ArrayExpression,elements:elements}},createAssignmentExpression:function(operator,left,right){return{type:Syntax.AssignmentExpression,operator:operator,left:left,right:right} -},createBinaryExpression:function(operator,left,right){var type=operator==="||"||operator==="&&"?Syntax.LogicalExpression:Syntax.BinaryExpression;return{type:type,operator:operator,left:left,right:right} -},createBlockStatement:function(body){return{type:Syntax.BlockStatement,body:body}},createBreakStatement:function(label){return{type:Syntax.BreakStatement,label:label}},createCallExpression:function(callee,args){return{type:Syntax.CallExpression,callee:callee,arguments:args} -},createCatchClause:function(param,body){return{type:Syntax.CatchClause,param:param,body:body}},createConditionalExpression:function(test,consequent,alternate){return{type:Syntax.ConditionalExpression,test:test,consequent:consequent,alternate:alternate} -},createContinueStatement:function(label){return{type:Syntax.ContinueStatement,label:label}},createDebuggerStatement:function(){return{type:Syntax.DebuggerStatement}},createDoWhileStatement:function(body,test){return{type:Syntax.DoWhileStatement,body:body,test:test} -},createEmptyStatement:function(){return{type:Syntax.EmptyStatement}},createExpressionStatement:function(expression){return{type:Syntax.ExpressionStatement,expression:expression}},createForStatement:function(init,test,update,body){return{type:Syntax.ForStatement,init:init,test:test,update:update,body:body} -},createForInStatement:function(left,right,body){return{type:Syntax.ForInStatement,left:left,right:right,body:body,each:false}},createFunctionDeclaration:function(id,params,defaults,body){return{type:Syntax.FunctionDeclaration,id:id,params:params,defaults:defaults,body:body,rest:null,generator:false,expression:false} -},createFunctionExpression:function(id,params,defaults,body){return{type:Syntax.FunctionExpression,id:id,params:params,defaults:defaults,body:body,rest:null,generator:false,expression:false}},createIdentifier:function(name){return{type:Syntax.Identifier,name:name} -},createIfStatement:function(test,consequent,alternate){return{type:Syntax.IfStatement,test:test,consequent:consequent,alternate:alternate}},createLabeledStatement:function(label,body){return{type:Syntax.LabeledStatement,label:label,body:body} -},createLiteral:function(token){return{type:Syntax.Literal,value:token.value,raw:source.slice(token.range[0],token.range[1])}},createMemberExpression:function(accessor,object,property){return{type:Syntax.MemberExpression,computed:accessor==="[",object:object,property:property} -},createNewExpression:function(callee,args){return{type:Syntax.NewExpression,callee:callee,arguments:args}},createObjectExpression:function(properties){return{type:Syntax.ObjectExpression,properties:properties} -},createPostfixExpression:function(operator,argument){return{type:Syntax.UpdateExpression,operator:operator,argument:argument,prefix:false}},createProgram:function(body){return{type:Syntax.Program,body:body} -},createProperty:function(kind,key,value){return{type:Syntax.Property,key:key,value:value,kind:kind}},createReturnStatement:function(argument){return{type:Syntax.ReturnStatement,argument:argument}},createSequenceExpression:function(expressions){return{type:Syntax.SequenceExpression,expressions:expressions} -},createSwitchCase:function(test,consequent){return{type:Syntax.SwitchCase,test:test,consequent:consequent}},createSwitchStatement:function(discriminant,cases){return{type:Syntax.SwitchStatement,discriminant:discriminant,cases:cases} -},createThisExpression:function(){return{type:Syntax.ThisExpression}},createThrowStatement:function(argument){return{type:Syntax.ThrowStatement,argument:argument}},createTryStatement:function(block,guardedHandlers,handlers,finalizer){return{type:Syntax.TryStatement,block:block,guardedHandlers:guardedHandlers,handlers:handlers,finalizer:finalizer} -},createUnaryExpression:function(operator,argument){if(operator==="++"||operator==="--"){return{type:Syntax.UpdateExpression,operator:operator,argument:argument,prefix:true}}return{type:Syntax.UnaryExpression,operator:operator,argument:argument} -},createVariableDeclaration:function(declarations,kind){return{type:Syntax.VariableDeclaration,declarations:declarations,kind:kind}},createVariableDeclarator:function(id,init){return{type:Syntax.VariableDeclarator,id:id,init:init} -},createWhileStatement:function(test,body){return{type:Syntax.WhileStatement,test:test,body:body}},createWithStatement:function(object,body){return{type:Syntax.WithStatement,object:object,body:body}}}; -function peekLineTerminator(){var pos,line,start,found;pos=index;line=lineNumber;start=lineStart;skipComment();found=lineNumber!==line;index=pos;lineNumber=line;lineStart=start;return found}function throwError(token,messageFormat){var error,args=Array.prototype.slice.call(arguments,2),msg=messageFormat.replace(/%(\d)/g,function(whole,index){assert(index>="||op===">>>="||op==="&="||op==="^="||op==="|=" -}function consumeSemicolon(){var line;if(source.charCodeAt(index)===59){lex();return}line=lineNumber;skipComment();if(lineNumber!==line){return}if(match(";")){lex();return}if(lookahead.type!==Token.EOF&&!match("}")){throwUnexpected(lookahead) -}}function isLeftHandSide(expr){return expr.type===Syntax.Identifier||expr.type===Syntax.MemberExpression}function parseArrayInitialiser(){var elements=[];expect("[");while(!match("]")){if(match(",")){lex(); -elements.push(null)}else{elements.push(parseAssignmentExpression());if(!match("]")){expect(",")}}}expect("]");return delegate.createArrayExpression(elements)}function parsePropertyFunction(param,first){var previousStrict,body; -previousStrict=strict;delegate.markStart();body=parseFunctionSourceElements();if(first&&strict&&isRestrictedWord(param[0].name)){throwErrorTolerant(first,Messages.StrictParamName)}strict=previousStrict; -return delegate.markEnd(delegate.createFunctionExpression(null,param,[],body))}function parseObjectPropertyKey(){var token;delegate.markStart();token=lex();if(token.type===Token.StringLiteral||token.type===Token.NumericLiteral){if(strict&&token.octal){throwErrorTolerant(token,Messages.StrictOctalLiteral) -}return delegate.markEnd(delegate.createLiteral(token))}return delegate.markEnd(delegate.createIdentifier(token.value))}function parseObjectProperty(){var token,key,id,value,param;token=lookahead;delegate.markStart(); -if(token.type===Token.Identifier){id=parseObjectPropertyKey();if(token.value==="get"&&!match(":")){key=parseObjectPropertyKey();expect("(");expect(")");value=parsePropertyFunction([]);return delegate.markEnd(delegate.createProperty("get",key,value)) -}if(token.value==="set"&&!match(":")){key=parseObjectPropertyKey();expect("(");token=lookahead;if(token.type!==Token.Identifier){throwUnexpected(lex())}param=[parseVariableIdentifier()];expect(")");value=parsePropertyFunction(param,token); -return delegate.markEnd(delegate.createProperty("set",key,value))}expect(":");value=parseAssignmentExpression();return delegate.markEnd(delegate.createProperty("init",id,value))}if(token.type===Token.EOF||token.type===Token.Punctuator){throwUnexpected(token) -}else{key=parseObjectPropertyKey();expect(":");value=parseAssignmentExpression();return delegate.markEnd(delegate.createProperty("init",key,value))}}function parseObjectInitialiser(){var properties=[],property,name,key,kind,map={},toString=String; -expect("{");while(!match("}")){property=parseObjectProperty();if(property.key.type===Syntax.Identifier){name=property.key.name}else{name=toString(property.key.value)}kind=property.kind==="init"?PropertyKind.Data:property.kind==="get"?PropertyKind.Get:PropertyKind.Set; -key="$"+name;if(Object.prototype.hasOwnProperty.call(map,key)){if(map[key]===PropertyKind.Data){if(strict&&kind===PropertyKind.Data){throwErrorTolerant({},Messages.StrictDuplicateProperty)}else if(kind!==PropertyKind.Data){throwErrorTolerant({},Messages.AccessorDataProperty) -}}else{if(kind===PropertyKind.Data){throwErrorTolerant({},Messages.AccessorDataProperty)}else if(map[key]&kind){throwErrorTolerant({},Messages.AccessorGetSet)}}map[key]|=kind}else{map[key]=kind}properties.push(property); -if(!match("}")){expect(",")}}expect("}");return delegate.createObjectExpression(properties)}function parseGroupExpression(){var expr;delegate.markStart();expect("(");expr=parseExpression();expect(")"); -return delegate.markGroupEnd(expr)}function parsePrimaryExpression(){var type,token,expr;if(match("(")){return parseGroupExpression()}type=lookahead.type;delegate.markStart();if(type===Token.Identifier){expr=delegate.createIdentifier(lex().value) -}else if(type===Token.StringLiteral||type===Token.NumericLiteral){if(strict&&lookahead.octal){throwErrorTolerant(lookahead,Messages.StrictOctalLiteral)}expr=delegate.createLiteral(lex())}else if(type===Token.Keyword){if(matchKeyword("this")){lex(); -expr=delegate.createThisExpression()}else if(matchKeyword("function")){expr=parseFunctionExpression()}}else if(type===Token.BooleanLiteral){token=lex();token.value=token.value==="true";expr=delegate.createLiteral(token) -}else if(type===Token.NullLiteral){token=lex();token.value=null;expr=delegate.createLiteral(token)}else if(match("[")){expr=parseArrayInitialiser()}else if(match("{")){expr=parseObjectInitialiser()}else if(match("/")||match("/=")){expr=delegate.createLiteral(scanRegExp()) -}if(expr){return delegate.markEnd(expr)}throwUnexpected(lex())}function parseArguments(){var args=[];expect("(");if(!match(")")){while(index":case"<=":case">=":case"instanceof":prec=7; -break;case"in":prec=allowIn?7:0;break;case"<<":case">>":case">>>":prec=8;break;case"+":case"-":prec=9;break;case"*":case"/":case"%":prec=11;break;default:break}return prec}function parseBinaryExpression(){var expr,token,prec,previousAllowIn,stack,right,operator,left,i; -previousAllowIn=state.allowIn;state.allowIn=true;expr=parseUnaryExpression();token=lookahead;prec=binaryPrecedence(token,previousAllowIn);if(prec===0){return expr}token.prec=prec;lex();stack=[expr,token,parseUnaryExpression()]; -while((prec=binaryPrecedence(lookahead,previousAllowIn))>0){while(stack.length>2&&prec<=stack[stack.length-2].prec){right=stack.pop();operator=stack.pop().value;left=stack.pop();stack.push(delegate.createBinaryExpression(operator,left,right)) -}token=lex();token.prec=prec;stack.push(token);stack.push(parseUnaryExpression())}state.allowIn=previousAllowIn;i=stack.length-1;expr=stack[i];while(i>1){expr=delegate.createBinaryExpression(stack[i-1].value,stack[i-2],expr); -i-=2}return expr}function parseConditionalExpression(){var expr,previousAllowIn,consequent,alternate;delegate.markStart();expr=parseBinaryExpression();if(match("?")){lex();previousAllowIn=state.allowIn; -state.allowIn=true;consequent=parseAssignmentExpression();state.allowIn=previousAllowIn;expect(":");alternate=parseAssignmentExpression();expr=delegate.markEnd(delegate.createConditionalExpression(expr,consequent,alternate)) -}else{delegate.markEnd({})}return expr}function parseAssignmentExpression(){var token,marker,left,right,node;token=lookahead;marker=createLocationMarker();node=left=parseConditionalExpression();if(matchAssign()){if(!isLeftHandSide(left)){throwError({},Messages.InvalidLHSInAssignment) -}if(strict&&left.type===Syntax.Identifier&&isRestrictedWord(left.name)){throwErrorTolerant(token,Messages.StrictLHSAssignment)}token=lex();right=parseAssignmentExpression();node=delegate.createAssignmentExpression(token.value,left,right) -}if(marker){marker.end();return marker.applyIf(node)}return node}function parseExpression(){var marker,expr;marker=createLocationMarker();expr=parseAssignmentExpression();if(match(",")){expr=delegate.createSequenceExpression([expr]); -while(index0){if(extra.comments[extra.comments.length-1].range[1]>start){return -}}extra.comments.push({type:type,value:value,range:[start,end],loc:loc})}function scanComment(){var comment,ch,loc,start,blockComment,lineComment;comment="";blockComment=false;lineComment=false;while(index=length){lineComment=false;comment+=ch;loc.end={line:lineNumber,column:length-lineStart};addComment("Line",comment,start,length,loc)}else{comment+=ch -}}else if(blockComment){if(isLineTerminator(ch.charCodeAt(0))){if(ch==="\r"&&source[index+1]==="\n"){++index;comment+="\r\n"}else{comment+=ch}++lineNumber;++index;lineStart=index;if(index>=length){throwError({},Messages.UnexpectedToken,"ILLEGAL") -}}else{ch=source[index++];if(index>=length){throwError({},Messages.UnexpectedToken,"ILLEGAL")}comment+=ch;if(ch==="*"){ch=source[index];if(ch==="/"){comment=comment.substr(0,comment.length-1);blockComment=false; -++index;loc.end={line:lineNumber,column:index-lineStart};addComment("Block",comment,start,index,loc);comment=""}}}}else if(ch==="/"){ch=source[index+1];if(ch==="/"){loc={start:{line:lineNumber,column:index-lineStart}}; -start=index;index+=2;lineComment=true;if(index>=length){loc.end={line:lineNumber,column:index-lineStart};lineComment=false;addComment("Line",comment,start,index,loc)}}else if(ch==="*"){start=index;index+=2; -blockComment=true;loc={start:{line:lineNumber,column:index-lineStart-2}};if(index>=length){throwError({},Messages.UnexpectedToken,"ILLEGAL")}}else{break}}else if(isWhiteSpace(ch.charCodeAt(0))){++index -}else if(isLineTerminator(ch.charCodeAt(0))){++index;if(ch==="\r"&&source[index]==="\n"){++index}++lineNumber;lineStart=index}else{break}}}function filterCommentLocation(){var i,entry,comment,comments=[]; -for(i=0;i0){token=extra.tokens[extra.tokens.length-1]; -if(token.range[0]===pos&&token.type==="Punctuator"){if(token.value==="/"||token.value==="/="){extra.tokens.pop()}}}extra.tokens.push({type:"RegularExpression",value:regex.literal,range:[pos,index],loc:loc}) -}return regex}function filterTokenLocation(){var i,entry,token,tokens=[];for(i=0;i0?1:0;lineStart=0;length=source.length;lookahead=null;state={allowIn:true,labelSet:{},inFunctionBody:false,inIteration:false,inSwitch:false}; -extra={};options=options||{};options.tokens=true;extra.tokens=[];extra.tokenize=true;extra.openParenToken=-1;extra.openCurlyToken=-1;extra.range=typeof options.range==="boolean"&&options.range;extra.loc=typeof options.loc==="boolean"&&options.loc; -if(typeof options.comment==="boolean"&&options.comment){extra.comments=[]}if(typeof options.tolerant==="boolean"&&options.tolerant){extra.errors=[]}if(length>0){if(typeof source[0]==="undefined"){if(code instanceof String){source=code.valueOf() -}}}patch();try{peek();if(lookahead.type===Token.EOF){return extra.tokens}token=lex();while(lookahead.type!==Token.EOF){try{token=lex()}catch(lexError){token=lookahead;if(extra.errors){extra.errors.push(lexError); -break}else{throw lexError}}}filterTokenLocation();tokens=extra.tokens;if(typeof extra.comments!=="undefined"){filterCommentLocation();tokens.comments=extra.comments}if(typeof extra.errors!=="undefined"){tokens.errors=extra.errors -}}catch(e){throw e}finally{unpatch();extra={}}return tokens}function parse(code,options){var program,toString;toString=String;if(typeof code!=="string"&&!(code instanceof String)){code=toString(code)}delegate=SyntaxTreeDelegate; -source=code;index=0;lineNumber=source.length>0?1:0;lineStart=0;length=source.length;lookahead=null;state={allowIn:true,labelSet:{},inFunctionBody:false,inIteration:false,inSwitch:false};extra={};if(typeof options!=="undefined"){extra.range=typeof options.range==="boolean"&&options.range; -extra.loc=typeof options.loc==="boolean"&&options.loc;if(typeof options.range==="boolean"&&options.range){state.rangeStack=[];delegate=extend(delegate,{markStart:function(){skipComment();state.rangeStack.push(index) -}});delegate=extend(delegate,{markEnd:function(node){node.range=[state.rangeStack.pop(),index];return node}})}if(typeof options.loc==="boolean"&&options.loc){state.locStack=[];delegate=extend(delegate,{markStart:function(){skipComment(); -state.locStack.push({line:lineNumber,column:index-lineStart});if(state.rangeStack){state.rangeStack.push(index)}}});delegate=extend(delegate,{markEnd:function(node){if(state.rangeStack){node.range=[state.rangeStack.pop(),index] -}node.loc={};node.loc.start=state.locStack.pop();node.loc.end={line:lineNumber,column:index-lineStart};if(options.source!==null&&options.source!==undefined){node.loc.source=toString(options.source)}return node -}});delegate=extend(delegate,{markGroupEnd:function(node){if(state.rangeStack){node.groupRange=[state.rangeStack.pop(),index]}node.groupLoc={};node.groupLoc.start=state.locStack.pop();node.groupLoc.end={line:lineNumber,column:index-lineStart}; -if(options.source!==null&&options.source!==undefined){node.groupLoc.source=toString(options.source)}return node}})}if(extra.loc&&options.source!==null&&options.source!==undefined){delegate=extend(delegate,{postProcess:function(node){node.loc.source=toString(options.source); -return node}})}if(typeof options.tokens==="boolean"&&options.tokens){extra.tokens=[]}if(typeof options.comment==="boolean"&&options.comment){extra.comments=[]}if(typeof options.tolerant==="boolean"&&options.tolerant){extra.errors=[] -}}if(length>0){if(typeof source[0]==="undefined"){if(code instanceof String){source=code.valueOf()}}}patch();try{program=parseProgram();if(typeof extra.comments!=="undefined"){filterCommentLocation();program.comments=extra.comments -}if(typeof extra.tokens!=="undefined"){filterTokenLocation();program.tokens=extra.tokens}if(typeof extra.errors!=="undefined"){program.errors=extra.errors}if(extra.range||extra.loc){filterGroup(program.body) -}}catch(e){throw e}finally{unpatch();extra={}}return program}exports.version="1.1.0-dev";exports.tokenize=tokenize;exports.parse=parse;exports.Syntax=function(){var name,types={};if(typeof Object.create==="function"){types=Object.create(null) -}for(name in Syntax){if(Syntax.hasOwnProperty(name)){types[name]=Syntax[name]}}if(typeof Object.freeze==="function"){Object.freeze(types)}return types}()}); -/* END INSERT */ - -realExports.esprima = exports; -var esprima = exports; -/* Includes a minified jshint: http://www.jshint.com/ */ -// Avoid clobber: -exports = {}; - -/* INSERT jshint.js */ -var JSHINT;(function(){var require=function(file,cwd){var resolved=require.resolve(file,cwd||"/");var mod=require.modules[resolved];if(!mod)throw new Error("Failed to resolve module "+file+", tried "+resolved); -var cached=require.cache[resolved];var res=cached?cached.exports:mod();return res};require.paths=[];require.modules={};require.cache={};require.extensions=[".js",".coffee",".json"];require._core={assert:true,events:true,fs:true,path:true,vm:true}; -require.resolve=function(){return function(x,cwd){if(!cwd)cwd="/";if(require._core[x])return x;var path=require.modules.path();cwd=path.resolve("/",cwd);var y=cwd||"/";if(x.match(/^(?:\.\.?\/|\/)/)){var m=loadAsFileSync(path.resolve(y,x))||loadAsDirectorySync(path.resolve(y,x)); -if(m)return m}var n=loadNodeModulesSync(x,y);if(n)return n;throw new Error("Cannot find module '"+x+"'");function loadAsFileSync(x){x=path.normalize(x);if(require.modules[x]){return x}for(var i=0;i=0;i--){if(parts[i]==="node_modules")continue;var dir=parts.slice(0,i+1).join("/")+"/node_modules";dirs.push(dir)}return dirs}}}();require.alias=function(from,to){var path=require.modules.path(); -var res=null;try{res=require.resolve(from+"/package.json","/")}catch(err){res=require.resolve(from,"/")}var basedir=path.dirname(res);var keys=(Object.keys||function(obj){var res=[];for(var key in obj)res.push(key); -return res})(require.modules);for(var i=0;i 0\n var up = 0;\n for (var i = parts.length; i >= 0; i--) {\n var last = parts[i];\n if (last == '.') {\n parts.splice(i, 1);\n } else if (last === '..') {\n parts.splice(i, 1);\n up++;\n } else if (up) {\n parts.splice(i, 1);\n up--;\n }\n }\n\n // if the path is allowed to go above the root, restore leading ..s\n if (allowAboveRoot) {\n for (; up--; up) {\n parts.unshift('..');\n }\n }\n\n return parts;\n}\n\n// Regex to split a filename into [*, dir, basename, ext]\n// posix version\nvar splitPathRe = /^(.+\\/(?!$)|\\/)?((?:.+?)?(\\.[^.]*)?)$/;\n\n// path.resolve([from ...], to)\n// posix version\nexports.resolve = function() {\nvar resolvedPath = '',\n resolvedAbsolute = false;\n\nfor (var i = arguments.length; i >= -1 && !resolvedAbsolute; i--) {\n var path = (i >= 0)\n ? arguments[i]\n : process.cwd();\n\n // Skip empty and invalid entries\n if (typeof path !== 'string' || !path) {\n continue;\n }\n\n resolvedPath = path + '/' + resolvedPath;\n resolvedAbsolute = path.charAt(0) === '/';\n}\n\n// At this point the path should be resolved to a full absolute path, but\n// handle relative paths to be safe (might happen when process.cwd() fails)\n\n// Normalize the path\nresolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {\n return !!p;\n }), !resolvedAbsolute).join('/');\n\n return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';\n};\n\n// path.normalize(path)\n// posix version\nexports.normalize = function(path) {\nvar isAbsolute = path.charAt(0) === '/',\n trailingSlash = path.slice(-1) === '/';\n\n// Normalize the path\npath = normalizeArray(filter(path.split('/'), function(p) {\n return !!p;\n }), !isAbsolute).join('/');\n\n if (!path && !isAbsolute) {\n path = '.';\n }\n if (path && trailingSlash) {\n path += '/';\n }\n \n return (isAbsolute ? '/' : '') + path;\n};\n\n\n// posix version\nexports.join = function() {\n var paths = Array.prototype.slice.call(arguments, 0);\n return exports.normalize(filter(paths, function(p, index) {\n return p && typeof p === 'string';\n }).join('/'));\n};\n\n\nexports.dirname = function(path) {\n var dir = splitPathRe.exec(path)[1] || '';\n var isWindows = false;\n if (!dir) {\n // No dirname\n return '.';\n } else if (dir.length === 1 ||\n (isWindows && dir.length <= 3 && dir.charAt(1) === ':')) {\n // It is just a slash or a drive letter with a slash\n return dir;\n } else {\n // It is a full dirname, strip trailing slash\n return dir.substring(0, dir.length - 1);\n }\n};\n\n\nexports.basename = function(path, ext) {\n var f = splitPathRe.exec(path)[2] || '';\n // TODO: make this comparison case-insensitive on windows?\n if (ext && f.substr(-1 * ext.length) === ext) {\n f = f.substr(0, f.length - ext.length);\n }\n return f;\n};\n\n\nexports.extname = function(path) {\n return splitPathRe.exec(path)[3] || '';\n};\n\n//@ sourceURL=path")); -require.define("__browserify_process",Function(["require","module","exports","__dirname","__filename","process","global"],"var process = module.exports = {};\n\nprocess.nextTick = (function () {\n var canSetImmediate = typeof window !== 'undefined'\n && window.setImmediate;\n var canPost = typeof window !== 'undefined'\n && window.postMessage && window.addEventListener\n ;\n\n if (canSetImmediate) {\n return window.setImmediate;\n }\n\n if (canPost) {\n var queue = [];\n window.addEventListener('message', function (ev) {\n if (ev.source === window && ev.data === 'browserify-tick') {\n ev.stopPropagation();\n if (queue.length > 0) {\n var fn = queue.shift();\n fn();\n }\n }\n }, true);\n\n return function nextTick(fn) {\n queue.push(fn);\n window.postMessage('browserify-tick', '*');\n };\n }\n\n return function nextTick(fn) {\n setTimeout(fn, 0);\n };\n})();\n\nprocess.title = 'browser';\nprocess.browser = true;\nprocess.env = {};\nprocess.argv = [];\n\nprocess.binding = function (name) {\n if (name === 'evals') return (require)('vm')\n else throw new Error('No such module. (Possibly not yet loaded)')\n};\n\n(function () {\n var cwd = '/';\n var path;\n process.cwd = function () { return cwd };\n process.chdir = function (dir) {\n if (!path) path = require('path');\n cwd = path.resolve(dir, cwd);\n };\n})();\n\n//@ sourceURL=__browserify_process")); -require.define("/node_modules/underscore/package.json",Function(["require","module","exports","__dirname","__filename","process","global"],'module.exports = {"main":"underscore.js"}\n//@ sourceURL=/node_modules/underscore/package.json')); -require.define("/node_modules/underscore/underscore.js",Function(["require","module","exports","__dirname","__filename","process","global"],"// Underscore.js 1.4.4\n// http://underscorejs.org\n// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.\n// Underscore may be freely distributed under the MIT license.\n\n(function() {\n\n // Baseline setup\n // --------------\n\n // Establish the root object, `window` in the browser, or `global` on the server.\n var root = this;\n\n // Save the previous value of the `_` variable.\n var previousUnderscore = root._;\n\n // Establish the object that gets returned to break out of a loop iteration.\n var breaker = {};\n\n // Save bytes in the minified (but not gzipped) version:\n var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n // Create quick reference variables for speed access to core prototypes.\n var push = ArrayProto.push,\n slice = ArrayProto.slice,\n concat = ArrayProto.concat,\n toString = ObjProto.toString,\n hasOwnProperty = ObjProto.hasOwnProperty;\n\n // All **ECMAScript 5** native function implementations that we hope to use\n // are declared here.\n var\n nativeForEach = ArrayProto.forEach,\n nativeMap = ArrayProto.map,\n nativeReduce = ArrayProto.reduce,\n nativeReduceRight = ArrayProto.reduceRight,\n nativeFilter = ArrayProto.filter,\n nativeEvery = ArrayProto.every,\n nativeSome = ArrayProto.some,\n nativeIndexOf = ArrayProto.indexOf,\n nativeLastIndexOf = ArrayProto.lastIndexOf,\n nativeIsArray = Array.isArray,\n nativeKeys = Object.keys,\n nativeBind = FuncProto.bind;\n\n // Create a safe reference to the Underscore object for use below.\n var _ = function(obj) {\n if (obj instanceof _) return obj;\n if (!(this instanceof _)) return new _(obj);\n this._wrapped = obj;\n };\n\n // Export the Underscore object for **Node.js**, with\n // backwards-compatibility for the old `require()` API. If we're in\n // the browser, add `_` as a global object via a string identifier,\n // for Closure Compiler \"advanced\" mode.\n if (typeof exports !== 'undefined') {\n if (typeof module !== 'undefined' && module.exports) {\n exports = module.exports = _;\n }\n exports._ = _;\n } else {\n root._ = _;\n }\n\n // Current version.\n _.VERSION = '1.4.4';\n\n // Collection Functions\n // --------------------\n\n // The cornerstone, an `each` implementation, aka `forEach`.\n // Handles objects with the built-in `forEach`, arrays, and raw objects.\n // Delegates to **ECMAScript 5**'s native `forEach` if available.\n var each = _.each = _.forEach = function(obj, iterator, context) {\n if (obj == null) return;\n if (nativeForEach && obj.forEach === nativeForEach) {\n obj.forEach(iterator, context);\n } else if (obj.length === +obj.length) {\n for (var i = 0, l = obj.length; i < l; i++) {\n if (iterator.call(context, obj[i], i, obj) === breaker) return;\n }\n } else {\n for (var key in obj) {\n if (_.has(obj, key)) {\n if (iterator.call(context, obj[key], key, obj) === breaker) return;\n }\n }\n }\n };\n\n // Return the results of applying the iterator to each element.\n // Delegates to **ECMAScript 5**'s native `map` if available.\n _.map = _.collect = function(obj, iterator, context) {\n var results = [];\n if (obj == null) return results;\n if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);\n each(obj, function(value, index, list) {\n results[results.length] = iterator.call(context, value, index, list);\n });\n return results;\n };\n\n var reduceError = 'Reduce of empty array with no initial value';\n\n // **Reduce** builds up a single result from a list of values, aka `inject`,\n // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.\n _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {\n var initial = arguments.length > 2;\n if (obj == null) obj = [];\n if (nativeReduce && obj.reduce === nativeReduce) {\n if (context) iterator = _.bind(iterator, context);\n return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);\n }\n each(obj, function(value, index, list) {\n if (!initial) {\n memo = value;\n initial = true;\n } else {\n memo = iterator.call(context, memo, value, index, list);\n }\n });\n if (!initial) throw new TypeError(reduceError);\n return memo;\n };\n\n // The right-associative version of reduce, also known as `foldr`.\n // Delegates to **ECMAScript 5**'s native `reduceRight` if available.\n _.reduceRight = _.foldr = function(obj, iterator, memo, context) {\n var initial = arguments.length > 2;\n if (obj == null) obj = [];\n if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {\n if (context) iterator = _.bind(iterator, context);\n return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);\n }\n var length = obj.length;\n if (length !== +length) {\n var keys = _.keys(obj);\n length = keys.length;\n }\n each(obj, function(value, index, list) {\n index = keys ? keys[--length] : --length;\n if (!initial) {\n memo = obj[index];\n initial = true;\n } else {\n memo = iterator.call(context, memo, obj[index], index, list);\n }\n });\n if (!initial) throw new TypeError(reduceError);\n return memo;\n };\n\n // Return the first value which passes a truth test. Aliased as `detect`.\n _.find = _.detect = function(obj, iterator, context) {\n var result;\n any(obj, function(value, index, list) {\n if (iterator.call(context, value, index, list)) {\n result = value;\n return true;\n }\n });\n return result;\n };\n\n // Return all the elements that pass a truth test.\n // Delegates to **ECMAScript 5**'s native `filter` if available.\n // Aliased as `select`.\n _.filter = _.select = function(obj, iterator, context) {\n var results = [];\n if (obj == null) return results;\n if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);\n each(obj, function(value, index, list) {\n if (iterator.call(context, value, index, list)) results[results.length] = value;\n });\n return results;\n };\n\n // Return all the elements for which a truth test fails.\n _.reject = function(obj, iterator, context) {\n return _.filter(obj, function(value, index, list) {\n return !iterator.call(context, value, index, list);\n }, context);\n };\n\n // Determine whether all of the elements match a truth test.\n // Delegates to **ECMAScript 5**'s native `every` if available.\n // Aliased as `all`.\n _.every = _.all = function(obj, iterator, context) {\n iterator || (iterator = _.identity);\n var result = true;\n if (obj == null) return result;\n if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);\n each(obj, function(value, index, list) {\n if (!(result = result && iterator.call(context, value, index, list))) return breaker;\n });\n return !!result;\n };\n\n // Determine if at least one element in the object matches a truth test.\n // Delegates to **ECMAScript 5**'s native `some` if available.\n // Aliased as `any`.\n var any = _.some = _.any = function(obj, iterator, context) {\n iterator || (iterator = _.identity);\n var result = false;\n if (obj == null) return result;\n if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);\n each(obj, function(value, index, list) {\n if (result || (result = iterator.call(context, value, index, list))) return breaker;\n });\n return !!result;\n };\n\n // Determine if the array or object contains a given value (using `===`).\n // Aliased as `include`.\n _.contains = _.include = function(obj, target) {\n if (obj == null) return false;\n if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;\n return any(obj, function(value) {\n return value === target;\n });\n };\n\n // Invoke a method (with arguments) on every item in a collection.\n _.invoke = function(obj, method) {\n var args = slice.call(arguments, 2);\n var isFunc = _.isFunction(method);\n return _.map(obj, function(value) {\n return (isFunc ? method : value[method]).apply(value, args);\n });\n };\n\n // Convenience version of a common use case of `map`: fetching a property.\n _.pluck = function(obj, key) {\n return _.map(obj, function(value){ return value[key]; });\n };\n\n // Convenience version of a common use case of `filter`: selecting only objects\n // containing specific `key:value` pairs.\n _.where = function(obj, attrs, first) {\n if (_.isEmpty(attrs)) return first ? null : [];\n return _[first ? 'find' : 'filter'](obj, function(value) {\n for (var key in attrs) {\n if (attrs[key] !== value[key]) return false;\n }\n return true;\n });\n };\n\n // Convenience version of a common use case of `find`: getting the first object\n // containing specific `key:value` pairs.\n _.findWhere = function(obj, attrs) {\n return _.where(obj, attrs, true);\n };\n\n // Return the maximum element or (element-based computation).\n // Can't optimize arrays of integers longer than 65,535 elements.\n // See: https://bugs.webkit.org/show_bug.cgi?id=80797\n _.max = function(obj, iterator, context) {\n if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {\n return Math.max.apply(Math, obj);\n }\n if (!iterator && _.isEmpty(obj)) return -Infinity;\n var result = {computed : -Infinity, value: -Infinity};\n each(obj, function(value, index, list) {\n var computed = iterator ? iterator.call(context, value, index, list) : value;\n computed >= result.computed && (result = {value : value, computed : computed});\n });\n return result.value;\n };\n\n // Return the minimum element (or element-based computation).\n _.min = function(obj, iterator, context) {\n if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {\n return Math.min.apply(Math, obj);\n }\n if (!iterator && _.isEmpty(obj)) return Infinity;\n var result = {computed : Infinity, value: Infinity};\n each(obj, function(value, index, list) {\n var computed = iterator ? iterator.call(context, value, index, list) : value;\n computed < result.computed && (result = {value : value, computed : computed});\n });\n return result.value;\n };\n\n // Shuffle an array.\n _.shuffle = function(obj) {\n var rand;\n var index = 0;\n var shuffled = [];\n each(obj, function(value) {\n rand = _.random(index++);\n shuffled[index - 1] = shuffled[rand];\n shuffled[rand] = value;\n });\n return shuffled;\n };\n\n // An internal function to generate lookup iterators.\n var lookupIterator = function(value) {\n return _.isFunction(value) ? value : function(obj){ return obj[value]; };\n };\n\n // Sort the object's values by a criterion produced by an iterator.\n _.sortBy = function(obj, value, context) {\n var iterator = lookupIterator(value);\n return _.pluck(_.map(obj, function(value, index, list) {\n return {\n value : value,\n index : index,\n criteria : iterator.call(context, value, index, list)\n };\n }).sort(function(left, right) {\n var a = left.criteria;\n var b = right.criteria;\n if (a !== b) {\n if (a > b || a === void 0) return 1;\n if (a < b || b === void 0) return -1;\n }\n return left.index < right.index ? -1 : 1;\n }), 'value');\n };\n\n // An internal function used for aggregate \"group by\" operations.\n var group = function(obj, value, context, behavior) {\n var result = {};\n var iterator = lookupIterator(value || _.identity);\n each(obj, function(value, index) {\n var key = iterator.call(context, value, index, obj);\n behavior(result, key, value);\n });\n return result;\n };\n\n // Groups the object's values by a criterion. Pass either a string attribute\n // to group by, or a function that returns the criterion.\n _.groupBy = function(obj, value, context) {\n return group(obj, value, context, function(result, key, value) {\n (_.has(result, key) ? result[key] : (result[key] = [])).push(value);\n });\n };\n\n // Counts instances of an object that group by a certain criterion. Pass\n // either a string attribute to count by, or a function that returns the\n // criterion.\n _.countBy = function(obj, value, context) {\n return group(obj, value, context, function(result, key) {\n if (!_.has(result, key)) result[key] = 0;\n result[key]++;\n });\n };\n\n // Use a comparator function to figure out the smallest index at which\n // an object should be inserted so as to maintain order. Uses binary search.\n _.sortedIndex = function(array, obj, iterator, context) {\n iterator = iterator == null ? _.identity : lookupIterator(iterator);\n var value = iterator.call(context, obj);\n var low = 0, high = array.length;\n while (low < high) {\n var mid = (low + high) >>> 1;\n iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;\n }\n return low;\n };\n\n // Safely convert anything iterable into a real, live array.\n _.toArray = function(obj) {\n if (!obj) return [];\n if (_.isArray(obj)) return slice.call(obj);\n if (obj.length === +obj.length) return _.map(obj, _.identity);\n return _.values(obj);\n };\n\n // Return the number of elements in an object.\n _.size = function(obj) {\n if (obj == null) return 0;\n return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;\n };\n\n // Array Functions\n // ---------------\n\n // Get the first element of an array. Passing **n** will return the first N\n // values in the array. Aliased as `head` and `take`. The **guard** check\n // allows it to work with `_.map`.\n _.first = _.head = _.take = function(array, n, guard) {\n if (array == null) return void 0;\n return (n != null) && !guard ? slice.call(array, 0, n) : array[0];\n };\n\n // Returns everything but the last entry of the array. Especially useful on\n // the arguments object. Passing **n** will return all the values in\n // the array, excluding the last N. The **guard** check allows it to work with\n // `_.map`.\n _.initial = function(array, n, guard) {\n return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));\n };\n\n // Get the last element of an array. Passing **n** will return the last N\n // values in the array. The **guard** check allows it to work with `_.map`.\n _.last = function(array, n, guard) {\n if (array == null) return void 0;\n if ((n != null) && !guard) {\n return slice.call(array, Math.max(array.length - n, 0));\n } else {\n return array[array.length - 1];\n }\n };\n\n // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.\n // Especially useful on the arguments object. Passing an **n** will return\n // the rest N values in the array. The **guard**\n // check allows it to work with `_.map`.\n _.rest = _.tail = _.drop = function(array, n, guard) {\n return slice.call(array, (n == null) || guard ? 1 : n);\n };\n\n // Trim out all falsy values from an array.\n _.compact = function(array) {\n return _.filter(array, _.identity);\n };\n\n // Internal implementation of a recursive `flatten` function.\n var flatten = function(input, shallow, output) {\n each(input, function(value) {\n if (_.isArray(value)) {\n shallow ? push.apply(output, value) : flatten(value, shallow, output);\n } else {\n output.push(value);\n }\n });\n return output;\n };\n\n // Return a completely flattened version of an array.\n _.flatten = function(array, shallow) {\n return flatten(array, shallow, []);\n };\n\n // Return a version of the array that does not contain the specified value(s).\n _.without = function(array) {\n return _.difference(array, slice.call(arguments, 1));\n };\n\n // Produce a duplicate-free version of the array. If the array has already\n // been sorted, you have the option of using a faster algorithm.\n // Aliased as `unique`.\n _.uniq = _.unique = function(array, isSorted, iterator, context) {\n if (_.isFunction(isSorted)) {\n context = iterator;\n iterator = isSorted;\n isSorted = false;\n }\n var initial = iterator ? _.map(array, iterator, context) : array;\n var results = [];\n var seen = [];\n each(initial, function(value, index) {\n if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {\n seen.push(value);\n results.push(array[index]);\n }\n });\n return results;\n };\n\n // Produce an array that contains the union: each distinct element from all of\n // the passed-in arrays.\n _.union = function() {\n return _.uniq(concat.apply(ArrayProto, arguments));\n };\n\n // Produce an array that contains every item shared between all the\n // passed-in arrays.\n _.intersection = function(array) {\n var rest = slice.call(arguments, 1);\n return _.filter(_.uniq(array), function(item) {\n return _.every(rest, function(other) {\n return _.indexOf(other, item) >= 0;\n });\n });\n };\n\n // Take the difference between one array and a number of other arrays.\n // Only the elements present in just the first array will remain.\n _.difference = function(array) {\n var rest = concat.apply(ArrayProto, slice.call(arguments, 1));\n return _.filter(array, function(value){ return !_.contains(rest, value); });\n };\n\n // Zip together multiple lists into a single array -- elements that share\n // an index go together.\n _.zip = function() {\n var args = slice.call(arguments);\n var length = _.max(_.pluck(args, 'length'));\n var results = new Array(length);\n for (var i = 0; i < length; i++) {\n results[i] = _.pluck(args, \"\" + i);\n }\n return results;\n };\n\n // Converts lists into objects. Pass either a single array of `[key, value]`\n // pairs, or two parallel arrays of the same length -- one of keys, and one of\n // the corresponding values.\n _.object = function(list, values) {\n if (list == null) return {};\n var result = {};\n for (var i = 0, l = list.length; i < l; i++) {\n if (values) {\n result[list[i]] = values[i];\n } else {\n result[list[i][0]] = list[i][1];\n }\n }\n return result;\n };\n\n // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),\n // we need this function. Return the position of the first occurrence of an\n // item in an array, or -1 if the item is not included in the array.\n // Delegates to **ECMAScript 5**'s native `indexOf` if available.\n // If the array is large and already in sort order, pass `true`\n // for **isSorted** to use binary search.\n _.indexOf = function(array, item, isSorted) {\n if (array == null) return -1;\n var i = 0, l = array.length;\n if (isSorted) {\n if (typeof isSorted == 'number') {\n i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);\n } else {\n i = _.sortedIndex(array, item);\n return array[i] === item ? i : -1;\n }\n }\n if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);\n for (; i < l; i++) if (array[i] === item) return i;\n return -1;\n };\n\n // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.\n _.lastIndexOf = function(array, item, from) {\n if (array == null) return -1;\n var hasIndex = from != null;\n if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {\n return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);\n }\n var i = (hasIndex ? from : array.length);\n while (i--) if (array[i] === item) return i;\n return -1;\n };\n\n // Generate an integer Array containing an arithmetic progression. A port of\n // the native Python `range()` function. See\n // [the Python documentation](http://docs.python.org/library/functions.html#range).\n _.range = function(start, stop, step) {\n if (arguments.length <= 1) {\n stop = start || 0;\n start = 0;\n }\n step = arguments[2] || 1;\n\n var len = Math.max(Math.ceil((stop - start) / step), 0);\n var idx = 0;\n var range = new Array(len);\n\n while(idx < len) {\n range[idx++] = start;\n start += step;\n }\n\n return range;\n };\n\n // Function (ahem) Functions\n // ------------------\n\n // Create a function bound to a given object (assigning `this`, and arguments,\n // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if\n // available.\n _.bind = function(func, context) {\n if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n var args = slice.call(arguments, 2);\n return function() {\n return func.apply(context, args.concat(slice.call(arguments)));\n };\n };\n\n // Partially apply a function by creating a version that has had some of its\n // arguments pre-filled, without changing its dynamic `this` context.\n _.partial = function(func) {\n var args = slice.call(arguments, 1);\n return function() {\n return func.apply(this, args.concat(slice.call(arguments)));\n };\n };\n\n // Bind all of an object's methods to that object. Useful for ensuring that\n // all callbacks defined on an object belong to it.\n _.bindAll = function(obj) {\n var funcs = slice.call(arguments, 1);\n if (funcs.length === 0) funcs = _.functions(obj);\n each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });\n return obj;\n };\n\n // Memoize an expensive function by storing its results.\n _.memoize = function(func, hasher) {\n var memo = {};\n hasher || (hasher = _.identity);\n return function() {\n var key = hasher.apply(this, arguments);\n return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));\n };\n };\n\n // Delays a function for the given number of milliseconds, and then calls\n // it with the arguments supplied.\n _.delay = function(func, wait) {\n var args = slice.call(arguments, 2);\n return setTimeout(function(){ return func.apply(null, args); }, wait);\n };\n\n // Defers a function, scheduling it to run after the current call stack has\n // cleared.\n _.defer = function(func) {\n return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));\n };\n\n // Returns a function, that, when invoked, will only be triggered at most once\n // during a given window of time.\n _.throttle = function(func, wait) {\n var context, args, timeout, result;\n var previous = 0;\n var later = function() {\n previous = new Date;\n timeout = null;\n result = func.apply(context, args);\n };\n return function() {\n var now = new Date;\n var remaining = wait - (now - previous);\n context = this;\n args = arguments;\n if (remaining <= 0) {\n clearTimeout(timeout);\n timeout = null;\n previous = now;\n result = func.apply(context, args);\n } else if (!timeout) {\n timeout = setTimeout(later, remaining);\n }\n return result;\n };\n };\n\n // Returns a function, that, as long as it continues to be invoked, will not\n // be triggered. The function will be called after it stops being called for\n // N milliseconds. If `immediate` is passed, trigger the function on the\n // leading edge, instead of the trailing.\n _.debounce = function(func, wait, immediate) {\n var timeout, result;\n return function() {\n var context = this, args = arguments;\n var later = function() {\n timeout = null;\n if (!immediate) result = func.apply(context, args);\n };\n var callNow = immediate && !timeout;\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n if (callNow) result = func.apply(context, args);\n return result;\n };\n };\n\n // Returns a function that will be executed at most one time, no matter how\n // often you call it. Useful for lazy initialization.\n _.once = function(func) {\n var ran = false, memo;\n return function() {\n if (ran) return memo;\n ran = true;\n memo = func.apply(this, arguments);\n func = null;\n return memo;\n };\n };\n\n // Returns the first function passed as an argument to the second,\n // allowing you to adjust arguments, run code before and after, and\n // conditionally execute the original function.\n _.wrap = function(func, wrapper) {\n return function() {\n var args = [func];\n push.apply(args, arguments);\n return wrapper.apply(this, args);\n };\n };\n\n // Returns a function that is the composition of a list of functions, each\n // consuming the return value of the function that follows.\n _.compose = function() {\n var funcs = arguments;\n return function() {\n var args = arguments;\n for (var i = funcs.length - 1; i >= 0; i--) {\n args = [funcs[i].apply(this, args)];\n }\n return args[0];\n };\n };\n\n // Returns a function that will only be executed after being called N times.\n _.after = function(times, func) {\n if (times <= 0) return func();\n return function() {\n if (--times < 1) {\n return func.apply(this, arguments);\n }\n };\n };\n\n // Object Functions\n // ----------------\n\n // Retrieve the names of an object's properties.\n // Delegates to **ECMAScript 5**'s native `Object.keys`\n _.keys = nativeKeys || function(obj) {\n if (obj !== Object(obj)) throw new TypeError('Invalid object');\n var keys = [];\n for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;\n return keys;\n };\n\n // Retrieve the values of an object's properties.\n _.values = function(obj) {\n var values = [];\n for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);\n return values;\n };\n\n // Convert an object into a list of `[key, value]` pairs.\n _.pairs = function(obj) {\n var pairs = [];\n for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);\n return pairs;\n };\n\n // Invert the keys and values of an object. The values must be serializable.\n _.invert = function(obj) {\n var result = {};\n for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;\n return result;\n };\n\n // Return a sorted list of the function names available on the object.\n // Aliased as `methods`\n _.functions = _.methods = function(obj) {\n var names = [];\n for (var key in obj) {\n if (_.isFunction(obj[key])) names.push(key);\n }\n return names.sort();\n };\n\n // Extend a given object with all the properties in passed-in object(s).\n _.extend = function(obj) {\n each(slice.call(arguments, 1), function(source) {\n if (source) {\n for (var prop in source) {\n obj[prop] = source[prop];\n }\n }\n });\n return obj;\n };\n\n // Return a copy of the object only containing the whitelisted properties.\n _.pick = function(obj) {\n var copy = {};\n var keys = concat.apply(ArrayProto, slice.call(arguments, 1));\n each(keys, function(key) {\n if (key in obj) copy[key] = obj[key];\n });\n return copy;\n };\n\n // Return a copy of the object without the blacklisted properties.\n _.omit = function(obj) {\n var copy = {};\n var keys = concat.apply(ArrayProto, slice.call(arguments, 1));\n for (var key in obj) {\n if (!_.contains(keys, key)) copy[key] = obj[key];\n }\n return copy;\n };\n\n // Fill in a given object with default properties.\n _.defaults = function(obj) {\n each(slice.call(arguments, 1), function(source) {\n if (source) {\n for (var prop in source) {\n if (obj[prop] == null) obj[prop] = source[prop];\n }\n }\n });\n return obj;\n };\n\n // Create a (shallow-cloned) duplicate of an object.\n _.clone = function(obj) {\n if (!_.isObject(obj)) return obj;\n return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n };\n\n // Invokes interceptor with the obj, and then returns obj.\n // The primary purpose of this method is to \"tap into\" a method chain, in\n // order to perform operations on intermediate results within the chain.\n _.tap = function(obj, interceptor) {\n interceptor(obj);\n return obj;\n };\n\n // Internal recursive comparison function for `isEqual`.\n var eq = function(a, b, aStack, bStack) {\n // Identical objects are equal. `0 === -0`, but they aren't identical.\n // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.\n if (a === b) return a !== 0 || 1 / a == 1 / b;\n // A strict comparison is necessary because `null == undefined`.\n if (a == null || b == null) return a === b;\n // Unwrap any wrapped objects.\n if (a instanceof _) a = a._wrapped;\n if (b instanceof _) b = b._wrapped;\n // Compare `[[Class]]` names.\n var className = toString.call(a);\n if (className != toString.call(b)) return false;\n switch (className) {\n // Strings, numbers, dates, and booleans are compared by value.\n case '[object String]':\n // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n // equivalent to `new String(\"5\")`.\n return a == String(b);\n case '[object Number]':\n // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for\n // other numeric values.\n return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);\n case '[object Date]':\n case '[object Boolean]':\n // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n // millisecond representations. Note that invalid dates with millisecond representations\n // of `NaN` are not equivalent.\n return +a == +b;\n // RegExps are compared by their source patterns and flags.\n case '[object RegExp]':\n return a.source == b.source &&\n a.global == b.global &&\n a.multiline == b.multiline &&\n a.ignoreCase == b.ignoreCase;\n }\n if (typeof a != 'object' || typeof b != 'object') return false;\n // Assume equality for cyclic structures. The algorithm for detecting cyclic\n // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n var length = aStack.length;\n while (length--) {\n // Linear search. Performance is inversely proportional to the number of\n // unique nested structures.\n if (aStack[length] == a) return bStack[length] == b;\n }\n // Add the first object to the stack of traversed objects.\n aStack.push(a);\n bStack.push(b);\n var size = 0, result = true;\n // Recursively compare objects and arrays.\n if (className == '[object Array]') {\n // Compare array lengths to determine if a deep comparison is necessary.\n size = a.length;\n result = size == b.length;\n if (result) {\n // Deep compare the contents, ignoring non-numeric properties.\n while (size--) {\n if (!(result = eq(a[size], b[size], aStack, bStack))) break;\n }\n }\n } else {\n // Objects with different constructors are not equivalent, but `Object`s\n // from different frames are.\n var aCtor = a.constructor, bCtor = b.constructor;\n if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&\n _.isFunction(bCtor) && (bCtor instanceof bCtor))) {\n return false;\n }\n // Deep compare objects.\n for (var key in a) {\n if (_.has(a, key)) {\n // Count the expected number of properties.\n size++;\n // Deep compare each member.\n if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;\n }\n }\n // Ensure that both objects contain the same number of properties.\n if (result) {\n for (key in b) {\n if (_.has(b, key) && !(size--)) break;\n }\n result = !size;\n }\n }\n // Remove the first object from the stack of traversed objects.\n aStack.pop();\n bStack.pop();\n return result;\n };\n\n // Perform a deep comparison to check if two objects are equal.\n _.isEqual = function(a, b) {\n return eq(a, b, [], []);\n };\n\n // Is a given array, string, or object empty?\n // An \"empty\" object has no enumerable own-properties.\n _.isEmpty = function(obj) {\n if (obj == null) return true;\n if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;\n for (var key in obj) if (_.has(obj, key)) return false;\n return true;\n };\n\n // Is a given value a DOM element?\n _.isElement = function(obj) {\n return !!(obj && obj.nodeType === 1);\n };\n\n // Is a given value an array?\n // Delegates to ECMA5's native Array.isArray\n _.isArray = nativeIsArray || function(obj) {\n return toString.call(obj) == '[object Array]';\n };\n\n // Is a given variable an object?\n _.isObject = function(obj) {\n return obj === Object(obj);\n };\n\n // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.\n each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {\n _['is' + name] = function(obj) {\n return toString.call(obj) == '[object ' + name + ']';\n };\n });\n\n // Define a fallback version of the method in browsers (ahem, IE), where\n // there isn't any inspectable \"Arguments\" type.\n if (!_.isArguments(arguments)) {\n _.isArguments = function(obj) {\n return !!(obj && _.has(obj, 'callee'));\n };\n }\n\n // Optimize `isFunction` if appropriate.\n if (typeof (/./) !== 'function') {\n _.isFunction = function(obj) {\n return typeof obj === 'function';\n };\n }\n\n // Is a given object a finite number?\n _.isFinite = function(obj) {\n return isFinite(obj) && !isNaN(parseFloat(obj));\n };\n\n // Is the given value `NaN`? (NaN is the only number which does not equal itself).\n _.isNaN = function(obj) {\n return _.isNumber(obj) && obj != +obj;\n };\n\n // Is a given value a boolean?\n _.isBoolean = function(obj) {\n return obj === true || obj === false || toString.call(obj) == '[object Boolean]';\n };\n\n // Is a given value equal to null?\n _.isNull = function(obj) {\n return obj === null;\n };\n\n // Is a given variable undefined?\n _.isUndefined = function(obj) {\n return obj === void 0;\n };\n\n // Shortcut function for checking if an object has a given property directly\n // on itself (in other words, not on a prototype).\n _.has = function(obj, key) {\n return hasOwnProperty.call(obj, key);\n };\n\n // Utility Functions\n // -----------------\n\n // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n // previous owner. Returns a reference to the Underscore object.\n _.noConflict = function() {\n root._ = previousUnderscore;\n return this;\n };\n\n // Keep the identity function around for default iterators.\n _.identity = function(value) {\n return value;\n };\n\n // Run a function **n** times.\n _.times = function(n, iterator, context) {\n var accum = Array(n);\n for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);\n return accum;\n };\n\n // Return a random integer between min and max (inclusive).\n _.random = function(min, max) {\n if (max == null) {\n max = min;\n min = 0;\n }\n return min + Math.floor(Math.random() * (max - min + 1));\n };\n\n // List of HTML entities for escaping.\n var entityMap = {\n escape: {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n '/': '/'\n }\n };\n entityMap.unescape = _.invert(entityMap.escape);\n\n // Regexes containing the keys and values listed immediately above.\n var entityRegexes = {\n escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),\n unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')\n };\n\n // Functions for escaping and unescaping strings to/from HTML interpolation.\n _.each(['escape', 'unescape'], function(method) {\n _[method] = function(string) {\n if (string == null) return '';\n return ('' + string).replace(entityRegexes[method], function(match) {\n return entityMap[method][match];\n });\n };\n });\n\n // If the value of the named property is a function then invoke it;\n // otherwise, return it.\n _.result = function(object, property) {\n if (object == null) return null;\n var value = object[property];\n return _.isFunction(value) ? value.call(object) : value;\n };\n\n // Add your own custom functions to the Underscore object.\n _.mixin = function(obj) {\n each(_.functions(obj), function(name){\n var func = _[name] = obj[name];\n _.prototype[name] = function() {\n var args = [this._wrapped];\n push.apply(args, arguments);\n return result.call(this, func.apply(_, args));\n };\n });\n };\n\n // Generate a unique integer id (unique within the entire client session).\n // Useful for temporary DOM ids.\n var idCounter = 0;\n _.uniqueId = function(prefix) {\n var id = ++idCounter + '';\n return prefix ? prefix + id : id;\n };\n\n // By default, Underscore uses ERB-style template delimiters, change the\n // following template settings to use alternative delimiters.\n _.templateSettings = {\n evaluate : /<%([\\s\\S]+?)%>/g,\n interpolate : /<%=([\\s\\S]+?)%>/g,\n escape : /<%-([\\s\\S]+?)%>/g\n };\n\n // When customizing `templateSettings`, if you don't want to define an\n // interpolation, evaluation or escaping regex, we need one that is\n // guaranteed not to match.\n var noMatch = /(.)^/;\n\n // Certain characters need to be escaped so that they can be put into a\n // string literal.\n var escapes = {\n \"'\": \"'\",\n '\\\\': '\\\\',\n '\\r': 'r',\n '\\n': 'n',\n '\\t': 't',\n '\\u2028': 'u2028',\n '\\u2029': 'u2029'\n };\n\n var escaper = /\\\\|'|\\r|\\n|\\t|\\u2028|\\u2029/g;\n\n // JavaScript micro-templating, similar to John Resig's implementation.\n // Underscore templating handles arbitrary delimiters, preserves whitespace,\n // and correctly escapes quotes within interpolated code.\n _.template = function(text, data, settings) {\n var render;\n settings = _.defaults({}, settings, _.templateSettings);\n\n // Combine delimiters into one regular expression via alternation.\n var matcher = new RegExp([\n (settings.escape || noMatch).source,\n (settings.interpolate || noMatch).source,\n (settings.evaluate || noMatch).source\n ].join('|') + '|$', 'g');\n\n // Compile the template source, escaping string literals appropriately.\n var index = 0;\n var source = \"__p+='\";\n text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {\n source += text.slice(index, offset)\n .replace(escaper, function(match) { return '\\\\' + escapes[match]; });\n\n if (escape) {\n source += \"'+\\n((__t=(\" + escape + \"))==null?'':_.escape(__t))+\\n'\";\n }\n if (interpolate) {\n source += \"'+\\n((__t=(\" + interpolate + \"))==null?'':__t)+\\n'\";\n }\n if (evaluate) {\n source += \"';\\n\" + evaluate + \"\\n__p+='\";\n }\n index = offset + match.length;\n return match;\n });\n source += \"';\\n\";\n\n // If a variable is not specified, place data values in local scope.\n if (!settings.variable) source = 'with(obj||{}){\\n' + source + '}\\n';\n\n source = \"var __t,__p='',__j=Array.prototype.join,\" +\n \"print=function(){__p+=__j.call(arguments,'');};\\n\" +\n source + \"return __p;\\n\";\n\n try {\n render = new Function(settings.variable || 'obj', '_', source);\n } catch (e) {\n e.source = source;\n throw e;\n }\n\n if (data) return render(data, _);\n var template = function(data) {\n return render.call(this, data, _);\n };\n\n // Provide the compiled function source as a convenience for precompilation.\n template.source = 'function(' + (settings.variable || 'obj') + '){\\n' + source + '}';\n\n return template;\n };\n\n // Add a \"chain\" function, which will delegate to the wrapper.\n _.chain = function(obj) {\n return _(obj).chain();\n };\n\n // OOP\n // ---------------\n // If Underscore is called as a function, it returns a wrapped object that\n // can be used OO-style. This wrapper holds altered versions of all the\n // underscore functions. Wrapped objects may be chained.\n\n // Helper function to continue chaining intermediate results.\n var result = function(obj) {\n return this._chain ? _(obj).chain() : obj;\n };\n\n // Add all of the Underscore functions to the wrapper object.\n _.mixin(_);\n\n // Add all mutator Array functions to the wrapper.\n each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n var obj = this._wrapped;\n method.apply(obj, arguments);\n if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];\n return result.call(this, obj);\n };\n });\n\n // Add all accessor Array functions to the wrapper.\n each(['concat', 'join', 'slice'], function(name) {\n var method = ArrayProto[name];\n _.prototype[name] = function() {\n return result.call(this, method.apply(this._wrapped, arguments));\n };\n });\n\n _.extend(_.prototype, {\n\n // Start chaining a wrapped Underscore object.\n chain: function() {\n this._chain = true;\n return this;\n },\n\n // Extracts the result from a wrapped and chained object.\n value: function() {\n return this._wrapped;\n }\n\n });\n\n}).call(this);\n\n//@ sourceURL=/node_modules/underscore/underscore.js")); -require.define("events",Function(["require","module","exports","__dirname","__filename","process","global"],"if (!process.EventEmitter) process.EventEmitter = function () {};\n\nvar EventEmitter = exports.EventEmitter = process.EventEmitter;\nvar isArray = typeof Array.isArray === 'function'\n ? Array.isArray\n : function (xs) {\n return Object.prototype.toString.call(xs) === '[object Array]'\n }\n;\n\n// By default EventEmitters will print a warning if more than\n// 10 listeners are added to it. This is a useful default which\n// helps finding memory leaks.\n//\n// Obviously not all Emitters should be limited to 10. This function allows\n// that to be increased. Set to zero for unlimited.\nvar defaultMaxListeners = 10;\nEventEmitter.prototype.setMaxListeners = function(n) {\n if (!this._events) this._events = {};\n this._events.maxListeners = n;\n};\n\n\nEventEmitter.prototype.emit = function(type) {\n // If there is no 'error' event listener then throw.\n if (type === 'error') {\n if (!this._events || !this._events.error ||\n (isArray(this._events.error) && !this._events.error.length))\n {\n if (arguments[1] instanceof Error) {\n throw arguments[1]; // Unhandled 'error' event\n } else {\n throw new Error(\"Uncaught, unspecified 'error' event.\");\n }\n return false;\n }\n }\n\n if (!this._events) return false;\n var handler = this._events[type];\n if (!handler) return false;\n\n if (typeof handler == 'function') {\n switch (arguments.length) {\n // fast cases\n case 1:\n handler.call(this);\n break;\n case 2:\n handler.call(this, arguments[1]);\n break;\n case 3:\n handler.call(this, arguments[1], arguments[2]);\n break;\n // slower\n default:\n var args = Array.prototype.slice.call(arguments, 1);\n handler.apply(this, args);\n }\n return true;\n\n } else if (isArray(handler)) {\n var args = Array.prototype.slice.call(arguments, 1);\n\n var listeners = handler.slice();\n for (var i = 0, l = listeners.length; i < l; i++) {\n listeners[i].apply(this, args);\n }\n return true;\n\n } else {\n return false;\n }\n};\n\n// EventEmitter is defined in src/node_events.cc\n// EventEmitter.prototype.emit() is also defined there.\nEventEmitter.prototype.addListener = function(type, listener) {\n if ('function' !== typeof listener) {\n throw new Error('addListener only takes instances of Function');\n }\n\n if (!this._events) this._events = {};\n\n // To avoid recursion in the case that type == \"newListeners\"! Before\n // adding it to the listeners, first emit \"newListeners\".\n this.emit('newListener', type, listener);\n\n if (!this._events[type]) {\n // Optimize the case of one listener. Don't need the extra array object.\n this._events[type] = listener;\n } else if (isArray(this._events[type])) {\n\n // Check for listener leak\n if (!this._events[type].warned) {\n var m;\n if (this._events.maxListeners !== undefined) {\n m = this._events.maxListeners;\n } else {\n m = defaultMaxListeners;\n }\n\n if (m && m > 0 && this._events[type].length > m) {\n this._events[type].warned = true;\n console.error('(node) warning: possible EventEmitter memory ' +\n 'leak detected. %d listeners added. ' +\n 'Use emitter.setMaxListeners() to increase limit.',\n this._events[type].length);\n console.trace();\n }\n }\n\n // If we've already got an array, just append.\n this._events[type].push(listener);\n } else {\n // Adding the second element, need to change to array.\n this._events[type] = [this._events[type], listener];\n }\n\n return this;\n};\n\nEventEmitter.prototype.on = EventEmitter.prototype.addListener;\n\nEventEmitter.prototype.once = function(type, listener) {\n var self = this;\n self.on(type, function g() {\n self.removeListener(type, g);\n listener.apply(this, arguments);\n });\n\n return this;\n};\n\nEventEmitter.prototype.removeListener = function(type, listener) {\n if ('function' !== typeof listener) {\n throw new Error('removeListener only takes instances of Function');\n }\n\n // does not use listeners(), so no side effect of creating _events[type]\n if (!this._events || !this._events[type]) return this;\n\n var list = this._events[type];\n\n if (isArray(list)) {\n var i = list.indexOf(listener);\n if (i < 0) return this;\n list.splice(i, 1);\n if (list.length == 0)\n delete this._events[type];\n } else if (this._events[type] === listener) {\n delete this._events[type];\n }\n\n return this;\n};\n\nEventEmitter.prototype.removeAllListeners = function(type) {\n // does not use listeners(), so no side effect of creating _events[type]\n if (type && this._events && this._events[type]) this._events[type] = null;\n return this;\n};\n\nEventEmitter.prototype.listeners = function(type) {\n if (!this._events) this._events = {};\n if (!this._events[type]) this._events[type] = [];\n if (!isArray(this._events[type])) {\n this._events[type] = [this._events[type]];\n }\n return this._events[type];\n};\n\n//@ sourceURL=events")); -require.define("/src/shared/vars.js",Function(["require","module","exports","__dirname","__filename","process","global"],'// jshint -W001\n\n"use strict";\n\n// Identifiers provided by the ECMAScript standard.\n\nexports.reservedVars = {\n arguments : false,\n NaN : false\n};\n\nexports.ecmaIdentifiers = {\n Array : false,\n Boolean : false,\n Date : false,\n decodeURI : false,\n decodeURIComponent : false,\n encodeURI : false,\n encodeURIComponent : false,\n Error : false,\n "eval" : false,\n EvalError : false,\n Function : false,\n hasOwnProperty : false,\n isFinite : false,\n isNaN : false,\n JSON : false,\n Math : false,\n Map : false,\n Number : false,\n Object : false,\n parseInt : false,\n parseFloat : false,\n RangeError : false,\n ReferenceError : false,\n RegExp : false,\n Set : false,\n String : false,\n SyntaxError : false,\n TypeError : false,\n URIError : false,\n WeakMap : false\n};\n\n// Global variables commonly provided by a web browser environment.\n\nexports.browser = {\n ArrayBuffer : false,\n ArrayBufferView : false,\n Audio : false,\n Blob : false,\n addEventListener : false,\n applicationCache : false,\n atob : false,\n blur : false,\n btoa : false,\n clearInterval : false,\n clearTimeout : false,\n close : false,\n closed : false,\n DataView : false,\n DOMParser : false,\n defaultStatus : false,\n document : false,\n Element : false,\n event : false,\n FileReader : false,\n Float32Array : false,\n Float64Array : false,\n FormData : false,\n focus : false,\n frames : false,\n getComputedStyle : false,\n HTMLElement : false,\n HTMLAnchorElement : false,\n HTMLBaseElement : false,\n HTMLBlockquoteElement: false,\n HTMLBodyElement : false,\n HTMLBRElement : false,\n HTMLButtonElement : false,\n HTMLCanvasElement : false,\n HTMLDirectoryElement : false,\n HTMLDivElement : false,\n HTMLDListElement : false,\n HTMLFieldSetElement : false,\n HTMLFontElement : false,\n HTMLFormElement : false,\n HTMLFrameElement : false,\n HTMLFrameSetElement : false,\n HTMLHeadElement : false,\n HTMLHeadingElement : false,\n HTMLHRElement : false,\n HTMLHtmlElement : false,\n HTMLIFrameElement : false,\n HTMLImageElement : false,\n HTMLInputElement : false,\n HTMLIsIndexElement : false,\n HTMLLabelElement : false,\n HTMLLayerElement : false,\n HTMLLegendElement : false,\n HTMLLIElement : false,\n HTMLLinkElement : false,\n HTMLMapElement : false,\n HTMLMenuElement : false,\n HTMLMetaElement : false,\n HTMLModElement : false,\n HTMLObjectElement : false,\n HTMLOListElement : false,\n HTMLOptGroupElement : false,\n HTMLOptionElement : false,\n HTMLParagraphElement : false,\n HTMLParamElement : false,\n HTMLPreElement : false,\n HTMLQuoteElement : false,\n HTMLScriptElement : false,\n HTMLSelectElement : false,\n HTMLStyleElement : false,\n HTMLTableCaptionElement: false,\n HTMLTableCellElement : false,\n HTMLTableColElement : false,\n HTMLTableElement : false,\n HTMLTableRowElement : false,\n HTMLTableSectionElement: false,\n HTMLTextAreaElement : false,\n HTMLTitleElement : false,\n HTMLUListElement : false,\n HTMLVideoElement : false,\n history : false,\n Int16Array : false,\n Int32Array : false,\n Int8Array : false,\n Image : false,\n length : false,\n localStorage : false,\n location : false,\n MessageChannel : false,\n MessageEvent : false,\n MessagePort : false,\n moveBy : false,\n moveTo : false,\n MutationObserver : false,\n name : false,\n Node : false,\n NodeFilter : false,\n navigator : false,\n onbeforeunload : true,\n onblur : true,\n onerror : true,\n onfocus : true,\n onload : true,\n onresize : true,\n onunload : true,\n open : false,\n openDatabase : false,\n opener : false,\n Option : false,\n parent : false,\n print : false,\n removeEventListener : false,\n resizeBy : false,\n resizeTo : false,\n screen : false,\n scroll : false,\n scrollBy : false,\n scrollTo : false,\n sessionStorage : false,\n setInterval : false,\n setTimeout : false,\n SharedWorker : false,\n status : false,\n top : false,\n Uint16Array : false,\n Uint32Array : false,\n Uint8Array : false,\n Uint8ClampedArray : false,\n WebSocket : false,\n window : false,\n Worker : false,\n XMLHttpRequest : false,\n XMLSerializer : false,\n XPathEvaluator : false,\n XPathException : false,\n XPathExpression : false,\n XPathNamespace : false,\n XPathNSResolver : false,\n XPathResult : false\n};\n\nexports.devel = {\n alert : false,\n confirm: false,\n console: false,\n Debug : false,\n opera : false,\n prompt : false\n};\n\nexports.worker = {\n importScripts: true,\n postMessage : true,\n self : true\n};\n\n// Widely adopted global names that are not part of ECMAScript standard\nexports.nonstandard = {\n escape : false,\n unescape: false\n};\n\n// Globals provided by popular JavaScript environments.\n\nexports.couch = {\n "require" : false,\n respond : false,\n getRow : false,\n emit : false,\n send : false,\n start : false,\n sum : false,\n log : false,\n exports : false,\n module : false,\n provides : false\n};\n\nexports.node = {\n __filename : false,\n __dirname : false,\n Buffer : false,\n DataView : false,\n console : false,\n exports : true, // In Node it is ok to exports = module.exports = foo();\n GLOBAL : false,\n global : false,\n module : false,\n process : false,\n require : false,\n setTimeout : false,\n clearTimeout : false,\n setInterval : false,\n clearInterval: false\n};\n\nexports.phantom = {\n phantom : true,\n require : true,\n WebPage : true\n};\n\nexports.rhino = {\n defineClass : false,\n deserialize : false,\n gc : false,\n help : false,\n importPackage: false,\n "java" : false,\n load : false,\n loadClass : false,\n print : false,\n quit : false,\n readFile : false,\n readUrl : false,\n runCommand : false,\n seal : false,\n serialize : false,\n spawn : false,\n sync : false,\n toint32 : false,\n version : false\n};\n\nexports.wsh = {\n ActiveXObject : true,\n Enumerator : true,\n GetObject : true,\n ScriptEngine : true,\n ScriptEngineBuildVersion : true,\n ScriptEngineMajorVersion : true,\n ScriptEngineMinorVersion : true,\n VBArray : true,\n WSH : true,\n WScript : true,\n XDomainRequest : true\n};\n\n// Globals provided by popular JavaScript libraries.\n\nexports.dojo = {\n dojo : false,\n dijit : false,\n dojox : false,\n define : false,\n "require": false\n};\n\nexports.jquery = {\n "$" : false,\n jQuery : false\n};\n\nexports.mootools = {\n "$" : false,\n "$$" : false,\n Asset : false,\n Browser : false,\n Chain : false,\n Class : false,\n Color : false,\n Cookie : false,\n Core : false,\n Document : false,\n DomReady : false,\n DOMEvent : false,\n DOMReady : false,\n Drag : false,\n Element : false,\n Elements : false,\n Event : false,\n Events : false,\n Fx : false,\n Group : false,\n Hash : false,\n HtmlTable : false,\n Iframe : false,\n IframeShim : false,\n InputValidator: false,\n instanceOf : false,\n Keyboard : false,\n Locale : false,\n Mask : false,\n MooTools : false,\n Native : false,\n Options : false,\n OverText : false,\n Request : false,\n Scroller : false,\n Slick : false,\n Slider : false,\n Sortables : false,\n Spinner : false,\n Swiff : false,\n Tips : false,\n Type : false,\n typeOf : false,\n URI : false,\n Window : false\n};\n\nexports.prototypejs = {\n "$" : false,\n "$$" : false,\n "$A" : false,\n "$F" : false,\n "$H" : false,\n "$R" : false,\n "$break" : false,\n "$continue" : false,\n "$w" : false,\n Abstract : false,\n Ajax : false,\n Class : false,\n Enumerable : false,\n Element : false,\n Event : false,\n Field : false,\n Form : false,\n Hash : false,\n Insertion : false,\n ObjectRange : false,\n PeriodicalExecuter: false,\n Position : false,\n Prototype : false,\n Selector : false,\n Template : false,\n Toggle : false,\n Try : false,\n Autocompleter : false,\n Builder : false,\n Control : false,\n Draggable : false,\n Draggables : false,\n Droppables : false,\n Effect : false,\n Sortable : false,\n SortableObserver : false,\n Sound : false,\n Scriptaculous : false\n};\n\nexports.yui = {\n YUI : false,\n Y : false,\n YUI_config: false\n};\n\n\n//@ sourceURL=/src/shared/vars.js')); -require.define("/src/shared/messages.js",Function(["require","module","exports","__dirname","__filename","process","global"],'"use strict";\n\nvar _ = require("underscore");\n\nvar errors = {\n // JSHint options\n E001: "Bad option: \'{a}\'.",\n E002: "Bad option value.",\n\n // JSHint input\n E003: "Expected a JSON value.",\n E004: "Input is neither a string nor an array of strings.",\n E005: "Input is empty.",\n E006: "Unexpected early end of program.",\n\n // Strict mode\n E007: "Missing \\"use strict\\" statement.",\n E008: "Strict violation.",\n E009: "Option \'validthis\' can\'t be used in a global scope.",\n E010: "\'with\' is not allowed in strict mode.",\n\n // Constants\n E011: "const \'{a}\' has already been declared.",\n E012: "const \'{a}\' is initialized to \'undefined\'.",\n E013: "Attempting to override \'{a}\' which is a constant.",\n\n // Regular expressions\n E014: "A regular expression literal can be confused with \'/=\'.",\n E015: "Unclosed regular expression.",\n E016: "Invalid regular expression.",\n\n // Tokens\n E017: "Unclosed comment.",\n E018: "Unbegun comment.",\n E019: "Unmatched \'{a}\'.",\n E020: "Expected \'{a}\' to match \'{b}\' from line {c} and instead saw \'{d}\'.",\n E021: "Expected \'{a}\' and instead saw \'{b}\'.",\n E022: "Line breaking error \'{a}\'.",\n E023: "Missing \'{a}\'.",\n E024: "Unexpected \'{a}\'.",\n E025: "Missing \':\' on a case clause.",\n E026: "Missing \'}\' to match \'{\' from line {a}.",\n E027: "Missing \']\' to match \'[\' form line {a}.",\n E028: "Illegal comma.",\n E029: "Unclosed string.",\n\n // Everything else\n E030: "Expected an identifier and instead saw \'{a}\'.",\n E031: "Bad assignment.", // FIXME: Rephrase\n E032: "Expected a small integer and instead saw \'{a}\'.",\n E033: "Expected an operator and instead saw \'{a}\'.",\n E034: "get/set are ES5 features.",\n E035: "Missing property name.",\n E036: "Expected to see a statement and instead saw a block.",\n E037: "Constant {a} was not declared correctly.",\n E038: "Variable {a} was not declared correctly.",\n E039: "Function declarations are not invocable. Wrap the whole function invocation in parens.",\n E040: "Each value should have its own case label.",\n E041: "Unrecoverable syntax error.",\n E042: "Stopping.",\n E043: "Too many errors."\n};\n\nvar warnings = {\n W001: "\'hasOwnProperty\' is a really bad name.",\n W002: "Value of \'{a}\' may be overwritten in IE.",\n W003: "\'{a}\' was used before it was defined.",\n W004: "\'{a}\' is already defined.",\n W005: "A dot following a number can be confused with a decimal point.",\n W006: "Confusing minuses.",\n W007: "Confusing pluses.",\n W008: "A leading decimal point can be confused with a dot: \'{a}\'.",\n W009: "The array literal notation [] is preferrable.",\n W010: "The object literal notation {} is preferrable.",\n W011: "Unexpected space after \'{a}\'.",\n W012: "Unexpected space before \'{a}\'.",\n W013: "Missing space after \'{a}\'.",\n W014: "Bad line breaking before \'{a}\'.",\n W015: "Expected \'{a}\' to have an indentation at {b} instead at {c}.",\n W016: "Unexpected use of \'{a}\'.",\n W017: "Bad operand.",\n W018: "Confusing use of \'{a}\'.",\n W019: "Use the isNaN function to compare with NaN.",\n W020: "Read only.",\n W021: "\'{a}\' is a function.",\n W022: "Do not assign to the exception parameter.",\n W023: "Expected an identifier in an assignment and instead saw a function invocation.",\n W024: "Expected an identifier and instead saw \'{a}\' (a reserved word).",\n W025: "Missing name in function declaration.",\n W026: "Inner functions should be listed at the top of the outer function.",\n W027: "Unreachable \'{a}\' after \'{b}\'.",\n W028: "Label \'{a}\' on {b} statement.",\n W029: "Label \'{a}\' looks like a javascript url.",\n W030: "Expected an assignment or function call and instead saw an expression.",\n W031: "Do not use \'new\' for side effects.",\n W032: "Unnecessary semicolon.",\n W033: "Missing semicolon.",\n W034: "Unnecessary directive \\"{a}\\".",\n W035: "Empty block.",\n W036: "Unexpected /*member \'{a}\'.",\n W037: "\'{a}\' is a statement label.",\n W038: "\'{a}\' used out of scope.",\n W039: "\'{a}\' is not allowed.",\n W040: "Possible strict violation.",\n W041: "Use \'{a}\' to compare with \'{b}\'.",\n W042: "Avoid EOL escaping.",\n W043: "Bad escaping of EOL. Use option multistr if needed.",\n W044: "Bad escaping.",\n W045: "Bad number \'{a}\'.",\n W046: "Don\'t use extra leading zeros \'{a}\'.",\n W047: "A trailing decimal point can be confused with a dot: \'{a}\'.",\n W048: "Unexpected control character in regular expression.",\n W049: "Unexpected escaped character \'{a}\' in regular expression.",\n W050: "JavaScript URL.",\n W051: "Variables should not be deleted.",\n W052: "Unexpected \'{a}\'.",\n W053: "Do not use {a} as a constructor.",\n W054: "The Function constructor is a form of eval.",\n W055: "A constructor name should start with an uppercase letter.",\n W056: "Bad constructor.",\n W057: "Weird construction. Is \'new\' unnecessary?",\n W058: "Missing \'()\' invoking a constructor.",\n W059: "Avoid arguments.{a}.",\n W060: "document.write can be a form of eval.",\n W061: "eval can be harmful.",\n W062: "Wrap an immediate function invocation in parens " +\n "to assist the reader in understanding that the expression " +\n "is the result of a function, and not the function itself.",\n W063: "Math is not a function.",\n W064: "Missing \'new\' prefix when invoking a constructor.",\n W065: "Missing radix parameter.",\n W066: "Implied eval. Consider passing a function instead of a string.",\n W067: "Bad invocation.",\n W068: "Wrapping non-IIFE function literals in parens is unnecessary.",\n W069: "[\'{a}\'] is better written in dot notation.",\n W070: "Extra comma. (it breaks older versions of IE)",\n W071: "This function has too many statements. ({a})",\n W072: "This function has too many parameters. ({a})",\n W073: "Blocks are nested too deeply. ({a})",\n W074: "This function\'s cyclomatic complexity is too high. ({a})",\n W075: "Duplicate key \'{a}\'.",\n W076: "Unexpected parameter \'{a}\' in get {b} function.",\n W077: "Expected a single parameter in set {a} function.",\n W078: "Setter is defined without getter.",\n W079: "Redefinition of \'{a}\'.",\n W080: "It\'s not necessary to initialize \'{a}\' to \'undefined\'.",\n W081: "Too many var statements.",\n W082: "Function declarations should not be placed in blocks. " +\n "Use a function expression or move the statement to the top of " +\n "the outer function.",\n W083: "Don\'t make functions within a loop.",\n W084: "Expected a conditional expression and instead saw an assignment.",\n W085: "Don\'t use \'with\'.",\n W086: "Expected a \'break\' statement before \'{a}\'.",\n W087: "Forgotten \'debugger\' statement?",\n W088: "Creating global \'for\' variable. Should be \'for (var {a} ...\'.",\n W089: "The body of a for in should be wrapped in an if statement to filter " +\n "unwanted properties from the prototype.",\n W090: "\'{a}\' is not a statement label.",\n W091: "\'{a}\' is out of scope.",\n W092: "Wrap the /regexp/ literal in parens to disambiguate the slash operator.",\n W093: "Did you mean to return a conditional instead of an assignment?",\n W094: "Unexpected comma.",\n W095: "Expected a string and instead saw {a}.",\n W096: "The \'{a}\' key may produce unexpected results.",\n W097: "Use the function form of \\"use strict\\".",\n W098: "\'{a}\' is defined but never used.",\n W099: "Mixed spaces and tabs.",\n W100: "This character may get silently deleted by one or more browsers.",\n W101: "Line is too long.",\n W102: "Trailing whitespace.",\n W103: "The \'{a}\' property is deprecated.",\n W104: "\'{a}\' is only available in JavaScript 1.7.",\n W105: "Unexpected {a} in \'{b}\'.",\n W106: "Identifier \'{a}\' is not in camel case.",\n W107: "Script URL.",\n W108: "Strings must use doublequote.",\n W109: "Strings must use singlequote.",\n W110: "Mixed double and single quotes.",\n W112: "Unclosed string.",\n W113: "Control character in string: {a}.",\n W114: "Avoid {a}.",\n W115: "Octal literals are not allowed in strict mode.",\n W116: "Expected \'{a}\' and instead saw \'{b}\'.",\n W117: "\'{a}\' is not defined.",\n};\n\nvar info = {\n I001: "Comma warnings can be turned off with \'laxcomma\'."\n};\n\nexports.errors = {};\nexports.warnings = {};\nexports.info = {};\n\n_.each(errors, function (desc, code) {\n exports.errors[code] = { code: code, desc: desc };\n});\n\n_.each(warnings, function (desc, code) {\n exports.warnings[code] = { code: code, desc: desc };\n});\n\n_.each(info, function (desc, code) {\n exports.info[code] = { code: code, desc: desc };\n});\n\n//@ sourceURL=/src/shared/messages.js')); -require.define("/src/stable/lex.js",Function(["require","module","exports","__dirname","__filename","process","global"],'/*\n * Lexical analysis and token construction.\n */\n\n"use strict";\n\nvar _ = require("underscore");\nvar events = require("events");\nvar reg = require("./reg.js");\nvar state = require("./state.js").state;\n\n// Some of these token types are from JavaScript Parser API\n// while others are specific to JSHint parser.\n// JS Parser API: https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API\n\nvar Token = {\n Identifier: 1,\n Punctuator: 2,\n NumericLiteral: 3,\n StringLiteral: 4,\n Comment: 5,\n Keyword: 6,\n NullLiteral: 7,\n BooleanLiteral: 8,\n RegExp: 9\n};\n\n// This is auto generated from the unicode tables.\n// The tables are at:\n// http://www.fileformat.info/info/unicode/category/Lu/list.htm\n// http://www.fileformat.info/info/unicode/category/Ll/list.htm\n// http://www.fileformat.info/info/unicode/category/Lt/list.htm\n// http://www.fileformat.info/info/unicode/category/Lm/list.htm\n// http://www.fileformat.info/info/unicode/category/Lo/list.htm\n// http://www.fileformat.info/info/unicode/category/Nl/list.htm\n\nvar unicodeLetterTable = [\n 170, 170, 181, 181, 186, 186, 192, 214,\n 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750,\n 880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908,\n 910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366,\n 1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610,\n 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775,\n 1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957,\n 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069,\n 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2308, 2361,\n 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431,\n 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482,\n 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,\n 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608,\n 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654,\n 2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736,\n 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785,\n 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867,\n 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,\n 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970,\n 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001,\n 3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123,\n 3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212,\n 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261,\n 3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344,\n 3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455,\n 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526,\n 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716,\n 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,\n 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760,\n 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805,\n 3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138,\n 4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198,\n 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4304, 4346,\n 4348, 4348, 4352, 4680, 4682, 4685, 4688, 4694, 4696, 4696,\n 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789,\n 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,\n 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,\n 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900,\n 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000,\n 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312,\n 6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516,\n 6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823,\n 6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7104, 7141,\n 7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409,\n 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013,\n 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061,\n 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140,\n 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188,\n 8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455,\n 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486,\n 8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,\n 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358,\n 11360, 11492, 11499, 11502, 11520, 11557, 11568, 11621,\n 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694,\n 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,\n 11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295,\n 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438,\n 12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589,\n 12593, 12686, 12704, 12730, 12784, 12799, 13312, 13312,\n 19893, 19893, 19968, 19968, 40907, 40907, 40960, 42124,\n 42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539,\n 42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783,\n 42786, 42888, 42891, 42894, 42896, 42897, 42912, 42921,\n 43002, 43009, 43011, 43013, 43015, 43018, 43020, 43042,\n 43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259,\n 43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442,\n 43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595,\n 43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697,\n 43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714,\n 43739, 43741, 43777, 43782, 43785, 43790, 43793, 43798,\n 43808, 43814, 43816, 43822, 43968, 44002, 44032, 44032,\n 55203, 55203, 55216, 55238, 55243, 55291, 63744, 64045,\n 64048, 64109, 64112, 64217, 64256, 64262, 64275, 64279,\n 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316,\n 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,\n 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,\n 65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370,\n 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495,\n 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594,\n 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786,\n 65856, 65908, 66176, 66204, 66208, 66256, 66304, 66334,\n 66352, 66378, 66432, 66461, 66464, 66499, 66504, 66511,\n 66513, 66517, 66560, 66717, 67584, 67589, 67592, 67592,\n 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669,\n 67840, 67861, 67872, 67897, 68096, 68096, 68112, 68115,\n 68117, 68119, 68121, 68147, 68192, 68220, 68352, 68405,\n 68416, 68437, 68448, 68466, 68608, 68680, 69635, 69687,\n 69763, 69807, 73728, 74606, 74752, 74850, 77824, 78894,\n 92160, 92728, 110592, 110593, 119808, 119892, 119894, 119964,\n 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980,\n 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069,\n 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121,\n 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144,\n 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570,\n 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686,\n 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779,\n 131072, 131072, 173782, 173782, 173824, 173824, 177972, 177972,\n 177984, 177984, 178205, 178205, 194560, 195101\n];\n\nvar identifierStartTable = [];\n\nfor (var i = 0; i < 128; i++) {\n identifierStartTable[i] =\n i === 36 || // $\n i >= 65 && i <= 90 || // A-Z\n i === 95 || // _\n i >= 97 && i <= 122; // a-z\n}\n\nvar identifierPartTable = [];\n\nfor (var i = 0; i < 128; i++) {\n identifierPartTable[i] =\n identifierStartTable[i] || // $, _, A-Z, a-z\n i >= 48 && i <= 57; // 0-9\n}\n\n/*\n * Lexer for JSHint.\n *\n * This object does a char-by-char scan of the provided source code\n * and produces a sequence of tokens.\n *\n * var lex = new Lexer("var i = 0;");\n * lex.start();\n * lex.token(); // returns the next token\n *\n * You have to use the token() method to move the lexer forward\n * but you don\'t have to use its return value to get tokens. In addition\n * to token() method returning the next token, the Lexer object also\n * emits events.\n *\n * lex.on("Identifier", function (data) {\n * if (data.name.indexOf("_") >= 0) {\n * // Produce a warning.\n * }\n * });\n *\n * Note that the token() method returns tokens in a JSLint-compatible\n * format while the event emitter uses a slightly modified version of\n * Mozilla\'s JavaScript Parser API. Eventually, we will move away from\n * JSLint format.\n */\nfunction Lexer(source) {\n var lines = source;\n\n if (typeof lines === "string") {\n lines = lines\n .replace(/\\r\\n/g, "\\n")\n .replace(/\\r/g, "\\n")\n .split("\\n");\n }\n\n // If the first line is a shebang (#!), make it a blank and move on.\n // Shebangs are used by Node scripts.\n\n if (lines[0] && lines[0].substr(0, 2) === "#!") {\n lines[0] = "";\n }\n\n this.emitter = new events.EventEmitter();\n this.source = source;\n this.lines = lines;\n this.prereg = true;\n\n this.line = 0;\n this.char = 1;\n this.from = 1;\n this.input = "";\n\n for (var i = 0; i < state.option.indent; i += 1) {\n state.tab += " ";\n }\n}\n\nLexer.prototype = {\n _lines: [],\n\n get lines() {\n this._lines = state.lines;\n return this._lines;\n },\n\n set lines(val) {\n this._lines = val;\n state.lines = this._lines;\n },\n\n /*\n * Return the next i character without actually moving the\n * char pointer.\n */\n peek: function (i) {\n return this.input.charAt(i || 0);\n },\n\n /*\n * Move the char pointer forward i times.\n */\n skip: function (i) {\n i = i || 1;\n this.char += i;\n this.input = this.input.slice(i);\n },\n\n /*\n * Subscribe to a token event. The API for this method is similar\n * Underscore.js i.e. you can subscribe to multiple events with\n * one call:\n *\n * lex.on("Identifier Number", function (data) {\n * // ...\n * });\n */\n on: function (names, listener) {\n names.split(" ").forEach(function (name) {\n this.emitter.on(name, listener);\n }.bind(this));\n },\n\n /*\n * Trigger a token event. All arguments will be passed to each\n * listener.\n */\n trigger: function () {\n this.emitter.emit.apply(this.emitter, Array.prototype.slice.call(arguments));\n },\n\n /*\n * Extract a punctuator out of the next sequence of characters\n * or return \'null\' if its not possible.\n *\n * This method\'s implementation was heavily influenced by the\n * scanPunctuator function in the Esprima parser\'s source code.\n */\n scanPunctuator: function () {\n var ch1 = this.peek();\n var ch2, ch3, ch4;\n\n switch (ch1) {\n // Most common single-character punctuators\n case ".":\n if ((/^[0-9]$/).test(this.peek(1))) {\n return null;\n }\n\n /* falls through */\n case "(":\n case ")":\n case ";":\n case ",":\n case "{":\n case "}":\n case "[":\n case "]":\n case ":":\n case "~":\n case "?":\n return {\n type: Token.Punctuator,\n value: ch1\n };\n\n // A pound sign (for Node shebangs)\n case "#":\n return {\n type: Token.Punctuator,\n value: ch1\n };\n\n // We\'re at the end of input\n case "":\n return null;\n }\n\n // Peek more characters\n\n ch2 = this.peek(1);\n ch3 = this.peek(2);\n ch4 = this.peek(3);\n\n // 4-character punctuator: >>>=\n\n if (ch1 === ">" && ch2 === ">" && ch3 === ">" && ch4 === "=") {\n return {\n type: Token.Punctuator,\n value: ">>>="\n };\n }\n\n // 3-character punctuators: === !== >>> <<= >>=\n\n if (ch1 === "=" && ch2 === "=" && ch3 === "=") {\n return {\n type: Token.Punctuator,\n value: "==="\n };\n }\n\n if (ch1 === "!" && ch2 === "=" && ch3 === "=") {\n return {\n type: Token.Punctuator,\n value: "!=="\n };\n }\n\n if (ch1 === ">" && ch2 === ">" && ch3 === ">") {\n return {\n type: Token.Punctuator,\n value: ">>>"\n };\n }\n\n if (ch1 === "<" && ch2 === "<" && ch3 === "=") {\n return {\n type: Token.Punctuator,\n value: "<<="\n };\n }\n\n if (ch1 === ">" && ch2 === ">" && ch3 === "=") {\n return {\n type: Token.Punctuator,\n value: "<<="\n };\n }\n\n // 2-character punctuators: <= >= == != ++ -- << >> && ||\n // += -= *= %= &= |= ^= (but not /=, see below)\n if (ch1 === ch2 && ("+-<>&|".indexOf(ch1) >= 0)) {\n return {\n type: Token.Punctuator,\n value: ch1 + ch2\n };\n }\n\n if ("<>=!+-*%&|^".indexOf(ch1) >= 0) {\n if (ch2 === "=") {\n return {\n type: Token.Punctuator,\n value: ch1 + ch2\n };\n }\n\n return {\n type: Token.Punctuator,\n value: ch1\n };\n }\n\n // Special case: /=. We need to make sure that this is an\n // operator and not a regular expression.\n\n if (ch1 === "/") {\n if (ch2 === "=" && /\\/=(?!(\\S*\\/[gim]?))/.test(this.input)) {\n // /= is not a part of a regular expression, return it as a\n // punctuator.\n return {\n type: Token.Punctuator,\n value: "/="\n };\n }\n\n return {\n type: Token.Punctuator,\n value: "/"\n };\n }\n\n return null;\n },\n\n /*\n * Extract a comment out of the next sequence of characters and/or\n * lines or return \'null\' if its not possible. Since comments can\n * span across multiple lines this method has to move the char\n * pointer.\n *\n * In addition to normal JavaScript comments (// and /*) this method\n * also recognizes JSHint- and JSLint-specific comments such as\n * /*jshint, /*jslint, /*globals and so on.\n */\n scanComments: function () {\n var ch1 = this.peek();\n var ch2 = this.peek(1);\n var rest = this.input.substr(2);\n var startLine = this.line;\n var startChar = this.char;\n\n // Create a comment token object and make sure it\n // has all the data JSHint needs to work with special\n // comments.\n\n function commentToken(label, body, opt) {\n var special = ["jshint", "jslint", "members", "member", "globals", "global", "exported"];\n var isSpecial = false;\n var value = label + body;\n var commentType = "plain";\n opt = opt || {};\n\n if (opt.isMultiline) {\n value += "*/";\n }\n\n special.forEach(function (str) {\n if (isSpecial) {\n return;\n }\n\n // Don\'t recognize any special comments other than jshint for single-line\n // comments. This introduced many problems with legit comments.\n if (label === "//" && str !== "jshint") {\n return;\n }\n\n if (body.substr(0, str.length) === str) {\n isSpecial = true;\n label = label + str;\n body = body.substr(str.length);\n }\n\n if (!isSpecial && body.charAt(0) === " " && body.substr(1, str.length) === str) {\n isSpecial = true;\n label = label + " " + str;\n body = body.substr(str.length + 1);\n }\n\n if (!isSpecial) {\n return;\n }\n\n switch (str) {\n case "member":\n commentType = "members";\n break;\n case "global":\n commentType = "globals";\n break;\n default:\n commentType = str;\n }\n });\n\n return {\n type: Token.Comment,\n commentType: commentType,\n value: value,\n body: body,\n isSpecial: isSpecial,\n isMultiline: opt.isMultiline || false,\n isMalformed: opt.isMalformed || false\n };\n }\n\n // End of unbegun comment. Raise an error and skip that input.\n if (ch1 === "*" && ch2 === "/") {\n this.trigger("error", {\n code: "E018",\n line: startLine,\n character: startChar\n });\n\n this.skip(2);\n return null;\n }\n\n // Comments must start either with // or /*\n if (ch1 !== "/" || (ch2 !== "*" && ch2 !== "/")) {\n return null;\n }\n\n // One-line comment\n if (ch2 === "/") {\n this.skip(this.input.length); // Skip to the EOL.\n return commentToken("//", rest);\n }\n\n var body = "";\n\n /* Multi-line comment */\n if (ch2 === "*") {\n this.skip(2);\n\n while (this.peek() !== "*" || this.peek(1) !== "/") {\n if (this.peek() === "") { // End of Line\n body += "\\n";\n\n // If we hit EOF and our comment is still unclosed,\n // trigger an error and end the comment implicitly.\n if (!this.nextLine()) {\n this.trigger("error", {\n code: "E017",\n line: startLine,\n character: startChar\n });\n\n return commentToken("/*", body, {\n isMultiline: true,\n isMalformed: true\n });\n }\n } else {\n body += this.peek();\n this.skip();\n }\n }\n\n this.skip(2);\n return commentToken("/*", body, { isMultiline: true });\n }\n },\n\n /*\n * Extract a keyword out of the next sequence of characters or\n * return \'null\' if its not possible.\n */\n scanKeyword: function () {\n var result = /^[a-zA-Z_$][a-zA-Z0-9_$]*/.exec(this.input);\n var keywords = [\n "if", "in", "do", "var", "for", "new",\n "try", "let", "this", "else", "case",\n "void", "with", "enum", "while", "break",\n "catch", "throw", "const", "yield", "class",\n "super", "return", "typeof", "delete",\n "switch", "export", "import", "default",\n "finally", "extends", "function", "continue",\n "debugger", "instanceof"\n ];\n\n if (result && keywords.indexOf(result[0]) >= 0) {\n return {\n type: Token.Keyword,\n value: result[0]\n };\n }\n\n return null;\n },\n\n /*\n * Extract a JavaScript identifier out of the next sequence of\n * characters or return \'null\' if its not possible. In addition,\n * to Identifier this method can also produce BooleanLiteral\n * (true/false) and NullLiteral (null).\n */\n scanIdentifier: function () {\n var id = "";\n var index = 0;\n var type, char;\n\n // Detects any character in the Unicode categories "Uppercase\n // letter (Lu)", "Lowercase letter (Ll)", "Titlecase letter\n // (Lt)", "Modifier letter (Lm)", "Other letter (Lo)", or\n // "Letter number (Nl)".\n //\n // Both approach and unicodeLetterTable were borrowed from\n // Google\'s Traceur.\n\n function isUnicodeLetter(code) {\n for (var i = 0; i < unicodeLetterTable.length;) {\n if (code < unicodeLetterTable[i++]) {\n return false;\n }\n\n if (code <= unicodeLetterTable[i++]) {\n return true;\n }\n }\n\n return false;\n }\n\n function isHexDigit(str) {\n return (/^[0-9a-fA-F]$/).test(str);\n }\n\n var readUnicodeEscapeSequence = function () {\n /*jshint validthis:true */\n index += 1;\n\n if (this.peek(index) !== "u") {\n return null;\n }\n\n var ch1 = this.peek(index + 1);\n var ch2 = this.peek(index + 2);\n var ch3 = this.peek(index + 3);\n var ch4 = this.peek(index + 4);\n var code;\n\n if (isHexDigit(ch1) && isHexDigit(ch2) && isHexDigit(ch3) && isHexDigit(ch4)) {\n code = parseInt(ch1 + ch2 + ch3 + ch4, 16);\n\n if (isUnicodeLetter(code)) {\n index += 5;\n return "\\\\u" + ch1 + ch2 + ch3 + ch4;\n }\n\n return null;\n }\n\n return null;\n }.bind(this);\n\n var getIdentifierStart = function () {\n /*jshint validthis:true */\n var chr = this.peek(index);\n var code = chr.charCodeAt(0);\n\n if (code === 92) {\n return readUnicodeEscapeSequence();\n }\n\n if (code < 128) {\n if (identifierStartTable[code]) {\n index += 1;\n return chr;\n }\n\n return null;\n }\n\n if (isUnicodeLetter(code)) {\n index += 1;\n return chr;\n }\n\n return null;\n }.bind(this);\n\n var getIdentifierPart = function () {\n /*jshint validthis:true */\n var chr = this.peek(index);\n var code = chr.charCodeAt(0);\n\n if (code === 92) {\n return readUnicodeEscapeSequence();\n }\n\n if (code < 128) {\n if (identifierPartTable[code]) {\n index += 1;\n return chr;\n }\n\n return null;\n }\n\n if (isUnicodeLetter(code)) {\n index += 1;\n return chr;\n }\n\n return null;\n }.bind(this);\n\n char = getIdentifierStart();\n if (char === null) {\n return null;\n }\n\n id = char;\n for (;;) {\n char = getIdentifierPart();\n\n if (char === null) {\n break;\n }\n\n id += char;\n }\n\n switch (id) {\n case "true":\n case "false":\n type = Token.BooleanLiteral;\n break;\n case "null":\n type = Token.NullLiteral;\n break;\n default:\n type = Token.Identifier;\n }\n\n return {\n type: type,\n value: id\n };\n },\n\n /*\n * Extract a numeric literal out of the next sequence of\n * characters or return \'null\' if its not possible. This method\n * supports all numeric literals described in section 7.8.3\n * of the EcmaScript 5 specification.\n *\n * This method\'s implementation was heavily influenced by the\n * scanNumericLiteral function in the Esprima parser\'s source code.\n */\n scanNumericLiteral: function () {\n var index = 0;\n var value = "";\n var length = this.input.length;\n var char = this.peek(index);\n var bad;\n\n function isDecimalDigit(str) {\n return (/^[0-9]$/).test(str);\n }\n\n function isOctalDigit(str) {\n return (/^[0-7]$/).test(str);\n }\n\n function isHexDigit(str) {\n return (/^[0-9a-fA-F]$/).test(str);\n }\n\n function isIdentifierStart(ch) {\n return (ch === "$") || (ch === "_") || (ch === "\\\\") ||\n (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");\n }\n\n // Numbers must start either with a decimal digit or a point.\n\n if (char !== "." && !isDecimalDigit(char)) {\n return null;\n }\n\n if (char !== ".") {\n value = this.peek(index);\n index += 1;\n char = this.peek(index);\n\n if (value === "0") {\n // Base-16 numbers.\n if (char === "x" || char === "X") {\n index += 1;\n value += char;\n\n while (index < length) {\n char = this.peek(index);\n if (!isHexDigit(char)) {\n break;\n }\n value += char;\n index += 1;\n }\n\n if (value.length <= 2) { // 0x\n return {\n type: Token.NumericLiteral,\n value: value,\n isMalformed: true\n };\n }\n\n if (index < length) {\n char = this.peek(index);\n if (isIdentifierStart(char)) {\n return null;\n }\n }\n\n return {\n type: Token.NumericLiteral,\n value: value,\n base: 16,\n isMalformed: false\n };\n }\n\n // Base-8 numbers.\n if (isOctalDigit(char)) {\n index += 1;\n value += char;\n bad = false;\n\n while (index < length) {\n char = this.peek(index);\n\n // Numbers like \'019\' (note the 9) are not valid octals\n // but we still parse them and mark as malformed.\n\n if (isDecimalDigit(char)) {\n bad = true;\n } else if (!isOctalDigit(char)) {\n break;\n }\n value += char;\n index += 1;\n }\n\n if (index < length) {\n char = this.peek(index);\n if (isIdentifierStart(char)) {\n return null;\n }\n }\n\n return {\n type: Token.NumericLiteral,\n value: value,\n base: 8,\n isMalformed: false\n };\n }\n\n // Decimal numbers that start with \'0\' such as \'09\' are illegal\n // but we still parse them and return as malformed.\n\n if (isDecimalDigit(char)) {\n index += 1;\n value += char;\n }\n }\n\n while (index < length) {\n char = this.peek(index);\n if (!isDecimalDigit(char)) {\n break;\n }\n value += char;\n index += 1;\n }\n }\n\n // Decimal digits.\n\n if (char === ".") {\n value += char;\n index += 1;\n\n while (index < length) {\n char = this.peek(index);\n if (!isDecimalDigit(char)) {\n break;\n }\n value += char;\n index += 1;\n }\n }\n\n // Exponent part.\n\n if (char === "e" || char === "E") {\n value += char;\n index += 1;\n char = this.peek(index);\n\n if (char === "+" || char === "-") {\n value += this.peek(index);\n index += 1;\n }\n\n char = this.peek(index);\n if (isDecimalDigit(char)) {\n value += char;\n index += 1;\n\n while (index < length) {\n char = this.peek(index);\n if (!isDecimalDigit(char)) {\n break;\n }\n value += char;\n index += 1;\n }\n } else {\n return null;\n }\n }\n\n if (index < length) {\n char = this.peek(index);\n if (isIdentifierStart(char)) {\n return null;\n }\n }\n\n return {\n type: Token.NumericLiteral,\n value: value,\n base: 10,\n isMalformed: !isFinite(value)\n };\n },\n\n /*\n * Extract a string out of the next sequence of characters and/or\n * lines or return \'null\' if its not possible. Since strings can\n * span across multiple lines this method has to move the char\n * pointer.\n *\n * This method recognizes pseudo-multiline JavaScript strings:\n *\n * var str = "hello\\\n * world";\n */\n scanStringLiteral: function () {\n var quote = this.peek();\n\n // String must start with a quote.\n if (quote !== "\\"" && quote !== "\'") {\n return null;\n }\n\n // In JSON strings must always use double quotes.\n if (state.jsonMode && quote !== "\\"") {\n this.trigger("warning", {\n code: "W108",\n line: this.line,\n character: this.char // +1?\n });\n }\n\n var value = "";\n var startLine = this.line;\n var startChar = this.char;\n var allowNewLine = false;\n\n this.skip();\n\n while (this.peek() !== quote) {\n while (this.peek() === "") { // End Of Line\n\n // If an EOL is not preceded by a backslash, show a warning\n // and proceed like it was a legit multi-line string where\n // author simply forgot to escape the newline symbol.\n //\n // Another approach is to implicitly close a string on EOL\n // but it generates too many false positives.\n\n if (!allowNewLine) {\n this.trigger("warning", {\n code: "W112",\n line: this.line,\n character: this.char\n });\n } else {\n allowNewLine = false;\n\n // Otherwise show a warning if multistr option was not set.\n // For JSON, show warning no matter what.\n\n if (!state.option.multistr) {\n this.trigger("warning", {\n code: "W043",\n line: this.line,\n character: this.char\n });\n } else if (state.jsonMode) {\n this.trigger("warning", {\n code: "W042",\n line: this.line,\n character: this.char\n });\n }\n }\n\n // If we get an EOF inside of an unclosed string, show an\n // error and implicitly close it at the EOF point.\n\n if (!this.nextLine()) {\n this.trigger("error", {\n code: "E029",\n line: startLine,\n character: startChar\n });\n\n return {\n type: Token.StringLiteral,\n value: value,\n isUnclosed: true,\n quote: quote\n };\n }\n }\n\n allowNewLine = false;\n var char = this.peek();\n var jump = 1; // A length of a jump, after we\'re done\n // parsing this character.\n\n if (char < " ") {\n // Warn about a control character in a string.\n this.trigger("warning", {\n code: "W113",\n line: this.line,\n character: this.char,\n data: [ "" ]\n });\n }\n\n // Special treatment for some escaped characters.\n\n if (char === "\\\\") {\n this.skip();\n char = this.peek();\n\n switch (char) {\n case "\'":\n if (state.jsonMode) {\n this.trigger("warning", {\n code: "W114",\n line: this.line,\n character: this.char,\n data: [ "\\\\\'" ]\n });\n }\n break;\n case "b":\n char = "\\b";\n break;\n case "f":\n char = "\\f";\n break;\n case "n":\n char = "\\n";\n break;\n case "r":\n char = "\\r";\n break;\n case "t":\n char = "\\t";\n break;\n case "0":\n char = "\\0";\n\n // Octal literals fail in strict mode.\n // Check if the number is between 00 and 07.\n var n = parseInt(this.peek(1), 10);\n if (n >= 0 && n <= 7 && state.directive["use strict"]) {\n this.trigger("warning", {\n code: "W115",\n line: this.line,\n character: this.char\n });\n }\n break;\n case "u":\n char = String.fromCharCode(parseInt(this.input.substr(1, 4), 16));\n jump = 5;\n break;\n case "v":\n if (state.jsonMode) {\n this.trigger("warning", {\n code: "W114",\n line: this.line,\n character: this.char,\n data: [ "\\\\v" ]\n });\n }\n\n char = "\\v";\n break;\n case "x":\n var x = parseInt(this.input.substr(1, 2), 16);\n\n if (state.jsonMode) {\n this.trigger("warning", {\n code: "W114",\n line: this.line,\n character: this.char,\n data: [ "\\\\x-" ]\n });\n }\n\n char = String.fromCharCode(x);\n jump = 3;\n break;\n case "\\\\":\n case "\\"":\n case "/":\n break;\n case "":\n allowNewLine = true;\n char = "";\n break;\n case "!":\n if (value.slice(value.length - 2) === "<") {\n break;\n }\n\n /*falls through */\n default:\n // Weird escaping.\n this.trigger("warning", {\n code: "W044",\n line: this.line,\n character: this.char\n });\n }\n }\n\n value += char;\n this.skip(jump);\n }\n\n this.skip();\n return {\n type: Token.StringLiteral,\n value: value,\n isUnclosed: false,\n quote: quote\n };\n },\n\n /*\n * Extract a regular expression out of the next sequence of\n * characters and/or lines or return \'null\' if its not possible.\n *\n * This method is platform dependent: it accepts almost any\n * regular expression values but then tries to compile and run\n * them using system\'s RegExp object. This means that there are\n * rare edge cases where one JavaScript engine complains about\n * your regular expression while others don\'t.\n */\n scanRegExp: function () {\n var index = 0;\n var length = this.input.length;\n var char = this.peek();\n var value = char;\n var body = "";\n var flags = [];\n var malformed = false;\n var isCharSet = false;\n var terminated;\n\n var scanUnexpectedChars = function () {\n // Unexpected control character\n if (char < " ") {\n malformed = true;\n this.trigger("warning", {\n code: "W048",\n line: this.line,\n character: this.char\n });\n }\n\n // Unexpected escaped character\n if (char === "<") {\n malformed = true;\n this.trigger("warning", {\n code: "W049",\n line: this.line,\n character: this.char,\n data: [ char ]\n });\n }\n }.bind(this);\n\n // Regular expressions must start with \'/\'\n if (!this.prereg || char !== "/") {\n return null;\n }\n\n index += 1;\n terminated = false;\n\n // Try to get everything in between slashes. A couple of\n // cases aside (see scanUnexpectedChars) we don\'t really\n // care whether the resulting expression is valid or not.\n // We will check that later using the RegExp object.\n\n while (index < length) {\n char = this.peek(index);\n value += char;\n body += char;\n\n if (isCharSet) {\n if (char === "]") {\n if (this.peek(index - 1) !== "\\\\" || this.peek(index - 2) === "\\\\") {\n isCharSet = false;\n }\n }\n\n if (char === "\\\\") {\n index += 1;\n char = this.peek(index);\n body += char;\n value += char;\n\n scanUnexpectedChars();\n }\n\n index += 1;\n continue;\n }\n\n if (char === "\\\\") {\n index += 1;\n char = this.peek(index);\n body += char;\n value += char;\n\n scanUnexpectedChars();\n\n if (char === "/") {\n index += 1;\n continue;\n }\n\n if (char === "[") {\n index += 1;\n continue;\n }\n }\n\n if (char === "[") {\n isCharSet = true;\n index += 1;\n continue;\n }\n\n if (char === "/") {\n body = body.substr(0, body.length - 1);\n terminated = true;\n index += 1;\n break;\n }\n\n index += 1;\n }\n\n // A regular expression that was never closed is an\n // error from which we cannot recover.\n\n if (!terminated) {\n this.trigger("error", {\n code: "E015",\n line: this.line,\n character: this.from\n });\n\n return void this.trigger("fatal", {\n line: this.line,\n from: this.from\n });\n }\n\n // Parse flags (if any).\n\n while (index < length) {\n char = this.peek(index);\n if (!/[gim]/.test(char)) {\n break;\n }\n flags.push(char);\n value += char;\n index += 1;\n }\n\n // Check regular expression for correctness.\n\n try {\n new RegExp(body, flags.join(""));\n } catch (err) {\n malformed = true;\n this.trigger("error", {\n code: "E016",\n line: this.line,\n character: this.char,\n data: [ err.message ] // Platform dependent!\n });\n }\n\n return {\n type: Token.RegExp,\n value: value,\n flags: flags,\n isMalformed: malformed\n };\n },\n\n /*\n * Scan for any occurence of mixed tabs and spaces. If smarttabs option\n * is on, ignore tabs followed by spaces.\n *\n * Tabs followed by one space followed by a block comment are allowed.\n */\n scanMixedSpacesAndTabs: function () {\n var at, match;\n\n if (state.option.smarttabs) {\n // Negative look-behind for "//"\n match = this.input.match(/(\\/\\/)? \\t/);\n at = match && !match[1] ? 0 : -1;\n } else {\n at = this.input.search(/ \\t|\\t [^\\*]/);\n }\n\n return at;\n },\n\n /*\n * Scan for characters that get silently deleted by one or more browsers.\n */\n scanUnsafeChars: function () {\n return this.input.search(reg.unsafeChars);\n },\n\n /*\n * Produce the next raw token or return \'null\' if no tokens can be matched.\n * This method skips over all space characters.\n */\n next: function () {\n this.from = this.char;\n\n // Move to the next non-space character.\n var start;\n if (/\\s/.test(this.peek())) {\n start = this.char;\n\n while (/\\s/.test(this.peek())) {\n this.from += 1;\n this.skip();\n }\n\n if (this.peek() === "") { // EOL\n if (state.option.trailing) {\n this.trigger("warning", { code: "W102", line: this.line, character: start });\n }\n }\n }\n\n // Methods that work with multi-line structures and move the\n // character pointer.\n\n var match = this.scanComments() ||\n this.scanStringLiteral();\n\n if (match) {\n return match;\n }\n\n // Methods that don\'t move the character pointer.\n\n match =\n this.scanRegExp() ||\n this.scanPunctuator() ||\n this.scanKeyword() ||\n this.scanIdentifier() ||\n this.scanNumericLiteral();\n\n if (match) {\n this.skip(match.value.length);\n return match;\n }\n\n // No token could be matched, give up.\n\n return null;\n },\n\n /*\n * Switch to the next line and reset all char pointers. Once\n * switched, this method also checks for mixed spaces and tabs\n * and other minor warnings.\n */\n nextLine: function () {\n var char;\n\n if (this.line >= this.lines.length) {\n return false;\n }\n\n this.input = this.lines[this.line];\n this.line += 1;\n this.char = 1;\n this.from = 1;\n\n char = this.scanMixedSpacesAndTabs();\n if (char >= 0) {\n this.trigger("warning", { code: "W099", line: this.line, character: char + 1 });\n }\n\n this.input = this.input.replace(/\\t/g, state.tab);\n char = this.scanUnsafeChars();\n\n if (char >= 0) {\n this.trigger("warning", { code: "W100", line: this.line, character: char });\n }\n\n // If there is a limit on line length, warn when lines get too\n // long.\n\n if (state.option.maxlen && state.option.maxlen < this.input.length) {\n this.trigger("warning", { code: "W101", line: this.line, character: this.input.length });\n }\n\n return true;\n },\n\n /*\n * This is simply a synonym for nextLine() method with a friendlier\n * public name.\n */\n start: function () {\n this.nextLine();\n },\n\n /*\n * Produce the next token. This function is called by advance() to get\n * the next token. It retuns a token in a JSLint-compatible format.\n */\n token: function () {\n var token;\n\n function isReserved(token, isProperty) {\n if (!token.reserved) {\n return false;\n }\n\n if (token.meta && token.meta.isFutureReservedWord) {\n // ES3 FutureReservedWord in an ES5 environment.\n if (state.option.es5 && !token.meta.es5) {\n return false;\n }\n\n // Some ES5 FutureReservedWord identifiers are active only\n // within a strict mode environment.\n if (token.meta.strictOnly) {\n if (!state.option.strict && !state.directive["use strict"]) {\n return false;\n }\n }\n\n if (isProperty) {\n return false;\n }\n }\n\n return true;\n }\n\n // Produce a token object.\n var create = function (type, value, isProperty) {\n /*jshint validthis:true */\n var obj;\n\n if (type !== "(endline)" && type !== "(end)") {\n this.prereg = false;\n }\n\n if (type === "(punctuator)") {\n switch (value) {\n case ".":\n case ")":\n case "~":\n case "#":\n case "]":\n this.prereg = false;\n break;\n default:\n this.prereg = true;\n }\n\n obj = Object.create(state.syntax[value] || state.syntax["(error)"]);\n }\n\n if (type === "(identifier)") {\n if (value === "return" || value === "case" || value === "typeof") {\n this.prereg = true;\n }\n\n if (_.has(state.syntax, value)) {\n obj = Object.create(state.syntax[value] || state.syntax["(error)"]);\n\n // If this can\'t be a reserved keyword, reset the object.\n if (!isReserved(obj, isProperty && type === "(identifier)")) {\n obj = null;\n }\n }\n }\n\n if (!obj) {\n obj = Object.create(state.syntax[type]);\n }\n\n obj.identifier = (type === "(identifier)");\n obj.type = obj.type || type;\n obj.value = value;\n obj.line = this.line;\n obj.character = this.char;\n obj.from = this.from;\n\n if (isProperty && obj.identifier) {\n obj.isProperty = isProperty;\n }\n\n return obj;\n }.bind(this);\n\n for (;;) {\n if (!this.input.length) {\n return create(this.nextLine() ? "(endline)" : "(end)", "");\n }\n\n token = this.next();\n\n if (!token) {\n if (this.input.length) {\n // Unexpected character.\n this.trigger("error", {\n code: "E024",\n line: this.line,\n character: this.char,\n data: [ this.peek() ]\n });\n\n this.input = "";\n }\n\n continue;\n }\n\n switch (token.type) {\n case Token.StringLiteral:\n this.trigger("String", {\n line: this.line,\n char: this.char,\n from: this.from,\n value: token.value,\n quote: token.quote\n });\n\n return create("(string)", token.value);\n case Token.Identifier:\n this.trigger("Identifier", {\n line: this.line,\n char: this.char,\n from: this.form,\n name: token.value,\n isProperty: state.tokens.curr.id === "."\n });\n\n /* falls through */\n case Token.Keyword:\n case Token.NullLiteral:\n case Token.BooleanLiteral:\n return create("(identifier)", token.value, state.tokens.curr.id === ".");\n\n case Token.NumericLiteral:\n if (token.isMalformed) {\n this.trigger("warning", {\n code: "W045",\n line: this.line,\n character: this.char,\n data: [ token.value ]\n });\n }\n\n if (state.jsonMode && token.base === 16) {\n this.trigger("warning", {\n code: "W114",\n line: this.line,\n character: this.char,\n data: [ "0x-" ]\n });\n }\n\n if (state.directive["use strict"] && token.base === 8) {\n this.trigger("warning", {\n code: "W115",\n line: this.line,\n character: this.char\n });\n }\n\n this.trigger("Number", {\n line: this.line,\n char: this.char,\n from: this.from,\n value: token.value,\n base: token.base,\n isMalformed: token.malformed\n });\n\n return create("(number)", token.value);\n\n case Token.RegExp:\n return create("(regexp)", token.value);\n\n case Token.Comment:\n state.tokens.curr.comment = true;\n\n if (token.isSpecial) {\n return {\n value: token.value,\n body: token.body,\n type: token.commentType,\n isSpecial: token.isSpecial,\n line: this.line,\n character: this.char,\n from: this.from\n };\n }\n\n break;\n\n case "":\n break;\n\n default:\n return create("(punctuator)", token.value);\n }\n }\n }\n};\n\nexports.Lexer = Lexer;\n//@ sourceURL=/src/stable/lex.js')); -require.define("/src/stable/reg.js",Function(["require","module","exports","__dirname","__filename","process","global"],'/*\n * Regular expressions. Some of these are stupidly long.\n */\n\n/*jshint maxlen:1000 */\n\n"use string";\n\n// Unsafe comment or string (ax)\nexports.unsafeString =\n /@cc|<\\/?|script|\\]\\s*\\]|<\\s*!|</i;\n\n// Unsafe characters that are silently deleted by one or more browsers (cx)\nexports.unsafeChars =\n /[\\u0000-\\u001f\\u007f-\\u009f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/;\n\n// Characters in strings that need escaping (nx and nxg)\nexports.needEsc =\n /[\\u0000-\\u001f&<"\\/\\\\\\u007f-\\u009f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/;\n\nexports.needEscGlobal =\n /[\\u0000-\\u001f&<"\\/\\\\\\u007f-\\u009f\\u00ad\\u0600-\\u0604\\u070f\\u17b4\\u17b5\\u200c-\\u200f\\u2028-\\u202f\\u2060-\\u206f\\ufeff\\ufff0-\\uffff]/g;\n\n// Star slash (lx)\nexports.starSlash = /\\*\\//;\n\n// Identifier (ix)\nexports.identifier = /^([a-zA-Z_$][a-zA-Z0-9_$]*)$/;\n\n// JavaScript URL (jx)\nexports.javascriptURL = /^(?:javascript|jscript|ecmascript|vbscript|mocha|livescript)\\s*:/i;\n\n// Catches /* falls through */ comments (ft)\nexports.fallsThrough = /^\\s*\\/\\*\\s*falls\\sthrough\\s*\\*\\/\\s*$/;\n//@ sourceURL=/src/stable/reg.js')); -require.define("/src/stable/state.js",Function(["require","module","exports","__dirname","__filename","process","global"],'"use strict";\n\nvar state = {\n syntax: {},\n\n reset: function () {\n this.tokens = {\n prev: null,\n next: null,\n curr: null\n },\n\n this.option = {};\n this.directive = {};\n this.jsonMode = false;\n this.lines = [];\n this.tab = "";\n this.cache = {}; // Node.JS doesn\'t have Map. Sniff.\n }\n};\n\nexports.state = state;\n//@ sourceURL=/src/stable/state.js')); -require.define("/src/stable/style.js",Function(["require","module","exports","__dirname","__filename","process","global"],'"use strict";\n\nexports.register = function (linter) {\n // Check for properties named __proto__. This special property was\n // deprecated and then re-introduced for ES6.\n\n linter.on("Identifier", function style_scanProto(data) {\n if (linter.getOption("proto")) {\n return;\n }\n\n if (data.name === "__proto__") {\n linter.warn("W103", {\n line: data.line,\n char: data.char,\n data: [ data.name ]\n });\n }\n });\n\n // Check for properties named __iterator__. This is a special property\n // available only in browsers with JavaScript 1.7 implementation.\n\n linter.on("Identifier", function style_scanIterator(data) {\n if (linter.getOption("iterator")) {\n return;\n }\n\n if (data.name === "__iterator__") {\n linter.warn("W104", {\n line: data.line,\n char: data.char,\n data: [ data.name ]\n });\n }\n });\n\n // Check for dangling underscores.\n\n linter.on("Identifier", function style_scanDangling(data) {\n if (!linter.getOption("nomen")) {\n return;\n }\n\n // Underscore.js\n if (data.name === "_") {\n return;\n }\n\n // In Node, __dirname and __filename should be ignored.\n if (linter.getOption("node")) {\n if (/^(__dirname|__filename)$/.test(data.name) && !data.isProperty) {\n return;\n }\n }\n\n if (/^(_+.*|.*_+)$/.test(data.name)) {\n linter.warn("W105", {\n line: data.line,\n char: data.from,\n data: [ "dangling \'_\'", data.name ]\n });\n }\n });\n\n // Check that all identifiers are using camelCase notation.\n // Exceptions: names like MY_VAR and _myVar.\n\n linter.on("Identifier", function style_scanCamelCase(data) {\n if (!linter.getOption("camelcase")) {\n return;\n }\n\n if (data.name.replace(/^_+/, "").indexOf("_") > -1 && !data.name.match(/^[A-Z0-9_]*$/)) {\n linter.warn("W106", {\n line: data.line,\n char: data.from,\n data: [ data.name ]\n });\n }\n });\n\n // Enforce consistency in style of quoting.\n\n linter.on("String", function style_scanQuotes(data) {\n var quotmark = linter.getOption("quotmark");\n var code;\n\n if (!quotmark) {\n return;\n }\n\n // If quotmark is set to \'single\' warn about all double-quotes.\n\n if (quotmark === "single" && data.quote !== "\'") {\n code = "W109";\n }\n\n // If quotmark is set to \'double\' warn about all single-quotes.\n\n if (quotmark === "double" && data.quote !== "\\"") {\n code = "W108";\n }\n\n // If quotmark is set to true, remember the first quotation style\n // and then warn about all others.\n\n if (quotmark === true) {\n if (!linter.getCache("quotmark")) {\n linter.setCache("quotmark", data.quote);\n }\n\n if (linter.getCache("quotmark") !== data.quote) {\n code = "W110";\n }\n }\n\n if (code) {\n linter.warn(code, {\n line: data.line,\n char: data.char,\n });\n }\n });\n\n linter.on("Number", function style_scanNumbers(data) {\n if (data.value.charAt(0) === ".") {\n // Warn about a leading decimal point.\n linter.warn("W008", {\n line: data.line,\n char: data.char,\n data: [ data.value ]\n });\n }\n\n if (data.value.substr(data.value.length - 1) === ".") {\n // Warn about a trailing decimal point.\n linter.warn("W047", {\n line: data.line,\n char: data.char,\n data: [ data.value ]\n });\n }\n\n if (/^00+/.test(data.value)) {\n // Multiple leading zeroes.\n linter.warn("W046", {\n line: data.line,\n char: data.char,\n data: [ data.value ]\n });\n }\n });\n\n // Warn about script URLs.\n\n linter.on("String", function style_scanJavaScriptURLs(data) {\n var re = /^(?:javascript|jscript|ecmascript|vbscript|mocha|livescript)\\s*:/i;\n\n if (linter.getOption("scripturl")) {\n return;\n }\n\n if (re.test(data.value)) {\n linter.warn("W107", {\n line: data.line,\n char: data.char\n });\n }\n });\n};\n//@ sourceURL=/src/stable/style.js')); -require.define("/src/stable/jshint.js",Function(["require","module","exports","__dirname","__filename","process","global"],'/*!\n * JSHint, by JSHint Community.\n *\n * This file (and this file only) is licensed under the same slightly modified\n * MIT license that JSLint is. It stops evil-doers everywhere:\n *\n * Copyright (c) 2002 Douglas Crockford (www.JSLint.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining\n * a copy of this software and associated documentation files (the "Software"),\n * to deal in the Software without restriction, including without limitation\n * the rights to use, copy, modify, merge, publish, distribute, sublicense,\n * and/or sell copies of the Software, and to permit persons to whom\n * the Software is furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included\n * in all copies or substantial portions of the Software.\n *\n * The Software shall be used for Good, not Evil.\n *\n * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n * DEALINGS IN THE SOFTWARE.\n *\n */\n\n/*jshint quotmark:double */\n\nvar _ = require("underscore");\nvar events = require("events");\nvar vars = require("../shared/vars.js");\nvar messages = require("../shared/messages.js");\nvar Lexer = require("./lex.js").Lexer;\nvar reg = require("./reg.js");\nvar state = require("./state.js").state;\nvar style = require("./style.js");\n\n// We build the application inside a function so that we produce only a single\n// global variable. That function will be invoked immediately, and its return\n// value is the JSHINT function itself.\n\nvar JSHINT = (function () {\n "use strict";\n\n var anonname, // The guessed name for anonymous functions.\n\n// These are operators that should not be used with the ! operator.\n\n bang = {\n "<" : true,\n "<=" : true,\n "==" : true,\n "===": true,\n "!==": true,\n "!=" : true,\n ">" : true,\n ">=" : true,\n "+" : true,\n "-" : true,\n "*" : true,\n "/" : true,\n "%" : true\n },\n\n // These are the JSHint boolean options.\n boolOptions = {\n asi : true, // if automatic semicolon insertion should be tolerated\n bitwise : true, // if bitwise operators should not be allowed\n boss : true, // if advanced usage of assignments should be allowed\n browser : true, // if the standard browser globals should be predefined\n camelcase : true, // if identifiers should be required in camel case\n couch : true, // if CouchDB globals should be predefined\n curly : true, // if curly braces around all blocks should be required\n debug : true, // if debugger statements should be allowed\n devel : true, // if logging globals should be predefined (console, alert, etc.)\n dojo : true, // if Dojo Toolkit globals should be predefined\n eqeqeq : true, // if === should be required\n eqnull : true, // if == null comparisons should be tolerated\n es5 : true, // if ES5 syntax should be allowed\n esnext : true, // if es.next specific syntax should be allowed\n evil : true, // if eval should be allowed\n expr : true, // if ExpressionStatement should be allowed as Programs\n forin : true, // if for in statements must filter\n funcscope : true, // if only function scope should be used for scope tests\n gcl : true, // if JSHint should be compatible with Google Closure Linter\n globalstrict: true, // if global "use strict"; should be allowed (also enables \'strict\')\n immed : true, // if immediate invocations must be wrapped in parens\n iterator : true, // if the `__iterator__` property should be allowed\n jquery : true, // if jQuery globals should be predefined\n lastsemic : true, // if semicolons may be ommitted for the trailing\n // statements inside of a one-line blocks.\n latedef : true, // if the use before definition should not be tolerated\n laxbreak : true, // if line breaks should not be checked\n laxcomma : true, // if line breaks should not be checked around commas\n loopfunc : true, // if functions should be allowed to be defined within\n // loops\n mootools : true, // if MooTools globals should be predefined\n multistr : true, // allow multiline strings\n newcap : true, // if constructor names must be capitalized\n noarg : true, // if arguments.caller and arguments.callee should be\n // disallowed\n node : true, // if the Node.js environment globals should be\n // predefined\n noempty : true, // if empty blocks should be disallowed\n nonew : true, // if using `new` for side-effects should be disallowed\n nonstandard : true, // if non-standard (but widely adopted) globals should\n // be predefined\n nomen : true, // if names should be checked\n onevar : true, // if only one var statement per function should be\n // allowed\n passfail : true, // if the scan should stop on first error\n phantom : true, // if PhantomJS symbols should be allowed\n plusplus : true, // if increment/decrement should not be allowed\n proto : true, // if the `__proto__` property should be allowed\n prototypejs : true, // if Prototype and Scriptaculous globals should be\n // predefined\n rhino : true, // if the Rhino environment globals should be predefined\n undef : true, // if variables should be declared before used\n scripturl : true, // if script-targeted URLs should be tolerated\n shadow : true, // if variable shadowing should be tolerated\n smarttabs : true, // if smarttabs should be tolerated\n // (http://www.emacswiki.org/emacs/SmartTabs)\n strict : true, // require the "use strict"; pragma\n sub : true, // if all forms of subscript notation are tolerated\n supernew : true, // if `new function () { ... };` and `new Object;`\n // should be tolerated\n trailing : true, // if trailing whitespace rules apply\n validthis : true, // if \'this\' inside a non-constructor function is valid.\n // This is a function scoped option only.\n withstmt : true, // if with statements should be allowed\n white : true, // if strict whitespace rules apply\n worker : true, // if Web Worker script symbols should be allowed\n wsh : true, // if the Windows Scripting Host environment globals\n // should be predefined\n yui : true, // YUI variables should be predefined\n\n // Obsolete options\n onecase : true, // if one case switch statements should be allowed\n regexp : true, // if the . should not be allowed in regexp literals\n regexdash : true // if unescaped first/last dash (-) inside brackets\n // should be tolerated\n },\n\n // These are the JSHint options that can take any value\n // (we use this object to detect invalid options)\n valOptions = {\n maxlen : false,\n indent : false,\n maxerr : false,\n predef : false,\n quotmark : false, //\'single\'|\'double\'|true\n scope : false,\n maxstatements: false, // {int} max statements per function\n maxdepth : false, // {int} max nested block depth per function\n maxparams : false, // {int} max params per function\n maxcomplexity: false, // {int} max cyclomatic complexity per function\n unused : true // warn if variables are unused. Available options:\n // false - don\'t check for unused variables\n // true - "vars" + check last function param\n // "vars" - skip checking unused function params\n // "strict" - "vars" + check all function params\n },\n\n // These are JSHint boolean options which are shared with JSLint\n // where the definition in JSHint is opposite JSLint\n invertedOptions = {\n bitwise : true,\n forin : true,\n newcap : true,\n nomen : true,\n plusplus: true,\n regexp : true,\n undef : true,\n white : true,\n\n // Inverted and renamed, use JSHint name here\n eqeqeq : true,\n onevar : true\n },\n\n // These are JSHint boolean options which are shared with JSLint\n // where the name has been changed but the effect is unchanged\n renamedOptions = {\n eqeq : "eqeqeq",\n vars : "onevar",\n windows: "wsh"\n },\n\n declared, // Globals that were declared using /*global ... */ syntax.\n exported, // Variables that are used outside of the current file.\n\n functionicity = [\n "closure", "exception", "global", "label",\n "outer", "unused", "var"\n ],\n\n funct, // The current function\n functions, // All of the functions\n\n global, // The global scope\n ignored, // Ignored warnings\n implied, // Implied globals\n inblock,\n indent,\n lookahead,\n lex,\n member,\n membersOnly,\n noreach,\n predefined, // Global variables defined by option\n\n scope, // The current scope\n stack,\n unuseds,\n urls,\n useESNextSyntax,\n warnings,\n\n extraModules = [],\n emitter = new events.EventEmitter();\n\n function checkOption(name, t) {\n name = name.trim();\n\n if (/^-W\\d{3}$/g.test(name)) {\n return true;\n }\n\n if (valOptions[name] === undefined && boolOptions[name] === undefined) {\n if (t.type !== "jslint" || renamedOptions[name] === undefined) {\n error("E001", t, name);\n return false;\n }\n }\n\n return true;\n }\n\n function isString(obj) {\n return Object.prototype.toString.call(obj) === "[object String]";\n }\n\n function isIdentifier(tkn, value) {\n if (!tkn)\n return false;\n\n if (!tkn.identifier || tkn.value !== value)\n return false;\n\n return true;\n }\n\n function isReserved(token) {\n if (!token.reserved) {\n return false;\n }\n\n if (token.meta && token.meta.isFutureReservedWord) {\n // ES3 FutureReservedWord in an ES5 environment.\n if (state.option.es5 && !token.meta.es5) {\n return false;\n }\n\n // Some ES5 FutureReservedWord identifiers are active only\n // within a strict mode environment.\n if (token.meta.strictOnly) {\n if (!state.option.strict && !state.directive["use strict"]) {\n return false;\n }\n }\n\n if (token.isProperty) {\n return false;\n }\n }\n\n return true;\n }\n\n function supplant(str, data) {\n return str.replace(/\\{([^{}]*)\\}/g, function (a, b) {\n var r = data[b];\n return typeof r === "string" || typeof r === "number" ? r : a;\n });\n }\n\n function combine(t, o) {\n var n;\n for (n in o) {\n if (_.has(o, n) && !_.has(JSHINT.blacklist, n)) {\n t[n] = o[n];\n }\n }\n }\n\n function updatePredefined() {\n Object.keys(JSHINT.blacklist).forEach(function (key) {\n delete predefined[key];\n });\n }\n\n function assume() {\n if (state.option.couch) {\n combine(predefined, vars.couch);\n }\n\n if (state.option.rhino) {\n combine(predefined, vars.rhino);\n }\n\n if (state.option.phantom) {\n combine(predefined, vars.phantom);\n }\n\n if (state.option.prototypejs) {\n combine(predefined, vars.prototypejs);\n }\n\n if (state.option.node) {\n combine(predefined, vars.node);\n }\n\n if (state.option.devel) {\n combine(predefined, vars.devel);\n }\n\n if (state.option.dojo) {\n combine(predefined, vars.dojo);\n }\n\n if (state.option.browser) {\n combine(predefined, vars.browser);\n }\n\n if (state.option.nonstandard) {\n combine(predefined, vars.nonstandard);\n }\n\n if (state.option.jquery) {\n combine(predefined, vars.jquery);\n }\n\n if (state.option.mootools) {\n combine(predefined, vars.mootools);\n }\n\n if (state.option.worker) {\n combine(predefined, vars.worker);\n }\n\n if (state.option.wsh) {\n combine(predefined, vars.wsh);\n }\n\n if (state.option.esnext) {\n useESNextSyntax();\n }\n\n if (state.option.globalstrict && state.option.strict !== false) {\n state.option.strict = true;\n }\n\n if (state.option.yui) {\n combine(predefined, vars.yui);\n }\n }\n\n\n // Produce an error warning.\n function quit(code, line, chr) {\n var percentage = Math.floor((line / state.lines.length) * 100);\n var message = messages.errors[code].desc;\n\n throw {\n name: "JSHintError",\n line: line,\n character: chr,\n message: message + " (" + percentage + "% scanned).",\n raw: message\n };\n }\n\n function isundef(scope, code, token, a) {\n return JSHINT.undefs.push([scope, code, token, a]);\n }\n\n function warning(code, t, a, b, c, d) {\n var ch, l, w, msg;\n\n if (/^W\\d{3}$/.test(code)) {\n if (ignored[code]) {\n return;\n }\n\n msg = messages.warnings[code];\n } else if (/E\\d{3}/.test(code)) {\n msg = messages.errors[code];\n } else if (/I\\d{3}/.test(code)) {\n msg = messages.info[code];\n }\n\n t = t || state.tokens.next;\n if (t.id === "(end)") { // `~\n t = state.tokens.curr;\n }\n\n l = t.line || 0;\n ch = t.from || 0;\n\n w = {\n id: "(error)",\n raw: msg.desc,\n code: msg.code,\n evidence: state.lines[l - 1] || "",\n line: l,\n character: ch,\n scope: JSHINT.scope,\n a: a,\n b: b,\n c: c,\n d: d\n };\n\n w.reason = supplant(msg.desc, w);\n JSHINT.errors.push(w);\n\n if (state.option.passfail) {\n quit("E042", l, ch);\n }\n\n warnings += 1;\n if (warnings >= state.option.maxerr) {\n quit("E043", l, ch);\n }\n\n return w;\n }\n\n function warningAt(m, l, ch, a, b, c, d) {\n return warning(m, {\n line: l,\n from: ch\n }, a, b, c, d);\n }\n\n function error(m, t, a, b, c, d) {\n warning(m, t, a, b, c, d);\n }\n\n function errorAt(m, l, ch, a, b, c, d) {\n return error(m, {\n line: l,\n from: ch\n }, a, b, c, d);\n }\n\n // Tracking of "internal" scripts, like eval containing a static string\n function addInternalSrc(elem, src) {\n var i;\n i = {\n id: "(internal)",\n elem: elem,\n value: src\n };\n JSHINT.internals.push(i);\n return i;\n }\n\n function addlabel(t, type, tkn) {\n // Define t in the current function in the current scope.\n if (type === "exception") {\n if (_.has(funct["(context)"], t)) {\n if (funct[t] !== true && !state.option.node) {\n warning("W002", state.tokens.next, t);\n }\n }\n }\n\n if (_.has(funct, t) && !funct["(global)"]) {\n if (funct[t] === true) {\n if (state.option.latedef)\n warning("W003", state.tokens.next, t);\n } else {\n if (!state.option.shadow && type !== "exception") {\n warning("W004", state.tokens.next, t);\n }\n }\n }\n\n funct[t] = type;\n\n if (tkn) {\n funct["(tokens)"][t] = tkn;\n }\n\n if (funct["(global)"]) {\n global[t] = funct;\n if (_.has(implied, t)) {\n if (state.option.latedef) {\n warning("W003", state.tokens.next, t);\n }\n\n delete implied[t];\n }\n } else {\n scope[t] = funct;\n }\n }\n\n function doOption() {\n var nt = state.tokens.next;\n var body = nt.body.split(",").map(function (s) { return s.trim(); });\n var predef = {};\n\n if (nt.type === "globals") {\n body.forEach(function (g) {\n g = g.split(":");\n var key = g[0];\n var val = g[1];\n\n if (key.charAt(0) === "-") {\n key = key.slice(1);\n val = false;\n\n JSHINT.blacklist[key] = key;\n updatePredefined();\n } else {\n predef[key] = (val === "true");\n }\n });\n\n combine(predefined, predef);\n\n for (var key in predef) {\n if (_.has(predef, key)) {\n declared[key] = nt;\n }\n }\n }\n\n if (nt.type === "exported") {\n body.forEach(function (e) {\n exported[e] = true;\n });\n }\n\n if (nt.type === "members") {\n membersOnly = membersOnly || {};\n\n body.forEach(function (m) {\n var ch1 = m.charAt(0);\n var ch2 = m.charAt(m.length - 1);\n\n if (ch1 === ch2 && (ch1 === "\\"" || ch1 === "\'")) {\n m = m\n .substr(1, m.length - 2)\n .replace("\\\\b", "\\b")\n .replace("\\\\t", "\\t")\n .replace("\\\\n", "\\n")\n .replace("\\\\v", "\\v")\n .replace("\\\\f", "\\f")\n .replace("\\\\r", "\\r")\n .replace("\\\\\\\\", "\\\\")\n .replace("\\\\\\"", "\\"");\n }\n\n membersOnly[m] = false;\n });\n }\n\n var numvals = [\n "maxstatements",\n "maxparams",\n "maxdepth",\n "maxcomplexity",\n "maxerr",\n "maxlen",\n "indent"\n ];\n\n if (nt.type === "jshint" || nt.type === "jslint") {\n body.forEach(function (g) {\n g = g.split(":");\n var key = (g[0] || "").trim();\n var val = (g[1] || "").trim();\n\n if (!checkOption(key, nt)) {\n return;\n }\n\n if (numvals.indexOf(key) >= 0) {\n val = +val;\n\n if (typeof val !== "number" || !isFinite(val) || val <= 0 || Math.floor(val) !== val) {\n error("E032", nt, g[1].trim());\n return;\n }\n\n if (key === "indent") {\n state.option["(explicitIndent)"] = true;\n }\n\n state.option[key] = val;\n return;\n }\n\n if (key === "validthis") {\n // `validthis` is valid only within a function scope.\n if (funct["(global)"]) {\n error("E009");\n } else {\n if (val === "true" || val === "false") {\n state.option.validthis = (val === "true");\n } else {\n error("E002", nt);\n }\n }\n return;\n }\n\n if (key === "quotmark") {\n switch (val) {\n case "true":\n case "false":\n state.option.quotmark = (val === "true");\n break;\n case "double":\n case "single":\n state.option.quotmark = val;\n break;\n default:\n error("E002", nt);\n }\n return;\n }\n\n if (key === "unused") {\n switch (val) {\n case "true":\n state.option.unused = true;\n break;\n case "false":\n state.option.unused = false;\n break;\n case "vars":\n case "strict":\n state.option.unused = val;\n break;\n default:\n error("E002", nt);\n }\n return;\n }\n\n if (/^-W\\d{3}$/g.test(key)) {\n ignored[key.slice(1)] = true;\n return;\n }\n\n var tn;\n if (val === "true" || val === "false") {\n if (nt.type === "jslint") {\n tn = renamedOptions[key] || key;\n state.option[tn] = (val === "true");\n\n if (invertedOptions[tn] !== undefined) {\n state.option[tn] = !state.option[tn];\n }\n } else {\n state.option[key] = (val === "true");\n }\n\n if (key === "newcap") {\n state.option["(explicitNewcap)"] = true;\n }\n return;\n }\n\n error("E002", nt);\n });\n\n assume();\n }\n }\n\n // We need a peek function. If it has an argument, it peeks that much farther\n // ahead. It is used to distinguish\n // for ( var i in ...\n // from\n // for ( var i = ...\n\n function peek(p) {\n var i = p || 0, j = 0, t;\n\n while (j <= i) {\n t = lookahead[j];\n if (!t) {\n t = lookahead[j] = lex.token();\n }\n j += 1;\n }\n return t;\n }\n\n // Produce the next token. It looks for programming errors.\n\n function advance(id, t) {\n switch (state.tokens.curr.id) {\n case "(number)":\n if (state.tokens.next.id === ".") {\n warning("W005", state.tokens.curr);\n }\n break;\n case "-":\n if (state.tokens.next.id === "-" || state.tokens.next.id === "--") {\n warning("W006");\n }\n break;\n case "+":\n if (state.tokens.next.id === "+" || state.tokens.next.id === "++") {\n warning("W007");\n }\n break;\n }\n\n if (state.tokens.curr.type === "(string)" || state.tokens.curr.identifier) {\n anonname = state.tokens.curr.value;\n }\n\n if (id && state.tokens.next.id !== id) {\n if (t) {\n if (state.tokens.next.id === "(end)") {\n error("E019", t, t.id);\n } else {\n error("E020", state.tokens.next, id, t.id, t.line, state.tokens.next.value);\n }\n } else if (state.tokens.next.type !== "(identifier)" || state.tokens.next.value !== id) {\n warning("W116", state.tokens.next, id, state.tokens.next.value);\n }\n }\n\n state.tokens.prev = state.tokens.curr;\n state.tokens.curr = state.tokens.next;\n for (;;) {\n state.tokens.next = lookahead.shift() || lex.token();\n\n if (!state.tokens.next) { // No more tokens left, give up\n quit("E041", state.tokens.curr.line);\n }\n\n if (state.tokens.next.id === "(end)" || state.tokens.next.id === "(error)") {\n return;\n }\n\n if (state.tokens.next.isSpecial) {\n doOption();\n } else {\n if (state.tokens.next.id !== "(endline)") {\n break;\n }\n }\n }\n }\n\n\n // This is the heart of JSHINT, the Pratt parser. In addition to parsing, it\n // is looking for ad hoc lint patterns. We add .fud to Pratt\'s model, which is\n // like .nud except that it is only used on the first token of a statement.\n // Having .fud makes it much easier to define statement-oriented languages like\n // JavaScript. I retained Pratt\'s nomenclature.\n\n // .nud Null denotation\n // .fud First null denotation\n // .led Left denotation\n // lbp Left binding power\n // rbp Right binding power\n\n // They are elements of the parsing method called Top Down Operator Precedence.\n\n function expression(rbp, initial) {\n var left, isArray = false, isObject = false;\n\n if (state.tokens.next.id === "(end)")\n error("E006", state.tokens.curr);\n\n advance();\n\n if (initial) {\n anonname = "anonymous";\n funct["(verb)"] = state.tokens.curr.value;\n }\n\n if (initial === true && state.tokens.curr.fud) {\n left = state.tokens.curr.fud();\n } else {\n if (state.tokens.curr.nud) {\n left = state.tokens.curr.nud();\n } else {\n error("E030", state.tokens.curr, state.tokens.curr.id);\n }\n\n while (rbp < state.tokens.next.lbp) {\n isArray = state.tokens.curr.value === "Array";\n isObject = state.tokens.curr.value === "Object";\n\n // #527, new Foo.Array(), Foo.Array(), new Foo.Object(), Foo.Object()\n // Line breaks in IfStatement heads exist to satisfy the checkJSHint\n // "Line too long." error.\n if (left && (left.value || (left.first && left.first.value))) {\n // If the left.value is not "new", or the left.first.value is a "."\n // then safely assume that this is not "new Array()" and possibly\n // not "new Object()"...\n if (left.value !== "new" ||\n (left.first && left.first.value && left.first.value === ".")) {\n isArray = false;\n // ...In the case of Object, if the left.value and state.tokens.curr.value\n // are not equal, then safely assume that this not "new Object()"\n if (left.value !== state.tokens.curr.value) {\n isObject = false;\n }\n }\n }\n\n advance();\n\n if (isArray && state.tokens.curr.id === "(" && state.tokens.next.id === ")") {\n warning("W009", state.tokens.curr);\n }\n\n if (isObject && state.tokens.curr.id === "(" && state.tokens.next.id === ")") {\n warning("W010", state.tokens.curr);\n }\n\n if (state.tokens.curr.led) {\n left = state.tokens.curr.led(left);\n } else {\n error("E033", state.tokens.curr, state.tokens.curr.id);\n }\n }\n }\n return left;\n }\n\n\n// Functions for conformance of style.\n\n function adjacent(left, right) {\n left = left || state.tokens.curr;\n right = right || state.tokens.next;\n if (state.option.white) {\n if (left.character !== right.from && left.line === right.line) {\n left.from += (left.character - left.from);\n warning("W011", left, left.value);\n }\n }\n }\n\n function nobreak(left, right) {\n left = left || state.tokens.curr;\n right = right || state.tokens.next;\n if (state.option.white && (left.character !== right.from || left.line !== right.line)) {\n warning("W012", right, right.value);\n }\n }\n\n function nospace(left, right) {\n left = left || state.tokens.curr;\n right = right || state.tokens.next;\n if (state.option.white && !left.comment) {\n if (left.line === right.line) {\n adjacent(left, right);\n }\n }\n }\n\n function nonadjacent(left, right) {\n if (state.option.white) {\n left = left || state.tokens.curr;\n right = right || state.tokens.next;\n\n if (left.value === ";" && right.value === ";") {\n return;\n }\n\n if (left.line === right.line && left.character === right.from) {\n left.from += (left.character - left.from);\n warning("W013", left, left.value);\n }\n }\n }\n\n function nobreaknonadjacent(left, right) {\n left = left || state.tokens.curr;\n right = right || state.tokens.next;\n if (!state.option.laxbreak && left.line !== right.line) {\n warning("W014", right, right.id);\n } else if (state.option.white) {\n left = left || state.tokens.curr;\n right = right || state.tokens.next;\n if (left.character === right.from) {\n left.from += (left.character - left.from);\n warning("W013", left, left.value);\n }\n }\n }\n\n function indentation(bias) {\n if (!state.option.white && !state.option["(explicitIndent)"]) {\n return;\n }\n\n if (state.tokens.next.id === "(end)") {\n return;\n }\n\n var i = indent + (bias || 0);\n if (state.tokens.next.from !== i) {\n warning("W015", state.tokens.next, state.tokens.next.value, i, state.tokens.next.from);\n }\n }\n\n function nolinebreak(t) {\n t = t || state.tokens.curr;\n if (t.line !== state.tokens.next.line) {\n warning("E022", t, t.value);\n }\n }\n\n\n function comma(opts) {\n opts = opts || {};\n\n if (state.tokens.curr.line !== state.tokens.next.line) {\n if (!state.option.laxcomma) {\n if (comma.first) {\n warning("I001");\n comma.first = false;\n }\n warning("W014", state.tokens.curr, state.tokens.next.id);\n }\n } else if (!state.tokens.curr.comment &&\n state.tokens.curr.character !== state.tokens.next.from && state.option.white) {\n state.tokens.curr.from += (state.tokens.curr.character - state.tokens.curr.from);\n warning("W011", state.tokens.curr, state.tokens.curr.value);\n }\n\n advance(",");\n\n // TODO: This is a temporary solution to fight against false-positives in\n // arrays and objects with trailing commas (see GH-363). The best solution\n // would be to extract all whitespace rules out of parser.\n\n if (state.tokens.next.value !== "]" && state.tokens.next.value !== "}") {\n nonadjacent(state.tokens.curr, state.tokens.next);\n }\n\n if (state.tokens.next.identifier) {\n // Keywords that cannot follow a comma operator.\n switch (state.tokens.next.value) {\n case "break":\n case "case":\n case "catch":\n case "continue":\n case "default":\n case "do":\n case "else":\n case "finally":\n case "for":\n case "if":\n case "in":\n case "instanceof":\n case "return":\n case "switch":\n case "throw":\n case "try":\n case "var":\n case "while":\n case "with":\n error("E024", state.tokens.next, state.tokens.next.value);\n return;\n }\n }\n\n if (state.tokens.next.type === "(punctuator)") {\n switch (state.tokens.next.value) {\n case "}":\n case "]":\n case ",":\n if (opts.allowTrailing) {\n return;\n }\n\n /* falls through */\n case ")":\n error("E024", state.tokens.next, state.tokens.next.value);\n }\n }\n }\n\n // Functional constructors for making the symbols that will be inherited by\n // tokens.\n\n function symbol(s, p) {\n var x = state.syntax[s];\n if (!x || typeof x !== "object") {\n state.syntax[s] = x = {\n id: s,\n lbp: p,\n value: s\n };\n }\n return x;\n }\n\n function delim(s) {\n return symbol(s, 0);\n }\n\n function stmt(s, f) {\n var x = delim(s);\n x.identifier = x.reserved = true;\n x.fud = f;\n return x;\n }\n\n function blockstmt(s, f) {\n var x = stmt(s, f);\n x.block = true;\n return x;\n }\n\n function reserveName(x) {\n var c = x.id.charAt(0);\n if ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z")) {\n x.identifier = x.reserved = true;\n }\n return x;\n }\n\n function prefix(s, f) {\n var x = symbol(s, 150);\n reserveName(x);\n x.nud = (typeof f === "function") ? f : function () {\n this.right = expression(150);\n this.arity = "unary";\n if (this.id === "++" || this.id === "--") {\n if (state.option.plusplus) {\n warning("W016", this, this.id);\n } else if ((!this.right.identifier || isReserved(this.right)) &&\n this.right.id !== "." && this.right.id !== "[") {\n warning("W017", this);\n }\n }\n return this;\n };\n return x;\n }\n\n function type(s, f) {\n var x = delim(s);\n x.type = s;\n x.nud = f;\n return x;\n }\n\n function reserve(name, func) {\n var x = type(name, func);\n x.identifier = true;\n x.reserved = true;\n return x;\n }\n\n function FutureReservedWord(name, meta) {\n var x = type(name, function () {\n return this;\n });\n\n meta = meta || {};\n meta.isFutureReservedWord = true;\n\n x.value = name;\n x.identifier = true;\n x.reserved = true;\n x.meta = meta;\n\n return x;\n }\n\n function reservevar(s, v) {\n return reserve(s, function () {\n if (typeof v === "function") {\n v(this);\n }\n return this;\n });\n }\n\n function infix(s, f, p, w) {\n var x = symbol(s, p);\n reserveName(x);\n x.led = function (left) {\n if (!w) {\n nobreaknonadjacent(state.tokens.prev, state.tokens.curr);\n nonadjacent(state.tokens.curr, state.tokens.next);\n }\n if (s === "in" && left.id === "!") {\n warning("W018", left, "!");\n }\n if (typeof f === "function") {\n return f(left, this);\n } else {\n this.left = left;\n this.right = expression(p);\n return this;\n }\n };\n return x;\n }\n\n function relation(s, f) {\n var x = symbol(s, 100);\n\n x.led = function (left) {\n nobreaknonadjacent(state.tokens.prev, state.tokens.curr);\n nonadjacent(state.tokens.curr, state.tokens.next);\n var right = expression(100);\n\n if (isIdentifier(left, "NaN") || isIdentifier(right, "NaN")) {\n warning("W019", this);\n } else if (f) {\n f.apply(this, [left, right]);\n }\n\n if (!left || !right) {\n quit("E041", state.tokens.curr.line);\n }\n\n if (left.id === "!") {\n warning("W018", left, "!");\n }\n\n if (right.id === "!") {\n warning("W018", right, "!");\n }\n\n this.left = left;\n this.right = right;\n return this;\n };\n return x;\n }\n\n function isPoorRelation(node) {\n return node &&\n ((node.type === "(number)" && +node.value === 0) ||\n (node.type === "(string)" && node.value === "") ||\n (node.type === "null" && !state.option.eqnull) ||\n node.type === "true" ||\n node.type === "false" ||\n node.type === "undefined");\n }\n\n function assignop(s) {\n symbol(s, 20).exps = true;\n\n return infix(s, function (left, that) {\n that.left = left;\n\n if (predefined[left.value] === false &&\n scope[left.value]["(global)"] === true) {\n warning("W020", left);\n } else if (left["function"]) {\n warning("W021", left, left.value);\n }\n\n if (left) {\n if (state.option.esnext && funct[left.value] === "const") {\n error("E013", left, left.value);\n }\n\n if (left.id === "." || left.id === "[") {\n if (!left.left || left.left.value === "arguments") {\n warning("E031", that);\n }\n that.right = expression(19);\n return that;\n } else if (left.identifier && !isReserved(left)) {\n if (funct[left.value] === "exception") {\n warning("W022", left);\n }\n that.right = expression(19);\n return that;\n }\n\n if (left === state.syntax["function"]) {\n warning("W023", state.tokens.curr);\n }\n }\n\n error("E031", that);\n }, 20);\n }\n\n\n function bitwise(s, f, p) {\n var x = symbol(s, p);\n reserveName(x);\n x.led = (typeof f === "function") ? f : function (left) {\n if (state.option.bitwise) {\n warning("W016", this, this.id);\n }\n this.left = left;\n this.right = expression(p);\n return this;\n };\n return x;\n }\n\n\n function bitwiseassignop(s) {\n symbol(s, 20).exps = true;\n return infix(s, function (left, that) {\n if (state.option.bitwise) {\n warning("W016", that, that.id);\n }\n nonadjacent(state.tokens.prev, state.tokens.curr);\n nonadjacent(state.tokens.curr, state.tokens.next);\n if (left) {\n if (left.id === "." || left.id === "[" ||\n (left.identifier && !isReserved(left))) {\n expression(19);\n return that;\n }\n if (left === state.syntax["function"]) {\n warning("W023", state.tokens.curr);\n }\n return that;\n }\n error("E031", that);\n }, 20);\n }\n\n\n function suffix(s) {\n var x = symbol(s, 150);\n\n x.led = function (left) {\n if (state.option.plusplus) {\n warning("W016", this, this.id);\n } else if ((!left.identifier || isReserved(left)) && left.id !== "." && left.id !== "[") {\n warning("W017", this);\n }\n\n this.left = left;\n return this;\n };\n return x;\n }\n\n // fnparam means that this identifier is being defined as a function\n // argument (see identifier())\n // prop means that this identifier is that of an object property\n\n function optionalidentifier(fnparam, prop) {\n if (!state.tokens.next.identifier) {\n return;\n }\n\n advance();\n\n var curr = state.tokens.curr;\n var meta = curr.meta || {};\n var val = state.tokens.curr.value;\n\n if (!isReserved(curr)) {\n return val;\n }\n\n if (prop) {\n if (state.option.es5 || meta.isFutureReservedWord) {\n return val;\n }\n }\n\n if (fnparam && val === "undefined") {\n return val;\n }\n\n warning("W024", state.tokens.curr, state.tokens.curr.id);\n return val;\n }\n\n // fnparam means that this identifier is being defined as a function\n // argument\n // prop means that this identifier is that of an object property\n function identifier(fnparam, prop) {\n var i = optionalidentifier(fnparam, prop);\n if (i) {\n return i;\n }\n if (state.tokens.curr.id === "function" && state.tokens.next.id === "(") {\n warning("W025");\n } else {\n error("E030", state.tokens.next, state.tokens.next.value);\n }\n }\n\n\n function reachable(s) {\n var i = 0, t;\n if (state.tokens.next.id !== ";" || noreach) {\n return;\n }\n for (;;) {\n t = peek(i);\n if (t.reach) {\n return;\n }\n if (t.id !== "(endline)") {\n if (t.id === "function") {\n if (!state.option.latedef) {\n break;\n }\n\n warning("W026", t);\n break;\n }\n\n warning("W027", t, t.value, s);\n break;\n }\n i += 1;\n }\n }\n\n\n function statement(noindent) {\n var i = indent, r, s = scope, t = state.tokens.next;\n\n if (t.id === ";") {\n advance(";");\n return;\n }\n\n // Is this a labelled statement?\n var res = isReserved(t);\n\n // We\'re being more tolerant here: if someone uses\n // a FutureReservedWord as a label, we warn but proceed\n // anyway.\n\n if (res && t.meta && t.meta.isFutureReservedWord) {\n warning("W024", t, t.id);\n res = false;\n }\n\n if (t.identifier && !res && peek().id === ":") {\n advance();\n advance(":");\n scope = Object.create(s);\n addlabel(t.value, "label");\n\n if (!state.tokens.next.labelled && state.tokens.next.value !== "{") {\n warning("W028", state.tokens.next, t.value, state.tokens.next.value);\n }\n\n if (reg.javascriptURL.test(t.value + ":")) {\n warning("W029", t, t.value);\n }\n\n state.tokens.next.label = t.value;\n t = state.tokens.next;\n }\n\n // Is it a lonely block?\n\n if (t.id === "{") {\n block(true, true);\n return;\n }\n\n // Parse the statement.\n\n if (!noindent) {\n indentation();\n }\n r = expression(0, true);\n\n // Look for the final semicolon.\n\n if (!t.block) {\n if (!state.option.expr && (!r || !r.exps)) {\n warning("W030", state.tokens.curr);\n } else if (state.option.nonew && r.id === "(" && r.left.id === "new") {\n warning("W031", t);\n }\n\n if (state.tokens.next.id === ",") {\n return comma();\n }\n\n if (state.tokens.next.id !== ";") {\n if (!state.option.asi) {\n // If this is the last statement in a block that ends on\n // the same line *and* option lastsemic is on, ignore the warning.\n // Otherwise, complain about missing semicolon.\n if (!state.option.lastsemic || state.tokens.next.id !== "}" ||\n state.tokens.next.line !== state.tokens.curr.line) {\n warningAt("W033", state.tokens.curr.line, state.tokens.curr.character);\n }\n }\n } else {\n adjacent(state.tokens.curr, state.tokens.next);\n advance(";");\n nonadjacent(state.tokens.curr, state.tokens.next);\n }\n }\n\n // Restore the indentation.\n\n indent = i;\n scope = s;\n return r;\n }\n\n\n function statements(startLine) {\n var a = [], p;\n\n while (!state.tokens.next.reach && state.tokens.next.id !== "(end)") {\n if (state.tokens.next.id === ";") {\n p = peek();\n\n if (!p || (p.id !== "(" && p.id !== "[")) {\n warning("W032");\n }\n\n advance(";");\n } else {\n a.push(statement(startLine === state.tokens.next.line));\n }\n }\n return a;\n }\n\n\n /*\n * read all directives\n * recognizes a simple form of asi, but always\n * warns, if it is used\n */\n function directives() {\n var i, p, pn;\n\n for (;;) {\n if (state.tokens.next.id === "(string)") {\n p = peek(0);\n if (p.id === "(endline)") {\n i = 1;\n do {\n pn = peek(i);\n i = i + 1;\n } while (pn.id === "(endline)");\n\n if (pn.id !== ";") {\n if (pn.id !== "(string)" && pn.id !== "(number)" &&\n pn.id !== "(regexp)" && pn.identifier !== true &&\n pn.id !== "}") {\n break;\n }\n warning("W033", state.tokens.next);\n } else {\n p = pn;\n }\n } else if (p.id === "}") {\n // Directive with no other statements, warn about missing semicolon\n warning("W033", p);\n } else if (p.id !== ";") {\n break;\n }\n\n indentation();\n advance();\n if (state.directive[state.tokens.curr.value]) {\n warning("W034", state.tokens.curr, state.tokens.curr.value);\n }\n\n if (state.tokens.curr.value === "use strict") {\n if (!state.option["(explicitNewcap)"])\n state.option.newcap = true;\n state.option.undef = true;\n }\n\n // there\'s no directive negation, so always set to true\n state.directive[state.tokens.curr.value] = true;\n\n if (p.id === ";") {\n advance(";");\n }\n continue;\n }\n break;\n }\n }\n\n\n /*\n * Parses a single block. A block is a sequence of statements wrapped in\n * braces.\n *\n * ordinary - true for everything but function bodies and try blocks.\n * stmt - true if block can be a single statement (e.g. in if/for/while).\n * isfunc - true if block is a function body\n */\n function block(ordinary, stmt, isfunc) {\n var a,\n b = inblock,\n old_indent = indent,\n m,\n s = scope,\n t,\n line,\n d;\n\n inblock = ordinary;\n\n if (!ordinary || !state.option.funcscope)\n scope = Object.create(scope);\n\n nonadjacent(state.tokens.curr, state.tokens.next);\n t = state.tokens.next;\n\n var metrics = funct["(metrics)"];\n metrics.nestedBlockDepth += 1;\n metrics.verifyMaxNestedBlockDepthPerFunction();\n\n if (state.tokens.next.id === "{") {\n advance("{");\n line = state.tokens.curr.line;\n if (state.tokens.next.id !== "}") {\n indent += state.option.indent;\n while (!ordinary && state.tokens.next.from > indent) {\n indent += state.option.indent;\n }\n\n if (isfunc) {\n m = {};\n for (d in state.directive) {\n if (_.has(state.directive, d)) {\n m[d] = state.directive[d];\n }\n }\n directives();\n\n if (state.option.strict && funct["(context)"]["(global)"]) {\n if (!m["use strict"] && !state.directive["use strict"]) {\n warning("E007");\n }\n }\n }\n\n a = statements(line);\n\n metrics.statementCount += a.length;\n\n if (isfunc) {\n state.directive = m;\n }\n\n indent -= state.option.indent;\n if (line !== state.tokens.next.line) {\n indentation();\n }\n } else if (line !== state.tokens.next.line) {\n indentation();\n }\n advance("}", t);\n indent = old_indent;\n } else if (!ordinary) {\n error("E021", state.tokens.next, "{", state.tokens.next.value);\n } else {\n if (!stmt || state.option.curly) {\n warning("W116", state.tokens.next, "{", state.tokens.next.value);\n }\n\n noreach = true;\n indent += state.option.indent;\n // test indentation only if statement is in new line\n a = [statement(state.tokens.next.line === state.tokens.curr.line)];\n indent -= state.option.indent;\n noreach = false;\n }\n funct["(verb)"] = null;\n if (!ordinary || !state.option.funcscope) scope = s;\n inblock = b;\n if (ordinary && state.option.noempty && (!a || a.length === 0)) {\n warning("W035");\n }\n metrics.nestedBlockDepth -= 1;\n return a;\n }\n\n\n function countMember(m) {\n if (membersOnly && typeof membersOnly[m] !== "boolean") {\n warning("W036", state.tokens.curr, m);\n }\n if (typeof member[m] === "number") {\n member[m] += 1;\n } else {\n member[m] = 1;\n }\n }\n\n\n function note_implied(tkn) {\n var name = tkn.value, line = tkn.line, a = implied[name];\n if (typeof a === "function") {\n a = false;\n }\n\n if (!a) {\n a = [line];\n implied[name] = a;\n } else if (a[a.length - 1] !== line) {\n a.push(line);\n }\n }\n\n\n // Build the syntax table by declaring the syntactic elements of the language.\n\n type("(number)", function () {\n return this;\n });\n\n type("(string)", function () {\n return this;\n });\n\n state.syntax["(identifier)"] = {\n type: "(identifier)",\n lbp: 0,\n identifier: true,\n nud: function () {\n var v = this.value,\n s = scope[v],\n f;\n\n if (typeof s === "function") {\n // Protection against accidental inheritance.\n s = undefined;\n } else if (typeof s === "boolean") {\n f = funct;\n funct = functions[0];\n addlabel(v, "var");\n s = funct;\n funct = f;\n }\n\n // The name is in scope and defined in the current function.\n if (funct === s) {\n // Change \'unused\' to \'var\', and reject labels.\n switch (funct[v]) {\n case "unused":\n funct[v] = "var";\n break;\n case "unction":\n funct[v] = "function";\n this["function"] = true;\n break;\n case "function":\n this["function"] = true;\n break;\n case "label":\n warning("W037", state.tokens.curr, v);\n break;\n }\n } else if (funct["(global)"]) {\n // The name is not defined in the function. If we are in the global\n // scope, then we have an undefined variable.\n //\n // Operators typeof and delete do not raise runtime errors even if\n // the base object of a reference is null so no need to display warning\n // if we\'re inside of typeof or delete.\n\n if (typeof predefined[v] !== "boolean") {\n // Attempting to subscript a null reference will throw an\n // error, even within the typeof and delete operators\n if (!(anonname === "typeof" || anonname === "delete") ||\n (state.tokens.next && (state.tokens.next.value === "." ||\n state.tokens.next.value === "["))) {\n\n isundef(funct, "W117", state.tokens.curr, v);\n }\n }\n\n note_implied(state.tokens.curr);\n } else {\n // If the name is already defined in the current\n // function, but not as outer, then there is a scope error.\n\n switch (funct[v]) {\n case "closure":\n case "function":\n case "var":\n case "unused":\n warning("W038", state.tokens.curr, v);\n break;\n case "label":\n warning("W037", state.tokens.curr, v);\n break;\n case "outer":\n case "global":\n break;\n default:\n // If the name is defined in an outer function, make an outer entry,\n // and if it was unused, make it var.\n if (s === true) {\n funct[v] = true;\n } else if (s === null) {\n warning("W039", state.tokens.curr, v);\n note_implied(state.tokens.curr);\n } else if (typeof s !== "object") {\n // Operators typeof and delete do not raise runtime errors even\n // if the base object of a reference is null so no need to\n //\n // display warning if we\'re inside of typeof or delete.\n // Attempting to subscript a null reference will throw an\n // error, even within the typeof and delete operators\n if (!(anonname === "typeof" || anonname === "delete") ||\n (state.tokens.next &&\n (state.tokens.next.value === "." || state.tokens.next.value === "["))) {\n\n isundef(funct, "W117", state.tokens.curr, v);\n }\n funct[v] = true;\n note_implied(state.tokens.curr);\n } else {\n switch (s[v]) {\n case "function":\n case "unction":\n this["function"] = true;\n s[v] = "closure";\n funct[v] = s["(global)"] ? "global" : "outer";\n break;\n case "var":\n case "unused":\n s[v] = "closure";\n funct[v] = s["(global)"] ? "global" : "outer";\n break;\n case "closure":\n funct[v] = s["(global)"] ? "global" : "outer";\n break;\n case "label":\n warning("W037", state.tokens.curr, v);\n }\n }\n }\n }\n return this;\n },\n led: function () {\n error("E033", state.tokens.next, state.tokens.next.value);\n }\n };\n\n type("(regexp)", function () {\n return this;\n });\n\n // ECMAScript parser\n\n delim("(endline)");\n delim("(begin)");\n delim("(end)").reach = true;\n delim("(error)").reach = true;\n delim("}").reach = true;\n delim(")");\n delim("]");\n delim("\\"").reach = true;\n delim("\'").reach = true;\n delim(";");\n delim(":").reach = true;\n delim(",");\n delim("#");\n\n reserve("else");\n reserve("case").reach = true;\n reserve("catch");\n reserve("default").reach = true;\n reserve("finally");\n reservevar("arguments", function (x) {\n if (state.directive["use strict"] && funct["(global)"]) {\n warning("E008", x);\n }\n });\n reservevar("eval");\n reservevar("false");\n reservevar("Infinity");\n reservevar("null");\n reservevar("this", function (x) {\n if (state.directive["use strict"] && !state.option.validthis && ((funct["(statement)"] &&\n funct["(name)"].charAt(0) > "Z") || funct["(global)"])) {\n warning("W040", x);\n }\n });\n reservevar("true");\n reservevar("undefined");\n\n assignop("=", "assign", 20);\n assignop("+=", "assignadd", 20);\n assignop("-=", "assignsub", 20);\n assignop("*=", "assignmult", 20);\n assignop("/=", "assigndiv", 20).nud = function () {\n error("E014");\n };\n assignop("%=", "assignmod", 20);\n\n bitwiseassignop("&=", "assignbitand", 20);\n bitwiseassignop("|=", "assignbitor", 20);\n bitwiseassignop("^=", "assignbitxor", 20);\n bitwiseassignop("<<=", "assignshiftleft", 20);\n bitwiseassignop(">>=", "assignshiftright", 20);\n bitwiseassignop(">>>=", "assignshiftrightunsigned", 20);\n infix("?", function (left, that) {\n that.left = left;\n that.right = expression(10);\n advance(":");\n that["else"] = expression(10);\n return that;\n }, 30);\n\n infix("||", "or", 40);\n infix("&&", "and", 50);\n bitwise("|", "bitor", 70);\n bitwise("^", "bitxor", 80);\n bitwise("&", "bitand", 90);\n relation("==", function (left, right) {\n var eqnull = state.option.eqnull && (left.value === "null" || right.value === "null");\n\n if (!eqnull && state.option.eqeqeq)\n warning("W116", this, "===", "==");\n else if (isPoorRelation(left))\n warning("W041", this, "===", left.value);\n else if (isPoorRelation(right))\n warning("W041", this, "===", right.value);\n\n return this;\n });\n relation("===");\n relation("!=", function (left, right) {\n var eqnull = state.option.eqnull &&\n (left.value === "null" || right.value === "null");\n\n if (!eqnull && state.option.eqeqeq) {\n warning("W116", this, "!==", "!=");\n } else if (isPoorRelation(left)) {\n warning("W041", this, "!==", left.value);\n } else if (isPoorRelation(right)) {\n warning("W041", this, "!==", right.value);\n }\n return this;\n });\n relation("!==");\n relation("<");\n relation(">");\n relation("<=");\n relation(">=");\n bitwise("<<", "shiftleft", 120);\n bitwise(">>", "shiftright", 120);\n bitwise(">>>", "shiftrightunsigned", 120);\n infix("in", "in", 120);\n infix("instanceof", "instanceof", 120);\n infix("+", function (left, that) {\n var right = expression(130);\n if (left && right && left.id === "(string)" && right.id === "(string)") {\n left.value += right.value;\n left.character = right.character;\n if (!state.option.scripturl && reg.javascriptURL.test(left.value)) {\n warning("W050", left);\n }\n return left;\n }\n that.left = left;\n that.right = right;\n return that;\n }, 130);\n prefix("+", "num");\n prefix("+++", function () {\n warning("W007");\n this.right = expression(150);\n this.arity = "unary";\n return this;\n });\n infix("+++", function (left) {\n warning("W007");\n this.left = left;\n this.right = expression(130);\n return this;\n }, 130);\n infix("-", "sub", 130);\n prefix("-", "neg");\n prefix("---", function () {\n warning("W006");\n this.right = expression(150);\n this.arity = "unary";\n return this;\n });\n infix("---", function (left) {\n warning("W006");\n this.left = left;\n this.right = expression(130);\n return this;\n }, 130);\n infix("*", "mult", 140);\n infix("/", "div", 140);\n infix("%", "mod", 140);\n\n suffix("++", "postinc");\n prefix("++", "preinc");\n state.syntax["++"].exps = true;\n\n suffix("--", "postdec");\n prefix("--", "predec");\n state.syntax["--"].exps = true;\n prefix("delete", function () {\n var p = expression(0);\n if (!p || (p.id !== "." && p.id !== "[")) {\n warning("W051");\n }\n this.first = p;\n return this;\n }).exps = true;\n\n prefix("~", function () {\n if (state.option.bitwise) {\n warning("W052", this, "~");\n }\n expression(150);\n return this;\n });\n\n prefix("!", function () {\n this.right = expression(150);\n this.arity = "unary";\n\n if (!this.right) { // \'!\' followed by nothing? Give up.\n quit("E041", this.line || 0);\n }\n\n if (bang[this.right.id] === true) {\n warning("W018", this, "!");\n }\n return this;\n });\n\n prefix("typeof", "typeof");\n prefix("new", function () {\n var c = expression(155), i;\n if (c && c.id !== "function") {\n if (c.identifier) {\n c["new"] = true;\n switch (c.value) {\n case "Number":\n case "String":\n case "Boolean":\n case "Math":\n case "JSON":\n warning("W053", state.tokens.prev, c.value);\n break;\n case "Function":\n if (!state.option.evil) {\n warning("W054");\n }\n break;\n case "Date":\n case "RegExp":\n break;\n default:\n if (c.id !== "function") {\n i = c.value.substr(0, 1);\n if (state.option.newcap && (i < "A" || i > "Z") && !_.has(global, c.value)) {\n warning("W055", state.tokens.curr);\n }\n }\n }\n } else {\n if (c.id !== "." && c.id !== "[" && c.id !== "(") {\n warning("W056", state.tokens.curr);\n }\n }\n } else {\n if (!state.option.supernew)\n warning("W057", this);\n }\n adjacent(state.tokens.curr, state.tokens.next);\n if (state.tokens.next.id !== "(" && !state.option.supernew) {\n warning("W058", state.tokens.curr, state.tokens.curr.value);\n }\n this.first = c;\n return this;\n });\n state.syntax["new"].exps = true;\n\n prefix("void").exps = true;\n\n infix(".", function (left, that) {\n adjacent(state.tokens.prev, state.tokens.curr);\n nobreak();\n var m = identifier(false, true);\n\n if (typeof m === "string") {\n countMember(m);\n }\n\n that.left = left;\n that.right = m;\n\n if (m && m === "hasOwnProperty" && state.tokens.next.value === "=") {\n warning("W001");\n }\n\n if (left && left.value === "arguments" && (m === "callee" || m === "caller")) {\n if (state.option.noarg)\n warning("W059", left, m);\n else if (state.directive["use strict"])\n error("E008");\n } else if (!state.option.evil && left && left.value === "document" &&\n (m === "write" || m === "writeln")) {\n warning("W060", left);\n }\n\n if (!state.option.evil && (m === "eval" || m === "execScript")) {\n warning("W061");\n }\n\n return that;\n }, 160, true);\n\n infix("(", function (left, that) {\n if (state.tokens.prev.id !== "}" && state.tokens.prev.id !== ")") {\n nobreak(state.tokens.prev, state.tokens.curr);\n }\n\n nospace();\n if (state.option.immed && !left.immed && left.id === "function") {\n warning("W062");\n }\n\n var n = 0;\n var p = [];\n\n if (left) {\n if (left.type === "(identifier)") {\n if (left.value.match(/^[A-Z]([A-Z0-9_$]*[a-z][A-Za-z0-9_$]*)?$/)) {\n if ("Number String Boolean Date Object".indexOf(left.value) === -1) {\n if (left.value === "Math") {\n warning("W063", left);\n } else if (state.option.newcap) {\n warning("W064", left);\n }\n }\n }\n }\n }\n\n if (state.tokens.next.id !== ")") {\n for (;;) {\n p[p.length] = expression(10);\n n += 1;\n if (state.tokens.next.id !== ",") {\n break;\n }\n comma();\n }\n }\n\n advance(")");\n nospace(state.tokens.prev, state.tokens.curr);\n\n if (typeof left === "object") {\n if (left.value === "parseInt" && n === 1) {\n warning("W065", state.tokens.curr);\n }\n if (!state.option.evil) {\n if (left.value === "eval" || left.value === "Function" ||\n left.value === "execScript") {\n warning("W061", left);\n\n if (p[0] && [0].id === "(string)") {\n addInternalSrc(left, p[0].value);\n }\n } else if (p[0] && p[0].id === "(string)" &&\n (left.value === "setTimeout" ||\n left.value === "setInterval")) {\n warning("W066", left);\n addInternalSrc(left, p[0].value);\n\n // window.setTimeout/setInterval\n } else if (p[0] && p[0].id === "(string)" &&\n left.value === "." &&\n left.left.value === "window" &&\n (left.right === "setTimeout" ||\n left.right === "setInterval")) {\n warning("W066", left);\n addInternalSrc(left, p[0].value);\n }\n }\n if (!left.identifier && left.id !== "." && left.id !== "[" &&\n left.id !== "(" && left.id !== "&&" && left.id !== "||" &&\n left.id !== "?") {\n warning("W067", left);\n }\n }\n\n that.left = left;\n return that;\n }, 155, true).exps = true;\n\n prefix("(", function () {\n nospace();\n\n if (state.tokens.next.id === "function") {\n state.tokens.next.immed = true;\n }\n\n var exprs = [];\n\n if (state.tokens.next.id !== ")") {\n for (;;) {\n exprs.push(expression(0));\n if (state.tokens.next.id !== ",") {\n break;\n }\n comma();\n }\n }\n\n advance(")", this);\n nospace(state.tokens.prev, state.tokens.curr);\n if (state.option.immed && exprs[0].id === "function") {\n if (state.tokens.next.id !== "(" &&\n (state.tokens.next.id !== "." || (peek().value !== "call" && peek().value !== "apply"))) {\n warning("W068", this);\n }\n }\n\n return exprs[0];\n });\n\n infix("[", function (left, that) {\n nobreak(state.tokens.prev, state.tokens.curr);\n nospace();\n var e = expression(0), s;\n if (e && e.type === "(string)") {\n if (!state.option.evil && (e.value === "eval" || e.value === "execScript")) {\n warning("W061", that);\n }\n\n countMember(e.value);\n if (!state.option.sub && reg.identifier.test(e.value)) {\n s = state.syntax[e.value];\n if (!s || !isReserved(s)) {\n warning("W069", state.tokens.prev, e.value);\n }\n }\n }\n advance("]", that);\n\n if (e && e.value === "hasOwnProperty" && state.tokens.next.value === "=") {\n warning("W001");\n }\n\n nospace(state.tokens.prev, state.tokens.curr);\n that.left = left;\n that.right = e;\n return that;\n }, 160, true);\n\n prefix("[", function () {\n var b = state.tokens.curr.line !== state.tokens.next.line;\n this.first = [];\n if (b) {\n indent += state.option.indent;\n if (state.tokens.next.from === indent + state.option.indent) {\n indent += state.option.indent;\n }\n }\n while (state.tokens.next.id !== "(end)") {\n while (state.tokens.next.id === ",") {\n if (!state.option.es5)\n warning("W070");\n advance(",");\n }\n if (state.tokens.next.id === "]") {\n break;\n }\n if (b && state.tokens.curr.line !== state.tokens.next.line) {\n indentation();\n }\n this.first.push(expression(10));\n if (state.tokens.next.id === ",") {\n comma({ allowTrailing: true });\n if (state.tokens.next.id === "]" && !state.option.es5) {\n warning("W070", state.tokens.curr);\n break;\n }\n } else {\n break;\n }\n }\n if (b) {\n indent -= state.option.indent;\n indentation();\n }\n advance("]", this);\n return this;\n }, 160);\n\n\n function property_name() {\n var id = optionalidentifier(false, true);\n\n if (!id) {\n if (state.tokens.next.id === "(string)") {\n id = state.tokens.next.value;\n advance();\n } else if (state.tokens.next.id === "(number)") {\n id = state.tokens.next.value.toString();\n advance();\n }\n }\n\n if (id === "hasOwnProperty") {\n warning("W001");\n }\n\n return id;\n }\n\n\n function functionparams() {\n var next = state.tokens.next;\n var params = [];\n var ident;\n\n advance("(");\n nospace();\n\n if (state.tokens.next.id === ")") {\n advance(")");\n return;\n }\n\n for (;;) {\n ident = identifier(true);\n params.push(ident);\n addlabel(ident, "unused", state.tokens.curr);\n if (state.tokens.next.id === ",") {\n comma();\n } else {\n advance(")", next);\n nospace(state.tokens.prev, state.tokens.curr);\n return params;\n }\n }\n }\n\n\n function doFunction(name, statement) {\n var f;\n var oldOption = state.option;\n var oldScope = scope;\n\n state.option = Object.create(state.option);\n scope = Object.create(scope);\n\n funct = {\n "(name)" : name || "\\"" + anonname + "\\"",\n "(line)" : state.tokens.next.line,\n "(character)": state.tokens.next.character,\n "(context)" : funct,\n "(breakage)" : 0,\n "(loopage)" : 0,\n "(metrics)" : createMetrics(state.tokens.next),\n "(scope)" : scope,\n "(statement)": statement,\n "(tokens)" : {}\n };\n\n f = funct;\n state.tokens.curr.funct = funct;\n\n functions.push(funct);\n\n if (name) {\n addlabel(name, "function");\n }\n\n funct["(params)"] = functionparams();\n funct["(metrics)"].verifyMaxParametersPerFunction(funct["(params)"]);\n\n block(false, false, true);\n\n funct["(metrics)"].verifyMaxStatementsPerFunction();\n funct["(metrics)"].verifyMaxComplexityPerFunction();\n funct["(unusedOption)"] = state.option.unused;\n\n scope = oldScope;\n state.option = oldOption;\n funct["(last)"] = state.tokens.curr.line;\n funct["(lastcharacter)"] = state.tokens.curr.character;\n funct = funct["(context)"];\n\n return f;\n }\n\n function createMetrics(functionStartToken) {\n return {\n statementCount: 0,\n nestedBlockDepth: -1,\n ComplexityCount: 1,\n verifyMaxStatementsPerFunction: function () {\n if (state.option.maxstatements &&\n this.statementCount > state.option.maxstatements) {\n warning("W071", functionStartToken, this.statementCount);\n }\n },\n\n verifyMaxParametersPerFunction: function (params) {\n params = params || [];\n\n if (state.option.maxparams && params.length > state.option.maxparams) {\n warning("W072", functionStartToken, params.length);\n }\n },\n\n verifyMaxNestedBlockDepthPerFunction: function () {\n if (state.option.maxdepth &&\n this.nestedBlockDepth > 0 &&\n this.nestedBlockDepth === state.option.maxdepth + 1) {\n warning("W073", null, this.nestedBlockDepth);\n }\n },\n\n verifyMaxComplexityPerFunction: function () {\n var max = state.option.maxcomplexity;\n var cc = this.ComplexityCount;\n if (max && cc > max) {\n warning("W074", functionStartToken, cc);\n }\n }\n };\n }\n\n function increaseComplexityCount() {\n funct["(metrics)"].ComplexityCount += 1;\n }\n\n // Parse assignments that were found instead of conditionals.\n // For example: if (a = 1) { ... }\n\n function parseCondAssignment() {\n switch (state.tokens.next.id) {\n case "=":\n case "+=":\n case "-=":\n case "*=":\n case "%=":\n case "&=":\n case "|=":\n case "^=":\n case "/=":\n if (!state.option.boss) {\n warning("W084");\n }\n\n advance(state.tokens.next.id);\n expression(20);\n }\n }\n\n\n (function (x) {\n x.nud = function () {\n var b, f, i, p, t;\n var props = {}; // All properties, including accessors\n\n function saveProperty(name, tkn) {\n if (props[name] && _.has(props, name))\n warning("W075", state.tokens.next, i);\n else\n props[name] = {};\n\n props[name].basic = true;\n props[name].basictkn = tkn;\n }\n\n function saveSetter(name, tkn) {\n if (props[name] && _.has(props, name)) {\n if (props[name].basic || props[name].setter)\n warning("W075", state.tokens.next, i);\n } else {\n props[name] = {};\n }\n\n props[name].setter = true;\n props[name].setterToken = tkn;\n }\n\n function saveGetter(name) {\n if (props[name] && _.has(props, name)) {\n if (props[name].basic || props[name].getter)\n warning("W075", state.tokens.next, i);\n } else {\n props[name] = {};\n }\n\n props[name].getter = true;\n props[name].getterToken = state.tokens.curr;\n }\n\n b = state.tokens.curr.line !== state.tokens.next.line;\n if (b) {\n indent += state.option.indent;\n if (state.tokens.next.from === indent + state.option.indent) {\n indent += state.option.indent;\n }\n }\n\n for (;;) {\n if (state.tokens.next.id === "}") {\n break;\n }\n\n if (b) {\n indentation();\n }\n\n if (state.tokens.next.value === "get" && peek().id !== ":") {\n advance("get");\n\n if (!state.option.es5) {\n error("E034");\n }\n\n i = property_name();\n if (!i) {\n error("E035");\n }\n\n saveGetter(i);\n t = state.tokens.next;\n adjacent(state.tokens.curr, state.tokens.next);\n f = doFunction();\n p = f["(params)"];\n\n if (p) {\n warning("W076", t, p[0], i);\n }\n\n adjacent(state.tokens.curr, state.tokens.next);\n } else if (state.tokens.next.value === "set" && peek().id !== ":") {\n advance("set");\n\n if (!state.option.es5) {\n error("E034");\n }\n\n i = property_name();\n if (!i) {\n error("E035");\n }\n\n saveSetter(i, state.tokens.next);\n t = state.tokens.next;\n adjacent(state.tokens.curr, state.tokens.next);\n f = doFunction();\n p = f["(params)"];\n\n if (!p || p.length !== 1) {\n warning("W077", t, i);\n }\n } else {\n i = property_name();\n saveProperty(i, state.tokens.next);\n\n if (typeof i !== "string") {\n break;\n }\n\n advance(":");\n nonadjacent(state.tokens.curr, state.tokens.next);\n expression(10);\n }\n\n countMember(i);\n if (state.tokens.next.id === ",") {\n comma({ allowTrailing: true });\n if (state.tokens.next.id === ",") {\n warning("W070", state.tokens.curr);\n } else if (state.tokens.next.id === "}" && !state.option.es5) {\n warning("W070", state.tokens.curr);\n }\n } else {\n break;\n }\n }\n if (b) {\n indent -= state.option.indent;\n indentation();\n }\n advance("}", this);\n\n // Check for lonely setters if in the ES5 mode.\n if (state.option.es5) {\n for (var name in props) {\n if (_.has(props, name) && props[name].setter && !props[name].getter) {\n warning("W078", props[name].setterToken);\n }\n }\n }\n return this;\n };\n x.fud = function () {\n error("E036", state.tokens.curr);\n };\n }(delim("{")));\n\n // This Function is called when esnext option is set to true\n // it adds the `const` statement to JSHINT\n\n useESNextSyntax = function () {\n var conststatement = stmt("const", function (prefix) {\n var id, name, value;\n\n this.first = [];\n for (;;) {\n nonadjacent(state.tokens.curr, state.tokens.next);\n id = identifier();\n if (funct[id] === "const") {\n warning("E011", null, id);\n }\n if (funct["(global)"] && predefined[id] === false) {\n warning("W079", state.tokens.curr, id);\n }\n addlabel(id, "const");\n if (prefix) {\n break;\n }\n name = state.tokens.curr;\n this.first.push(state.tokens.curr);\n\n if (state.tokens.next.id !== "=") {\n warning("E012", state.tokens.curr, id);\n }\n\n if (state.tokens.next.id === "=") {\n nonadjacent(state.tokens.curr, state.tokens.next);\n advance("=");\n nonadjacent(state.tokens.curr, state.tokens.next);\n if (state.tokens.next.id === "undefined") {\n warning("W080", state.tokens.curr, id);\n }\n if (peek(0).id === "=" && state.tokens.next.identifier) {\n error("E037", state.tokens.next, state.tokens.next.value);\n }\n value = expression(0);\n name.first = value;\n }\n\n if (state.tokens.next.id !== ",") {\n break;\n }\n comma();\n }\n return this;\n });\n conststatement.exps = true;\n };\n\n var varstatement = stmt("var", function (prefix) {\n // JavaScript does not have block scope. It only has function scope. So,\n // declaring a variable in a block can have unexpected consequences.\n var id, name, value;\n\n if (funct["(onevar)"] && state.option.onevar) {\n warning("W081");\n } else if (!funct["(global)"]) {\n funct["(onevar)"] = true;\n }\n\n this.first = [];\n\n for (;;) {\n nonadjacent(state.tokens.curr, state.tokens.next);\n id = identifier();\n\n if (state.option.esnext && funct[id] === "const") {\n warning("E011", null, id);\n }\n\n if (funct["(global)"] && predefined[id] === false) {\n warning("W079", state.tokens.curr, id);\n }\n\n addlabel(id, "unused", state.tokens.curr);\n\n if (prefix) {\n break;\n }\n\n name = state.tokens.curr;\n this.first.push(state.tokens.curr);\n\n if (state.tokens.next.id === "=") {\n nonadjacent(state.tokens.curr, state.tokens.next);\n advance("=");\n nonadjacent(state.tokens.curr, state.tokens.next);\n if (state.tokens.next.id === "undefined") {\n warning("W080", state.tokens.curr, id);\n }\n if (peek(0).id === "=" && state.tokens.next.identifier) {\n error("E038", state.tokens.next, state.tokens.next.value);\n }\n value = expression(0);\n name.first = value;\n }\n if (state.tokens.next.id !== ",") {\n break;\n }\n comma();\n }\n return this;\n });\n varstatement.exps = true;\n\n blockstmt("function", function () {\n if (inblock) {\n warning("W082", state.tokens.curr);\n\n }\n var i = identifier();\n if (state.option.esnext && funct[i] === "const") {\n warning("E011", null, i);\n }\n adjacent(state.tokens.curr, state.tokens.next);\n addlabel(i, "unction", state.tokens.curr);\n\n doFunction(i, { statement: true });\n if (state.tokens.next.id === "(" && state.tokens.next.line === state.tokens.curr.line) {\n error("E039");\n }\n return this;\n });\n\n prefix("function", function () {\n var i = optionalidentifier();\n if (i || state.option.gcl) {\n adjacent(state.tokens.curr, state.tokens.next);\n } else {\n nonadjacent(state.tokens.curr, state.tokens.next);\n }\n doFunction(i);\n if (!state.option.loopfunc && funct["(loopage)"]) {\n warning("W083");\n }\n return this;\n });\n\n blockstmt("if", function () {\n var t = state.tokens.next;\n increaseComplexityCount();\n advance("(");\n nonadjacent(this, t);\n nospace();\n expression(20);\n parseCondAssignment();\n advance(")", t);\n nospace(state.tokens.prev, state.tokens.curr);\n block(true, true);\n if (state.tokens.next.id === "else") {\n nonadjacent(state.tokens.curr, state.tokens.next);\n advance("else");\n if (state.tokens.next.id === "if" || state.tokens.next.id === "switch") {\n statement(true);\n } else {\n block(true, true);\n }\n }\n return this;\n });\n\n blockstmt("try", function () {\n var b;\n\n function doCatch() {\n var oldScope = scope;\n var e;\n\n advance("catch");\n nonadjacent(state.tokens.curr, state.tokens.next);\n advance("(");\n\n scope = Object.create(oldScope);\n\n e = state.tokens.next.value;\n if (state.tokens.next.type !== "(identifier)") {\n e = null;\n warning("E030", state.tokens.next, e);\n }\n\n advance();\n advance(")");\n\n funct = {\n "(name)" : "(catch)",\n "(line)" : state.tokens.next.line,\n "(character)": state.tokens.next.character,\n "(context)" : funct,\n "(breakage)" : funct["(breakage)"],\n "(loopage)" : funct["(loopage)"],\n "(scope)" : scope,\n "(statement)": false,\n "(metrics)" : createMetrics(state.tokens.next),\n "(catch)" : true,\n "(tokens)" : {}\n };\n\n if (e) {\n addlabel(e, "exception");\n }\n\n state.tokens.curr.funct = funct;\n functions.push(funct);\n\n block(false);\n\n scope = oldScope;\n\n funct["(last)"] = state.tokens.curr.line;\n funct["(lastcharacter)"] = state.tokens.curr.character;\n funct = funct["(context)"];\n }\n\n block(false);\n\n if (state.tokens.next.id === "catch") {\n increaseComplexityCount();\n doCatch();\n b = true;\n }\n\n if (state.tokens.next.id === "finally") {\n advance("finally");\n block(false);\n return;\n } else if (!b) {\n error("E021", state.tokens.next, "catch", state.tokens.next.value);\n }\n\n return this;\n });\n\n blockstmt("while", function () {\n var t = state.tokens.next;\n funct["(breakage)"] += 1;\n funct["(loopage)"] += 1;\n increaseComplexityCount();\n advance("(");\n nonadjacent(this, t);\n nospace();\n expression(20);\n parseCondAssignment();\n advance(")", t);\n nospace(state.tokens.prev, state.tokens.curr);\n block(true, true);\n funct["(breakage)"] -= 1;\n funct["(loopage)"] -= 1;\n return this;\n }).labelled = true;\n\n blockstmt("with", function () {\n var t = state.tokens.next;\n if (state.directive["use strict"]) {\n error("E010", state.tokens.curr);\n } else if (!state.option.withstmt) {\n warning("W085", state.tokens.curr);\n }\n\n advance("(");\n nonadjacent(this, t);\n nospace();\n expression(0);\n advance(")", t);\n nospace(state.tokens.prev, state.tokens.curr);\n block(true, true);\n\n return this;\n });\n\n blockstmt("switch", function () {\n var t = state.tokens.next,\n g = false;\n funct["(breakage)"] += 1;\n advance("(");\n nonadjacent(this, t);\n nospace();\n this.condition = expression(20);\n advance(")", t);\n nospace(state.tokens.prev, state.tokens.curr);\n nonadjacent(state.tokens.curr, state.tokens.next);\n t = state.tokens.next;\n advance("{");\n nonadjacent(state.tokens.curr, state.tokens.next);\n indent += state.option.indent;\n this.cases = [];\n\n for (;;) {\n switch (state.tokens.next.id) {\n case "case":\n switch (funct["(verb)"]) {\n case "break":\n case "case":\n case "continue":\n case "return":\n case "switch":\n case "throw":\n break;\n default:\n // You can tell JSHint that you don\'t use break intentionally by\n // adding a comment /* falls through */ on a line just before\n // the next `case`.\n if (!reg.fallsThrough.test(state.lines[state.tokens.next.line - 2])) {\n warning("W086", state.tokens.curr, "case");\n }\n }\n indentation(-state.option.indent);\n advance("case");\n this.cases.push(expression(20));\n increaseComplexityCount();\n g = true;\n advance(":");\n funct["(verb)"] = "case";\n break;\n case "default":\n switch (funct["(verb)"]) {\n case "break":\n case "continue":\n case "return":\n case "throw":\n break;\n default:\n // Do not display a warning if \'default\' is the first statement or if\n // there is a special /* falls through */ comment.\n if (this.cases.length) {\n if (!reg.fallsThrough.test(state.lines[state.tokens.next.line - 2])) {\n warning("W086", state.tokens.curr, "default");\n }\n }\n }\n indentation(-state.option.indent);\n advance("default");\n g = true;\n advance(":");\n break;\n case "}":\n indent -= state.option.indent;\n indentation();\n advance("}", t);\n funct["(breakage)"] -= 1;\n funct["(verb)"] = undefined;\n return;\n case "(end)":\n error("E023", state.tokens.next, "}");\n return;\n default:\n if (g) {\n switch (state.tokens.curr.id) {\n case ",":\n error("E040");\n return;\n case ":":\n g = false;\n statements();\n break;\n default:\n error("E025", state.tokens.curr);\n return;\n }\n } else {\n if (state.tokens.curr.id === ":") {\n advance(":");\n error("E024", state.tokens.curr, ":");\n statements();\n } else {\n error("E021", state.tokens.next, "case", state.tokens.next.value);\n return;\n }\n }\n }\n }\n }).labelled = true;\n\n stmt("debugger", function () {\n if (!state.option.debug) {\n warning("W087");\n }\n return this;\n }).exps = true;\n\n (function () {\n var x = stmt("do", function () {\n funct["(breakage)"] += 1;\n funct["(loopage)"] += 1;\n increaseComplexityCount();\n\n this.first = block(true);\n advance("while");\n var t = state.tokens.next;\n nonadjacent(state.tokens.curr, t);\n advance("(");\n nospace();\n expression(20);\n parseCondAssignment();\n advance(")", t);\n nospace(state.tokens.prev, state.tokens.curr);\n funct["(breakage)"] -= 1;\n funct["(loopage)"] -= 1;\n return this;\n });\n x.labelled = true;\n x.exps = true;\n }());\n\n blockstmt("for", function () {\n var s, t = state.tokens.next;\n funct["(breakage)"] += 1;\n funct["(loopage)"] += 1;\n increaseComplexityCount();\n advance("(");\n nonadjacent(this, t);\n nospace();\n if (peek(state.tokens.next.id === "var" ? 1 : 0).id === "in") {\n if (state.tokens.next.id === "var") {\n advance("var");\n varstatement.fud.call(varstatement, true);\n } else {\n switch (funct[state.tokens.next.value]) {\n case "unused":\n funct[state.tokens.next.value] = "var";\n break;\n case "var":\n break;\n default:\n warning("W088", state.tokens.next, state.tokens.next.value);\n }\n advance();\n }\n advance("in");\n expression(20);\n advance(")", t);\n s = block(true, true);\n if (state.option.forin && s && (s.length > 1 || typeof s[0] !== "object" ||\n s[0].value !== "if")) {\n warning("W089", this);\n }\n funct["(breakage)"] -= 1;\n funct["(loopage)"] -= 1;\n return this;\n } else {\n if (state.tokens.next.id !== ";") {\n if (state.tokens.next.id === "var") {\n advance("var");\n varstatement.fud.call(varstatement);\n } else {\n for (;;) {\n expression(0, "for");\n if (state.tokens.next.id !== ",") {\n break;\n }\n comma();\n }\n }\n }\n nolinebreak(state.tokens.curr);\n advance(";");\n if (state.tokens.next.id !== ";") {\n expression(20);\n parseCondAssignment();\n }\n nolinebreak(state.tokens.curr);\n advance(";");\n if (state.tokens.next.id === ";") {\n error("E021", state.tokens.next, ")", ";");\n }\n if (state.tokens.next.id !== ")") {\n for (;;) {\n expression(0, "for");\n if (state.tokens.next.id !== ",") {\n break;\n }\n comma();\n }\n }\n advance(")", t);\n nospace(state.tokens.prev, state.tokens.curr);\n block(true, true);\n funct["(breakage)"] -= 1;\n funct["(loopage)"] -= 1;\n return this;\n }\n }).labelled = true;\n\n\n stmt("break", function () {\n var v = state.tokens.next.value;\n\n if (funct["(breakage)"] === 0)\n warning("W052", state.tokens.next, this.value);\n\n if (!state.option.asi)\n nolinebreak(this);\n\n if (state.tokens.next.id !== ";") {\n if (state.tokens.curr.line === state.tokens.next.line) {\n if (funct[v] !== "label") {\n warning("W090", state.tokens.next, v);\n } else if (scope[v] !== funct) {\n warning("W091", state.tokens.next, v);\n }\n this.first = state.tokens.next;\n advance();\n }\n }\n reachable("break");\n return this;\n }).exps = true;\n\n\n stmt("continue", function () {\n var v = state.tokens.next.value;\n\n if (funct["(breakage)"] === 0)\n warning("W052", state.tokens.next, this.value);\n\n if (!state.option.asi)\n nolinebreak(this);\n\n if (state.tokens.next.id !== ";") {\n if (state.tokens.curr.line === state.tokens.next.line) {\n if (funct[v] !== "label") {\n warning("W090", state.tokens.next, v);\n } else if (scope[v] !== funct) {\n warning("W091", state.tokens.next, v);\n }\n this.first = state.tokens.next;\n advance();\n }\n } else if (!funct["(loopage)"]) {\n warning("W052", state.tokens.next, this.value);\n }\n reachable("continue");\n return this;\n }).exps = true;\n\n\n stmt("return", function () {\n if (this.line === state.tokens.next.line) {\n if (state.tokens.next.id === "(regexp)")\n warning("W092");\n\n if (state.tokens.next.id !== ";" && !state.tokens.next.reach) {\n nonadjacent(state.tokens.curr, state.tokens.next);\n this.first = expression(0);\n\n if (this.first.type === "(punctuator)" && this.first.value === "=" && !state.option.boss) {\n warningAt("W093", this.first.line, this.first.character);\n }\n }\n } else if (!state.option.asi) {\n nolinebreak(this); // always warn (Line breaking error)\n }\n reachable("return");\n return this;\n }).exps = true;\n\n\n stmt("throw", function () {\n nolinebreak(this);\n nonadjacent(state.tokens.curr, state.tokens.next);\n this.first = expression(20);\n reachable("throw");\n return this;\n }).exps = true;\n\n // Future Reserved Words\n\n FutureReservedWord("abstract");\n FutureReservedWord("boolean");\n FutureReservedWord("byte");\n FutureReservedWord("char");\n FutureReservedWord("class", { es5: true });\n FutureReservedWord("double");\n FutureReservedWord("enum", { es5: true });\n FutureReservedWord("export", { es5: true });\n FutureReservedWord("extends", { es5: true });\n FutureReservedWord("final");\n FutureReservedWord("float");\n FutureReservedWord("goto");\n FutureReservedWord("implements", { es5: true, strictOnly: true });\n FutureReservedWord("import", { es5: true });\n FutureReservedWord("int");\n FutureReservedWord("interface");\n FutureReservedWord("let", { es5: true, strictOnly: true });\n FutureReservedWord("long");\n FutureReservedWord("native");\n FutureReservedWord("package", { es5: true, strictOnly: true });\n FutureReservedWord("private", { es5: true, strictOnly: true });\n FutureReservedWord("protected", { es5: true, strictOnly: true });\n FutureReservedWord("public", { es5: true, strictOnly: true });\n FutureReservedWord("short");\n FutureReservedWord("static", { es5: true, strictOnly: true });\n FutureReservedWord("super", { es5: true });\n FutureReservedWord("synchronized");\n FutureReservedWord("throws");\n FutureReservedWord("transient");\n FutureReservedWord("volatile");\n FutureReservedWord("yield", { es5: true, strictOnly: true });\n\n // Parse JSON\n\n function jsonValue() {\n\n function jsonObject() {\n var o = {}, t = state.tokens.next;\n advance("{");\n if (state.tokens.next.id !== "}") {\n for (;;) {\n if (state.tokens.next.id === "(end)") {\n error("E026", state.tokens.next, t.line);\n } else if (state.tokens.next.id === "}") {\n warning("W094", state.tokens.curr);\n break;\n } else if (state.tokens.next.id === ",") {\n error("E028", state.tokens.next);\n } else if (state.tokens.next.id !== "(string)") {\n warning("W095", state.tokens.next, state.tokens.next.value);\n }\n if (o[state.tokens.next.value] === true) {\n warning("W075", state.tokens.next, state.tokens.next.value);\n } else if ((state.tokens.next.value === "__proto__" &&\n !state.option.proto) || (state.tokens.next.value === "__iterator__" &&\n !state.option.iterator)) {\n warning("W096", state.tokens.next, state.tokens.next.value);\n } else {\n o[state.tokens.next.value] = true;\n }\n advance();\n advance(":");\n jsonValue();\n if (state.tokens.next.id !== ",") {\n break;\n }\n advance(",");\n }\n }\n advance("}");\n }\n\n function jsonArray() {\n var t = state.tokens.next;\n advance("[");\n if (state.tokens.next.id !== "]") {\n for (;;) {\n if (state.tokens.next.id === "(end)") {\n error("E027", state.tokens.next, t.line);\n } else if (state.tokens.next.id === "]") {\n warning("W094", state.tokens.curr);\n break;\n } else if (state.tokens.next.id === ",") {\n error("E028", state.tokens.next);\n }\n jsonValue();\n if (state.tokens.next.id !== ",") {\n break;\n }\n advance(",");\n }\n }\n advance("]");\n }\n\n switch (state.tokens.next.id) {\n case "{":\n jsonObject();\n break;\n case "[":\n jsonArray();\n break;\n case "true":\n case "false":\n case "null":\n case "(number)":\n case "(string)":\n advance();\n break;\n case "-":\n advance("-");\n if (state.tokens.curr.character !== state.tokens.next.from) {\n warning("W011", state.tokens.curr);\n }\n adjacent(state.tokens.curr, state.tokens.next);\n advance("(number)");\n break;\n default:\n error("E003", state.tokens.next);\n }\n }\n\n\n // The actual JSHINT function itself.\n var itself = function (s, o, g) {\n var a, i, k, x;\n var optionKeys;\n var newOptionObj = {};\n\n state.reset();\n\n if (o && o.scope) {\n JSHINT.scope = o.scope;\n } else {\n JSHINT.errors = [];\n JSHINT.undefs = [];\n JSHINT.internals = [];\n JSHINT.blacklist = {};\n JSHINT.scope = "(main)";\n }\n\n predefined = Object.create(null);\n combine(predefined, vars.ecmaIdentifiers);\n combine(predefined, vars.reservedVars);\n\n combine(predefined, g || {});\n\n declared = Object.create(null);\n exported = Object.create(null);\n ignored = Object.create(null);\n\n if (o) {\n a = o.predef;\n if (a) {\n if (!Array.isArray(a) && typeof a === "object") {\n a = Object.keys(a);\n }\n\n a.forEach(function (item) {\n var slice, prop;\n\n if (item[0] === "-") {\n slice = item.slice(1);\n JSHINT.blacklist[slice] = slice;\n } else {\n prop = Object.getOwnPropertyDescriptor(o.predef, item);\n predefined[item] = prop ? prop.value : false;\n }\n });\n }\n\n optionKeys = Object.keys(o);\n for (x = 0; x < optionKeys.length; x++) {\n if (/^-W\\d{3}$/g.test(optionKeys[x])) {\n ignored[optionKeys[x].slice(1)] = true;\n } else {\n newOptionObj[optionKeys[x]] = o[optionKeys[x]];\n\n if (optionKeys[x] === "newcap" && o[optionKeys[x]] === false)\n newOptionObj["(explicitNewcap)"] = true;\n\n if (optionKeys[x] === "indent")\n newOptionObj["(explicitIndent)"] = true;\n }\n }\n }\n\n state.option = newOptionObj;\n\n state.option.indent = state.option.indent || 4;\n state.option.maxerr = state.option.maxerr || 50;\n\n indent = 1;\n global = Object.create(predefined);\n scope = global;\n funct = {\n "(global)": true,\n "(name)": "(global)",\n "(scope)": scope,\n "(breakage)": 0,\n "(loopage)": 0,\n "(tokens)": {},\n "(metrics)": createMetrics(state.tokens.next)\n };\n functions = [funct];\n urls = [];\n stack = null;\n member = {};\n membersOnly = null;\n implied = {};\n inblock = false;\n lookahead = [];\n warnings = 0;\n unuseds = [];\n\n if (!isString(s) && !Array.isArray(s)) {\n errorAt("E004", 0);\n return false;\n }\n\n var api = {\n get isJSON() {\n return state.jsonMode;\n },\n\n getOption: function (name) {\n return state.option[name] || null;\n },\n\n getCache: function (name) {\n return state.cache[name];\n },\n\n setCache: function (name, value) {\n state.cache[name] = value;\n },\n\n warn: function (code, data) {\n warningAt.apply(null, [ code, data.line, data.char ].concat(data.data));\n },\n\n on: function (names, listener) {\n names.split(" ").forEach(function (name) {\n emitter.on(name, listener);\n }.bind(this));\n }\n };\n\n emitter.removeAllListeners();\n (extraModules || []).forEach(function (func) {\n func(api);\n });\n\n state.tokens.prev = state.tokens.curr = state.tokens.next = state.syntax["(begin)"];\n\n lex = new Lexer(s);\n\n lex.on("warning", function (ev) {\n warningAt.apply(null, [ ev.code, ev.line, ev.character].concat(ev.data));\n });\n\n lex.on("error", function (ev) {\n errorAt.apply(null, [ ev.code, ev.line, ev.character ].concat(ev.data));\n });\n\n lex.on("fatal", function (ev) {\n quit("E041", ev.line, ev.from);\n });\n\n lex.on("Identifier", function (ev) {\n emitter.emit("Identifier", ev);\n });\n\n lex.on("String", function (ev) {\n emitter.emit("String", ev);\n });\n\n lex.on("Number", function (ev) {\n emitter.emit("Number", ev);\n });\n\n lex.start();\n\n // Check options\n for (var name in o) {\n if (_.has(o, name)) {\n checkOption(name, state.tokens.curr);\n }\n }\n\n assume();\n\n // combine the passed globals after we\'ve assumed all our options\n combine(predefined, g || {});\n\n //reset values\n comma.first = true;\n\n try {\n advance();\n switch (state.tokens.next.id) {\n case "{":\n case "[":\n state.option.laxbreak = true;\n state.jsonMode = true;\n jsonValue();\n break;\n default:\n directives();\n\n if (state.directive["use strict"]) {\n if (!state.option.globalstrict && !state.option.node) {\n warning("W097", state.tokens.prev);\n }\n }\n\n statements();\n }\n advance((state.tokens.next && state.tokens.next.value !== ".") ? "(end)" : undefined);\n\n var markDefined = function (name, context) {\n do {\n if (typeof context[name] === "string") {\n // JSHINT marks unused variables as \'unused\' and\n // unused function declaration as \'unction\'. This\n // code changes such instances back \'var\' and\n // \'closure\' so that the code in JSHINT.data()\n // doesn\'t think they\'re unused.\n\n if (context[name] === "unused")\n context[name] = "var";\n else if (context[name] === "unction")\n context[name] = "closure";\n\n return true;\n }\n\n context = context["(context)"];\n } while (context);\n\n return false;\n };\n\n var clearImplied = function (name, line) {\n if (!implied[name])\n return;\n\n var newImplied = [];\n for (var i = 0; i < implied[name].length; i += 1) {\n if (implied[name][i] !== line)\n newImplied.push(implied[name][i]);\n }\n\n if (newImplied.length === 0)\n delete implied[name];\n else\n implied[name] = newImplied;\n };\n\n var warnUnused = function (name, tkn, type, unused_opt) {\n var line = tkn.line;\n var chr = tkn.character;\n\n if (unused_opt === undefined) {\n unused_opt = state.option.unused;\n }\n\n if (unused_opt === true) {\n unused_opt = "last-param";\n }\n\n var warnable_types = {\n "vars": ["var"],\n "last-param": ["var", "last-param"],\n "strict": ["var", "param", "last-param"]\n };\n\n if (unused_opt) {\n if (warnable_types[unused_opt] && warnable_types[unused_opt].indexOf(type) !== -1) {\n warningAt("W098", line, chr, name);\n }\n }\n\n unuseds.push({\n name: name,\n line: line,\n character: chr\n });\n };\n\n var checkUnused = function (func, key) {\n var type = func[key];\n var tkn = func["(tokens)"][key];\n\n if (key.charAt(0) === "(")\n return;\n\n if (type !== "unused" && type !== "unction")\n return;\n\n // Params are checked separately from other variables.\n if (func["(params)"] && func["(params)"].indexOf(key) !== -1)\n return;\n\n // Variable is in global scope and defined as exported.\n if (func["(global)"] && _.has(exported, key)) {\n return;\n }\n\n warnUnused(key, tkn, "var");\n };\n\n // Check queued \'x is not defined\' instances to see if they\'re still undefined.\n for (i = 0; i < JSHINT.undefs.length; i += 1) {\n k = JSHINT.undefs[i].slice(0);\n\n if (markDefined(k[2].value, k[0])) {\n clearImplied(k[2].value, k[2].line);\n } else if (state.option.undef) {\n warning.apply(warning, k.slice(1));\n }\n }\n\n functions.forEach(function (func) {\n if (func["(unusedOption)"] === false) {\n return;\n }\n\n for (var key in func) {\n if (_.has(func, key)) {\n checkUnused(func, key);\n }\n }\n\n if (!func["(params)"])\n return;\n\n var params = func["(params)"].slice();\n var param = params.pop();\n var type, unused_type;\n\n while (param) {\n type = func[param];\n unused_type = (params.length === func["(params)"].length - 1 ? "last-param" : "param");\n\n // \'undefined\' is a special case for (function (window, undefined) { ... })();\n // patterns.\n\n if (param === "undefined")\n return;\n\n if (type === "unused" || type === "unction") {\n warnUnused(param, func["(tokens)"][param], unused_type, func["(unusedOption)"]);\n }\n\n param = params.pop();\n }\n });\n\n for (var key in declared) {\n if (_.has(declared, key) && !_.has(global, key)) {\n warnUnused(key, declared[key], "var");\n }\n }\n\n } catch (err) {\n if (err && err.name === "JSHintError") {\n var nt = state.tokens.next || {};\n JSHINT.errors.push({\n scope : "(main)",\n raw : err.raw,\n reason : err.message,\n line : err.line || nt.line,\n character : err.character || nt.from\n }, null);\n } else {\n throw err;\n }\n }\n\n // Loop over the listed "internals", and check them as well.\n\n if (JSHINT.scope === "(main)") {\n o = o || {};\n\n for (i = 0; i < JSHINT.internals.length; i += 1) {\n k = JSHINT.internals[i];\n o.scope = k.elem;\n itself(k.value, o, g);\n }\n }\n\n return JSHINT.errors.length === 0;\n };\n\n // Modules.\n itself.addModule = function (func) {\n extraModules.push(func);\n };\n\n itself.addModule(style.register);\n\n // Data summary.\n itself.data = function () {\n var data = {\n functions: [],\n options: state.option\n };\n var implieds = [];\n var members = [];\n var fu, f, i, j, n, globals;\n\n if (itself.errors.length) {\n data.errors = itself.errors;\n }\n\n if (state.jsonMode) {\n data.json = true;\n }\n\n for (n in implied) {\n if (_.has(implied, n)) {\n implieds.push({\n name: n,\n line: implied[n]\n });\n }\n }\n\n if (implieds.length > 0) {\n data.implieds = implieds;\n }\n\n if (urls.length > 0) {\n data.urls = urls;\n }\n\n globals = Object.keys(scope);\n if (globals.length > 0) {\n data.globals = globals;\n }\n\n for (i = 1; i < functions.length; i += 1) {\n f = functions[i];\n fu = {};\n\n for (j = 0; j < functionicity.length; j += 1) {\n fu[functionicity[j]] = [];\n }\n\n for (j = 0; j < functionicity.length; j += 1) {\n if (fu[functionicity[j]].length === 0) {\n delete fu[functionicity[j]];\n }\n }\n\n fu.name = f["(name)"];\n fu.param = f["(params)"];\n fu.line = f["(line)"];\n fu.character = f["(character)"];\n fu.last = f["(last)"];\n fu.lastcharacter = f["(lastcharacter)"];\n data.functions.push(fu);\n }\n\n if (unuseds.length > 0) {\n data.unused = unuseds;\n }\n\n members = [];\n for (n in member) {\n if (typeof member[n] === "number") {\n data.member = member;\n break;\n }\n }\n\n return data;\n };\n\n itself.jshint = itself;\n\n return itself;\n}());\n\n// Make JSHINT a Node module, if possible.\nif (typeof exports === "object" && exports) {\n exports.JSHINT = JSHINT;\n}\n\n//@ sourceURL=/src/stable/jshint.js')); -require("/src/stable/jshint.js");JSHINT=require("/src/stable/jshint.js").JSHINT})(); -/* END INSERT */ - -realExports.JSHINT = JSHINT; -exports = realExports; - -// jshint-endignore - -})(typeof exports == "undefined" ? (typeof doctest == "undefined" ? doctest = {} : doctest) : exports); diff --git a/togetherjs/tests/doctestjs/examples/examples-2.html b/togetherjs/tests/doctestjs/examples/examples-2.html deleted file mode 100644 index 2cd88c4d9..000000000 --- a/togetherjs/tests/doctestjs/examples/examples-2.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Example - - - - - - - - - - -
-$ print('hey you')
-hey you
-$ print({'whatever': 'example', something: [1, 2, 3]});
-{something: [1, 2, 3], whatever: "example"}
-$ print(1/0)
-NaN
-$ function foo() {
->   console.log({hey: 'you'});
->   throw 'whatever';
-> }
-> foo();
-Error: whatever
-
- -
-print(1+2)
-/* =>
-   3
-*/
-
-function testme() {
-  setTimeout(function () {
-    print('hey you!');
-  }, 10);
-}
-testme();
-wait(200);
-/* => hey you! */
-
- -

-
-
-function stop() {
-  throw Abort();
-}
-
-print(1+2);
-/* => 3 */
-
-stop();
-/* => something */
-
-print(5+5);
-/* => 12 */
-
- - - - - - - diff --git a/togetherjs/tests/doctestjs/examples/examples-2.js b/togetherjs/tests/doctestjs/examples/examples-2.js deleted file mode 100644 index 6bd8c033c..000000000 --- a/togetherjs/tests/doctestjs/examples/examples-2.js +++ /dev/null @@ -1,13 +0,0 @@ -function factorial(n) { - if (n == 1) { - return n; - } - return n * factorial(n-1); -} - -print(factorial(4)); -/* => 24 */ -print(factorial(3)); -/* => - 20 -*/ \ No newline at end of file diff --git a/togetherjs/tests/doctestjs/examples/examples.html b/togetherjs/tests/doctestjs/examples/examples.html deleted file mode 100644 index f1fb64280..000000000 --- a/togetherjs/tests/doctestjs/examples/examples.html +++ /dev/null @@ -1,198 +0,0 @@ - - - - -Doctest.js: examples - - - - - - - - - - -
- -
- - - - -
- -

doctest.js - by - Ian Bicking -

- -
- - -

Doctest.js: examples

- -
- -
- -

Web Service

- -
- You can use doctest.js to test APIs; in fact, it's reasonable to use - it for acceptance tests of the web APIs themselves, not just to test - the Javascript wrappers around those APIs. -
- -
- In this example we'll access the Geonames - API. First we'll want some routines to help us later on. You - could put these into a separate .js file and include - it, but often (especially in an example ;) it's best to be fully - transparent and list all the routines out in the open... - -
-$ apiLocation = 'http://ws.geonames.org/';
-$ function query(endpoint, q) {
->   var url = apiLocation + endpoint;
->   jQuery.ajax({
->     url: url,
->     data: q,
->     dataType: "json",
->     success: Spy('success', {wait: true, ignoreThis: true}),
->     error: Spy('error')
->   });
-> }
-
- - Some things to notice about this example: - -
    -
  • apiLocation is hard coded, but you could read it - from the query string, allowing something like - test.html?apiLocation=http://localhost:8080. -
  • - -
  • We create a Spy for both success and failure, as we want to - track both of these. We could just use functions, but mostly - there's an advantage to being able to watch .called. - If you used {writes: true} you might not need the - .applies functions. -
  • - -
  • wait can be called from anywhere. That means - when you call this function doctest will wait until something is - called, and will test all the output since that time (either the - success or failure writeln()). Timeout is the other - possibility. -
  • -
- -
- -
- Now we'll use the routine to actually run a test: - -
-$ query('postalCodeSearchJSON', {postalcode: 9011, maxRows: 5});
-success({
-  postalCodes: [
-    {...}
-  ]
-}, ...)
-
- -
- -

Web Service/XML

- -
- What we do for JSON, we can also do for XML; in this case - it's just fetching a static XML Atom document. - -
-$.ajax({
-  url: './.resources/example.xml',
-  dataType: 'xml',
-  success: function (doc) {
-    gdoc = doc;
-    writeln(repr(doc));
-  },
-  error: Spy('error')
-});
-wait(0.5);
-/* =>
-<feed xmlns="http://www.w3.org/2005/Atom">
-  <title>Example Feed</title>
-  ...
-</feed>
-*/
-
-
- -
- -

Deferred/Promise

- -
- - You can also - print jquery.Deferred - (promise) objects once they've resolved, using - the printResolved() function. This also implicitly - calls wait() with the condition that all the promises - be resolved. Both errors and resolved values are printed. - -
-var def1 = $.Deferred();
-var def2 = $.Deferred();
-def1.resolve("Value 1!", "Value2!");
-setTimeout(function () {
-  def2.reject("sucka");
-}, 500);
-printResolved("def1", def1, "def2", def2);
-// => def1 Value 1! Value2! def2 Error: sucka
-  
-
- - - -
- -

Download

-

- You can download this project in either - zip or - tar formats. -

- -

You can also clone the project with Git - by running: -

$ git clone git://github.com/ianb/doctestjs
-

- - - -
- - - - - - diff --git a/togetherjs/tests/doctestjs/examples/long-running-tests.html b/togetherjs/tests/doctestjs/examples/long-running-tests.html deleted file mode 100644 index 0a701ce5d..000000000 --- a/togetherjs/tests/doctestjs/examples/long-running-tests.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Example - - - - - - - This is an example of tests that: -
    -
  • wait on long running processes
  • -
  • use an external javascript file
  • -
  • sections in the tests that will make them run in sequence
  • -
- -

-
-    
-
\ No newline at end of file
diff --git a/togetherjs/tests/doctestjs/examples/long-running-tests.js b/togetherjs/tests/doctestjs/examples/long-running-tests.js
deleted file mode 100644
index 801deef8a..000000000
--- a/togetherjs/tests/doctestjs/examples/long-running-tests.js
+++ /dev/null
@@ -1,39 +0,0 @@
-// = SECTION First
-var done = false;
-function foo() {
-    window.setTimeout(function() {
-        done = true;
-        some_var = true;
-    }, 2000);
-    wait(function(){return done;});
-}
-foo();
-print(done);
-// => false
-
-// = SECTION check output 1
-print(done);
-// => true
-print(some_var);
-// => true
-
-// = SECTION Second
-done = false;
-function foo2() {
-    window.setTimeout(function() {
-        done = true;
-        another_var = true;
-    }, 2000);
-    wait(function(){return done;});
-}
-foo2();
-print(done);
-// => false
-
-// = SECTION check output 2
-print(done);
-// => true
-print(another_var);
-// => true
-print(some_var);
-// => true
diff --git a/togetherjs/tests/doctestjs/index.html b/togetherjs/tests/doctestjs/index.html
deleted file mode 100644
index 0dc795fbd..000000000
--- a/togetherjs/tests/doctestjs/index.html
+++ /dev/null
@@ -1,208 +0,0 @@
-
-
-  
-    
-    
-    
-    Doctest.js: the humane Javascript test framework
-    
-    
-    
-    
-    
-    
-    
-    
-
-
-
-  
-  
-
-    
-
-

Doctest.js: A Humane Javascript Test Framework

- - - -
-
- -
-
- - - - - -

Doctest.js is a test runner and testing framework for Javascript.

- -

Doctest uses a novel approach to testing: example and expected result. Each test is a chunk of code that prints out results and side effects, and then the expected result is matched against that to see if the test passed or failed.

- -

An example (note: these are live examples — you can also try your own via the live demo): - -

-function capitalize(words) {
-  return words.replace(/\b[a-z]/g, function (m) {
-    return m[0].toUpperCase();
-  });
-}
-
-print(capitalize('some words'));
-// => Some Words
-
-print(capitalize('some 4ward words'));
-// => Some 4ward Words
-
-

- -

This is similar to something like assertEqual(capitalize('some words'), 'Some Words') — there's a kind of "equal" check every time you print something. Instead of doing stuff then testing every detail of what happened or what was returned, testing almost happens for you — you print out what you are interested in, and you can even punt: you can start by simply exercising everything that matters, and then inspecting that what happens is what you expect, and copying those results into the test. For instance: - -

-function getProperties(obj) {
-  var result = [];
-  for (i in obj) {
-    result.push(i);
-  }
-  result.sort();
-  return result;
-}
-
-print(getProperties({b: 1, a: 2}));
-// =>
-
-print(getProperties("a"));
-// =>
-
- -

- -

Look: the first example worked great, ["a", "b"]. The second example though wasn't right at all. "0"? Once we have a better idea we can adjust those tests: - -

-function getProperties(obj) {
-  var result = [];
-  for (i in obj) {
-    if (obj.hasOwnProperty(i)) {
-      result.push(i);
-    }
-  }
-  result.sort();
-  return result;
-}
-
-print(getProperties({b: 1, a: 2}));
-// => ["a", "b"]
-
-print(getProperties("a"));
-// => []
-
- -Well, that wasn't quite enough, but this kind of incremental development is exactly why doctest.js is so helpful: it shows you what you've done, it shows you both presence and absence. Most test frameworks only give you tools to test what is there, and not ensure that the things you don't want aren't there.

- -

Async testing made easy!

- -

The other great feature of doctest.js is how it lets you test async code.

- -

Async code is a bit tricky for all test runners. When you run the test, the result of the test won't be known until after you wait some time. Test runners might have ways to pause the test process while the test code completes. They also need ways to test that everything you wanted to happen will happen. Something that is often tricky in test code is to make sure that some callback was called — it's relatively easier to test that the callback was called correctly.

- -

Here doctest's ability to test both what's present and what's missing shines. And the test runner also gives you a great way to serialize your asynchronous tests.

- -

The core feature here is wait() — this lets you register a callback that will tell the test runner when this block of code is fully finished. An example: - -

-var now = Date.now();
-var done = false;
-
-setTimeout(function () {
-  done = true;
-  print('The timeout finished!', Date.now() - now);
-}, 300);
-
-wait(function () {return done;});
-// => The timeout finished! ...
-
- -(Notice we use ... to ignore the specific number that is printed: ellipsis act as a kind of wildcard) - -

- -

With Doctest you don't have to use fake setTimeout or fake async anywhere — you always use the real thing, and it's nearly as easy to test as async code. Frameworks that make this code synchronous are cheating you, because asynchronous code is the hardest of code and deserves to be tested accurately. But when a test framework makes synchronous easy and asynchronous hard, it's all too easy when in the depth of test development to take shortcuts.

- -

Mocking with Spy

- -

In addition there's a simply mocking framework in doctest with Spy. This lets you create a function that records over time it is called — and each time it is called it prints out how it is called. It also makes it easy to wait on the Spy to be called: - -

-var button = $('<button></button>');
-$('body').append(button);
-button.click(Spy('button.click'));
-button.click();
-Spy('button.click').wait();
-/* =>
-<button />.button.click({...})
-*/
-
- -You'll notice here that we also ignored the details of the object passed to the callback. We could use wildcards to match no part or any specific part of the argument called. Also note that the value of this is shown: people usually forget that this is a kind of implicit argument to many functions, but again doctest makes the implicit explicit. Also note that the actual output is always displayed, so you can use this to inspect aspects of the environment even if you don't want to test all fo them. -

- - -

Lineage

- -

Doctest is based on the Python doctest module originally written by Tim Peters. Spy was inspired some by Jasmine's Spy class, and carries over ideas from MiniMock.

- -

If you've used Python's doctest and found it annoying or not widely useful for doctest: doctest.js fixes all those problems: see this post for more.

- - -
-
-
- - - - -
- -
- - - - - - - - - - - - diff --git a/togetherjs/tests/doctestjs/package.json b/togetherjs/tests/doctestjs/package.json deleted file mode 100644 index 02319ced9..000000000 --- a/togetherjs/tests/doctestjs/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "doctestjs", - "version": "0.3.0", - "main": "doctest", - "bin": { - "doctest": "./bin/doctest" - }, - "description": "Example-based testing framework", - "keywords": [], - "author": { - "name": "Ian Bicking", - "email": "ian@ianbicking.org", - "web": "http://ianbicking.org", - "twitter": "ianbicking" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/ianb/doctestjs.git" - } -} diff --git a/togetherjs/tests/doctestjs/reference.html b/togetherjs/tests/doctestjs/reference.html deleted file mode 100644 index 4b26bf448..000000000 --- a/togetherjs/tests/doctestjs/reference.html +++ /dev/null @@ -1,580 +0,0 @@ - - - - - - - Doctest.js: The Reference - - - - - - - - - - - - - -
-
-

Doctest.js: The Reference

- - - -
-
- -
-
- - - - - -

-Author: Ian Bicking, -ian@ianbicking.org -

- -
- -

HTML page

- -

Your HTML page needs just a couple things to set it up for a test: - -

-<html>
- <head>
-  <script src="doctestjs/doctest.js"></script>
-  <link href="doctestjs/doctest.css" rel="stylesheet">
- </head>
- <body class="autodoctest">
-
-  <pre class="test / doctest">
-    test
-  </pre>
- </body>
-</html>
-
- -

- -

Notably you need <body class="autodoctest"> to get the tests to run automatically on page load. If you were invoking doctest explicitly (like is done on the Try It page) then you might leave this off.

- -

External code

- -

Often you won't want to write your test code inside the test itself. Instead you'll want to put it in its own .js file. Especially with test the code is valid Javascript, and you will probably want syntax highlighting and all that.

- -

To do this, use an href attribtue on your <pre> element, like: - -

-<pre class="test" href="./tests.js"></pre>
-
- -The contents of the element don't matter. The test will be loaded from that location (which could be a full URL but you'll probably have cross-origin errors if you tried that — this doesn't use <script> tags to load those scripts, it uses XMLHttpRequest). Note that the class is still required!

- -

You can also load it from a variable location, using query string parameters to find the file. The most common pattern would be like this: - -

-<pre class="test" data-href-pattern="./{test-name|default.js}"></pre>
-
- -Any URL parameters (like {test-name}) get filled in by the query string (?test-name=example.js). By default these values can only contain letters, numbers, _, - and . — this is to protect against loading scripts from unexpected locations via an implicitly unsafe parameters in the query string.

- -

If you want to use a different restriction on a variable name, use {variable-name:regular_expression} — and use ^ and $ to make sure to match the entire string.

- -

If you want a default (if the parameter isn't set or is empty) use | to separate out the default. The default should come after the regular expression. In the example the default is default.js. Don't use extra spaces around |!

- -

Note this works well with relative addresses like <a href="?test-name=foo.js">foo.js</a>

- -

Format of test

- -

There are two formats that a doctest can take. You've probably seen the test format, there is also the more traditional doctest format.

- -

doctest format

- -

This format is used like: - -

-<pre class="doctest">
-$ first_line();
-> continuation line
-output
-</pre>
-
- -

- -

The first line of course starts wtih $ and a space. Think of it like a Unix command line. And also similar to a command line shell the continuation line is >. Any line without a leading $ or > is considered expected output.

- -

Note you can still have multiple statements using a continuation line. The only difference between two lines with $ and one with a $ followed by > is that in the latter case the two lines are executed together and the output from both is combined into what is expected.

- -

test format

- -

In this format the expected output is in a comment, like: - -

-<pre class="test">
-statement_1();
-statement_2();
-// Some other comment
-// => expected output
-
-More statements
-
-/* =>
-expected output
-*/
-
- -

- -

Basically the test is split up by using the // => comments. Each chunk is executed independently, and the test may be paused at the point where the expected output is found.

- -

Test sections

- -

If you are using external test code (and test) you can include section headers like: - -

-// == SECTION A section header
-
- -

- -

You can have one or more ='s. The text A section header will become a header. Each section header turns into a new <pre> element.

- -

Compact <pre>'s

- -

Sometimes you'll have boilerplate code that sets up the test environment, and you'll be uninterested in that code (unless it fails). This might define helper functions, or run some really routine sanity tests. You can use this to make those test blocks small: - -

-<pre class="test expand-on-failure">
-  ...
-</pre>
-
- -This will make the test 3em tall unless there's a failure, at which point it expands to its full size.

- -

Printing/writing

- -

The print() function is pretty important, of course. (Note it also was called writeln(), a name which is still supported).

- -

print() will print out any strings given to it, and the repr() of any other objects, with a space between each argument.

- -

You can make a custom repr() for any object by adding a .repr() method. It should look like: - -

-MyObject.prototype.repr = function () {
-  return '[MyObject attr: ' + repr(this.attr) + ']';
-};
-
- -

- -

If you can't add a method to your object, you can also add a stringifier using repr.register().

- -

You might do this like: - -

-repr.register(
-  function (o) {
-    return o instanceof MyClass;
-  },
-  function (o, indentString) {
-    return '[MyClass attr: ' + repr(o.attr) + ']';
-  }
-);
-
- -The first function is a test that is applied to objects. If it returns true, then the second function is called to stringify the object. If your representation uses multiple lines, then you should indent subsequent lines with the string, like return '[Beginning\n' + indentString + ' end\n' + indentString + ']';. The beginning is never indented. - -

- -

Some of the objects that have custom representations:

- -
-
XML and DOM objects
-
These are displayed as the normal serialization, e.g., <input type="text" />. XML-style endings for empty tags are always used. Attributes are alphabetized. HTML tags are upppercase and attributes are lowercase, because the DOM seems to like that.
- -
XMLHttpRequest
-
These are displayed like [XMLHttpRequest STATE [STATUS]]. The STATE is one of UNSENT, OPENED, HEADERS_RECEIVED, LOADING and DONE. The [STATUS] is only displayed if the request has finished.
- -
- -

Arrays and objects are displayed like you'd think. This might include objects that you might not think of as "plain" objects. Also the object prototype is not displayed. You'll have to use something like print(o.constructor) if you want to be specific about classes.

- -

console.log()

- -

console.log() works a lot like print(). But unlike print() it doesn't make a test pass or fail, it's purely informative.

- -

All the methods on console should work, like console.warn(). The underlying normal console function is called, but in addition on a test-by-test basis these are collected and displayed.

- -

printResolved() for Deferred and Promises

- -

There's special support for the jQuery Deferred object, and generally Promises/A.

- -

This support is through the printResolved() function, which is basically equivalent to print() except any promise arguments will be waited on to resolve (proper resolution or an error). You can use it like this:

- -
-var def = $.Deferred();
-setTimeout(function () {
-  def.resolve("Resolved!");
-});
-printResolved(def);
-// => Resolved!
-
- -

Errors are printed out with Error: before the value, and multiple arguments are printed out with spaces between them, or a placeholder if there's no arguments. For instance:

- -
-var def = $.Deferred();
-def.reject({code: 1}, "a message");
-printResolved(def);
-// => Error: {code: 1} a message
-// Or you might not have any value at all:
-def = $.Deferred();
-def.resolve();
-printResolved(def);
-// => (resolved)
-
- -

Output matching

- -

The expected output is compared with the output you actually got (received).

- -

First all whitespace is normalized. Empty lines are removed from both sides. Leading spaces are removed (i.e., indentation does not matter). Multiple spaces are normalized to a single space.

- -

There are two wildcard patterns. Ellipsis — ... — means "match anything". This will match zero character, or multiple lines. You should be careful about matching too much.

- -

The shorter wildcard is ?. This matches letters, numbers, underscore, period, and question mark. Note if you want to match a string with such characters you might have to use "?".

- -

Also a special case, " matches ' and vice versa. Since in most contexts these mean the same thing, this lets you be agnostic.

- -

When you have a large expected text and got a lot of text, and that text differs just a bit, you'll see a line-by-line comparison of the two, to help you identify exactly where the problem is. Note if you use wildcards the line-by-line comparison might be very inaccurate.

- -

wait(), async code, and pausing the tests

- -

Often you'll want to let code run for a while on its own before you are done with testing a section of code. I.e., you want to let all the requests complete, DOM elements update, and so forth.

- -

Each section of code can be paused at the end. Code cannot be paused in the middle of a section. So before the output (i.e., before // =>) the test runner can wait and collect output.

- -

Anytime you call wait() inside a section of code it tells the test runner to wait at the end of the test, either until some condition is true or until some time has passed. If the condition doesn't complete an error/timeout message is print()'d.

- -

A half baked version of what happens is this: - -

-var printed = [], waiting = null;
-function print(arg) {
-  printed.push(arg);
-}
-function wait(condition) {
-  waiting = condition;
-}
-function checkOutput() {
-  if (printed.join('\n') != expectedOutput) {
-     fail();
-  }
-}
-eval(exampleCode);
-hardTimeout = 5000; // 5 seconds
-checkTime = 100; // check every 0.1 seconds
-if (waiting === null) {
-  checkOutput();
-} else if (typeof waiting == "number") {
-  setTimeout(checkOutput, waiting);
-} else {
-  var now = Date.now();
-  function checker() {
-    if (waiting()) {
-      checkOutput();
-    } else if (Date.now() - now > hardTimeout) {
-      print("Error: timed out");
-      checkOutput();
-    } else {
-      setTimeout(checker, hardTimeout);
-    }
-  }
-  setTimeout(checker, 0);
-}
-
- -Now you practically know how to write doctest yourself! -

- -

Specifically this is how you can run wait():

- -
-
wait()
-
This makes the test pause just for a moment. It's the same as wait(0).
- -
wait(milliseconds)
-
This forces the test to pause for the given number of milliseconds. Everything is always in milliseconds.
- -
wait(condition)
-
This calls condition() frequently until it returns true.
- -
wait(condition, timeout)
-
This calls condition up until timeout milliseconds. You can use this to extend the timeout. The default timeout is 5 seconds (5000).
-
- -

Spy, mocking, and watching functions

- -

Spy is used to create a mock object/function that can be used to track calls and inspect call order and arguments.

- -

The basic use is like this: - -

-func = Spy('func');
-func(1, 2, 3);
-
-obj = {a: 1, func: func};
-obj.func();
-
-/* =>
-func(1, 2, 3)
-{a: 1, func: Spy('func')}.func()
-*/
-
- -

- -

That is, every time the Spy is called it will print out the call, all its arguments, and if there was a bound this (as in the obj.func() example) then that value will be displayed as well.

- -

Each spy is named, and if you call Spy(name) with a name that has been used before you will get the same Spy object back.

- -

Spy can be invoked in a couple ways:

- -
-
Spy(name)
-
Just creates/gets the Spy with the given name.
- -
Spy(name, {options})
-
Create the Spy with some options (as described below)
- -
Spy(name, function () {...})
-
Creates a Spy that wraps another function that you provide. The Spy will be called first, and will print out the call, and then it will call the sub-function with the same arguments and this.
- -
Spy(name, function () {...}, {options})
-
Create a Spy that wraps a function and has extra options.
- -
- -

Note that your function can raise an exception, and the Spy will pass it through (though also note the exception using console.log()). You can use this to inspect how a library reacts to exceptions in callbacks.

- -

Spy options

- -

The options available:

- -
-
applies: function () {...}
-
This is the function that will be called when the Spy is called. It's the same as passing in the function as the second argument.
- -
writes: false (default true)
-
If this is false (default true) then it will not print out the call.
- -
returns: value (default undefined)
-
This is what the Spy returns when called (assuming you did not use applies). By default it simply returns undefined, which is what a function returns when you have no explicit return statement.
- -
throwError: exceptionObject
-
If given, when the Spy is called it'll do throw exceptionObject
- -
ignoreThis: true (default false)
-
If true, then this won't be printed out regardless of whether it is bound. This is useful when a library binds this carelessly.
- -
wrapArgs: true (default false)
-
If true then wrapping will be forced when the arguments are printed out. Otherwise wrapping is only applied if an argument is longer than 80 columns (the default for printing generally).
- -
wait: true (default false)
-
This is equivalent to calling Spy(name).wait(). You can also pass in a number, which will be the millisecond timeout, e.g., Spy(name, {wait: 10000}) to wait for 10 seconds for the Spy to be called.
- -
methods: {...}
-
Equivalent to calling Spy.methods({...}). See below for details.
- -
- -

You can also change Spy.defaultOptions if you want to override one option by default, for instance to turn off printing or ignore this.

- -

Spy methods

- -

Several methods are available:

- -
-
Spy().wait([timeout])
-
This makes the test pause until the Spy has been called. Sometimes you must use the method instead of Spy(name, {wait: true}). An example: - -
-SomeAPI.onload = Spy('SomeAPI.onload', function (data) {
-  SomeOtherAPI.save(data, Spy('SomeOtherAPI.save'));
-};
-Spy('SomeOtherAPI.save').wait();
-/* =>
-SomeAPI.onlaod({...})
-SomeOtherAPI.save()
-*/
-
- - In this case the Spy is created inside another method, and that method is not called right away. wait: true doesn't work in this case. Instead you should call Spy(name).wait() later. Since names are unique, this will be the same Spy object as referenced earlier. -
- -
Spy.on("obj.attr", [applies/options])
-
This replaces the attribute attr on the object obj with a Spy. The object must be defined at the top level (i.e., eval("obj") must return the object). This is basically the same as: - -
-obj = eval("obj");
-spy = Spy("obj.attr", [applies/options]);
-obj.attr = spy;
-
- -
- -
Spy.on(obj, "obj.attr", [applies/options])
-
This is the same as the previous form, except for use when obj is not a global variable.
- -
aSpy.formatCall()
-
When the Spy is called, generally this does: - -
-print(aSpy.formatCall());
-
- - If you use writes: false then this might be helpful. -
- -
aSpy.method("methodName", [applies/options])
-
This creates an attribute aSpy.methodName and assigns a Spy to that attribute. You may give the normal constructor arguments.
- -
aSpy.methods({methodName: [true or options], ...})
-
This creates multiple attributes at once. You may use {methodName: true} if you have no options to pass in.
- -
- -

Spy attributes

- -

Spies have several attributes to inspect how they have been called:

- -
-
aSpy.self and aSpy.selfList
-
This is the value of this as the spy was called. As the spy is called multiple times each this value is appended to selfList, forming a history.
- -
aSpy.args and aSpy.argList
-
This is the list of arguments that the function was called with. .argList gives the history of past calls.
- -
- - -

Aborting your tests

- -

Tests often has prerequesites. Perhaps some browsers aren't supported. Maybe you need a server setup. Normally doctest will run through all the tests regardless of failures, but when basic prerequesites are missing this creates lots of chatter and failures with no purpose.

- -

To stop the tests from running call Abort(). This will still run the rest of the test block (up until the next // =>).

- -

jshint

- -

A helper is provided to help you run JSHint regularly on your code. Just do this:

- -
-jshint("source-filename.js", [jshint options]);
-
- -

You can give a full URL, but you can also just give the filename. When given a filename then all the <script> tags are searched for that filename. The source is fetched and JSHint is run on that source.

- -

You may give options to suppress or enforce checks. In addition you may list the known issues that you wish to ignore: issues are printed out in order, and are matched like any other text. You might want to use this to simply see the errors without checking them for anything in paticular: - -

-jshint("source-filename");
-// => ...
-
- -

- -

NosyXMLHttpRequest

- -

Sometimes you may want to watch the progress of XMLHttpRequest requests — both how the request is constructed and its result. You can use NosyXMLHttpRequest to wrap request objects.

- -

You probably want to use it like: - -

-XMLHttpRequest = NosyXMLHttpRequest.factory("request");
-
-

- -

The name will be used when showing output (e.g., request.setRequestHeader('X-Something', 'value')).

- - -

Node.js

- -

Doctest.js has some Node support. You must use the comment-based test format in stand-alone Javascript files. Then:

- -
-$ npm install -g doctestjs
-$ doctest test.js
-
- -

This will print a success or failure message, and will exit with a code (the number of failures) if the test does not pass.

- -

If you do not wish to install the package globally, do:

- -
-$ npm install doctestjs
-$ node_modules/.bin/doctest test.js
-
- - - -
- -
-
- - - - - - - - - -
- -
- - - - - - - - - - - - diff --git a/togetherjs/tests/doctestjs/try.html b/togetherjs/tests/doctestjs/try.html deleted file mode 100644 index 18f52091e..000000000 --- a/togetherjs/tests/doctestjs/try.html +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - Doctest.js: Try It! - - - - - - - - - - - - - - -
-
-

Doctest.js: Try It!

- - - -
-
- -
-
- - - - -
- - - - - - -
- -
- -
-
-

-      
-
- - - -
- -
- - - - - - - - - - - - diff --git a/togetherjs/tests/doctestjs/tutorial.html b/togetherjs/tests/doctestjs/tutorial.html deleted file mode 100644 index 75355490e..000000000 --- a/togetherjs/tests/doctestjs/tutorial.html +++ /dev/null @@ -1,659 +0,0 @@ - - - - - - - A doctest.js tutorial - - - - - - - - - - - - - -
-
-

Doctest.js: A Tutorial

- - - -
-
- -
-
- - - - - - - -
- -
-
-
-

What's it like?

-
- -

- -So you've decided to finally get religion when it comes to testing your Javascript code? Or, you feel like testing just isn't as easy as it could be, and want to find a better way to test your Javascript code? Or even: you've thought about or tried doing Test Driven Development but you've found it hard to get going? Let's do this... - -

- -

- -Doctest.js is basically example code and then expected output. This is really what most tests look like, but instead of lots of assertEqual(example, expected) this example/expected combination is embedded into the structure of the test. - -

- - - -

- -I'm going to get right into how the test code looks, but to actually use doctest.js you have to setup an HTML file in a specific format. That is described later in the HTML section. - -

- -

- -The structure looks like something you've probably seen before. We add one new function, print(), that works a lot like console.log(). Then we have a comment that shows what we expect to be output. A really simple example: - -

-function factorial(n) {
-  if (typeof n != "number") {
-    throw "You must give a number";
-  }
-  if (n <= 0) {
-    return 1;
-  }
-  return n * factorial(n-1);
-}
-
-print(factorial(4))
-// => 25
-    
- -

- -

- -See what I did there? 25 is totally the wrong answer! Also see what happened, the test just ran and told us so! There's also a summary of all the tests; if you do nothing it shows up at the top of the page, but in the interest of introducing the summary, here it is: - -

- -

- -

- -You'll notice it shows a failure (or more than one — it's the summary for all the examples in this tutorial). It also has a link to each failure, so you can jump to the problematic section. - -

- - - -

- -Let's look at what we did there: print(factorial(4)) and // => 25 — the output is just a comment that starts with =>. - -

- -
- -
-
-

Testing for error conditions

-
- -

- -You can also test errors: - -

-print(factorial(null));
-// => Error: You must give a number
-
- -When an exception is thrown it will print out Error: (error text) which you can match against. This way you can test for error conditions just like you test how "correct" invocations work. Note that the print() isn't really necessary here, you could do this just as well: - -
-factorial(null);
-// => Error: You must give a number
-
- -

- -
- -
-
-

print() and output matching

-
- - - -

- -print() pretty-prints things. This is important, because you have to "expect" the same output that print() produces. You can give multiple arguments, like with console.log. - -

-print({someProperty: 123, something: {a: 1, b: 2}, "foo": 123.1032, "another-property": [1,2,3,4]});
-/* =>
-{
-  "another-property": [1, 2, 3, 4],
-  foo: 123.1032,
-  someProperty: 123,
-  something: {a: 1, b: 2}
-}
-*/
-
- -You might notice that the attributes are alphabetized and are quoted only when necessary. If it's a small object it stays on one line: - -
-print({someProperty: 123});
-// => {someProperty: 123}
-
- -

- -

- -But sometimes the output is unpredictable; or rather you can predict it will change. When that's the case you can basically put a wildcard in the expected output: ... — that will match anything, including multiple lines. In addition you can use ? to match one word-like-thing (a number, symbol, etc; not " or whitespace or other symbols). You can use it like this: - -

-print({
-  date: new Date(),
-  timestamp: Date.now()
-});
-
-// => {date: ..., timestamp: ?}
-
- -

- -

- -You might notice that it passes, but you still get to see the actual output. This is a great way to show information that you can review, without actually testing. For instance, you might be testing something that connects to a server, in that case you might want to do this: - -

-var server = {url: "http://localhost:8000"} // or some calculated value
-print(server.url);
-// => ...
-
- -Now if everything seems breaky you can be 100% sure of what server you are connecting to. - -

- -

- -If you have a variable that is dynamic but you still care about the value, you should do something like this: - -

-var date = Date.now();
-print(date == date, date);
-// => true ...
-
- -Think of this pattern of print(x == y) as a kind of assertEqual() equivalent. - -

- -
- -
-
-

Testing async code

-
- - - -

- -This is all well and good, but lots of code in Javascript is asynchronous, meaning that you don't just call a function that returns a value. Doctest.js has an answer to that too: a great answer! - -

- -

- -For our example we'll use XMLHttpRequest, a common source of asynchronosity. We'll test a request (just a loopback request, but if you are testing a foreign service you'd need CORS access). When we instantiate and setup the request we don't have anything really to test — we want to test what happens when the request completes. - -

- -

- -To do this we'll use wait() — when this function is called the test runner will wait at the point where it sees // =>, for a certain amount of time or until a certain condition is met. Only then will it compare all the output that has happened to what was expected, and run the next chunk of test. - -

- -

- -You can use this like: wait(function () {return true when done}) or wait(millisecondsToWait). We'll use the first form, which is almost always better, since it allows the test to continue more quickly. Tests also always time out eventually (by default the timeout is 5000 milliseconds, i.e., 5 seconds — by convention everything in Javascript is in milliseconds). - -

-var endpoint = location.href;
-print(endpoint);
-// => ...
-
-var req = new XMLHttpRequest();
-req.open("GET", endpoint);
-req.onreadystatechange = function () {
-  if (req.readyState != 4) {
-    // hasn't actually finished
-    return;
-  }
-  print("Result:", req.status, req.getResponseHeader('content-type'));
-};
-req.send();
-
-wait(function () {return req.readyState == 4;});
-
-print("Current state:", req.readyState);
-
-/* =>
-Current state: 1
-Result: 200 text/html
-*/
-
- -I put in something tricky there to try to clarify what wait() really does. You'll notice there's a call to wait() that makes sure that req.readyState == 4 (that's the code that means the request is finished). But right after when we do print(req.readyState) it shows a readyState of 1. That's because the entire block is printed (from the previous // => output up until the next one). But the test runner keeps collecting output and doesn't run the next section until that wait() clause returns true. - -

- -

- -Another thing to note is that wait() needs to be called when that block of code is run — it can't be inside a function that isn't called. That said, if you write test helper functions (and you should!), it often works well to put those calls in the helper function. We'll see an example of that next... - -

- -
- -
-
-

The Spy

-
- - - -

- -Note: the next example will use some jQuery, just for the heck of it, though there is no special support for jQuery or other frameworks in Doctest. - -

- -

- -If you use these tools you might end up writing code like this quite a lot: - -

-// We've embedded a button just below this element
-var button = $('#example-button');
-// Just to highlight what we're working with:
-button.css({border: '1px dotted #f00'});
-button.click(function () {
-  print('Button clicked');
-});
-
-// Now we test that our event handler gets called when we do an artificial click of the button:
-button.click();
-// => Button clicked
-
- - - -

- -

- -But maybe we are curious about the arguments passed to that handler — even though we ignored the arguments, there was one passed. And we might want to show what this is bound to; this is kind of like an invisible extra argument passed to every function invocation. We could make a fancier print() statement there. But instead, there's also a handy tool for tracking calls: Spy. - -

- -

- -An example: - -

-var button = $('#example-button2');
-button.css({border: '1px dotted #00f'});
-button.click(Spy('button.click'));
-button.click();
-
-// => ...
-
- - - -

- -

- -That's a lot more information! Let's break it down: - -

-<button id="example-button2" style="border: 1px solid rgb(0, 0, 255); " type="button">Example Button 2</button>.button.click({ ...
-
- -There's two bits of information here. The first is the value (in blue) of this, which is the #example-button2 element. You'll notice it shows the HTML of the element. If you want Spy to ignore this you can use Spy('button.click', {ignoreThis: true}). - -

- -

- -The second value (in green) is the name we gave the Spy when we created it. Note that Spy names are also identifiers, that is, Spy('button.click') === Spy('button.click'). - -

- -

- -Next of course is all the arguments. There's a lot of arguments there. They are... interesting. You'll notice some references to ...recursive... which is what you get when you have self-referencing data structures. But maybe you want to test just a little of that structure without testing all of it. You might do something like this: - -

-// Spy('button.click') fetches the same Spy we were using before, which still has all its call information
-// .formatCall() shows the way the Spy was last called.
-print(Spy('button.click').formatCall());
-
-/* =>
-<button...</button>.button.click({
-  currentTarget: <button...
-  ...
-  timeStamp: ?,
-  type: "click"
-})
-*/
-
-
- -So we've tested that the type is click, that it has a timeStamp (though not the value) and that the currentTarget is a button (presumably the button we bound it to). We still get to see all the other information, we just aren't testing it. This can be helpful in the future when you realize there's more you want to test — you can look at the test output and transcribe more into the test. Or when something fails later you might want to inspect that output to make sure everything is what you expect (and when you see something unexpected that's also a great time to expand your test). - -

- -

- -Spies have a bunch of options, and act as a kind of mock object as well. You can pass in options as the second argument, like Spy('name', {options...}). Some highlights: - - -

-
applies
-
This is a function that the Spy "wraps". So if you do Spy('click', {applies: function (event) {this.remove(); return false;}}) then you'll get the same output printed, but you'll also run this.remove().
- -
writes
-
If you set this to false then it won't automatically print out the calls. The values of the calls will still be recorded, and you can use aSpy.formatCall() to see them.
- -
ignoreThis
-

Lots of code binds this without intending too. It's really easy in Javascript to do this. For instance, if you do handlers[i]() then this will be handlers. (Instead you might do var handler = handlers[i]; handler())

- -

Anyway, sometimes you don't care about this, and using {ignoreThis: true} lets you do that. -

- -
returns
-
If you want the Spy to return a value when its called, give the value here. Normally it returns undefined.
- -
throwError
-
This makes the Spy throw the given error anytime it is called.
- -
wait
-
If you use Spy('name', {wait: true}) then the test will wait until the Spy has been called. This is a pretty common pattern. It's basically the same as Spy('name').wait().
- -
- -

- -

- -You can set values like Spy.defaultOptions.writes = false if you want to set one of these by default. - -

- -

- -If you want to inspect how the Spy has been called, you can check a few attributes: - -

- -
.called
-
True once this Spy has been called.
- -
.self and .selfList
-
This is the value of this, or .selfList contains a history for each call.
- -
.args and .argList
-
The arguments the function was called with, or .argList is a history of arguments.
- -
- -

- -
- -
-
-

console.log

-
- -

- -This isn't a feature you have to do anything about, it's just there for you, so I'm just going to point it out. - -

- -

- -When you use console.log (or any of its friends, like console.warn) those messages will be captured (in addition to going to the log as normal), and the output will be shown in the specific test where they happened. A quick example: - -

-function enumProps(object) {
-  console.log('obj', object);
-  var result = {}
-  for (var attr in object) {
-    if (typeof object[attr] == "number" && attr.toUpperCase() == attr) {
-      result[object[attr]] = attr;
-    }
-  }
-  return result;
-}
-
-print(enumProps($('#example-button')[0]));
-
-// => {...}
-
-
- -You can think of it a little like print() goes to stdout, and console.log() goes to stderr. - -

- -
- -
-
-

Giving Up

-
- -

- -Tests often require some feature or setup to be usable at all. When it's not setup right you'll just get a bunch of meaningless failures. For this reason there's a way to abort all your tests. If you call Abort() then no further tests will be run. If you want to connect to a server, for instance, you might check that the server is really there, and if not then just abort the rest of the tests. For example: - -

-$.ajax({
-  url: '/ping',
-  success: Spy('ping', {wait: true, ignoreThis: true}),
-  error: function () {
-    Abort("Server isn't up");
-  }
-});
-
-// => ping(...)
-
- -

- -
- -
-
-

Setting Up Your HTML

-
- - - -

- -I wanted to show you all the cool features of doctest first, but you can't actually use any of them unless you set up a test runner page. Luckily the page is pretty simple. Let's say you've put doctest.js into doctest/: - -

-<DOCTYPE html>
-<html>
-  <head>
-    <meta charset="UTF-8">
-    <title>My Test</title>
-    <script src="doctest/doctest.js"></script>
-    <link href="doctest/doctest.css" rel="stylesheet">
-    <script src="mylibrary.js"></script>
-  </head>
-  <body class="autodoctest">
-
-  A test:
-
-<pre class="test">
-test goes here
-</pre>
-
-  </body></html>
-
- -

- -

- -Mostly it's just boilerplate: you have to include doctest.js and doctest.css and of course any libraries or dependencies of the thing you are testing. You also must use <body class="autodoctest"> — that's what tells doctest.js you want to find and run tests right away. - -

- -

- -Each test then is in a <pre class="test">. You might not want to actully write your tests inside the HTML, and instead put them in a separate Javascript file. To do that use: - -

-<pre class="test" href="./my_tests.js"></pre>
-
- -This will load the test code from ./my_tests.js and inline it into the element. This is how I personally write most of my tests, though when moving between a narrative and tests (as I am doing in this tutorial) it is nice to keep the tests together with the descriptions. - -

- -

- -Note that specifically when you use href="URL.js" you can split the tests into sections by including the comment // == SECTION Your Section Header Name in the included file, and you'll get multiple elements with headers automatically. - -

- -

- -A pass/fail summary is automatically added to the top of the page, though you can use <div id="doctest-output"></div> to position it someplace specific (as we did in this tutorial). - -

- -
- - -
-
-

Feedback?

-
- -

Was something in this tutorial confusing? Is there a testing problem or pattern you think this tutorial should talk about? Please give feedback in the form of an new issue. Thanks!

- -
- - -

Live Demo...

- -
- - - - - -
- -
- - - - - - - - - - - - diff --git a/togetherjs/tests/func_ace.js b/togetherjs/tests/func_ace.js deleted file mode 100644 index 8b136a240..000000000 --- a/togetherjs/tests/func_ace.js +++ /dev/null @@ -1,127 +0,0 @@ -// =SECTION Setup - -$("#fixture").empty(); -var aceSrc = "./ace.js"; -var script = $(" - - - - - - - - - - - - - - - - -
- Functional tests: -
    -
  1. misc
  2. -
  3. notifications
  4. -
  5. walkthrough
  6. -
  7. peer status
  8. -
  9. forms
  10. -
  11. ace
  12. -
  13. codemirror
  14. -
-
- Unit tests: -
    -
  1. storage
  2. -
  3. resolves
  4. -
  5. elementFinder
  6. - -
  7. ot text
  8. -
  9. linkify
  10. -
  11. console
  12. -
  13. misc (small)
  14. -
-

- Manual testing
- - Manual tests -

-
- -
xxx
-
- -
-
- - diff --git a/togetherjs/tests/interactive.js b/togetherjs/tests/interactive.js deleted file mode 100644 index 145eb86bf..000000000 --- a/togetherjs/tests/interactive.js +++ /dev/null @@ -1,235 +0,0 @@ -// =SECTION Setup - -// Local config overrides -var config = localStorage.getItem("interactiveOverrides"); -if (config) { - config = JSON.parse(config); - window.TogetherJSConfig = config; - for (var a in config) { - TogetherJS.config(a, config[a]); - } -} - -Test.require("ui", "chat", "util", "session", "jquery", "storage", "peers", "cursor", "windowing", "elementFinder", "templates-en-US"); -// => Loaded ... - -printChained( - Test.resetSettings(), - storage.settings.set("seenIntroDialog", true), - storage.settings.set("seenWalkthrough", true), - storage.settings.set("dontShowRtcInfo", true), - Test.startTogetherJS()); - -// => ... - -function addPeer(id) { - var name = "Faker"; - if (id) { - name += " " + id; - } - id = id || 'faker'; - var color = "#" + Math.floor(Math.random() * 0xffffff).toString(16); - Test.newPeer({ - name: name, - color: color, - clientId: id - }); - var len = peers.getAllPeers().length; - var pageHeight = $(document).height(); - var left = (len * 40) % $(window).width(); - var top = len % 2 ? (len * 10) : (pageHeight - 100 - len * 10); - Test.incoming({ - type: "cursor-update", - top: top, - left: left, - clientId: id - }); - Test.incoming({ - type: "scroll-update", - clientId: id, - position: { - location: "body", - offset: 20, - absoluteTop: 20, - documentHeight: $(document).height() - } - }); -} - -function pick(seq) { - if (! seq) { - seq = peers.getAllPeers(true); - } - return seq[Math.floor(Math.random() * seq.length)]; -} - -addPeer(); -// => ... - -// =SECTION Controls - -Test.addControl( - - $("").click(function () { - //$(".togetherjs-cursor").fadeOut(); - }), - - $("").click(function () { - //$(".togetherjs-cursor").fadeOut(); - }), - - $("").click(function () { - addPeer(); - }), - $("").click(function () { - addPeer("faker-" + Math.floor(Math.random() * 1000)); - }), - $('').click(function () { - var peer = pick(); - Test.incoming({ - type: "bye", - clientId: peer.id - }); - - }), - $('').click(function () { - var peer = pick(); - Test.incoming({ - type: "bye", - clientId: peer.id, - reason: "declined-join" - }); - }) -); - -Test.addControl( - $('').keypress(function (ev) { - if (ev.which == 13) { - Test.newPeer({url: ev.target.value}); - ev.target.value = ""; - } - }), - $('').click(function () { - Test.newPeer({url: "http://example.com/?" + Date.now()}); - }), - $('').click(function () { - var url = location.href.replace(/\#.*/, ""); - Test.newPeer({url: url}); - }) -); - -Test.addControl($('').click(function () { - var peer = pick(); - Test.incoming({ - type: "url-change-nudge", - clientId: peer.id, - url: peer.url, - to: peers.Self.id - }); -})); - -var focused = {}; -Test.addControl($('').click(function () { - var peer = pick(); - if (focused[peer.id]) { - Test.incoming({ - type: "form-focus", - element: null, - clientId: peer.id, - url: peer.url - }); - focused[peer.id] = null; - return; - } - var els = $("#controls textarea:visible, #controls input:visible, #controls select:visible"); - - var el = $(els[Math.floor(els.length * Math.random())]); - focused[peer.id] = el; - Test.incoming({ - type: "form-focus", - element: elementFinder.elementLocation(el), - clientId: peer.id, - url: peer.url - }); -})); - -Test.addControl($('').keypress(function (event) { - var el = $(event.target); - if (event.which == 13) { - var peer = pick(); - Test.incoming({ - type: "chat", - text: el.val(), - messageId: 'message-' + Date.now(), - clientId: peer.id - }); - el.val(""); - } -})); - -var el = $('
' + - '' + - '
'); -el.find("#idle-check").change(function (event) { - if (event.target.checked) { - TogetherJSTestSpy.setIdleTime(100); - } else { - TogetherJSTestSpy.setIdleTime(3*60*1000); - } -}); - -el.find("#expire-check").change(function (event) { - if (event.target.checked) { - TogetherJSTestSpy.setByeTime(10*1000); - } else { - TogetherJSTestSpy.setIdleTime(10*60*1000); - } -}); - -el.find("#include-hash").change(function (event) { - var config = localStorage.getItem("interactiveOverrides"); - if (config) { - config = JSON.parse(config); - } else { - config = {}; - } - config.includeHashInUrl = event.target.checked; - localStorage.setItem("interactiveOverrides", JSON.stringify(config)); - alert("Reload required"); -}); -if (TogetherJS.config.get("includeHashInUrl")) { - el.find("#include-hash").prop("checked", true); -} - -Test.addControl(el); - -Test.addControl( - $('').click(function () { - Test.incoming({ - type: "idle-status", - idle: "inactive", - clientId: "faker" - }); - }), - $('').click(function () { - Test.incoming({ - type: "idle-status", - idle: "active", - clientId: "faker" - }); - }), - $('').click(function () { - Test.incoming({ - type: "keydown", - clientId: "faker" - }); - }) -); - -Test.addControl($('').keypress(function (event) { - var el = $(event.target); - if (event.which == 13) { - TogetherJS.config("toolName", el.val() || null); - el.val(""); - } -})); diff --git a/togetherjs/tests/mobiletest.html b/togetherjs/tests/mobiletest.html deleted file mode 100644 index 4ecedd334..000000000 --- a/togetherjs/tests/mobiletest.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - Mobile test - - - - - - - - - -

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sollicitudin eu metus vel lobortis. Donec ac posuere diam, ut sollicitudin elit. Proin nulla ipsum, dignissim porttitor dui congue, euismod pellentesque quam. Nunc suscipit elit iaculis risus pharetra, vitae faucibus nulla molestie. In et ornare magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. In ultricies, orci eu condimentum commodo, est urna porta metus, sed euismod ante lorem quis sapien. Maecenas semper dui sed est imperdiet blandit.

- -

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sollicitudin eu metus vel lobortis. Donec ac posuere diam, ut sollicitudin elit. Proin nulla ipsum, dignissim porttitor dui congue, euismod pellentesque quam. Nunc suscipit elit iaculis risus pharetra, vitae faucibus nulla molestie. In et ornare magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. In ultricies, orci eu condimentum commodo, est urna porta metus, sed euismod ante lorem quis sapien. Maecenas semper dui sed est imperdiet blandit.

- -

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sollicitudin eu metus vel lobortis. Donec ac posuere diam, ut sollicitudin elit. Proin nulla ipsum, dignissim porttitor dui congue, euismod pellentesque quam. Nunc suscipit elit iaculis risus pharetra, vitae faucibus nulla molestie. In et ornare magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. In ultricies, orci eu condimentum commodo, est urna porta metus, sed euismod ante lorem quis sapien. Maecenas semper dui sed est imperdiet blandit.

- - - diff --git a/togetherjs/tests/test_console.js b/togetherjs/tests/test_console.js deleted file mode 100644 index f99116c90..000000000 --- a/togetherjs/tests/test_console.js +++ /dev/null @@ -1,22 +0,0 @@ -/*global tjconsole */ -var origConsole = window.console; -Test.require({tjconsole: "console"}); -// => Loaded modules: console - -tjconsole.warn("hey", {a: 1, b: 2}); -tjconsole.log(1, 2, 3, {repr: function () {return (null).foo;}}); -tjconsole.trace(); -print(tjconsole.toString()); - -/* => -TogetherJS base URL: ... -User Agent: ... -Page loaded: 20...Z -Age: ... minutes -URL: ... -------+------+---------------------------------------------- - ... warn hey {"a":1,"b":2} - ... log 1 2 3 {} - ... ... - - */ diff --git a/togetherjs/tests/test_elementFinder.js b/togetherjs/tests/test_elementFinder.js deleted file mode 100644 index 3e01384f2..000000000 --- a/togetherjs/tests/test_elementFinder.js +++ /dev/null @@ -1,25 +0,0 @@ -Test.require("elementFinder"); -// => Loaded modules: ... - -var els = $(document.body).find("*"); -els.each(function (index, el) { - el = $(el); - var loc; - try { - loc = elementFinder.elementLocation(el); - } catch (e) { - console.trace(); - print("Error: cannot get location for", el, ":", e); - return; - } - var result = elementFinder.findElement(loc); - if (result != el[0]) { - print("Bad element:", loc, el); - print("Resolved to:", result); - } else { - console.log("Resolved element", loc, el); - } -}); -print("done."); - -// => done. diff --git a/togetherjs/tests/test_linkify.js b/togetherjs/tests/test_linkify.js deleted file mode 100644 index dea731516..000000000 --- a/togetherjs/tests/test_linkify.js +++ /dev/null @@ -1,19 +0,0 @@ -/*global linkify */ -Test.require("linkify"); -// => Loaded modules: linkify - -print(linkify($("this is a test"))); -// => this is a test -print(linkify($("http://foo.com test"))); -/* => - - http://foo.com -test -*/ - -print(linkify($("yahoo (http://yahoo.com)"))); -/* => -yahoo ( - http://yahoo.com -) -*/ diff --git a/togetherjs/tests/test_misc.js b/togetherjs/tests/test_misc.js deleted file mode 100644 index 40636e3be..000000000 --- a/togetherjs/tests/test_misc.js +++ /dev/null @@ -1,18 +0,0 @@ -/*global util */ -Test.require("util"); -// => Loaded modules: util - -util.assertValidUrl("http://foo.com"); -// => -util.assertValidUrl("//foobar"); -// => -util.assertValidUrl("data:image/png,asdf"); -// => -util.assertValidUrl("javascript:alert()"); -// => Error: AssertionError: ... -util.assertValidUrl("foobar.com"); -// => Error: AssertionError: ... -util.assertValidUrl("http://test.com); something: foo"); -// => Error: AssertionError: ... -util.assertValidUrl("HTTPS://test.com"); -// => diff --git a/togetherjs/tests/test_ot.js b/togetherjs/tests/test_ot.js deleted file mode 100644 index 4485d6941..000000000 --- a/togetherjs/tests/test_ot.js +++ /dev/null @@ -1,161 +0,0 @@ -// =SECTION Setup expand-on-failure - -Test.require("ot", "util", "randomutil"); -// => Loaded... - -var generator = randomutil(1); - -var Client = util.Class({ - - constructor: function (clientId, hub, text) { - this.clientId = clientId; - this.hub = hub; - this.text = text || ""; - this.history = ot.History(clientId, this.text); - this.queuedChanges = []; - this.hub.addClient(this); - }, - - makeChange: function () { - var delta = ot.TextReplace.random(this.text, generator); - var orig = this.text; - this.text = delta.apply(this.text); - var change = this.history.addDelta(delta); - this.text = this.history.getState(); - this.queuedChanges.push(change); - console.log("internal:", change+"", this.clientId); - console.log(" from:", orig); - console.log(" to:", this.text); - }, - - incoming: function (change) { - console.log("incoming change", change); - var delta = this.history.add(change); - console.log("resulting delta", delta+""); - var orig = this.text; - console.log("applying", JSON.stringify(this.text), delta+""); - this.text = delta.apply(this.text); - this.text = this.history.getState(); - console.log(" change:", change+"", this.clientId); - console.log(" from:", orig); - console.log(" to:", this.text); - if (this.text != this.history.getState()) { - console.log("INVALID FORWARD DELTA"); - console.log("Delta applied:", delta+""); - console.log("Produces", JSON.stringify(this.text), "instead of", JSON.stringify(this.history.getState()), "from", JSON.stringify(orig)); - } - }, - - flush: function () { - this.queuedChanges.forEach(function (c) { - this.hub.send(c, this); - }, this); - this.queuedChanges = []; - } -}); - -var hub = { - - clients: [], - - addClient: function (client) { - this.clients.push(client); - }, - - send: function (change, client) { - util.assert(client); - for (var i=0; i - -// =SECTION Test fixture expand-on-failure - -var error; - -function check() { - hub.flushAll(); - var text; - var textId; - var history; - hub.clients.forEach(function (c) { - console.log("----------------------------------------"); - c.history.logHistory(); - }); - for (var i=0; i done. diff --git a/togetherjs/tests/test_ot_text.js b/togetherjs/tests/test_ot_text.js deleted file mode 100644 index 323682c60..000000000 --- a/togetherjs/tests/test_ot_text.js +++ /dev/null @@ -1,316 +0,0 @@ -// =SECTION Setup expand-on-failure - -Test.require("ot", "util", "randomutil"); -// => Loaded... - -var generator = randomutil(1); -generator.defaultChars = "XYZ/_ "; - -function run() { - var base = "abcdefg"; - console.log("Start:", JSON.stringify(base) + "/" + base.length); - var delta1 = ot.TextReplace.random(base, generator); - var delta2 = ot.TextReplace.random(base, generator); - console.log("Delta1:", delta1+""); - console.log("Delta2:", delta2+""); - var sub1 = delta1.transpose(delta2); - console.log("Translated", delta1, "to", sub1[0]); - var text1a = sub1[0].apply(delta2.apply(base)); - console.log(" trans", delta2, "to", sub1[1]); - var text1b = sub1[1].apply(delta1.apply(base)); - console.log("first text:", JSON.stringify(text1a), JSON.stringify(text1b)); - if (text1a != text1b) { - print("Not equal"); - throw new Error("Error; not equal"); - } - var sub2 = delta2.transpose(delta1); - console.log(" Translate", delta2, "to", sub2[0]); - var text2a = sub2[0].apply(delta1.apply(base)); - console.log(" trans", delta1, "to", sub2[1]); - var text2b = sub2[1].apply(delta2.apply(base)); - console.log("second text:", JSON.stringify(text2a), JSON.stringify(text2b)); - if (text2a != text2b) { - print("Not equal"); - throw new Error("Error; not equal"); - } - console.clear(); -} - -// => - -// =SECTION Simple Test Setup expand-on-failure - -function r(start, length, text) { - return ot.TextReplace(start, length, text); -} - -function trans(text, d1, d2) { - var sub = d1.transpose(d2); - console.log(JSON.stringify(text), "+", d1+"", "->", JSON.stringify(d1.apply(text))); - console.log(JSON.stringify(text), "+", d2+"", "->", JSON.stringify(d2.apply(text))); - var d1prime = sub[0]; - var d2prime = sub[1]; - if (d1.equals(d1prime)) { - print(d1, "stays same"); - } else { - print(d1, "becomes", d1prime); - } - if (d2.equals(d2prime)) { - print(d2, "stays same"); - } else { - print(d2, "becomes", d2prime); - } - var text1 = d1prime.apply(d2.apply(text)); - var text2 = d2prime.apply(d1.apply(text)); - print(d2, "+", d1prime, "->", text1); - print(d1, "+", d2prime, "->", text2); - if (text1 != text2) { - print("Error: text mismatch"); - } -} - -// =SECTION Two insertions - -var text = "abcdef"; -var ins1 = r(0, 0, "X"); -var ins2 = r(1, 0, "Y"); - -trans(text, ins1, ins2); -/* => -[insert "X" @0] stays same -[insert "Y" @1] becomes [insert "Y" @2] -[insert "Y" @1] + [insert "X" @0] -> XaYbcdef -[insert "X" @0] + [insert "Y" @2] -> XaYbcdef - */ - -trans(text, ins2, ins1); -/* => -[insert "Y" @1] becomes [insert "Y" @2] -[insert "X" @0] stays same -[insert "X" @0] + [insert "Y" @2] -> XaYbcdef -[insert "Y" @1] + [insert "X" @0] -> XaYbcdef - */ - -ins1 = r(0, 0, "X"); -ins2 = r(0, 0, "Y"); - -trans(text, ins1, ins2); -/* => -[insert "X" @0] becomes [insert "X" @1] -[insert "Y" @0] stays same -[insert "Y" @0] + [insert "X" @1] -> YXabcdef -[insert "X" @0] + [insert "Y" @0] -> YXabcdef - */ - -// As we see in this example, precedence matters (YX vs XY): -trans(text, ins2, ins1); -/* => -[insert "Y" @0] becomes [insert "Y" @1] -[insert "X" @0] stays same -[insert "X" @0] + [insert "Y" @1] -> XYabcdef -[insert "Y" @0] + [insert "X" @0] -> XYabcdef - */ - - -// =SECTION Two Deletions - -text = "abcdef"; -var del1 = r(0, 1, ""); -var del2 = r(1, 1, ""); - -trans(text, del1, del2); -/* => -[delete 1 chars @0] stays same -[delete 1 chars @1] becomes [delete 1 chars @0] -[delete 1 chars @1] + [delete 1 chars @0] -> cdef -[delete 1 chars @0] + [delete 1 chars @0] -> cdef - */ - -trans(text, del2, del1); -/* => -[delete 1 chars @1] becomes [delete 1 chars @0] -[delete 1 chars @0] stays same -[delete 1 chars @0] + [delete 1 chars @0] -> cdef -[delete 1 chars @1] + [delete 1 chars @0] -> cdef - */ - -trans(text, r(0, 2, ""), r(0, 1, "")); -/* => -[delete 2 chars @0] becomes [delete 1 chars @0] -[delete 1 chars @0] becomes [no-op] -[delete 1 chars @0] + [delete 1 chars @0] -> cdef -[delete 2 chars @0] + [no-op] -> cdef - */ - -trans(text, r(0, 1, ""), r(1, 1, "")); -/* => -[delete 1 chars @0] stays same -[delete 1 chars @1] becomes [delete 1 chars @0] -[delete 1 chars @1] + [delete 1 chars @0] -> cdef -[delete 1 chars @0] + [delete 1 chars @0] -> cdef - */ - -trans(text, r(0, 4, ""), r(1, 1, "")); -/* => -[delete 4 chars @0] becomes [delete 3 chars @0] -[delete 1 chars @1] becomes [no-op] -[delete 1 chars @1] + [delete 3 chars @0] -> ef -[delete 4 chars @0] + [no-op] -> ef - */ - -trans(text, r(1, 1, ""), r(0, 4, "")); -/* => -[delete 1 chars @1] becomes [no-op] -[delete 4 chars @0] becomes [delete 3 chars @0] -[delete 4 chars @0] + [no-op] -> ef -[delete 1 chars @1] + [delete 3 chars @0] -> ef - */ - -trans(text, r(0, 3, ""), r(2, 4, "")); -/* => -[delete 3 chars @0] becomes [delete 2 chars @0] -[delete 4 chars @2] becomes [delete 3 chars @0] -[delete 4 chars @2] + [delete 2 chars @0] -> -[delete 3 chars @0] + [delete 3 chars @0] -> - */ - -trans(text, r(2, 4, ""), r(0, 3, "")); -/* => -[delete 4 chars @2] becomes [delete 3 chars @0] -[delete 3 chars @0] becomes [delete 2 chars @0] -[delete 3 chars @0] + [delete 3 chars @0] -> -[delete 4 chars @2] + [delete 2 chars @0] -> - */ - - -// =SECTION Insertion and replacement - -trans(text, r(0, 0, "X"), r(2, 2, "Y")); -/* => -[insert "X" @0] stays same -[replace 2 chars with "Y" @2] becomes [replace 2 chars with "Y" @3] -[replace 2 chars with "Y" @2] + [insert "X" @0] -> XabYef -[insert "X" @0] + [replace 2 chars with "Y" @3] -> XabYef -*/ - -trans(text, r(2, 2, "Y"), r(0, 0, "X")); -/* => -[replace 2 chars with "Y" @2] becomes [replace 2 chars with "Y" @3] -[insert "X" @0] stays same -[insert "X" @0] + [replace 2 chars with "Y" @3] -> XabYef -[replace 2 chars with "Y" @2] + [insert "X" @0] -> XabYef -*/ - -trans(text, r(2, 0, "X"), r(2, 2, "Y")); -/* => -[insert "X" @2] stays same -[replace 2 chars with "Y" @2] becomes [replace 2 chars with "Y" @3] -[replace 2 chars with "Y" @2] + [insert "X" @2] -> abXYef -[insert "X" @2] + [replace 2 chars with "Y" @3] -> abXYef -*/ - -trans(text, r(2, 2, "Y"), r(2, 0, "X")); -/* => -[replace 2 chars with "Y" @2] becomes [replace 2 chars with "Y" @3] -[insert "X" @2] stays same -[insert "X" @2] + [replace 2 chars with "Y" @3] -> abXYef -[replace 2 chars with "Y" @2] + [insert "X" @2] -> abXYef -*/ - -trans(text, r(1, 0, "X"), r(0, 3, "Y")); -/* => -[insert "X" @1] stays same -[replace 3 chars with "Y" @0] becomes [replace 4 chars with "YX" @0] -[replace 3 chars with "Y" @0] + [insert "X" @1] -> YXdef -[insert "X" @1] + [replace 4 chars with "YX" @0] -> YXdef -*/ - -trans(text, r(0, 3, "Y"), r(1, 0, "X")); -/* => -[replace 3 chars with "Y" @0] becomes [replace 4 chars with "XY" @0] -[insert "X" @1] becomes [insert "X" @0] -[insert "X" @1] + [replace 4 chars with "XY" @0] -> XYdef -[replace 3 chars with "Y" @0] + [insert "X" @0] -> XYdef -*/ - -trans(text, r(4, 0, "X"), r(2, 2, "Y")); -/* => -[insert "X" @4] becomes [insert "X" @3] -[replace 2 chars with "Y" @2] stays same -[replace 2 chars with "Y" @2] + [insert "X" @3] -> abYXef -[insert "X" @4] + [replace 2 chars with "Y" @2] -> abYXef -*/ - -trans(text, r(2, 2, "Y"), r(4, 0, "X")); -/* => -[replace 2 chars with "Y" @2] stays same -[insert "X" @4] becomes [insert "X" @3] -[insert "X" @4] + [replace 2 chars with "Y" @2] -> abYXef -[replace 2 chars with "Y" @2] + [insert "X" @3] -> abYXef -*/ - -trans(text, r(0, 0, "X"), r(2, 2, "Y")); -/* => -[insert "X" @0] stays same -[replace 2 chars with "Y" @2] becomes [replace 2 chars with "Y" @3] -[replace 2 chars with "Y" @2] + [insert "X" @0] -> XabYef -[insert "X" @0] + [replace 2 chars with "Y" @3] -> XabYef -*/ - -// =SECTION Test (TP1) - -for (var i=0; i<1000; i++) { - console.log("Run", i); - run(); -} -print("done."); - -// => done. - -// =SECTION Test (TP2 attempt) - - -function runTp2() { - var base = "abcdefg"; - console.log("Start:", JSON.stringify(base) + "/" + base.length); - var deltaFirst = ot.TextReplace.random(base, generator); - var delta1 = ot.TextReplace.random(base, generator); - var delta2 = ot.TextReplace.random(base, generator); - console.log("Delta First:", deltaFirst+""); - console.log("Delta1:", delta1+""); - console.log("Delta2:", delta2+""); - // Now we'll try two orderings: - // first + 1 + 2 - // first + 2 + 1 - var delta1Trans = delta1.transpose(deltaFirst)[0]; - console.log("Translate:", deltaFirst, "+ (", delta1, "becomes", delta1Trans, ")"); - var delta2Trans = delta2.transpose(deltaFirst)[0]; - console.log("Translate:", deltaFirst, "+ (", delta2, "becomes", delta2Trans, ")"); - var delta1Trans_a = delta1Trans.transpose(delta2Trans)[0]; - console.log("Translate:", delta2Trans, "+ (", delta1Trans, "becomes", delta1Trans_a, ")"); - var delta2Trans_a = delta1Trans.transpose(delta2Trans)[1]; - console.log("Translate:", delta1Trans, "+ (", delta2Trans, "becomes", delta2Trans_a, ")"); - var text1 = delta1Trans_a.apply(delta2Trans.apply(deltaFirst.apply(base))); - console.log("text1:", JSON.stringify(base), "->", JSON.stringify(text1)); - console.log(" ", deltaFirst, deltaFirst.apply(base)); - console.log(" ", delta2Trans, delta2Trans.apply(deltaFirst.apply(base))); - console.log(" ", delta1Trans_a, text1); - var text2 = delta2Trans_a.apply(delta1Trans.apply(deltaFirst.apply(base))); - console.log("text2:", JSON.stringify(base), "->", JSON.stringify(text2)); - console.log(" ", deltaFirst, deltaFirst.apply(base)); - console.log(" ", delta1Trans, delta1Trans.apply(deltaFirst.apply(base))); - console.log(" ", delta2Trans_a, text2); - if (text1 != text2) { - print("Not equal"); - throw new Error("Error; not equal"); - } -} - -for (var i=0; i<1000; i++) { - console.clear(); - console.log("Run", i); - runTp2(); -} -print("done."); - -// => done. diff --git a/togetherjs/tests/test_resolves.js b/togetherjs/tests/test_resolves.js deleted file mode 100644 index ef4f933c7..000000000 --- a/togetherjs/tests/test_resolves.js +++ /dev/null @@ -1,36 +0,0 @@ -Test.require("util"); -// => Loaded modules: util - -var def = util.Deferred(); - -setTimeout(util.resolver(def, function () { - return 'ok'; -})); - -printResolved('item', def); - -// => item ok - -def = util.Deferred(); -var chained = util.Deferred(); -setTimeout(util.resolver(def, function () { - return chained; -})); -setTimeout(function () { - chained.resolve("second"); -}); - -printResolved('item2', def); - -// => item2 second - -var defs = [util.Deferred(), util.Deferred(), util.Deferred()]; -var result = util.resolveMany(defs); -setTimeout(function () { - defs[0].resolve('first'); - defs[1].resolve('second'); - defs[2].resolve('last'); -}); - -printResolved(result); -// => ["first", "second", "last"] diff --git a/togetherjs/tests/test_storage.js b/togetherjs/tests/test_storage.js deleted file mode 100644 index 0c2422e71..000000000 --- a/togetherjs/tests/test_storage.js +++ /dev/null @@ -1,18 +0,0 @@ -/*global storage */ -Test.require("storage", "util"); -// => Loaded modules: storage util - -print(storage); -// => [storage for localStorage] - -printResolved(storage.clear(), storage.tab.clear()); -// => (resolved) (resolved) - -printResolved(storage.keys(), storage.tab.keys()); -// => [] [] - -printResolved(storage.tab.set("foo", "bar")); -// => (resolved) - -printResolved(storage.tab.keys()); -// => ["foo"] diff --git a/togetherjs/tests/testutils.js b/togetherjs/tests/testutils.js deleted file mode 100644 index 5dd417a13..000000000 --- a/togetherjs/tests/testutils.js +++ /dev/null @@ -1,248 +0,0 @@ -TogetherJSTestSpy = {}; - -var Test = {}; - -/* Loads the modules that are listed as individual arguments, and adds - them to the global scope. Blocks on the loading. Use like: - - Test.require("foo", "bar"); - // => ... - foo.someFunction()... - - If you want to alias something, do: - - Test.require({myConsole: "console"}) - // => ... - myConsole.log() -*/ -Test.require = function () { - var done = false; - var args = Array.prototype.slice.call(arguments); - var modules = []; - var aliases = {}; - args.forEach(function (m) { - if (typeof m == "object") { - for (var alias in m) { - if (m.hasOwnProperty(alias)) { - modules.push(m[alias]); - aliases[m[alias]] = alias; - } - } - } else { - modules.push(m); - } - }); - - function loadModules() { - if (! modules.length) { - print("Require loaded"); - done = true; - return; - } - TogetherJS.require(modules, function () { - for (var i=0; i= args.length) { - done = true; - return; - } - var f = args[index]; - if (!f.then) { f = f(); } - f.then(function () { - if (! arguments.length) { - print("(done)"); - } else { - print.apply(null, arguments); - } - check(1); - }, function () { - if (! arguments.length) { - print("(error)"); - } else { - print.apply(null, ["Error:"].concat(arguments)); - } - check(1); - }); - function check(increment) { - index += increment; - setTimeout(run); - } - } - wait(function () {return done;}); - run(); -} - -Test.incoming = function (msg) { - TogetherJSTestSpy.getChannel().onmessage(msg); -}; - -Test.addControl = function () { - var div = $("
"); - var el; - for (var i=0; i - - - - TogetherJS interactions - - - - - - - - - - - - - - - -
    -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
  • -
- -
- - -
-
- ↑ -
- Participant name box and cursor -
-
- - -
- Participant entered -
- - -
- Participant left -
- - -
- Click to close chat -
- -
- -
-
-
- -
- -
- -
- - - - - diff --git a/togetherjs/tests/togetherjs-animations/js/animations.js b/togetherjs/tests/togetherjs-animations/js/animations.js deleted file mode 100644 index 6b8ecb62d..000000000 --- a/togetherjs/tests/togetherjs-animations/js/animations.js +++ /dev/null @@ -1,163 +0,0 @@ - // Methods - - function participantScaleUp() { - $('#participant-avatar').transition({ opacity: 1, scale: 2 }); - } - - function participantScaleDown() { - $('#participant-avatar').transition({ opacity: 0, scale: 0 }); - } - - function notificationSlideIn() { - $('#notification').css({ - left: "+=84px", - opacity: 0, - "zIndex": 8888 - }); - $('#notification').animate({ - "left": "-=164px", - opacity: 1, - "zIndex": 9999 - }, "fast"); - }; - - function notificationSlideInParticipantLeave() { - $('#notification2').css({ - left: "+=84px", - opacity: 0, - "zIndex": 8888 - }); - $('#notification2').animate({ - "left": "-=164px", - opacity: 1, - "zIndex": 9999 - }, "fast"); - }; - - function notificationChat() { - $('#notification3').css({ - left: "+=84px", - opacity: 0, - "zIndex": 8888 - }); - $('#notification3').animate({ - "left": "-=164px", - opacity: 1, - "zIndex": 9999 - }, "fast"); - }; - - function closeNotification() { - $('#notification').transition({ - perspective: '300px', - rotateX: '-90deg', - delay: 3000, - opacity: 0 - }); - }; - - function closeNotification2() { - $('#notification2').transition({ - perspective: '300px', - rotateX: '-90deg', - delay: 3000, - opacity: 0 - }); - }; - - function closeNotification3() { - $('#notification3').transition({ - perspective: '300px', - rotateX: '-90deg', - opacity: 0 - }); - }; - - function cursorPopIn() { - scaleUp($("#participant-box"), 10); - } - - function cursorPopOut() { - $("#participant-box") - .transition({ opacity: 0, scale: 0 }) - } - - function scaleUp(el, size) { - var height = el.height(); - var width = el.width(); - var buffer = size / 2; - el.animate({ - opacity: 1, - width: (width + size) + "px", - height: (height + size) + "px", - marginLeft: -buffer + "px", - paddingLeft: buffer + "px" - }).animate({ - width: width + "px", - height: height + "px", - marginLeft: 0, - paddingLeft: 0 - }); - } - - // Objects - - //chat notification - function chatnotification() { - notificationChat(); - closeNotification(); - }; - - // when the participant enters the session - function participantEnter() { - notificationSlideIn(); - participantScaleUp(); - closeNotification(); - cursorPopIn(); - }; - - // when the participant leaves the session - function participantLeave() { - notificationSlideInParticipantLeave(); - participantScaleDown(); - closeNotification2(); - cursorPopOut(); - } - - // when the participant is down the page or up the page - function participantCursorRotateDown() { - $('#participant-cursor').transition({ - rotate: '-180deg' - }); - } - - // when the user is at the same location as the participant - function participantCursorRotateUp() { - $('#participant-cursor').transition({ - rotate: '-30deg' - }); - } - - // NEEDS WORK when user presses a button in the dock to popout a window - function windowPopOut() { - $("#windowpopout") - //.css({ transformOrigin: '100px 50px' }) - .transition({ x: '-110', y: '100', opacity: 1, scale: 2.2 }, 100, 'ease') - .transition({ scale: 2 }, 100, 'ease') - } - - // when the participant is typing - function participantTyping() { - var count = 0; - - setInterval(function(){ - count++; - document.getElementById('participant-typing').innerHTML = new Array(count % 5).join('.'); - }, 150); - - } - - function transformPers() { - alert("transform"); - - } diff --git a/togetherjs/tests/togetherjs-animations/js/jquery-1.8.3.min.js b/togetherjs/tests/togetherjs-animations/js/jquery-1.8.3.min.js deleted file mode 100644 index 83589daa7..000000000 --- a/togetherjs/tests/togetherjs-animations/js/jquery-1.8.3.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v1.8.3 jquery.com | jquery.org/license */ -(function(e,t){function _(e){var t=M[e]={};return v.each(e.split(y),function(e,n){t[n]=!0}),t}function H(e,n,r){if(r===t&&e.nodeType===1){var i="data-"+n.replace(P,"-$1").toLowerCase();r=e.getAttribute(i);if(typeof r=="string"){try{r=r==="true"?!0:r==="false"?!1:r==="null"?null:+r+""===r?+r:D.test(r)?v.parseJSON(r):r}catch(s){}v.data(e,n,r)}else r=t}return r}function B(e){var t;for(t in e){if(t==="data"&&v.isEmptyObject(e[t]))continue;if(t!=="toJSON")return!1}return!0}function et(){return!1}function tt(){return!0}function ut(e){return!e||!e.parentNode||e.parentNode.nodeType===11}function at(e,t){do e=e[t];while(e&&e.nodeType!==1);return e}function ft(e,t,n){t=t||0;if(v.isFunction(t))return v.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return v.grep(e,function(e,r){return e===t===n});if(typeof t=="string"){var r=v.grep(e,function(e){return e.nodeType===1});if(it.test(t))return v.filter(t,r,!n);t=v.filter(t,r)}return v.grep(e,function(e,r){return v.inArray(e,t)>=0===n})}function lt(e){var t=ct.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function At(e,t){if(t.nodeType!==1||!v.hasData(e))return;var n,r,i,s=v._data(e),o=v._data(t,s),u=s.events;if(u){delete o.handle,o.events={};for(n in u)for(r=0,i=u[n].length;r").appendTo(i.body),n=t.css("display");t.remove();if(n==="none"||n===""){Pt=i.body.appendChild(Pt||v.extend(i.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!Ht||!Pt.createElement)Ht=(Pt.contentWindow||Pt.contentDocument).document,Ht.write(""),Ht.close();t=Ht.body.appendChild(Ht.createElement(e)),n=Dt(t,"display"),i.body.removeChild(Pt)}return Wt[e]=n,n}function fn(e,t,n,r){var i;if(v.isArray(t))v.each(t,function(t,i){n||sn.test(e)?r(e,i):fn(e+"["+(typeof i=="object"?t:"")+"]",i,n,r)});else if(!n&&v.type(t)==="object")for(i in t)fn(e+"["+i+"]",t[i],n,r);else r(e,t)}function Cn(e){return function(t,n){typeof t!="string"&&(n=t,t="*");var r,i,s,o=t.toLowerCase().split(y),u=0,a=o.length;if(v.isFunction(n))for(;u)[^>]*$|#([\w\-]*)$)/,E=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,S=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,T=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,N=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,C=/^-ms-/,k=/-([\da-z])/gi,L=function(e,t){return(t+"").toUpperCase()},A=function(){i.addEventListener?(i.removeEventListener("DOMContentLoaded",A,!1),v.ready()):i.readyState==="complete"&&(i.detachEvent("onreadystatechange",A),v.ready())},O={};v.fn=v.prototype={constructor:v,init:function(e,n,r){var s,o,u,a;if(!e)return this;if(e.nodeType)return this.context=this[0]=e,this.length=1,this;if(typeof e=="string"){e.charAt(0)==="<"&&e.charAt(e.length-1)===">"&&e.length>=3?s=[null,e,null]:s=w.exec(e);if(s&&(s[1]||!n)){if(s[1])return n=n instanceof v?n[0]:n,a=n&&n.nodeType?n.ownerDocument||n:i,e=v.parseHTML(s[1],a,!0),E.test(s[1])&&v.isPlainObject(n)&&this.attr.call(e,n,!0),v.merge(this,e);o=i.getElementById(s[2]);if(o&&o.parentNode){if(o.id!==s[2])return r.find(e);this.length=1,this[0]=o}return this.context=i,this.selector=e,this}return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e)}return v.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),v.makeArray(e,this))},selector:"",jquery:"1.8.3",length:0,size:function(){return this.length},toArray:function(){return l.call(this)},get:function(e){return e==null?this.toArray():e<0?this[this.length+e]:this[e]},pushStack:function(e,t,n){var r=v.merge(this.constructor(),e);return r.prevObject=this,r.context=this.context,t==="find"?r.selector=this.selector+(this.selector?" ":"")+n:t&&(r.selector=this.selector+"."+t+"("+n+")"),r},each:function(e,t){return v.each(this,e,t)},ready:function(e){return v.ready.promise().done(e),this},eq:function(e){return e=+e,e===-1?this.slice(e):this.slice(e,e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(l.apply(this,arguments),"slice",l.call(arguments).join(","))},map:function(e){return this.pushStack(v.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:[].sort,splice:[].splice},v.fn.init.prototype=v.fn,v.extend=v.fn.extend=function(){var e,n,r,i,s,o,u=arguments[0]||{},a=1,f=arguments.length,l=!1;typeof u=="boolean"&&(l=u,u=arguments[1]||{},a=2),typeof u!="object"&&!v.isFunction(u)&&(u={}),f===a&&(u=this,--a);for(;a0)return;r.resolveWith(i,[v]),v.fn.trigger&&v(i).trigger("ready").off("ready")},isFunction:function(e){return v.type(e)==="function"},isArray:Array.isArray||function(e){return v.type(e)==="array"},isWindow:function(e){return e!=null&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return e==null?String(e):O[h.call(e)]||"object"},isPlainObject:function(e){if(!e||v.type(e)!=="object"||e.nodeType||v.isWindow(e))return!1;try{if(e.constructor&&!p.call(e,"constructor")&&!p.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||p.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw new Error(e)},parseHTML:function(e,t,n){var r;return!e||typeof e!="string"?null:(typeof t=="boolean"&&(n=t,t=0),t=t||i,(r=E.exec(e))?[t.createElement(r[1])]:(r=v.buildFragment([e],t,n?null:[]),v.merge([],(r.cacheable?v.clone(r.fragment):r.fragment).childNodes)))},parseJSON:function(t){if(!t||typeof t!="string")return null;t=v.trim(t);if(e.JSON&&e.JSON.parse)return e.JSON.parse(t);if(S.test(t.replace(T,"@").replace(N,"]").replace(x,"")))return(new Function("return "+t))();v.error("Invalid JSON: "+t)},parseXML:function(n){var r,i;if(!n||typeof n!="string")return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(s){r=t}return(!r||!r.documentElement||r.getElementsByTagName("parsererror").length)&&v.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&g.test(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(C,"ms-").replace(k,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,n,r){var i,s=0,o=e.length,u=o===t||v.isFunction(e);if(r){if(u){for(i in e)if(n.apply(e[i],r)===!1)break}else for(;s0&&e[0]&&e[a-1]||a===0||v.isArray(e));if(f)for(;u-1)a.splice(n,1),i&&(n<=o&&o--,n<=u&&u--)}),this},has:function(e){return v.inArray(e,a)>-1},empty:function(){return a=[],this},disable:function(){return a=f=n=t,this},disabled:function(){return!a},lock:function(){return f=t,n||c.disable(),this},locked:function(){return!f},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],a&&(!r||f)&&(i?f.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},v.extend({Deferred:function(e){var t=[["resolve","done",v.Callbacks("once memory"),"resolved"],["reject","fail",v.Callbacks("once memory"),"rejected"],["notify","progress",v.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return v.Deferred(function(n){v.each(t,function(t,r){var s=r[0],o=e[t];i[r[1]](v.isFunction(o)?function(){var e=o.apply(this,arguments);e&&v.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===i?n:this,[e])}:n[s])}),e=null}).promise()},promise:function(e){return e!=null?v.extend(e,r):r}},i={};return r.pipe=r.then,v.each(t,function(e,s){var o=s[2],u=s[3];r[s[1]]=o.add,u&&o.add(function(){n=u},t[e^1][2].disable,t[2][2].lock),i[s[0]]=o.fire,i[s[0]+"With"]=o.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=l.call(arguments),r=n.length,i=r!==1||e&&v.isFunction(e.promise)?r:0,s=i===1?e:v.Deferred(),o=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?l.call(arguments):r,n===u?s.notifyWith(t,n):--i||s.resolveWith(t,n)}},u,a,f;if(r>1){u=new Array(r),a=new Array(r),f=new Array(r);for(;t
a",n=p.getElementsByTagName("*"),r=p.getElementsByTagName("a")[0];if(!n||!r||!n.length)return{};s=i.createElement("select"),o=s.appendChild(i.createElement("option")),u=p.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:r.getAttribute("href")==="/a",opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:u.value==="on",optSelected:o.selected,getSetAttribute:p.className!=="t",enctype:!!i.createElement("form").enctype,html5Clone:i.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:i.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},u.checked=!0,t.noCloneChecked=u.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!o.disabled;try{delete p.test}catch(d){t.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",h=function(){t.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick"),p.detachEvent("onclick",h)),u=i.createElement("input"),u.value="t",u.setAttribute("type","radio"),t.radioValue=u.value==="t",u.setAttribute("checked","checked"),u.setAttribute("name","t"),p.appendChild(u),a=i.createDocumentFragment(),a.appendChild(p.lastChild),t.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,t.appendChecked=u.checked,a.removeChild(u),a.appendChild(p);if(p.attachEvent)for(l in{submit:!0,change:!0,focusin:!0})f="on"+l,c=f in p,c||(p.setAttribute(f,"return;"),c=typeof p[f]=="function"),t[l+"Bubbles"]=c;return v(function(){var n,r,s,o,u="padding:0;margin:0;border:0;display:block;overflow:hidden;",a=i.getElementsByTagName("body")[0];if(!a)return;n=i.createElement("div"),n.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",a.insertBefore(n,a.firstChild),r=i.createElement("div"),n.appendChild(r),r.innerHTML="
t
",s=r.getElementsByTagName("td"),s[0].style.cssText="padding:0;margin:0;border:0;display:none",c=s[0].offsetHeight===0,s[0].style.display="",s[1].style.display="none",t.reliableHiddenOffsets=c&&s[0].offsetHeight===0,r.innerHTML="",r.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=r.offsetWidth===4,t.doesNotIncludeMarginInBodyOffset=a.offsetTop!==1,e.getComputedStyle&&(t.pixelPosition=(e.getComputedStyle(r,null)||{}).top!=="1%",t.boxSizingReliable=(e.getComputedStyle(r,null)||{width:"4px"}).width==="4px",o=i.createElement("div"),o.style.cssText=r.style.cssText=u,o.style.marginRight=o.style.width="0",r.style.width="1px",r.appendChild(o),t.reliableMarginRight=!parseFloat((e.getComputedStyle(o,null)||{}).marginRight)),typeof r.style.zoom!="undefined"&&(r.innerHTML="",r.style.cssText=u+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=r.offsetWidth===3,r.style.display="block",r.style.overflow="visible",r.innerHTML="
",r.firstChild.style.width="5px",t.shrinkWrapBlocks=r.offsetWidth!==3,n.style.zoom=1),a.removeChild(n),n=r=s=o=null}),a.removeChild(p),n=r=s=o=u=a=p=null,t}();var D=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;v.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(v.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?v.cache[e[v.expando]]:e[v.expando],!!e&&!B(e)},data:function(e,n,r,i){if(!v.acceptData(e))return;var s,o,u=v.expando,a=typeof n=="string",f=e.nodeType,l=f?v.cache:e,c=f?e[u]:e[u]&&u;if((!c||!l[c]||!i&&!l[c].data)&&a&&r===t)return;c||(f?e[u]=c=v.deletedIds.pop()||v.guid++:c=u),l[c]||(l[c]={},f||(l[c].toJSON=v.noop));if(typeof n=="object"||typeof n=="function")i?l[c]=v.extend(l[c],n):l[c].data=v.extend(l[c].data,n);return s=l[c],i||(s.data||(s.data={}),s=s.data),r!==t&&(s[v.camelCase(n)]=r),a?(o=s[n],o==null&&(o=s[v.camelCase(n)])):o=s,o},removeData:function(e,t,n){if(!v.acceptData(e))return;var r,i,s,o=e.nodeType,u=o?v.cache:e,a=o?e[v.expando]:v.expando;if(!u[a])return;if(t){r=n?u[a]:u[a].data;if(r){v.isArray(t)||(t in r?t=[t]:(t=v.camelCase(t),t in r?t=[t]:t=t.split(" ")));for(i=0,s=t.length;i1,null,!1))},removeData:function(e){return this.each(function(){v.removeData(this,e)})}}),v.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=v._data(e,t),n&&(!r||v.isArray(n)?r=v._data(e,t,v.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=v.queue(e,t),r=n.length,i=n.shift(),s=v._queueHooks(e,t),o=function(){v.dequeue(e,t)};i==="inprogress"&&(i=n.shift(),r--),i&&(t==="fx"&&n.unshift("inprogress"),delete s.stop,i.call(e,o,s)),!r&&s&&s.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return v._data(e,n)||v._data(e,n,{empty:v.Callbacks("once memory").add(function(){v.removeData(e,t+"queue",!0),v.removeData(e,n,!0)})})}}),v.fn.extend({queue:function(e,n){var r=2;return typeof e!="string"&&(n=e,e="fx",r--),arguments.length1)},removeAttr:function(e){return this.each(function(){v.removeAttr(this,e)})},prop:function(e,t){return v.access(this,v.prop,e,t,arguments.length>1)},removeProp:function(e){return e=v.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,s,o,u;if(v.isFunction(e))return this.each(function(t){v(this).addClass(e.call(this,t,this.className))});if(e&&typeof e=="string"){t=e.split(y);for(n=0,r=this.length;n=0)r=r.replace(" "+n[s]+" "," ");i.className=e?v.trim(r):""}}}return this},toggleClass:function(e,t){var n=typeof e,r=typeof t=="boolean";return v.isFunction(e)?this.each(function(n){v(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if(n==="string"){var i,s=0,o=v(this),u=t,a=e.split(y);while(i=a[s++])u=r?u:!o.hasClass(i),o[u?"addClass":"removeClass"](i)}else if(n==="undefined"||n==="boolean")this.className&&v._data(this,"__className__",this.className),this.className=this.className||e===!1?"":v._data(this,"__className__")||""})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;n=0)return!0;return!1},val:function(e){var n,r,i,s=this[0];if(!arguments.length){if(s)return n=v.valHooks[s.type]||v.valHooks[s.nodeName.toLowerCase()],n&&"get"in n&&(r=n.get(s,"value"))!==t?r:(r=s.value,typeof r=="string"?r.replace(R,""):r==null?"":r);return}return i=v.isFunction(e),this.each(function(r){var s,o=v(this);if(this.nodeType!==1)return;i?s=e.call(this,r,o.val()):s=e,s==null?s="":typeof s=="number"?s+="":v.isArray(s)&&(s=v.map(s,function(e){return e==null?"":e+""})),n=v.valHooks[this.type]||v.valHooks[this.nodeName.toLowerCase()];if(!n||!("set"in n)||n.set(this,s,"value")===t)this.value=s})}}),v.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,s=e.type==="select-one"||i<0,o=s?null:[],u=s?i+1:r.length,a=i<0?u:s?i:0;for(;a=0}),n.length||(e.selectedIndex=-1),n}}},attrFn:{},attr:function(e,n,r,i){var s,o,u,a=e.nodeType;if(!e||a===3||a===8||a===2)return;if(i&&v.isFunction(v.fn[n]))return v(e)[n](r);if(typeof e.getAttribute=="undefined")return v.prop(e,n,r);u=a!==1||!v.isXMLDoc(e),u&&(n=n.toLowerCase(),o=v.attrHooks[n]||(X.test(n)?F:j));if(r!==t){if(r===null){v.removeAttr(e,n);return}return o&&"set"in o&&u&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r)}return o&&"get"in o&&u&&(s=o.get(e,n))!==null?s:(s=e.getAttribute(n),s===null?t:s)},removeAttr:function(e,t){var n,r,i,s,o=0;if(t&&e.nodeType===1){r=t.split(y);for(;o=0}})});var $=/^(?:textarea|input|select)$/i,J=/^([^\.]*|)(?:\.(.+)|)$/,K=/(?:^|\s)hover(\.\S+|)\b/,Q=/^key/,G=/^(?:mouse|contextmenu)|click/,Y=/^(?:focusinfocus|focusoutblur)$/,Z=function(e){return v.event.special.hover?e:e.replace(K,"mouseenter$1 mouseleave$1")};v.event={add:function(e,n,r,i,s){var o,u,a,f,l,c,h,p,d,m,g;if(e.nodeType===3||e.nodeType===8||!n||!r||!(o=v._data(e)))return;r.handler&&(d=r,r=d.handler,s=d.selector),r.guid||(r.guid=v.guid++),a=o.events,a||(o.events=a={}),u=o.handle,u||(o.handle=u=function(e){return typeof v=="undefined"||!!e&&v.event.triggered===e.type?t:v.event.dispatch.apply(u.elem,arguments)},u.elem=e),n=v.trim(Z(n)).split(" ");for(f=0;f=0&&(y=y.slice(0,-1),a=!0),y.indexOf(".")>=0&&(b=y.split("."),y=b.shift(),b.sort());if((!s||v.event.customEvent[y])&&!v.event.global[y])return;n=typeof n=="object"?n[v.expando]?n:new v.Event(y,n):new v.Event(y),n.type=y,n.isTrigger=!0,n.exclusive=a,n.namespace=b.join("."),n.namespace_re=n.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,h=y.indexOf(":")<0?"on"+y:"";if(!s){u=v.cache;for(f in u)u[f].events&&u[f].events[y]&&v.event.trigger(n,r,u[f].handle.elem,!0);return}n.result=t,n.target||(n.target=s),r=r!=null?v.makeArray(r):[],r.unshift(n),p=v.event.special[y]||{};if(p.trigger&&p.trigger.apply(s,r)===!1)return;m=[[s,p.bindType||y]];if(!o&&!p.noBubble&&!v.isWindow(s)){g=p.delegateType||y,l=Y.test(g+y)?s:s.parentNode;for(c=s;l;l=l.parentNode)m.push([l,g]),c=l;c===(s.ownerDocument||i)&&m.push([c.defaultView||c.parentWindow||e,g])}for(f=0;f=0:v.find(h,this,null,[s]).length),u[h]&&f.push(c);f.length&&w.push({elem:s,matches:f})}d.length>m&&w.push({elem:this,matches:d.slice(m)});for(r=0;r0?this.on(t,null,e,n):this.trigger(t)},Q.test(t)&&(v.event.fixHooks[t]=v.event.keyHooks),G.test(t)&&(v.event.fixHooks[t]=v.event.mouseHooks)}),function(e,t){function nt(e,t,n,r){n=n||[],t=t||g;var i,s,a,f,l=t.nodeType;if(!e||typeof e!="string")return n;if(l!==1&&l!==9)return[];a=o(t);if(!a&&!r)if(i=R.exec(e))if(f=i[1]){if(l===9){s=t.getElementById(f);if(!s||!s.parentNode)return n;if(s.id===f)return n.push(s),n}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(f))&&u(t,s)&&s.id===f)return n.push(s),n}else{if(i[2])return S.apply(n,x.call(t.getElementsByTagName(e),0)),n;if((f=i[3])&&Z&&t.getElementsByClassName)return S.apply(n,x.call(t.getElementsByClassName(f),0)),n}return vt(e.replace(j,"$1"),t,n,r,a)}function rt(e){return function(t){var n=t.nodeName.toLowerCase();return n==="input"&&t.type===e}}function it(e){return function(t){var n=t.nodeName.toLowerCase();return(n==="input"||n==="button")&&t.type===e}}function st(e){return N(function(t){return t=+t,N(function(n,r){var i,s=e([],n.length,t),o=s.length;while(o--)n[i=s[o]]&&(n[i]=!(r[i]=n[i]))})})}function ot(e,t,n){if(e===t)return n;var r=e.nextSibling;while(r){if(r===t)return-1;r=r.nextSibling}return 1}function ut(e,t){var n,r,s,o,u,a,f,l=L[d][e+" "];if(l)return t?0:l.slice(0);u=e,a=[],f=i.preFilter;while(u){if(!n||(r=F.exec(u)))r&&(u=u.slice(r[0].length)||u),a.push(s=[]);n=!1;if(r=I.exec(u))s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=r[0].replace(j," ");for(o in i.filter)(r=J[o].exec(u))&&(!f[o]||(r=f[o](r)))&&(s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=o,n.matches=r);if(!n)break}return t?u.length:u?nt.error(e):L(e,a).slice(0)}function at(e,t,r){var i=t.dir,s=r&&t.dir==="parentNode",o=w++;return t.first?function(t,n,r){while(t=t[i])if(s||t.nodeType===1)return e(t,n,r)}:function(t,r,u){if(!u){var a,f=b+" "+o+" ",l=f+n;while(t=t[i])if(s||t.nodeType===1){if((a=t[d])===l)return t.sizset;if(typeof a=="string"&&a.indexOf(f)===0){if(t.sizset)return t}else{t[d]=l;if(e(t,r,u))return t.sizset=!0,t;t.sizset=!1}}}else while(t=t[i])if(s||t.nodeType===1)if(e(t,r,u))return t}}function ft(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function lt(e,t,n,r,i){var s,o=[],u=0,a=e.length,f=t!=null;for(;u-1&&(s[f]=!(o[f]=c))}}else g=lt(g===o?g.splice(d,g.length):g),i?i(null,o,g,a):S.apply(o,g)})}function ht(e){var t,n,r,s=e.length,o=i.relative[e[0].type],u=o||i.relative[" "],a=o?1:0,f=at(function(e){return e===t},u,!0),l=at(function(e){return T.call(t,e)>-1},u,!0),h=[function(e,n,r){return!o&&(r||n!==c)||((t=n).nodeType?f(e,n,r):l(e,n,r))}];for(;a1&&ft(h),a>1&&e.slice(0,a-1).join("").replace(j,"$1"),n,a0,s=e.length>0,o=function(u,a,f,l,h){var p,d,v,m=[],y=0,w="0",x=u&&[],T=h!=null,N=c,C=u||s&&i.find.TAG("*",h&&a.parentNode||a),k=b+=N==null?1:Math.E;T&&(c=a!==g&&a,n=o.el);for(;(p=C[w])!=null;w++){if(s&&p){for(d=0;v=e[d];d++)if(v(p,a,f)){l.push(p);break}T&&(b=k,n=++o.el)}r&&((p=!v&&p)&&y--,u&&x.push(p))}y+=w;if(r&&w!==y){for(d=0;v=t[d];d++)v(x,m,a,f);if(u){if(y>0)while(w--)!x[w]&&!m[w]&&(m[w]=E.call(l));m=lt(m)}S.apply(l,m),T&&!u&&m.length>0&&y+t.length>1&&nt.uniqueSort(l)}return T&&(b=k,c=N),x};return o.el=0,r?N(o):o}function dt(e,t,n){var r=0,i=t.length;for(;r2&&(f=u[0]).type==="ID"&&t.nodeType===9&&!s&&i.relative[u[1].type]){t=i.find.ID(f.matches[0].replace($,""),t,s)[0];if(!t)return n;e=e.slice(u.shift().length)}for(o=J.POS.test(e)?-1:u.length-1;o>=0;o--){f=u[o];if(i.relative[l=f.type])break;if(c=i.find[l])if(r=c(f.matches[0].replace($,""),z.test(u[0].type)&&t.parentNode||t,s)){u.splice(o,1),e=r.length&&u.join("");if(!e)return S.apply(n,x.call(r,0)),n;break}}}return a(e,h)(r,t,s,n,z.test(e)),n}function mt(){}var n,r,i,s,o,u,a,f,l,c,h=!0,p="undefined",d=("sizcache"+Math.random()).replace(".",""),m=String,g=e.document,y=g.documentElement,b=0,w=0,E=[].pop,S=[].push,x=[].slice,T=[].indexOf||function(e){var t=0,n=this.length;for(;ti.cacheLength&&delete e[t.shift()],e[n+" "]=r},e)},k=C(),L=C(),A=C(),O="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",_=M.replace("w","w#"),D="([*^$|!~]?=)",P="\\["+O+"*("+M+")"+O+"*(?:"+D+O+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+_+")|)|)"+O+"*\\]",H=":("+M+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+P+")|[^:]|\\\\.)*|.*))\\)|)",B=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)",j=new RegExp("^"+O+"+|((?:^|[^\\\\])(?:\\\\.)*)"+O+"+$","g"),F=new RegExp("^"+O+"*,"+O+"*"),I=new RegExp("^"+O+"*([\\x20\\t\\r\\n\\f>+~])"+O+"*"),q=new RegExp(H),R=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,U=/^:not/,z=/[\x20\t\r\n\f]*[+~]/,W=/:not\($/,X=/h\d/i,V=/input|select|textarea|button/i,$=/\\(?!\\)/g,J={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),NAME:new RegExp("^\\[name=['\"]?("+M+")['\"]?\\]"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+H),POS:new RegExp(B,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),needsContext:new RegExp("^"+O+"*[>+~]|"+B,"i")},K=function(e){var t=g.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}},Q=K(function(e){return e.appendChild(g.createComment("")),!e.getElementsByTagName("*").length}),G=K(function(e){return e.innerHTML="",e.firstChild&&typeof e.firstChild.getAttribute!==p&&e.firstChild.getAttribute("href")==="#"}),Y=K(function(e){e.innerHTML="";var t=typeof e.lastChild.getAttribute("multiple");return t!=="boolean"&&t!=="string"}),Z=K(function(e){return e.innerHTML="",!e.getElementsByClassName||!e.getElementsByClassName("e").length?!1:(e.lastChild.className="e",e.getElementsByClassName("e").length===2)}),et=K(function(e){e.id=d+0,e.innerHTML="
",y.insertBefore(e,y.firstChild);var t=g.getElementsByName&&g.getElementsByName(d).length===2+g.getElementsByName(d+0).length;return r=!g.getElementById(d),y.removeChild(e),t});try{x.call(y.childNodes,0)[0].nodeType}catch(tt){x=function(e){var t,n=[];for(;t=this[e];e++)n.push(t);return n}}nt.matches=function(e,t){return nt(e,null,null,t)},nt.matchesSelector=function(e,t){return nt(t,null,null,[e]).length>0},s=nt.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(i===1||i===9||i===11){if(typeof e.textContent=="string")return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=s(e)}else if(i===3||i===4)return e.nodeValue}else for(;t=e[r];r++)n+=s(t);return n},o=nt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?t.nodeName!=="HTML":!1},u=nt.contains=y.contains?function(e,t){var n=e.nodeType===9?e.documentElement:e,r=t&&t.parentNode;return e===r||!!(r&&r.nodeType===1&&n.contains&&n.contains(r))}:y.compareDocumentPosition?function(e,t){return t&&!!(e.compareDocumentPosition(t)&16)}:function(e,t){while(t=t.parentNode)if(t===e)return!0;return!1},nt.attr=function(e,t){var n,r=o(e);return r||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):r||Y?e.getAttribute(t):(n=e.getAttributeNode(t),n?typeof e[t]=="boolean"?e[t]?t:null:n.specified?n.value:null:null)},i=nt.selectors={cacheLength:50,createPseudo:N,match:J,attrHandle:G?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},find:{ID:r?function(e,t,n){if(typeof t.getElementById!==p&&!n){var r=t.getElementById(e);return r&&r.parentNode?[r]:[]}}:function(e,n,r){if(typeof n.getElementById!==p&&!r){var i=n.getElementById(e);return i?i.id===e||typeof i.getAttributeNode!==p&&i.getAttributeNode("id").value===e?[i]:t:[]}},TAG:Q?function(e,t){if(typeof t.getElementsByTagName!==p)return t.getElementsByTagName(e)}:function(e,t){var n=t.getElementsByTagName(e);if(e==="*"){var r,i=[],s=0;for(;r=n[s];s++)r.nodeType===1&&i.push(r);return i}return n},NAME:et&&function(e,t){if(typeof t.getElementsByName!==p)return t.getElementsByName(name)},CLASS:Z&&function(e,t,n){if(typeof t.getElementsByClassName!==p&&!n)return t.getElementsByClassName(e)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace($,""),e[3]=(e[4]||e[5]||"").replace($,""),e[2]==="~="&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),e[1]==="nth"?(e[2]||nt.error(e[0]),e[3]=+(e[3]?e[4]+(e[5]||1):2*(e[2]==="even"||e[2]==="odd")),e[4]=+(e[6]+e[7]||e[2]==="odd")):e[2]&&nt.error(e[0]),e},PSEUDO:function(e){var t,n;if(J.CHILD.test(e[0]))return null;if(e[3])e[2]=e[3];else if(t=e[4])q.test(t)&&(n=ut(t,!0))&&(n=t.indexOf(")",t.length-n)-t.length)&&(t=t.slice(0,n),e[0]=e[0].slice(0,n)),e[2]=t;return e.slice(0,3)}},filter:{ID:r?function(e){return e=e.replace($,""),function(t){return t.getAttribute("id")===e}}:function(e){return e=e.replace($,""),function(t){var n=typeof t.getAttributeNode!==p&&t.getAttributeNode("id");return n&&n.value===e}},TAG:function(e){return e==="*"?function(){return!0}:(e=e.replace($,"").toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[d][e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==p&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r,i){var s=nt.attr(r,e);return s==null?t==="!=":t?(s+="",t==="="?s===n:t==="!="?s!==n:t==="^="?n&&s.indexOf(n)===0:t==="*="?n&&s.indexOf(n)>-1:t==="$="?n&&s.substr(s.length-n.length)===n:t==="~="?(" "+s+" ").indexOf(n)>-1:t==="|="?s===n||s.substr(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r){return e==="nth"?function(e){var t,i,s=e.parentNode;if(n===1&&r===0)return!0;if(s){i=0;for(t=s.firstChild;t;t=t.nextSibling)if(t.nodeType===1){i++;if(e===t)break}}return i-=r,i===n||i%n===0&&i/n>=0}:function(t){var n=t;switch(e){case"only":case"first":while(n=n.previousSibling)if(n.nodeType===1)return!1;if(e==="first")return!0;n=t;case"last":while(n=n.nextSibling)if(n.nodeType===1)return!1;return!0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||nt.error("unsupported pseudo: "+e);return r[d]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?N(function(e,n){var i,s=r(e,t),o=s.length;while(o--)i=T.call(e,s[o]),e[i]=!(n[i]=s[o])}):function(e){return r(e,0,n)}):r}},pseudos:{not:N(function(e){var t=[],n=[],r=a(e.replace(j,"$1"));return r[d]?N(function(e,t,n,i){var s,o=r(e,null,i,[]),u=e.length;while(u--)if(s=o[u])e[u]=!(t[u]=s)}):function(e,i,s){return t[0]=e,r(t,null,s,n),!n.pop()}}),has:N(function(e){return function(t){return nt(e,t).length>0}}),contains:N(function(e){return function(t){return(t.textContent||t.innerText||s(t)).indexOf(e)>-1}}),enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&!!e.checked||t==="option"&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},parent:function(e){return!i.pseudos.empty(e)},empty:function(e){var t;e=e.firstChild;while(e){if(e.nodeName>"@"||(t=e.nodeType)===3||t===4)return!1;e=e.nextSibling}return!0},header:function(e){return X.test(e.nodeName)},text:function(e){var t,n;return e.nodeName.toLowerCase()==="input"&&(t=e.type)==="text"&&((n=e.getAttribute("type"))==null||n.toLowerCase()===t)},radio:rt("radio"),checkbox:rt("checkbox"),file:rt("file"),password:rt("password"),image:rt("image"),submit:it("submit"),reset:it("reset"),button:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&e.type==="button"||t==="button"},input:function(e){return V.test(e.nodeName)},focus:function(e){var t=e.ownerDocument;return e===t.activeElement&&(!t.hasFocus||t.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},active:function(e){return e===e.ownerDocument.activeElement},first:st(function(){return[0]}),last:st(function(e,t){return[t-1]}),eq:st(function(e,t,n){return[n<0?n+t:n]}),even:st(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:st(function(e,t,n){for(var r=n<0?n+t:n;++r",e.querySelectorAll("[selected]").length||i.push("\\["+O+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||i.push(":checked")}),K(function(e){e.innerHTML="

",e.querySelectorAll("[test^='']").length&&i.push("[*^$]="+O+"*(?:\"\"|'')"),e.innerHTML="",e.querySelectorAll(":enabled").length||i.push(":enabled",":disabled")}),i=new RegExp(i.join("|")),vt=function(e,r,s,o,u){if(!o&&!u&&!i.test(e)){var a,f,l=!0,c=d,h=r,p=r.nodeType===9&&e;if(r.nodeType===1&&r.nodeName.toLowerCase()!=="object"){a=ut(e),(l=r.getAttribute("id"))?c=l.replace(n,"\\$&"):r.setAttribute("id",c),c="[id='"+c+"'] ",f=a.length;while(f--)a[f]=c+a[f].join("");h=z.test(e)&&r.parentNode||r,p=a.join(",")}if(p)try{return S.apply(s,x.call(h.querySelectorAll(p),0)),s}catch(v){}finally{l||r.removeAttribute("id")}}return t(e,r,s,o,u)},u&&(K(function(t){e=u.call(t,"div");try{u.call(t,"[test!='']:sizzle"),s.push("!=",H)}catch(n){}}),s=new RegExp(s.join("|")),nt.matchesSelector=function(t,n){n=n.replace(r,"='$1']");if(!o(t)&&!s.test(n)&&!i.test(n))try{var a=u.call(t,n);if(a||e||t.document&&t.document.nodeType!==11)return a}catch(f){}return nt(n,null,null,[t]).length>0})}(),i.pseudos.nth=i.pseudos.eq,i.filters=mt.prototype=i.pseudos,i.setFilters=new mt,nt.attr=v.attr,v.find=nt,v.expr=nt.selectors,v.expr[":"]=v.expr.pseudos,v.unique=nt.uniqueSort,v.text=nt.getText,v.isXMLDoc=nt.isXML,v.contains=nt.contains}(e);var nt=/Until$/,rt=/^(?:parents|prev(?:Until|All))/,it=/^.[^:#\[\.,]*$/,st=v.expr.match.needsContext,ot={children:!0,contents:!0,next:!0,prev:!0};v.fn.extend({find:function(e){var t,n,r,i,s,o,u=this;if(typeof e!="string")return v(e).filter(function(){for(t=0,n=u.length;t0)for(i=r;i=0:v.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,s=[],o=st.test(e)||typeof e!="string"?v(e,t||this.context):0;for(;r-1:v.find.matchesSelector(n,e)){s.push(n);break}n=n.parentNode}}return s=s.length>1?v.unique(s):s,this.pushStack(s,"closest",e)},index:function(e){return e?typeof e=="string"?v.inArray(this[0],v(e)):v.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(e,t){var n=typeof e=="string"?v(e,t):v.makeArray(e&&e.nodeType?[e]:e),r=v.merge(this.get(),n);return this.pushStack(ut(n[0])||ut(r[0])?r:v.unique(r))},addBack:function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}}),v.fn.andSelf=v.fn.addBack,v.each({parent:function(e){var t=e.parentNode;return t&&t.nodeType!==11?t:null},parents:function(e){return v.dir(e,"parentNode")},parentsUntil:function(e,t,n){return v.dir(e,"parentNode",n)},next:function(e){return at(e,"nextSibling")},prev:function(e){return at(e,"previousSibling")},nextAll:function(e){return v.dir(e,"nextSibling")},prevAll:function(e){return v.dir(e,"previousSibling")},nextUntil:function(e,t,n){return v.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return v.dir(e,"previousSibling",n)},siblings:function(e){return v.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return v.sibling(e.firstChild)},contents:function(e){return v.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:v.merge([],e.childNodes)}},function(e,t){v.fn[e]=function(n,r){var i=v.map(this,t,n);return nt.test(e)||(r=n),r&&typeof r=="string"&&(i=v.filter(r,i)),i=this.length>1&&!ot[e]?v.unique(i):i,this.length>1&&rt.test(e)&&(i=i.reverse()),this.pushStack(i,e,l.call(arguments).join(","))}}),v.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),t.length===1?v.find.matchesSelector(t[0],e)?[t[0]]:[]:v.find.matches(e,t)},dir:function(e,n,r){var i=[],s=e[n];while(s&&s.nodeType!==9&&(r===t||s.nodeType!==1||!v(s).is(r)))s.nodeType===1&&i.push(s),s=s[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)e.nodeType===1&&e!==t&&n.push(e);return n}});var ct="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",ht=/ jQuery\d+="(?:null|\d+)"/g,pt=/^\s+/,dt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,vt=/<([\w:]+)/,mt=/]","i"),Et=/^(?:checkbox|radio)$/,St=/checked\s*(?:[^=]|=\s*.checked.)/i,xt=/\/(java|ecma)script/i,Tt=/^\s*\s*$/g,Nt={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},Ct=lt(i),kt=Ct.appendChild(i.createElement("div"));Nt.optgroup=Nt.option,Nt.tbody=Nt.tfoot=Nt.colgroup=Nt.caption=Nt.thead,Nt.th=Nt.td,v.support.htmlSerialize||(Nt._default=[1,"X
","
"]),v.fn.extend({text:function(e){return v.access(this,function(e){return e===t?v.text(this):this.empty().append((this[0]&&this[0].ownerDocument||i).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(v.isFunction(e))return this.each(function(t){v(this).wrapAll(e.call(this,t))});if(this[0]){var t=v(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&e.firstChild.nodeType===1)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return v.isFunction(e)?this.each(function(t){v(this).wrapInner(e.call(this,t))}):this.each(function(){var t=v(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=v.isFunction(e);return this.each(function(n){v(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){v.nodeName(this,"body")||v(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(e,this.firstChild)})},before:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(e,this),"before",this.selector)}},after:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this.nextSibling)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(this,e),"after",this.selector)}},remove:function(e,t){var n,r=0;for(;(n=this[r])!=null;r++)if(!e||v.filter(e,[n]).length)!t&&n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),v.cleanData([n])),n.parentNode&&n.parentNode.removeChild(n);return this},empty:function(){var e,t=0;for(;(e=this[t])!=null;t++){e.nodeType===1&&v.cleanData(e.getElementsByTagName("*"));while(e.firstChild)e.removeChild(e.firstChild)}return this},clone:function(e,t){return e=e==null?!1:e,t=t==null?e:t,this.map(function(){return v.clone(this,e,t)})},html:function(e){return v.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return n.nodeType===1?n.innerHTML.replace(ht,""):t;if(typeof e=="string"&&!yt.test(e)&&(v.support.htmlSerialize||!wt.test(e))&&(v.support.leadingWhitespace||!pt.test(e))&&!Nt[(vt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(dt,"<$1>");try{for(;r1&&typeof f=="string"&&St.test(f))return this.each(function(){v(this).domManip(e,n,r)});if(v.isFunction(f))return this.each(function(i){var s=v(this);e[0]=f.call(this,i,n?s.html():t),s.domManip(e,n,r)});if(this[0]){i=v.buildFragment(e,this,l),o=i.fragment,s=o.firstChild,o.childNodes.length===1&&(o=s);if(s){n=n&&v.nodeName(s,"tr");for(u=i.cacheable||c-1;a0?this.clone(!0):this).get(),v(o[i])[t](r),s=s.concat(r);return this.pushStack(s,e,o.selector)}}),v.extend({clone:function(e,t,n){var r,i,s,o;v.support.html5Clone||v.isXMLDoc(e)||!wt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(kt.innerHTML=e.outerHTML,kt.removeChild(o=kt.firstChild));if((!v.support.noCloneEvent||!v.support.noCloneChecked)&&(e.nodeType===1||e.nodeType===11)&&!v.isXMLDoc(e)){Ot(e,o),r=Mt(e),i=Mt(o);for(s=0;r[s];++s)i[s]&&Ot(r[s],i[s])}if(t){At(e,o);if(n){r=Mt(e),i=Mt(o);for(s=0;r[s];++s)At(r[s],i[s])}}return r=i=null,o},clean:function(e,t,n,r){var s,o,u,a,f,l,c,h,p,d,m,g,y=t===i&&Ct,b=[];if(!t||typeof t.createDocumentFragment=="undefined")t=i;for(s=0;(u=e[s])!=null;s++){typeof u=="number"&&(u+="");if(!u)continue;if(typeof u=="string")if(!gt.test(u))u=t.createTextNode(u);else{y=y||lt(t),c=t.createElement("div"),y.appendChild(c),u=u.replace(dt,"<$1>"),a=(vt.exec(u)||["",""])[1].toLowerCase(),f=Nt[a]||Nt._default,l=f[0],c.innerHTML=f[1]+u+f[2];while(l--)c=c.lastChild;if(!v.support.tbody){h=mt.test(u),p=a==="table"&&!h?c.firstChild&&c.firstChild.childNodes:f[1]===""&&!h?c.childNodes:[];for(o=p.length-1;o>=0;--o)v.nodeName(p[o],"tbody")&&!p[o].childNodes.length&&p[o].parentNode.removeChild(p[o])}!v.support.leadingWhitespace&&pt.test(u)&&c.insertBefore(t.createTextNode(pt.exec(u)[0]),c.firstChild),u=c.childNodes,c.parentNode.removeChild(c)}u.nodeType?b.push(u):v.merge(b,u)}c&&(u=c=y=null);if(!v.support.appendChecked)for(s=0;(u=b[s])!=null;s++)v.nodeName(u,"input")?_t(u):typeof u.getElementsByTagName!="undefined"&&v.grep(u.getElementsByTagName("input"),_t);if(n){m=function(e){if(!e.type||xt.test(e.type))return r?r.push(e.parentNode?e.parentNode.removeChild(e):e):n.appendChild(e)};for(s=0;(u=b[s])!=null;s++)if(!v.nodeName(u,"script")||!m(u))n.appendChild(u),typeof u.getElementsByTagName!="undefined"&&(g=v.grep(v.merge([],u.getElementsByTagName("script")),m),b.splice.apply(b,[s+1,0].concat(g)),s+=g.length)}return b},cleanData:function(e,t){var n,r,i,s,o=0,u=v.expando,a=v.cache,f=v.support.deleteExpando,l=v.event.special;for(;(i=e[o])!=null;o++)if(t||v.acceptData(i)){r=i[u],n=r&&a[r];if(n){if(n.events)for(s in n.events)l[s]?v.event.remove(i,s):v.removeEvent(i,s,n.handle);a[r]&&(delete a[r],f?delete i[u]:i.removeAttribute?i.removeAttribute(u):i[u]=null,v.deletedIds.push(r))}}}}),function(){var e,t;v.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e=v.uaMatch(o.userAgent),t={},e.browser&&(t[e.browser]=!0,t.version=e.version),t.chrome?t.webkit=!0:t.webkit&&(t.safari=!0),v.browser=t,v.sub=function(){function e(t,n){return new e.fn.init(t,n)}v.extend(!0,e,this),e.superclass=this,e.fn=e.prototype=this(),e.fn.constructor=e,e.sub=this.sub,e.fn.init=function(r,i){return i&&i instanceof v&&!(i instanceof e)&&(i=e(i)),v.fn.init.call(this,r,i,t)},e.fn.init.prototype=e.fn;var t=e(i);return e}}();var Dt,Pt,Ht,Bt=/alpha\([^)]*\)/i,jt=/opacity=([^)]*)/,Ft=/^(top|right|bottom|left)$/,It=/^(none|table(?!-c[ea]).+)/,qt=/^margin/,Rt=new RegExp("^("+m+")(.*)$","i"),Ut=new RegExp("^("+m+")(?!px)[a-z%]+$","i"),zt=new RegExp("^([-+])=("+m+")","i"),Wt={BODY:"block"},Xt={position:"absolute",visibility:"hidden",display:"block"},Vt={letterSpacing:0,fontWeight:400},$t=["Top","Right","Bottom","Left"],Jt=["Webkit","O","Moz","ms"],Kt=v.fn.toggle;v.fn.extend({css:function(e,n){return v.access(this,function(e,n,r){return r!==t?v.style(e,n,r):v.css(e,n)},e,n,arguments.length>1)},show:function(){return Yt(this,!0)},hide:function(){return Yt(this)},toggle:function(e,t){var n=typeof e=="boolean";return v.isFunction(e)&&v.isFunction(t)?Kt.apply(this,arguments):this.each(function(){(n?e:Gt(this))?v(this).show():v(this).hide()})}}),v.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Dt(e,"opacity");return n===""?"1":n}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":v.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(!e||e.nodeType===3||e.nodeType===8||!e.style)return;var s,o,u,a=v.camelCase(n),f=e.style;n=v.cssProps[a]||(v.cssProps[a]=Qt(f,a)),u=v.cssHooks[n]||v.cssHooks[a];if(r===t)return u&&"get"in u&&(s=u.get(e,!1,i))!==t?s:f[n];o=typeof r,o==="string"&&(s=zt.exec(r))&&(r=(s[1]+1)*s[2]+parseFloat(v.css(e,n)),o="number");if(r==null||o==="number"&&isNaN(r))return;o==="number"&&!v.cssNumber[a]&&(r+="px");if(!u||!("set"in u)||(r=u.set(e,r,i))!==t)try{f[n]=r}catch(l){}},css:function(e,n,r,i){var s,o,u,a=v.camelCase(n);return n=v.cssProps[a]||(v.cssProps[a]=Qt(e.style,a)),u=v.cssHooks[n]||v.cssHooks[a],u&&"get"in u&&(s=u.get(e,!0,i)),s===t&&(s=Dt(e,n)),s==="normal"&&n in Vt&&(s=Vt[n]),r||i!==t?(o=parseFloat(s),r||v.isNumeric(o)?o||0:s):s},swap:function(e,t,n){var r,i,s={};for(i in t)s[i]=e.style[i],e.style[i]=t[i];r=n.call(e);for(i in t)e.style[i]=s[i];return r}}),e.getComputedStyle?Dt=function(t,n){var r,i,s,o,u=e.getComputedStyle(t,null),a=t.style;return u&&(r=u.getPropertyValue(n)||u[n],r===""&&!v.contains(t.ownerDocument,t)&&(r=v.style(t,n)),Ut.test(r)&&qt.test(n)&&(i=a.width,s=a.minWidth,o=a.maxWidth,a.minWidth=a.maxWidth=a.width=r,r=u.width,a.width=i,a.minWidth=s,a.maxWidth=o)),r}:i.documentElement.currentStyle&&(Dt=function(e,t){var n,r,i=e.currentStyle&&e.currentStyle[t],s=e.style;return i==null&&s&&s[t]&&(i=s[t]),Ut.test(i)&&!Ft.test(t)&&(n=s.left,r=e.runtimeStyle&&e.runtimeStyle.left,r&&(e.runtimeStyle.left=e.currentStyle.left),s.left=t==="fontSize"?"1em":i,i=s.pixelLeft+"px",s.left=n,r&&(e.runtimeStyle.left=r)),i===""?"auto":i}),v.each(["height","width"],function(e,t){v.cssHooks[t]={get:function(e,n,r){if(n)return e.offsetWidth===0&&It.test(Dt(e,"display"))?v.swap(e,Xt,function(){return tn(e,t,r)}):tn(e,t,r)},set:function(e,n,r){return Zt(e,n,r?en(e,t,r,v.support.boxSizing&&v.css(e,"boxSizing")==="border-box"):0)}}}),v.support.opacity||(v.cssHooks.opacity={get:function(e,t){return jt.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=v.isNumeric(t)?"alpha(opacity="+t*100+")":"",s=r&&r.filter||n.filter||"";n.zoom=1;if(t>=1&&v.trim(s.replace(Bt,""))===""&&n.removeAttribute){n.removeAttribute("filter");if(r&&!r.filter)return}n.filter=Bt.test(s)?s.replace(Bt,i):s+" "+i}}),v(function(){v.support.reliableMarginRight||(v.cssHooks.marginRight={get:function(e,t){return v.swap(e,{display:"inline-block"},function(){if(t)return Dt(e,"marginRight")})}}),!v.support.pixelPosition&&v.fn.position&&v.each(["top","left"],function(e,t){v.cssHooks[t]={get:function(e,n){if(n){var r=Dt(e,t);return Ut.test(r)?v(e).position()[t]+"px":r}}}})}),v.expr&&v.expr.filters&&(v.expr.filters.hidden=function(e){return e.offsetWidth===0&&e.offsetHeight===0||!v.support.reliableHiddenOffsets&&(e.style&&e.style.display||Dt(e,"display"))==="none"},v.expr.filters.visible=function(e){return!v.expr.filters.hidden(e)}),v.each({margin:"",padding:"",border:"Width"},function(e,t){v.cssHooks[e+t]={expand:function(n){var r,i=typeof n=="string"?n.split(" "):[n],s={};for(r=0;r<4;r++)s[e+$t[r]+t]=i[r]||i[r-2]||i[0];return s}},qt.test(e)||(v.cssHooks[e+t].set=Zt)});var rn=/%20/g,sn=/\[\]$/,on=/\r?\n/g,un=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,an=/^(?:select|textarea)/i;v.fn.extend({serialize:function(){return v.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?v.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||an.test(this.nodeName)||un.test(this.type))}).map(function(e,t){var n=v(this).val();return n==null?null:v.isArray(n)?v.map(n,function(e,n){return{name:t.name,value:e.replace(on,"\r\n")}}):{name:t.name,value:n.replace(on,"\r\n")}}).get()}}),v.param=function(e,n){var r,i=[],s=function(e,t){t=v.isFunction(t)?t():t==null?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};n===t&&(n=v.ajaxSettings&&v.ajaxSettings.traditional);if(v.isArray(e)||e.jquery&&!v.isPlainObject(e))v.each(e,function(){s(this.name,this.value)});else for(r in e)fn(r,e[r],n,s);return i.join("&").replace(rn,"+")};var ln,cn,hn=/#.*$/,pn=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,dn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,vn=/^(?:GET|HEAD)$/,mn=/^\/\//,gn=/\?/,yn=/)<[^<]*)*<\/script>/gi,bn=/([?&])_=[^&]*/,wn=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,En=v.fn.load,Sn={},xn={},Tn=["*/"]+["*"];try{cn=s.href}catch(Nn){cn=i.createElement("a"),cn.href="",cn=cn.href}ln=wn.exec(cn.toLowerCase())||[],v.fn.load=function(e,n,r){if(typeof e!="string"&&En)return En.apply(this,arguments);if(!this.length)return this;var i,s,o,u=this,a=e.indexOf(" ");return a>=0&&(i=e.slice(a,e.length),e=e.slice(0,a)),v.isFunction(n)?(r=n,n=t):n&&typeof n=="object"&&(s="POST"),v.ajax({url:e,type:s,dataType:"html",data:n,complete:function(e,t){r&&u.each(r,o||[e.responseText,t,e])}}).done(function(e){o=arguments,u.html(i?v("
").append(e.replace(yn,"")).find(i):e)}),this},v.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(e,t){v.fn[t]=function(e){return this.on(t,e)}}),v.each(["get","post"],function(e,n){v[n]=function(e,r,i,s){return v.isFunction(r)&&(s=s||i,i=r,r=t),v.ajax({type:n,url:e,data:r,success:i,dataType:s})}}),v.extend({getScript:function(e,n){return v.get(e,t,n,"script")},getJSON:function(e,t,n){return v.get(e,t,n,"json")},ajaxSetup:function(e,t){return t?Ln(e,v.ajaxSettings):(t=e,e=v.ajaxSettings),Ln(e,t),e},ajaxSettings:{url:cn,isLocal:dn.test(ln[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":Tn},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":e.String,"text html":!0,"text json":v.parseJSON,"text xml":v.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:Cn(Sn),ajaxTransport:Cn(xn),ajax:function(e,n){function T(e,n,s,a){var l,y,b,w,S,T=n;if(E===2)return;E=2,u&&clearTimeout(u),o=t,i=a||"",x.readyState=e>0?4:0,s&&(w=An(c,x,s));if(e>=200&&e<300||e===304)c.ifModified&&(S=x.getResponseHeader("Last-Modified"),S&&(v.lastModified[r]=S),S=x.getResponseHeader("Etag"),S&&(v.etag[r]=S)),e===304?(T="notmodified",l=!0):(l=On(c,w),T=l.state,y=l.data,b=l.error,l=!b);else{b=T;if(!T||e)T="error",e<0&&(e=0)}x.status=e,x.statusText=(n||T)+"",l?d.resolveWith(h,[y,T,x]):d.rejectWith(h,[x,T,b]),x.statusCode(g),g=t,f&&p.trigger("ajax"+(l?"Success":"Error"),[x,c,l?y:b]),m.fireWith(h,[x,T]),f&&(p.trigger("ajaxComplete",[x,c]),--v.active||v.event.trigger("ajaxStop"))}typeof e=="object"&&(n=e,e=t),n=n||{};var r,i,s,o,u,a,f,l,c=v.ajaxSetup({},n),h=c.context||c,p=h!==c&&(h.nodeType||h instanceof v)?v(h):v.event,d=v.Deferred(),m=v.Callbacks("once memory"),g=c.statusCode||{},b={},w={},E=0,S="canceled",x={readyState:0,setRequestHeader:function(e,t){if(!E){var n=e.toLowerCase();e=w[n]=w[n]||e,b[e]=t}return this},getAllResponseHeaders:function(){return E===2?i:null},getResponseHeader:function(e){var n;if(E===2){if(!s){s={};while(n=pn.exec(i))s[n[1].toLowerCase()]=n[2]}n=s[e.toLowerCase()]}return n===t?null:n},overrideMimeType:function(e){return E||(c.mimeType=e),this},abort:function(e){return e=e||S,o&&o.abort(e),T(0,e),this}};d.promise(x),x.success=x.done,x.error=x.fail,x.complete=m.add,x.statusCode=function(e){if(e){var t;if(E<2)for(t in e)g[t]=[g[t],e[t]];else t=e[x.status],x.always(t)}return this},c.url=((e||c.url)+"").replace(hn,"").replace(mn,ln[1]+"//"),c.dataTypes=v.trim(c.dataType||"*").toLowerCase().split(y),c.crossDomain==null&&(a=wn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===ln[1]&&a[2]===ln[2]&&(a[3]||(a[1]==="http:"?80:443))==(ln[3]||(ln[1]==="http:"?80:443)))),c.data&&c.processData&&typeof c.data!="string"&&(c.data=v.param(c.data,c.traditional)),kn(Sn,c,n,x);if(E===2)return x;f=c.global,c.type=c.type.toUpperCase(),c.hasContent=!vn.test(c.type),f&&v.active++===0&&v.event.trigger("ajaxStart");if(!c.hasContent){c.data&&(c.url+=(gn.test(c.url)?"&":"?")+c.data,delete c.data),r=c.url;if(c.cache===!1){var N=v.now(),C=c.url.replace(bn,"$1_="+N);c.url=C+(C===c.url?(gn.test(c.url)?"&":"?")+"_="+N:"")}}(c.data&&c.hasContent&&c.contentType!==!1||n.contentType)&&x.setRequestHeader("Content-Type",c.contentType),c.ifModified&&(r=r||c.url,v.lastModified[r]&&x.setRequestHeader("If-Modified-Since",v.lastModified[r]),v.etag[r]&&x.setRequestHeader("If-None-Match",v.etag[r])),x.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+(c.dataTypes[0]!=="*"?", "+Tn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)x.setRequestHeader(l,c.headers[l]);if(!c.beforeSend||c.beforeSend.call(h,x,c)!==!1&&E!==2){S="abort";for(l in{success:1,error:1,complete:1})x[l](c[l]);o=kn(xn,c,n,x);if(!o)T(-1,"No Transport");else{x.readyState=1,f&&p.trigger("ajaxSend",[x,c]),c.async&&c.timeout>0&&(u=setTimeout(function(){x.abort("timeout")},c.timeout));try{E=1,o.send(b,T)}catch(k){if(!(E<2))throw k;T(-1,k)}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var Mn=[],_n=/\?/,Dn=/(=)\?(?=&|$)|\?\?/,Pn=v.now();v.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Mn.pop()||v.expando+"_"+Pn++;return this[e]=!0,e}}),v.ajaxPrefilter("json jsonp",function(n,r,i){var s,o,u,a=n.data,f=n.url,l=n.jsonp!==!1,c=l&&Dn.test(f),h=l&&!c&&typeof a=="string"&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Dn.test(a);if(n.dataTypes[0]==="jsonp"||c||h)return s=n.jsonpCallback=v.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,o=e[s],c?n.url=f.replace(Dn,"$1"+s):h?n.data=a.replace(Dn,"$1"+s):l&&(n.url+=(_n.test(f)?"&":"?")+n.jsonp+"="+s),n.converters["script json"]=function(){return u||v.error(s+" was not called"),u[0]},n.dataTypes[0]="json",e[s]=function(){u=arguments},i.always(function(){e[s]=o,n[s]&&(n.jsonpCallback=r.jsonpCallback,Mn.push(s)),u&&v.isFunction(o)&&o(u[0]),u=o=t}),"script"}),v.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(e){return v.globalEval(e),e}}}),v.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),v.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=i.head||i.getElementsByTagName("head")[0]||i.documentElement;return{send:function(s,o){n=i.createElement("script"),n.async="async",e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,i){if(i||!n.readyState||/loaded|complete/.test(n.readyState))n.onload=n.onreadystatechange=null,r&&n.parentNode&&r.removeChild(n),n=t,i||o(200,"success")},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(0,1)}}}});var Hn,Bn=e.ActiveXObject?function(){for(var e in Hn)Hn[e](0,1)}:!1,jn=0;v.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&Fn()||In()}:Fn,function(e){v.extend(v.support,{ajax:!!e,cors:!!e&&"withCredentials"in e})}(v.ajaxSettings.xhr()),v.support.ajax&&v.ajaxTransport(function(n){if(!n.crossDomain||v.support.cors){var r;return{send:function(i,s){var o,u,a=n.xhr();n.username?a.open(n.type,n.url,n.async,n.username,n.password):a.open(n.type,n.url,n.async);if(n.xhrFields)for(u in n.xhrFields)a[u]=n.xhrFields[u];n.mimeType&&a.overrideMimeType&&a.overrideMimeType(n.mimeType),!n.crossDomain&&!i["X-Requested-With"]&&(i["X-Requested-With"]="XMLHttpRequest");try{for(u in i)a.setRequestHeader(u,i[u])}catch(f){}a.send(n.hasContent&&n.data||null),r=function(e,i){var u,f,l,c,h;try{if(r&&(i||a.readyState===4)){r=t,o&&(a.onreadystatechange=v.noop,Bn&&delete Hn[o]);if(i)a.readyState!==4&&a.abort();else{u=a.status,l=a.getAllResponseHeaders(),c={},h=a.responseXML,h&&h.documentElement&&(c.xml=h);try{c.text=a.responseText}catch(p){}try{f=a.statusText}catch(p){f=""}!u&&n.isLocal&&!n.crossDomain?u=c.text?200:404:u===1223&&(u=204)}}}catch(d){i||s(-1,d)}c&&s(u,f,c,l)},n.async?a.readyState===4?setTimeout(r,0):(o=++jn,Bn&&(Hn||(Hn={},v(e).unload(Bn)),Hn[o]=r),a.onreadystatechange=r):r()},abort:function(){r&&r(0,1)}}}});var qn,Rn,Un=/^(?:toggle|show|hide)$/,zn=new RegExp("^(?:([-+])=|)("+m+")([a-z%]*)$","i"),Wn=/queueHooks$/,Xn=[Gn],Vn={"*":[function(e,t){var n,r,i=this.createTween(e,t),s=zn.exec(t),o=i.cur(),u=+o||0,a=1,f=20;if(s){n=+s[2],r=s[3]||(v.cssNumber[e]?"":"px");if(r!=="px"&&u){u=v.css(i.elem,e,!0)||n||1;do a=a||".5",u/=a,v.style(i.elem,e,u+r);while(a!==(a=i.cur()/o)&&a!==1&&--f)}i.unit=r,i.start=u,i.end=s[1]?u+(s[1]+1)*n:n}return i}]};v.Animation=v.extend(Kn,{tweener:function(e,t){v.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;r-1,f={},l={},c,h;a?(l=i.position(),c=l.top,h=l.left):(c=parseFloat(o)||0,h=parseFloat(u)||0),v.isFunction(t)&&(t=t.call(e,n,s)),t.top!=null&&(f.top=t.top-s.top+c),t.left!=null&&(f.left=t.left-s.left+h),"using"in t?t.using.call(e,f):i.css(f)}},v.fn.extend({position:function(){if(!this[0])return;var e=this[0],t=this.offsetParent(),n=this.offset(),r=er.test(t[0].nodeName)?{top:0,left:0}:t.offset();return n.top-=parseFloat(v.css(e,"marginTop"))||0,n.left-=parseFloat(v.css(e,"marginLeft"))||0,r.top+=parseFloat(v.css(t[0],"borderTopWidth"))||0,r.left+=parseFloat(v.css(t[0],"borderLeftWidth"))||0,{top:n.top-r.top,left:n.left-r.left}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||i.body;while(e&&!er.test(e.nodeName)&&v.css(e,"position")==="static")e=e.offsetParent;return e||i.body})}}),v.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);v.fn[e]=function(i){return v.access(this,function(e,i,s){var o=tr(e);if(s===t)return o?n in o?o[n]:o.document.documentElement[i]:e[i];o?o.scrollTo(r?v(o).scrollLeft():s,r?s:v(o).scrollTop()):e[i]=s},e,i,arguments.length,null)}}),v.each({Height:"height",Width:"width"},function(e,n){v.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){v.fn[i]=function(i,s){var o=arguments.length&&(r||typeof i!="boolean"),u=r||(i===!0||s===!0?"margin":"border");return v.access(this,function(n,r,i){var s;return v.isWindow(n)?n.document.documentElement["client"+e]:n.nodeType===9?(s=n.documentElement,Math.max(n.body["scroll"+e],s["scroll"+e],n.body["offset"+e],s["offset"+e],s["client"+e])):i===t?v.css(n,r,i,u):v.style(n,r,i,u)},n,o?i:t,o,null)}})}),e.jQuery=e.$=v,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return v})})(window); \ No newline at end of file diff --git a/togetherjs/tests/togetherjs-animations/js/jquery.transit.min.js b/togetherjs/tests/togetherjs-animations/js/jquery.transit.min.js deleted file mode 100644 index 71e0b06f6..000000000 --- a/togetherjs/tests/togetherjs-animations/js/jquery.transit.min.js +++ /dev/null @@ -1,9 +0,0 @@ -/*! - * jQuery Transit - CSS3 transitions and transformations - * (c) 2011-2012 Rico Sta. Cruz - * MIT Licensed. - * - * http://ricostacruz.com/jquery.transit - * http://github.com/rstacruz/jquery.transit - */ -(function(k){k.transit={version:"0.9.9",propertyMap:{marginLeft:"margin",marginRight:"margin",marginBottom:"margin",marginTop:"margin",paddingLeft:"padding",paddingRight:"padding",paddingBottom:"padding",paddingTop:"padding"},enabled:true,useTransitionEnd:false};var d=document.createElement("div");var q={};function b(v){if(v in d.style){return v}var u=["Moz","Webkit","O","ms"];var r=v.charAt(0).toUpperCase()+v.substr(1);if(v in d.style){return v}for(var t=0;t-1;q.transition=b("transition");q.transitionDelay=b("transitionDelay");q.transform=b("transform");q.transformOrigin=b("transformOrigin");q.transform3d=e();var i={transition:"transitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",WebkitTransition:"webkitTransitionEnd",msTransition:"MSTransitionEnd"};var f=q.transitionEnd=i[q.transition]||null;for(var p in q){if(q.hasOwnProperty(p)&&typeof k.support[p]==="undefined"){k.support[p]=q[p]}}d=null;k.cssEase={_default:"ease","in":"ease-in",out:"ease-out","in-out":"ease-in-out",snap:"cubic-bezier(0,1,.5,1)",easeOutCubic:"cubic-bezier(.215,.61,.355,1)",easeInOutCubic:"cubic-bezier(.645,.045,.355,1)",easeInCirc:"cubic-bezier(.6,.04,.98,.335)",easeOutCirc:"cubic-bezier(.075,.82,.165,1)",easeInOutCirc:"cubic-bezier(.785,.135,.15,.86)",easeInExpo:"cubic-bezier(.95,.05,.795,.035)",easeOutExpo:"cubic-bezier(.19,1,.22,1)",easeInOutExpo:"cubic-bezier(1,0,0,1)",easeInQuad:"cubic-bezier(.55,.085,.68,.53)",easeOutQuad:"cubic-bezier(.25,.46,.45,.94)",easeInOutQuad:"cubic-bezier(.455,.03,.515,.955)",easeInQuart:"cubic-bezier(.895,.03,.685,.22)",easeOutQuart:"cubic-bezier(.165,.84,.44,1)",easeInOutQuart:"cubic-bezier(.77,0,.175,1)",easeInQuint:"cubic-bezier(.755,.05,.855,.06)",easeOutQuint:"cubic-bezier(.23,1,.32,1)",easeInOutQuint:"cubic-bezier(.86,0,.07,1)",easeInSine:"cubic-bezier(.47,0,.745,.715)",easeOutSine:"cubic-bezier(.39,.575,.565,1)",easeInOutSine:"cubic-bezier(.445,.05,.55,.95)",easeInBack:"cubic-bezier(.6,-.28,.735,.045)",easeOutBack:"cubic-bezier(.175, .885,.32,1.275)",easeInOutBack:"cubic-bezier(.68,-.55,.265,1.55)"};k.cssHooks["transit:transform"]={get:function(r){return k(r).data("transform")||new j()},set:function(s,r){var t=r;if(!(t instanceof j)){t=new j(t)}if(q.transform==="WebkitTransform"&&!a){s.style[q.transform]=t.toString(true)}else{s.style[q.transform]=t.toString()}k(s).data("transform",t)}};k.cssHooks.transform={set:k.cssHooks["transit:transform"].set};if(k.fn.jquery<"1.8"){k.cssHooks.transformOrigin={get:function(r){return r.style[q.transformOrigin]},set:function(r,s){r.style[q.transformOrigin]=s}};k.cssHooks.transition={get:function(r){return r.style[q.transition]},set:function(r,s){r.style[q.transition]=s}}}n("scale");n("translate");n("rotate");n("rotateX");n("rotateY");n("rotate3d");n("perspective");n("skewX");n("skewY");n("x",true);n("y",true);function j(r){if(typeof r==="string"){this.parse(r)}return this}j.prototype={setFromString:function(t,s){var r=(typeof s==="string")?s.split(","):(s.constructor===Array)?s:[s];r.unshift(t);j.prototype.set.apply(this,r)},set:function(s){var r=Array.prototype.slice.apply(arguments,[1]);if(this.setter[s]){this.setter[s].apply(this,r)}else{this[s]=r.join(",")}},get:function(r){if(this.getter[r]){return this.getter[r].apply(this)}else{return this[r]||0}},setter:{rotate:function(r){this.rotate=o(r,"deg")},rotateX:function(r){this.rotateX=o(r,"deg")},rotateY:function(r){this.rotateY=o(r,"deg")},scale:function(r,s){if(s===undefined){s=r}this.scale=r+","+s},skewX:function(r){this.skewX=o(r,"deg")},skewY:function(r){this.skewY=o(r,"deg")},perspective:function(r){this.perspective=o(r,"px")},x:function(r){this.set("translate",r,null)},y:function(r){this.set("translate",null,r)},translate:function(r,s){if(this._translateX===undefined){this._translateX=0}if(this._translateY===undefined){this._translateY=0}if(r!==null&&r!==undefined){this._translateX=o(r,"px")}if(s!==null&&s!==undefined){this._translateY=o(s,"px")}this.translate=this._translateX+","+this._translateY}},getter:{x:function(){return this._translateX||0},y:function(){return this._translateY||0},scale:function(){var r=(this.scale||"1,1").split(",");if(r[0]){r[0]=parseFloat(r[0])}if(r[1]){r[1]=parseFloat(r[1])}return(r[0]===r[1])?r[0]:r},rotate3d:function(){var t=(this.rotate3d||"0,0,0,0deg").split(",");for(var r=0;r<=3;++r){if(t[r]){t[r]=parseFloat(t[r])}}if(t[3]){t[3]=o(t[3],"deg")}return t}},parse:function(s){var r=this;s.replace(/([a-zA-Z0-9]+)\((.*?)\)/g,function(t,v,u){r.setFromString(v,u)})},toString:function(t){var s=[];for(var r in this){if(this.hasOwnProperty(r)){if((!q.transform3d)&&((r==="rotateX")||(r==="rotateY")||(r==="perspective")||(r==="transformOrigin"))){continue}if(r[0]!=="_"){if(t&&(r==="scale")){s.push(r+"3d("+this[r]+",1)")}else{if(t&&(r==="translate")){s.push(r+"3d("+this[r]+",0)")}else{s.push(r+"("+this[r]+")")}}}}}return s.join(" ")}};function m(s,r,t){if(r===true){s.queue(t)}else{if(r){s.queue(r,t)}else{t()}}}function h(s){var r=[];k.each(s,function(t){t=k.camelCase(t);t=k.transit.propertyMap[t]||k.cssProps[t]||t;t=c(t);if(k.inArray(t,r)===-1){r.push(t)}});return r}function g(s,v,x,r){var t=h(s);if(k.cssEase[x]){x=k.cssEase[x]}var w=""+l(v)+" "+x;if(parseInt(r,10)>0){w+=" "+l(r)}var u=[];k.each(t,function(z,y){u.push(y+" "+w)});return u.join(", ")}k.fn.transition=k.fn.transit=function(z,s,y,C){var D=this;var u=0;var w=true;if(typeof s==="function"){C=s;s=undefined}if(typeof y==="function"){C=y;y=undefined}if(typeof z.easing!=="undefined"){y=z.easing;delete z.easing}if(typeof z.duration!=="undefined"){s=z.duration;delete z.duration}if(typeof z.complete!=="undefined"){C=z.complete;delete z.complete}if(typeof z.queue!=="undefined"){w=z.queue;delete z.queue}if(typeof z.delay!=="undefined"){u=z.delay;delete z.delay}if(typeof s==="undefined"){s=k.fx.speeds._default}if(typeof y==="undefined"){y=k.cssEase._default}s=l(s);var E=g(z,s,y,u);var B=k.transit.enabled&&q.transition;var t=B?(parseInt(s,10)+parseInt(u,10)):0;if(t===0){var A=function(F){D.css(z);if(C){C.apply(D)}if(F){F()}};m(D,w,A);return D}var x={};var r=function(H){var G=false;var F=function(){if(G){D.unbind(f,F)}if(t>0){D.each(function(){this.style[q.transition]=(x[this]||null)})}if(typeof C==="function"){C.apply(D)}if(typeof H==="function"){H()}};if((t>0)&&(f)&&(k.transit.useTransitionEnd)){G=true;D.bind(f,F)}else{window.setTimeout(F,t)}D.each(function(){if(t>0){this.style[q.transition]=E}k(this).css(z)})};var v=function(F){this.offsetWidth;r(F)};m(D,w,v);return this};function n(s,r){if(!r){k.cssNumber[s]=true}k.transit.propertyMap[s]=q.transform;k.cssHooks[s]={get:function(v){var u=k(v).css("transit:transform");return u.get(s)},set:function(v,w){var u=k(v).css("transit:transform");u.setFromString(s,w);k(v).css({"transit:transform":u})}}}function c(r){return r.replace(/([A-Z])/g,function(s){return"-"+s.toLowerCase()})}function o(s,r){if((typeof s==="string")&&(!s.match(/^[\-0-9\.]+$/))){return s}else{return""+s+r}}function l(s){var r=s;if(k.fx.speeds[r]){r=k.fx.speeds[r]}return o(r,"ms")}k.transit.getTransitionValue=g})(jQuery); \ No newline at end of file diff --git a/togetherjs/togetherjs.js b/togetherjs/togetherjs.js deleted file mode 100644 index 91954ee4d..000000000 --- a/togetherjs/togetherjs.js +++ /dev/null @@ -1,932 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/*jshint scripturl:true */ -(function () { - - var defaultConfiguration = { - // Disables clicks for a certain element. - // (e.g., 'canvas' would not show clicks on canvas elements.) - // Setting this to true will disable clicks globally. - dontShowClicks: false, - // Experimental feature to echo clicks to certain elements across clients: - cloneClicks: false, - // Enable Mozilla or Google analytics on the page when TogetherJS is activated: - // FIXME: these don't seem to be working, and probably should be removed in favor - // of the hub analytics - enableAnalytics: false, - // The code to enable (this is defaulting to a Mozilla code): - analyticsCode: "UA-35433268-28", - // The base URL of the hub (gets filled in below): - hubBase: null, - // A function that will return the name of the user: - getUserName: null, - // A function that will return the color of the user: - getUserColor: null, - // A function that will return the avatar of the user: - getUserAvatar: null, - // The siteName is used in the walkthrough (defaults to document.title): - siteName: null, - // Whether to use the minimized version of the code (overriding the built setting) - useMinimizedCode: undefined, - // Append cache-busting queries (useful for development!) - cacheBust: true, - // Any events to bind to - on: {}, - // Hub events to bind to - hub_on: {}, - // Enables the alt-T alt-T TogetherJS shortcut; however, this setting - // must be enabled early as TogetherJSConfig_enableShortcut = true; - enableShortcut: false, - // The name of this tool as provided to users. The UI is updated to use this. - // Because of how it is used in text it should be a proper noun, e.g., - // "MySite's Collaboration Tool" - toolName: null, - // Used to auto-start TogetherJS with a {prefix: pageName, max: participants} - // Also with findRoom: "roomName" it will connect to the given room name - findRoom: null, - // If true, starts TogetherJS automatically (of course!) - autoStart: false, - // If true, then the "Join TogetherJS Session?" confirmation dialog - // won't come up - suppressJoinConfirmation: false, - // If true, then the "Invite a friend" window won't automatically come up - suppressInvite: false, - // A room in which to find people to invite to this session, - inviteFromRoom: null, - // This is used to keep sessions from crossing over on the same - // domain, if for some reason you want sessions that are limited - // to only a portion of the domain: - storagePrefix: "togetherjs", - // When true, we treat the entire URL, including the hash, as the identifier - // of the page; i.e., if you one person is on `http://example.com/#view1` - // and another person is at `http://example.com/#view2` then these two people - // are considered to be at completely different URLs - includeHashInUrl: false, - // When true, the WebRTC-based mic/chat will be disabled - disableWebRTC: false, - // When true, youTube videos will synchronize - youtube: true, - // Ignores the following console messages, disables all messages if set to true - ignoreMessages: ["cursor-update", "keydown", "scroll-update"], - // Ignores the following forms (will ignore all forms if set to true): - ignoreForms: [":password"], - // When undefined, attempts to use the browser's language - lang: undefined, - fallbackLang: "en-US", - // Overrides the UI's font-family; accepts any CSS font-family value, - // including a reference to a CSS custom property already defined on - // the host page (e.g. "var(--font-base)") - baseFont: null - }; - - var styleSheet = "/togetherjs/togetherjs.css"; - - var baseUrl = "__baseUrl__"; - if (baseUrl == "__" + "baseUrl__") { - // Reset the variable if it doesn't get substituted - baseUrl = ""; - } - // Allow override of baseUrl (this is done separately because it needs - // to be done very early) - if (window.TogetherJSConfig && window.TogetherJSConfig.baseUrl) { - baseUrl = window.TogetherJSConfig.baseUrl; - } - if (window.TogetherJSConfig_baseUrl) { - baseUrl = window.TogetherJSConfig_baseUrl; - } - defaultConfiguration.baseUrl = baseUrl; - - // True if this file should use minimized sub-resources: - var min = "__min__" == "__" + "min__" ? false : "__min__" == "yes"; - - var baseUrlOverride = localStorage.getItem("togetherjs.baseUrlOverride"); - if (baseUrlOverride) { - try { - baseUrlOverride = JSON.parse(baseUrlOverride); - } catch (e) { - baseUrlOverride = null; - } - if ((! baseUrlOverride) || baseUrlOverride.expiresAt < Date.now()) { - // Ignore because it has expired - localStorage.removeItem("togetherjs.baseUrlOverride"); - } else { - baseUrl = baseUrlOverride.baseUrl; - var logger = console.warn || console.log; - logger.call(console, "Using TogetherJS baseUrlOverride:", baseUrl); - logger.call(console, "To undo run: localStorage.removeItem('togetherjs.baseUrlOverride')"); - } - } - - var configOverride = localStorage.getItem("togetherjs.configOverride"); - if (configOverride) { - try { - configOverride = JSON.parse(configOverride); - } catch (e) { - configOverride = null; - } - if ((! configOverride) || configOverride.expiresAt < Date.now()) { - localStorage.removeItem("togetherjs.configOverride"); - } else { - var shownAny = false; - for (var attr in configOverride) { - if (! configOverride.hasOwnProperty(attr)) { - continue; - } - if (attr == "expiresAt" || ! configOverride.hasOwnProperty(attr)) { - continue; - } - if (! shownAny) { - console.warn("Using TogetherJS configOverride"); - console.warn("To undo run: localStorage.removeItem('togetherjs.configOverride')"); - } - window["TogetherJSConfig_" + attr] = configOverride[attr]; - console.log("Config override:", attr, "=", configOverride[attr]); - } - } - } - - var version = "unknown"; - // FIXME: we could/should use a version from the checkout, at least - // for production - var cacheBust = "__gitCommit__"; - if ((! cacheBust) || cacheBust == "__gitCommit__") { - cacheBust = Date.now() + ""; - } else { - version = cacheBust; - } - - // Make sure we have all of the console.* methods: - if (typeof console == "undefined") { - console = {}; - } - if (! console.log) { - console.log = function () {}; - } - ["debug", "info", "warn", "error"].forEach(function (method) { - if (! console[method]) { - console[method] = console.log; - } - }); - - if (! baseUrl) { - var scripts = document.getElementsByTagName("script"); - for (var i=0; i with togetherjs.js and togetherjs-min.js)"); - } - - function addStyle() { - var existing = document.getElementById("togetherjs-stylesheet"); - if (! existing) { - var link = document.createElement("link"); - link.id = "togetherjs-stylesheet"; - link.setAttribute("rel", "stylesheet"); - link.href = baseUrl + styleSheet + - (cacheBust ? ("?bust=" + cacheBust) : ''); - document.head.appendChild(link); - } - } - - function addScript(url) { - var script = document.createElement("script"); - script.src = baseUrl + url + - (cacheBust ? ("?bust=" + cacheBust) : ''); - document.head.appendChild(script); - } - - var TogetherJS = window.TogetherJS = function TogetherJS(event) { - var session; - if (TogetherJS.running) { - session = TogetherJS.require("session"); - session.close(); - return; - } - TogetherJS.startup.button = null; - try { - if (event && typeof event == "object") { - if (event.target && typeof event) { - TogetherJS.startup.button = event.target; - } else if (event.nodeType == 1) { - TogetherJS.startup.button = event; - } else if (event[0] && event[0].nodeType == 1) { - // Probably a jQuery element - TogetherJS.startup.button = event[0]; - } - } - } catch (e) { - console.warn("Error determining starting button:", e); - } - if (window.TowTruckConfig) { - console.warn("TowTruckConfig is deprecated; please use TogetherJSConfig"); - if (window.TogetherJSConfig) { - console.warn("Ignoring TowTruckConfig in favor of TogetherJSConfig"); - } else { - window.TogetherJSConfig = TowTruckConfig; - } - } - if (window.TogetherJSConfig && (! window.TogetherJSConfig.loaded)) { - TogetherJS.config(window.TogetherJSConfig); - window.TogetherJSConfig.loaded = true; - } - - // This handles loading configuration from global variables. This - // includes TogetherJSConfig_on_*, which are attributes folded into - // the "on" configuration value. - var attr; - var attrName; - var globalOns = {}; - for (attr in window) { - if (attr.indexOf("TogetherJSConfig_on_") === 0) { - attrName = attr.substr(("TogetherJSConfig_on_").length); - globalOns[attrName] = window[attr]; - } else if (attr.indexOf("TogetherJSConfig_") === 0) { - attrName = attr.substr(("TogetherJSConfig_").length); - TogetherJS.config(attrName, window[attr]); - } else if (attr.indexOf("TowTruckConfig_on_") === 0) { - attrName = attr.substr(("TowTruckConfig_on_").length); - console.warn("TowTruckConfig_* is deprecated, please rename", attr, "to TogetherJSConfig_on_" + attrName); - globalOns[attrName] = window[attr]; - } else if (attr.indexOf("TowTruckConfig_") === 0) { - attrName = attr.substr(("TowTruckConfig_").length); - console.warn("TowTruckConfig_* is deprecated, please rename", attr, "to TogetherJSConfig_" + attrName); - TogetherJS.config(attrName, window[attr]); - } - - - } - // FIXME: copy existing config? - // FIXME: do this directly in TogetherJS.config() ? - // FIXME: close these configs? - var ons = TogetherJS.config.get("on"); - for (attr in globalOns) { - if (globalOns.hasOwnProperty(attr)) { - // FIXME: should we avoid overwriting? Maybe use arrays? - ons[attr] = globalOns[attr]; - } - } - TogetherJS.config("on", ons); - for (attr in ons) { - TogetherJS.on(attr, ons[attr]); - } - var hubOns = TogetherJS.config.get("hub_on"); - if (hubOns) { - for (attr in hubOns) { - if (hubOns.hasOwnProperty(attr)) { - TogetherJS.hub.on(attr, hubOns[attr]); - } - } - } - if (!TogetherJS.config.close('cacheBust')) { - cacheBust = ''; - delete TogetherJS.requireConfig.urlArgs; - } - - if (! TogetherJS.startup.reason) { - // Then a call to TogetherJS() from a button must be started TogetherJS - TogetherJS.startup.reason = "started"; - } - - // FIXME: maybe I should just test for TogetherJS.require: - if (TogetherJS._loaded) { - session = TogetherJS.require("session"); - addStyle(); - session.start(); - return; - } - // A sort of signal to session.js to tell it to actually - // start itself (i.e., put up a UI and try to activate) - TogetherJS.startup._launch = true; - - addStyle(); - var minSetting = TogetherJS.config.get("useMinimizedCode"); - TogetherJS.config.close("useMinimizedCode"); - if (minSetting !== undefined) { - min = !! minSetting; - } - var requireConfig = TogetherJS._extend(TogetherJS.requireConfig); - var deps = ["session", "jquery"]; - var lang = TogetherJS.getConfig("lang"); - // [igoryen]: We should generate this value in Gruntfile.js, based on the available translations - var availableTranslations = { - "en-US": true, - "en": "en-US", - "es": "es-BO", - "es-BO": true, - "ru": true, - "ru-RU": "ru", - "pl": "pl-PL", - "pl-PL": true, - "de-DE": true, - "de": "de-DE" - }; - - if(lang === undefined) { - // BCP 47 mandates hyphens, not underscores, to separate lang parts - lang = navigator.language.replace(/_/g, "-"); - } - if (/-/.test(lang) && !availableTranslations[lang]) { - lang = lang.replace(/-.*$/, ''); - } - if (!availableTranslations[lang]) { - lang = TogetherJS.config.get("fallbackLang"); - } else if (availableTranslations[lang] !== true) { - lang = availableTranslations[lang]; - } - TogetherJS.config("lang", lang); - - var localeTemplates = "templates-" + lang; - deps.splice(0, 0, localeTemplates); - function callback(session, jquery) { - TogetherJS._loaded = true; - if (! min) { - TogetherJS.require = require.config({context: "togetherjs"}); - TogetherJS._requireObject = require; - } - } - if (! min) { - if (typeof require == "function") { - if (! require.config) { - console.warn("The global require (", require, ") is not requirejs; please use togetherjs-min.js"); - throw new Error("Conflict with window.require"); - } - TogetherJS.require = require.config(requireConfig); - } - } - if (typeof TogetherJS.require == "function") { - // This is an already-configured version of require - TogetherJS.require(deps, callback); - } else { - requireConfig.deps = deps; - requireConfig.callback = callback; - if (! min) { - window.require = requireConfig; - } - } - if (min) { - addScript("/togetherjs/togetherjsPackage.js"); - } else { - addScript("/togetherjs/libs/require.js"); - } - }; - - TogetherJS.pageLoaded = Date.now(); - - TogetherJS._extend = function (base, extensions) { - if (! extensions) { - extensions = base; - base = {}; - } - for (var a in extensions) { - if (extensions.hasOwnProperty(a)) { - base[a] = extensions[a]; - } - } - return base; - }; - - TogetherJS._startupInit = { - // What element, if any, was used to start the session: - button: null, - // The startReason is the reason TogetherJS was started. One of: - // null: not started - // started: hit the start button (first page view) - // joined: joined the session (first page view) - reason: null, - // Also, the session may have started on "this" page, or maybe is continued - // from a past page. TogetherJS.continued indicates the difference (false the - // first time TogetherJS is started or joined, true on later page loads). - continued: false, - // This is set to tell the session what shareId to use, if the boot - // code knows (mostly because the URL indicates the id). - _joinShareId: null, - // This tells session to start up immediately (otherwise it would wait - // for session.start() to be run) - _launch: false - }; - TogetherJS.startup = TogetherJS._extend(TogetherJS._startupInit); - TogetherJS.running = false; - - TogetherJS.requireConfig = { - context: "togetherjs", - baseUrl: baseUrl + "/togetherjs", - urlArgs: "bust=" + cacheBust, - paths: { - jquery: "libs/jquery-1.11.1.min", - walkabout: "libs/walkabout/walkabout", - esprima: "libs/walkabout/lib/esprima", - falafel: "libs/walkabout/lib/falafel", - tinycolor: "libs/tinycolor", - whrandom: "libs/whrandom/random" - } - }; - - TogetherJS._mixinEvents = function (proto) { - proto.on = function on(name, callback) { - if (typeof callback != "function") { - console.warn("Bad callback for", this, ".once(", name, ", ", callback, ")"); - throw "Error: .once() called with non-callback"; - } - if (name.search(" ") != -1) { - var names = name.split(/ +/g); - names.forEach(function (n) { - this.on(n, callback); - }, this); - return; - } - if (this._knownEvents && this._knownEvents.indexOf(name) == -1) { - var thisString = "" + this; - if (thisString.length > 20) { - thisString = thisString.substr(0, 20) + "..."; - } - console.warn(thisString + ".on('" + name + "', ...): unknown event"); - if (console.trace) { - console.trace(); - } - } - if (! this._listeners) { - this._listeners = {}; - } - if (! this._listeners[name]) { - this._listeners[name] = []; - } - if (this._listeners[name].indexOf(callback) == -1) { - this._listeners[name].push(callback); - } - }; - proto.once = function once(name, callback) { - if (typeof callback != "function") { - console.warn("Bad callback for", this, ".once(", name, ", ", callback, ")"); - throw "Error: .once() called with non-callback"; - } - var attr = "onceCallback_" + name; - // FIXME: maybe I should add the event name to the .once attribute: - if (! callback[attr]) { - callback[attr] = function onceCallback() { - callback.apply(this, arguments); - this.off(name, onceCallback); - delete callback[attr]; - }; - } - this.on(name, callback[attr]); - }; - proto.off = proto.removeListener = function off(name, callback) { - if (this._listenerOffs) { - // Defer the .off() call until the .emit() is done. - this._listenerOffs.push([name, callback]); - return; - } - if (name.search(" ") != -1) { - var names = name.split(/ +/g); - names.forEach(function (n) { - this.off(n, callback); - }, this); - return; - } - if ((! this._listeners) || ! this._listeners[name]) { - return; - } - var l = this._listeners[name], _len = l.length; - for (var i=0; i<_len; i++) { - if (l[i] == callback) { - l.splice(i, 1); - break; - } - } - }; - proto.emit = function emit(name) { - var offs = this._listenerOffs = []; - if ((! this._listeners) || ! this._listeners[name]) { - return; - } - var args = Array.prototype.slice.call(arguments, 1); - var l = this._listeners[name]; - l.forEach(function (callback) { - - callback.apply(this, args); - }, this); - delete this._listenerOffs; - if (offs.length) { - offs.forEach(function (item) { - this.off(item[0], item[1]); - }, this); - } - - }; - return proto; - }; - - /* This finalizes the unloading of TogetherJS, including unloading modules */ - TogetherJS._teardown = function () { - var requireObject = TogetherJS._requireObject || window.require; - // FIXME: this doesn't clear the context for min-case - if (requireObject.s && requireObject.s.contexts) { - delete requireObject.s.contexts.togetherjs; - } - TogetherJS._loaded = false; - TogetherJS.startup = TogetherJS._extend(TogetherJS._startupInit); - TogetherJS.running = false; - }; - - TogetherJS._mixinEvents(TogetherJS); - TogetherJS._knownEvents = ["ready", "close"]; - TogetherJS.toString = function () { - return "TogetherJS"; - }; - - var defaultHubBase = "__hubUrl__"; - if (defaultHubBase == "__" + "hubUrl"+ "__") { - // Substitution wasn't made - defaultHubBase = "https://hub.togetherjs.mozillalabs.com"; - } - defaultConfiguration.hubBase = defaultHubBase; - - TogetherJS._configuration = {}; - TogetherJS._defaultConfiguration = { - // Disables clicks for a certain element. - // (e.g., 'canvas' would not show clicks on canvas elements.) - // Setting this to true will disable clicks globally. - dontShowClicks: false, - // Experimental feature to echo clicks to certain elements across clients: - cloneClicks: false, - // Enable Mozilla or Google analytics on the page when TogetherJS is activated: - // FIXME: these don't seem to be working, and probably should be removed in favor - // of the hub analytics - enableAnalytics: false, - // The code to enable (this is defaulting to a Mozilla code): - analyticsCode: "UA-35433268-28", - // The base URL of the hub - hubBase: defaultHubBase, - // A function that will return the name of the user: - getUserName: null, - // A function that will return the color of the user: - getUserColor: null, - // A function that will return the avatar of the user: - getUserAvatar: null, - // The siteName is used in the walkthrough (defaults to document.title): - siteName: null, - // Whether to use the minimized version of the code (overriding the built setting) - useMinimizedCode: undefined, - // Any events to bind to - on: {}, - // Hub events to bind to - hub_on: {}, - // Enables the alt-T alt-T TogetherJS shortcut; however, this setting - // must be enabled early as TogetherJSConfig_enableShortcut = true; - enableShortcut: false, - // The name of this tool as provided to users. The UI is updated to use this. - // Because of how it is used in text it should be a proper noun, e.g., - // "MySite's Collaboration Tool" - toolName: null, - // Used to auto-start TogetherJS with a {prefix: pageName, max: participants} - // Also with findRoom: "roomName" it will connect to the given room name - findRoom: null, - // If true, starts TogetherJS automatically (of course!) - autoStart: false, - // If true, then the "Join TogetherJS Session?" confirmation dialog - // won't come up - suppressJoinConfirmation: false, - // If true, then the "Invite a friend" window won't automatically come up - suppressInvite: false, - // A room in which to find people to invite to this session, - inviteFromRoom: null, - // This is used to keep sessions from crossing over on the same - // domain, if for some reason you want sessions that are limited - // to only a portion of the domain: - storagePrefix: "togetherjs", - // When true, we treat the entire URL, including the hash, as the identifier - // of the page; i.e., if you one person is on `http://example.com/#view1` - // and another person is at `http://example.com/#view2` then these two people - // are considered to be at completely different URLs - includeHashInUrl: false, - // The language to present the tool in, such as "en-US" or "ru-RU" - // Note this must be set as TogetherJSConfig_lang, as it effects the loader - // and must be set as soon as this file is included - lang: null - }; - // FIXME: there's a point at which configuration can't be updated - // (e.g., hubBase after the TogetherJS has loaded). We should keep - // track of these and signal an error if someone attempts to - // reconfigure too late - - TogetherJS.getConfig = function (name) { // rename into TogetherJS.config.get()? - var value = TogetherJS._configuration[name]; - if (value === undefined) { - if (! TogetherJS._defaultConfiguration.hasOwnProperty(name)) { - console.error("Tried to load unknown configuration value:", name); - } - value = TogetherJS._defaultConfiguration[name]; - } - return value; - }; - TogetherJS._defaultConfiguration = defaultConfiguration; - TogetherJS._configTrackers = {}; - TogetherJS._configClosed = {}; - - /* TogetherJS.config(configurationObject) - or: TogetherJS.config(configName, value) - - Adds configuration to TogetherJS. You may also set the global variable TogetherJSConfig - and when TogetherJS is started that configuration will be loaded. - - Unknown configuration values will lead to console error messages. - */ - TogetherJS.config = function (name, maybeValue) { - var settings; - if (arguments.length == 1) { - if (typeof name != "object") { - throw new Error('TogetherJS.config(value) must have an object value (not: ' + name + ')'); - } - settings = name; - } else { - settings = {}; - settings[name] = maybeValue; - } - var i; - var tracker; - var attr; - for (attr in settings) { - if (settings.hasOwnProperty(attr)) { - if (TogetherJS._configClosed[attr] && TogetherJS.running) { - throw new Error("The configuration " + attr + " is finalized and cannot be changed"); - } - } - } - for (attr in settings) { - if (! settings.hasOwnProperty(attr)) { - continue; - } - if (attr == "loaded" || attr == "callToStart") { - continue; - } - if (! TogetherJS._defaultConfiguration.hasOwnProperty(attr)) { - console.warn("Unknown configuration value passed to TogetherJS.config():", attr); - } - var previous = TogetherJS._configuration[attr]; - var value = settings[attr]; - TogetherJS._configuration[attr] = value; - var trackers = TogetherJS._configTrackers[name] || []; - var failed = false; - for (i=0; i but not a ')); - }); - - TogetherJS.config.track("disableWebRTC", function (hide, previous) { - if (hide && ! previous) { - ui.container.find("#togetherjs-audio-button").hide(); - adjustDockSize(-1); - } else if ((! hide) && previous) { - ui.container.find("#togetherjs-audio-button").show(); - adjustDockSize(1); - } - }); - - }; - - // After prepareUI, this actually makes the interface live. We have - // to do this later because we call prepareUI when many components - // aren't initialized, so we don't even want the user to be able to - // interact with the interface. But activateUI is called once - // everything is loaded and ready for interaction. - ui.activateUI = function () { - if (deferringPrepareUI) { - console.warn("ui.activateUI called before document is ready; waiting..."); - deferringPrepareUI = "activate"; - return; - } - if (! ui.container) { - ui.prepareUI(); - } - var container = ui.container; - - //create the overlay - if($.browser.mobile) { - // $("body").append( "\x3cdiv class='overlay' style='position: absolute; top: 0; left: 0; background-color: rgba(0,0,0,0); width: 120%; height: 100%; z-index: 1000; margin: -10px'>\x3c/div>" ); - } - - // The share link: - ui.prepareShareLink(container); - container.find("input.togetherjs-share-link").on("keydown", function (event) { - if (event.which == 27) { - windowing.hide("#togetherjs-share"); - return false; - } - return undefined; - }); - session.on("shareId", updateShareLink); - - // The chat input element: - var input = container.find("#togetherjs-chat-input"); - input.bind("keydown", function (event) { - if (event.which == 13 && !event.shiftKey) { // Enter without Shift pressed - submitChat(); - return false; - } - if (event.which == 27) { // Escape - windowing.hide("#togetherjs-chat"); - return false; - } - }); - - function submitChat() { - var val = input.val(); - if ($.trim(val)) { - input.val(""); - // triggering the event manually to avoid the addition of newline character to the textarea: - input.trigger("input").trigger("propertychange"); - chat.submit(val); - } - } - // auto-resize textarea: - input.on("input propertychange", function () { - var $this = $(this); - var actualHeight = $this.height(); - // reset the height of textarea to remove trailing empty space (used for shrinking): - $this.height(TEXTAREA_LINE_HEIGHT); - this.scrollTop = 0; - // scroll to bottom: - this.scrollTop = 9999; - var newHeight = this.scrollTop + $this.height(); - var maxHeight = TEXTAREA_MAX_LINES * TEXTAREA_LINE_HEIGHT; - if (newHeight > maxHeight) { - newHeight = maxHeight; - this.style.overflowY = "scroll"; - } else { - this.style.overflowY = "hidden"; - } - this.style.height = newHeight + "px"; - var diff = newHeight - actualHeight; - $("#togetherjs-chat-input-box").height($("#togetherjs-chat-input-box").height() + diff); - $("#togetherjs-chat-messages").height($("#togetherjs-chat-messages").height() - diff); - return false; - }); - - util.testExpose({submitChat: submitChat}); - - // Moving the window: - // FIXME: this should probably be stickier, and not just move the window around - // so abruptly - var anchor = container.find("#togetherjs-dock-anchor"); - assert(anchor.length); - // FIXME: This is in place to temporarily disable dock dragging: - anchor = container.find("#togetherjs-dock-anchor-disabled"); - anchor.mousedown(function (event) { - var iface = $("#togetherjs-dock"); - // FIXME: switch to .offset() and pageX/Y - var startPos = panelPosition(); - function selectoff() { - return false; - } - function mousemove(event2) { - var fromRight = $window.width() + window.pageXOffset - event2.pageX; - var fromLeft = event2.pageX - window.pageXOffset; - var fromBottom = $window.height() + window.pageYOffset - event2.pageY; - // FIXME: this is to temporarily disable the bottom view: - fromBottom = 10000; - - var pos; - if (fromLeft < fromRight && fromLeft < fromBottom) { - pos = "left"; - } else if (fromRight < fromLeft && fromRight < fromBottom) { - pos = "right"; - } else { - pos = "bottom"; - } - iface.removeClass("togetherjs-dock-left"); - iface.removeClass("togetherjs-dock-right"); - iface.removeClass("togetherjs-dock-bottom"); - iface.addClass("togetherjs-dock-" + pos); - if (startPos && pos != startPos) { - windowing.hide(); - startPos = null; - } - } - $(document).bind("mousemove", mousemove); - // If you don't turn selection off it will still select text, and show a - // text selection cursor: - $(document).bind("selectstart", selectoff); - // FIXME: it seems like sometimes we lose the mouseup event, and it's as though - // the mouse is stuck down: - $(document).one("mouseup", function () { - $(document).unbind("mousemove", mousemove); - $(document).unbind("selectstart", selectoff); - }); - return false; - }); - - function openDock() { - $('.togetherjs-window').animate({ - opacity: 1 - }); - $('#togetherjs-dock-participants').animate({ - opacity: 1 - }); - $('#togetherjs-dock #togetherjs-buttons').animate({ - opacity: 1 - }); - - //for iphone - if($(window).width() < 480) { - $('.togetherjs-dock-right').animate({ - width: "204px" - }, { - duration:60, easing:"linear" - }); - } - - //for ipad - else { - $('.togetherjs-dock-right').animate({ - width: "27%" - }, { - duration:60, easing:"linear" - }); - } - - - // add bg overlay - // $("body").append( "\x3cdiv class='overlay' style='position: absolute; top: 0; left: -2px; background-color: rgba(0,0,0,0.5); width: 200%; height: 400%; z-index: 1000; margin: 0px;'>\x3c/div>" ); - - //disable vertical scrolling - // $("body").css({ - // "position": "fixed", - // top: 0, - // left: 0 - // }); - - //replace the anchor icon - var src = "/togetherjs/images/togetherjs-logo-close.png"; - $("#togetherjs-dock-anchor #togetherjs-dock-anchor-horizontal img").attr("src", src); - } - - function closeDock() { - //enable vertical scrolling - $("body").css({ - "position": "", - top: "", - left: "" - }); - - //replace the anchor icon - var src = "/togetherjs/images/togetherjs-logo-open.png"; - $("#togetherjs-dock-anchor #togetherjs-dock-anchor-horizontal img").attr("src", src); - - $('.togetherjs-window').animate({ - opacity: 0 - }); - $('#togetherjs-dock-participants').animate({ - opacity: 0 - }); - $('#togetherjs-dock #togetherjs-buttons').animate({ - opacity: 0 - }); - $('.togetherjs-dock-right').animate({ - width: "40px" - }, { - duration:60, easing:"linear" - }); - - // remove bg overlay - //$(".overlay").remove(); - } - - // Setting the anchor button + dock mobile actions - if($.browser.mobile) { - - // toggle the audio button - $("#togetherjs-audio-button").click(function () { - windowing.toggle("#togetherjs-rtc-not-supported"); - }); - - // toggle the profile button - $("#togetherjs-profile-button").click(function () { - windowing.toggle("#togetherjs-menu-window"); - }); - - // $("body").append( "\x3cdiv class='overlay' style='position: absolute; top: 0; left: -2px; background-color: rgba(0,0,0,0.5); width: 200%; height: 400%; z-index: 1000; margin: 0px'>\x3c/div>" ); - - //disable vertical scrolling - // $("body").css({ - // "position": "fixed", - // top: 0, - // left: 0 - // }); - - //replace the anchor icon - var src = "/togetherjs/images/togetherjs-logo-close.png"; - $("#togetherjs-dock-anchor #togetherjs-dock-anchor-horizontal img").attr("src", src); - - $("#togetherjs-dock-anchor").toggle(function() { - closeDock(); - },function(){ - openDock(); - }); - } - - $("#togetherjs-share-button").click(function () { - windowing.toggle("#togetherjs-share"); - }); - - $("#togetherjs-profile-button").click(function (event) { - if ($.browser.mobile) { - windowing.show("#togetherjs-menu-window"); - return false; - } - toggleMenu(); - event.stopPropagation(); - return false; - }); - - $("#togetherjs-menu-feedback, #togetherjs-menu-feedback-button").click(function(){ - windowing.hide(); - hideMenu(); - windowing.show("#togetherjs-feedback-form"); - }); - - $("#togetherjs-menu-help, #togetherjs-menu-help-button").click(function () { - windowing.hide(); - hideMenu(); - require(["walkthrough"], function (walkthrough) { - windowing.hide(); - walkthrough.start(false); - }); - }); - - $("#togetherjs-menu-update-name").click(function () { - var input = $("#togetherjs-menu .togetherjs-self-name"); - input.css({ - width: $("#togetherjs-menu").width() - 32 + "px" - }); - ui.displayToggle("#togetherjs-menu .togetherjs-self-name"); - $("#togetherjs-menu .togetherjs-self-name").focus(); - }); - - $("#togetherjs-menu-update-name-button").click(function () { - windowing.show("#togetherjs-edit-name-window"); - $("#togetherjs-edit-name-window input").focus(); - }); - - $("#togetherjs-menu .togetherjs-self-name").bind("keyup change", function (event) { - console.log("alrighty", event); - if (event.which == 13) { - ui.displayToggle("#togetherjs-self-name-display"); - return; - } - var val = $("#togetherjs-menu .togetherjs-self-name").val(); - console.log("values!!", val); - if (val) { - peers.Self.update({name: val}); - } - }); - - $("#togetherjs-menu-update-avatar, #togetherjs-menu-update-avatar-button").click(function () { - hideMenu(); - windowing.show("#togetherjs-avatar-edit"); - }); - - $("#togetherjs-menu-end, #togetherjs-menu-end-button").click(function () { - hideMenu(); - windowing.show("#togetherjs-confirm-end"); - }); - - $("#togetherjs-end-session").click(function () { - session.close(); - //$(".overlay").remove(); - - }); - - $("#togetherjs-menu-update-color").click(function () { - var picker = $("#togetherjs-pick-color"); - if (picker.is(":visible")) { - picker.hide(); - return; - } - picker.show(); - bindPicker(); - picker.find(".togetherjs-swatch-active").removeClass("togetherjs-swatch-active"); - picker.find(".togetherjs-swatch[data-color=\"" + peers.Self.color + "\"]").addClass("togetherjs-swatch-active"); - }); - - $("#togetherjs-pick-color").click(".togetherjs-swatch", function (event) { - var swatch = $(event.target); - var color = swatch.attr("data-color"); - peers.Self.update({ - color: color - }); - event.stopPropagation(); - return false; - }); - - $("#togetherjs-pick-color").click(function (event) { - $("#togetherjs-pick-color").hide(); - event.stopPropagation(); - return false; - }); - - COLORS.forEach(function (color) { - var el = templating.sub("swatch"); - el.attr("data-color", color); - var darkened = tinycolor.darken(color); - el.css({ - backgroundColor: color, - borderColor: darkened - }); - $("#togetherjs-pick-color").append(el); - }); - - $("#togetherjs-chat-button").click(function () { - windowing.toggle("#togetherjs-chat"); - }); - - session.on("display-window", function (id, element) { - if (id == "togetherjs-chat") { - if (! $.browser.mobile) { - $("#togetherjs-chat-input").focus(); - } - } else if (id == "togetherjs-share") { - var link = element.find("input.togetherjs-share-link"); - if (link.is(":visible")) { - link.focus().select(); - } - } - }); - - container.find("#togetherjs-chat-notifier").click(function (event) { - if ($(event.target).is("a") || container.is(".togetherjs-close")) { - return; - } - windowing.show("#togetherjs-chat"); - }); - - // FIXME: Don't think this makes sense - $(".togetherjs header.togetherjs-title").each(function (index, item) { - var button = $(''); - button.click(function (event) { - var window = button.closest(".togetherjs-window"); - windowing.hide(window); - }); - $(item).append(button); - }); - - $("#togetherjs-avatar-done").click(function () { - ui.displayToggle("#togetherjs-no-avatar-edit"); - }); - - $("#togetherjs-self-color").css({backgroundColor: peers.Self.color}); - - var avatar = peers.Self.avatar; - if (avatar) { - $("#togetherjs-self-avatar").attr("src", avatar); - } - - var starterButton = $("#togetherjs-starter button"); - starterButton.click(function () { - windowing.show("#togetherjs-about"); - }).addClass("togetherjs-running"); - if (starterButton.text() == "Start TogetherJS") { - starterButton.attr("data-start-text", starterButton.text()); - starterButton.text("End TogetherJS Session"); - } - - ui.activateAvatarEdit(container, { - onSave: function () { - windowing.hide("#togetherjs-avatar-edit"); - } - }); - - TogetherJS.config.track("inviteFromRoom", function (inviter, previous) { - if (inviter) { - container.find("#togetherjs-invite").show(); - } else { - container.find("#togetherjs-invite").hide(); - } - }); - - container.find("#togetherjs-menu-refresh-invite").click(refreshInvite); - container.find("#togetherjs-menu-invite-anyone").click(function () { - invite(null); - }); - - // The following lines should be at the end of this function - // (new code goes above) - session.emit("new-element", ui.container); - - if (finishedAt && finishedAt > Date.now()) { - setTimeout(function () { - finishedAt = null; - session.emit("ui-ready", ui); - }, finishedAt - Date.now()); - } else { - session.emit("ui-ready", ui); - } - - }; // End ui.activateUI() - - ui.activateAvatarEdit = function (container, options) { - options = options || {}; - var pendingImage = null; - - container.find(".togetherjs-avatar-save").prop("disabled", true); - - container.find(".togetherjs-avatar-save").click(function () { - if (pendingImage) { - peers.Self.update({avatar: pendingImage}); - container.find(".togetherjs-avatar-save").prop("disabled", true); - if (options.onSave) { - options.onSave(); - } - } - }); - - container.find(".togetherjs-upload-avatar").on("change", function () { - util.readFileImage(this).then(function (url) { - sizeDownImage(url).then(function (smallUrl) { - pendingImage = smallUrl; - container.find(".togetherjs-avatar-preview").css({ - backgroundImage: 'url(' + pendingImage + ')' - }); - container.find(".togetherjs-avatar-save").prop("disabled", false); - if (options.onPending) { - options.onPending(); - } - }); - }); - }); - - }; - - function sizeDownImage(imageUrl) { - return util.Deferred(function (def) { - var $canvas = $(""); - $canvas[0].height = session.AVATAR_SIZE; - $canvas[0].width = session.AVATAR_SIZE; - var context = $canvas[0].getContext("2d"); - var img = new Image(); - img.src = imageUrl; - // Sometimes the DOM updates immediately to call - // naturalWidth/etc, and sometimes it doesn't; using setTimeout - // gives it a chance to catch up - setTimeout(function () { - var width = img.naturalWidth || img.width; - var height = img.naturalHeight || img.height; - width = width * (session.AVATAR_SIZE / height); - height = session.AVATAR_SIZE; - context.drawImage(img, 0, 0, width, height); - def.resolve($canvas[0].toDataURL("image/png")); - }); - }); - } - - function fixupAvatars(container) { - /* All
elements need an element inside, - so we add that element here */ - container.find(".togetherjs-person").each(function () { - var $this = $(this); - var inner = $this.find(".togetherjs-person-avatar-swatch"); - if (! inner.length) { - $this.append('
'); - } - }); - } - - ui.prepareShareLink = function (container) { - container.find("input.togetherjs-share-link").click(function () { - $(this).select(); - }).change(function () { - updateShareLink(); - }); - container.find("a.togetherjs-share-link").click(function () { - // FIXME: this is currently opening up Bluetooth, not sharing a link - if (false && window.MozActivity) { - var activity = new MozActivity({ - name: "share", - data: { - type: "url", - url: $(this).attr("href") - } - }); - } - // FIXME: should show some help if you actually try to follow the link - // like this, instead of simply suppressing it - return false; - }); - updateShareLink(); - }; - - // Menu - - function showMenu(event) { - var el = $("#togetherjs-menu"); - assert(el.length); - el.show(); - bindMenu(); - $(document).bind("click", maybeHideMenu); - } - - function bindMenu() { - var el = $("#togetherjs-menu:visible"); - if (el.length) { - var bound = $("#togetherjs-profile-button"); - var boundOffset = bound.offset(); - el.css({ - top: boundOffset.top + bound.height() - $window.scrollTop() + "px", - left: (boundOffset.left + bound.width() - 10 - el.width() - $window.scrollLeft()) + "px" - }); - } - } - - function bindPicker() { - var picker = $("#togetherjs-pick-color:visible"); - if (picker.length) { - var menu = $("#togetherjs-menu-update-color"); - var menuOffset = menu.offset(); - picker.css({ - top: menuOffset.top + menu.height(), - left: menuOffset.left - }); - } - } - - session.on("resize", function () { - bindMenu(); - bindPicker(); - }); - - function toggleMenu() { - if ($("#togetherjs-menu").is(":visible")) { - hideMenu(); - } else { - showMenu(); - } - } - - function hideMenu() { - var el = $("#togetherjs-menu"); - el.hide(); - $(document).unbind("click", maybeHideMenu); - ui.displayToggle("#togetherjs-self-name-display"); - $("#togetherjs-pick-color").hide(); - } - - function maybeHideMenu(event) { - var t = event.target; - while (t) { - if (t.id == "togetherjs-menu") { - // Click inside the menu, ignore this - return; - } - t = t.parentNode; - } - hideMenu(); - } - - function adjustDockSize(buttons) { - /* Add or remove spots from the dock; positive number to - add button(s), negative number to remove button(s) - */ - assert(typeof buttons == "number"); - assert(buttons && Math.floor(buttons) == buttons); - var iface = $("#togetherjs-dock"); - var newHeight = iface.height() + (BUTTON_HEIGHT * buttons); - assert(newHeight >= BUTTON_HEIGHT * 3, "Height went too low (", newHeight, - "), should never be less than 3 buttons high (", BUTTON_HEIGHT * 3, ")"); - iface.css({ - height: newHeight + "px" - }); - } - - // Misc - - function updateShareLink() { - var input = $("input.togetherjs-share-link"); - var link = $("a.togetherjs-share-link"); - var display = $("#togetherjs-session-id"); - if (! session.shareId) { - input.val(""); - link.attr("href", "#"); - display.text("(none)"); - } else { - input.val(session.shareUrl()); - link.attr("href", session.shareUrl()); - display.text(session.shareId); - } - } - - session.on("close", function () { - - if($.browser.mobile) { - // remove bg overlay - //$(".overlay").remove(); - - //after hitting End, reset window draggin - $("body").css({ - "position": "", - top: "", - left: "" - }); - - } - - if (ui.container) { - ui.container.remove(); - ui.container = null; - } - // Clear out any other spurious elements: - $(".togetherjs").remove(); - var starterButton = $("#togetherjs-starter button"); - starterButton.removeClass("togetherjs-running"); - if (starterButton.attr("data-start-text")) { - starterButton.text(starterButton.attr("data-start-text")); - starterButton.attr("data-start-text", ""); - } - if (TogetherJS.startTarget) { - var el = $(TogetherJS.startTarget); - if (el.attr("data-start-togetherjs-html")) { - el.html(el.attr("data-start-togetherjs-html")); - } - el.removeClass("togetherjs-started"); - } - }); - - ui.chat = { - text: function (attrs) { - assert(typeof attrs.text == "string"); - assert(attrs.peer); - assert(attrs.messageId); - var date = attrs.date || Date.now(); - var lastEl = ui.container.find("#togetherjs-chat .togetherjs-chat-message"); - if (lastEl.length) { - lastEl = $(lastEl[lastEl.length-1]); - } - var lastDate = null; - if (lastEl) { - lastDate = parseInt(lastEl.attr("data-date"), 10); - } - if (lastEl && lastEl.attr("data-person") == attrs.peer.id && - lastDate && date < lastDate + COLLAPSE_MESSAGE_LIMIT) { - lastEl.attr("data-date", date); - var content = lastEl.find(".togetherjs-chat-content"); - assert(content.length); - attrs.text = content.text() + "\n" + attrs.text; - attrs.messageId = lastEl.attr("data-message-id"); - lastEl.remove(); - } - var el = templating.sub("chat-message", { - peer: attrs.peer, - content: attrs.text, - date: date - }); - linkify(el.find(".togetherjs-chat-content")); - el.attr("data-person", attrs.peer.id) - .attr("data-date", date) - .attr("data-message-id", attrs.messageId); - ui.chat.add(el, attrs.messageId, attrs.notify); - }, - - joinedSession: function (attrs) { - assert(attrs.peer); - var date = attrs.date || Date.now(); - var el = templating.sub("chat-joined", { - peer: attrs.peer, - date: date - }); - // FIXME: should bind the notification to the dock location - ui.chat.add(el, attrs.peer.className("join-message-"), 4000); - }, - - leftSession: function (attrs) { - assert(attrs.peer); - var date = attrs.date || Date.now(); - var el = templating.sub("chat-left", { - peer: attrs.peer, - date: date, - declinedJoin: attrs.declinedJoin - }); - // FIXME: should bind the notification to the dock location - ui.chat.add(el, attrs.peer.className("join-message-"), 4000); - }, - - system: function (attrs) { - assert(! attrs.peer); - assert(typeof attrs.text == "string"); - var date = attrs.date || Date.now(); - var el = templating.sub("chat-system", { - content: attrs.text, - date: date - }); - ui.chat.add(el, undefined, true); - }, - - clear: deferForContainer(function () { - var container = ui.container.find("#togetherjs-chat-messages"); - container.empty(); - }), - - urlChange: function (attrs) { - assert(attrs.peer); - assert(typeof attrs.url == "string"); - assert(typeof attrs.sameUrl == "boolean"); - var messageId = attrs.peer.className("url-change-"); - // FIXME: duplicating functionality in .add(): - var realId = "togetherjs-chat-" + messageId; - var date = attrs.date || Date.now(); - var title; - // FIXME: strip off common domain from msg.url? E.g., if I'm on - // http://example.com/foobar, and someone goes to http://example.com/baz then - // show only /baz - // FIXME: truncate long titles - if (attrs.title) { - title = attrs.title + " (" + attrs.url + ")"; - } else { - title = attrs.url; - } - var el = templating.sub("url-change", { - peer: attrs.peer, - date: date, - href: attrs.url, - title: title, - sameUrl: attrs.sameUrl - }); - el.find(".togetherjs-nudge").click(function () { - attrs.peer.nudge(); - return false; - }); - el.find(".togetherjs-follow").click(function () { - var url = attrs.peer.url; - if (attrs.peer.urlHash) { - url += attrs.peer.urlHash; - } - location.href = url; - }); - var notify = ! attrs.sameUrl; - if (attrs.sameUrl && ! $("#" + realId).length) { - // Don't bother showing a same-url notification, if no previous notification - // had been shown - return; - } - ui.chat.add(el, messageId, notify); - }, - - invite: function (attrs) { - assert(attrs.peer); - assert(typeof attrs.url == "string"); - var messageId = attrs.peer.className("invite-"); - var date = attrs.date || Date.now(); - var hrefTitle = attrs.url.replace(/\#?&togetherjs=.*/, "").replace(/^\w+:\/\//, ""); - var el = templating.sub("invite", { - peer: attrs.peer, - date: date, - href: attrs.url, - hrefTitle: hrefTitle, - forEveryone: attrs.forEveryone - }); - if (attrs.forEveryone) { - el.find("a").click(function () { - // FIXME: hacky way to do this: - chat.submit("Followed link to " + attrs.url); - }); - } - ui.chat.add(el, messageId, true); - }, - - hideTimeout: null, - - add: deferForContainer(function (el, id, notify) { - if (id) { - el.attr("id", "togetherjs-chat-" + util.safeClassName(id)); - } - var container = ui.container.find("#togetherjs-chat-messages"); - assert(container.length); - var popup = ui.container.find("#togetherjs-chat-notifier"); - container.append(el); - ui.chat.scroll(); - var doNotify = !! notify; - var section = popup.find("#togetherjs-chat-notifier-message"); - if (notify && visibilityApi.hidden()) { - ui.container.find("#togetherjs-notification")[0].play(); - } - if (id && section.data("message-id") == id) { - doNotify = true; - } - if (container.is(":visible")) { - doNotify = false; - } - if (doNotify) { - section.empty(); - section.append(el.clone(true, true)); - if (section.data("message-id") != id) { - section.data("message-id", id || ""); - windowing.show(popup); - } else if (! popup.is(":visible")) { - windowing.show(popup); - } - if (typeof notify == "number") { - // This is the amount of time we're supposed to notify - if (this.hideTimeout) { - clearTimeout(this.hideTimeout); - this.hideTimeout = null; - } - this.hideTimeout = setTimeout((function () { - windowing.hide(popup); - this.hideTimeout = null; - }).bind(this), notify); - } - } - }), - - scroll: deferForContainer(function () { - var container = ui.container.find("#togetherjs-chat-messages")[0]; - container.scrollTop = container.scrollHeight; - }) - - }; - - session.on("display-window", function (id, win) { - if (id == "togetherjs-chat") { - ui.chat.scroll(); - windowing.hide("#togetherjs-chat-notifier"); - } - }); - - /* This class is bound to peers.Peer instances as peer.view. - The .update() method is regularly called by peer objects when info changes. */ - ui.PeerView = util.Class({ - - constructor: function (peer) { - assert(peer.isSelf !== undefined, "PeerView instantiated with non-Peer object"); - this.peer = peer; - this.dockClick = this.dockClick.bind(this); - }, - - /* Takes an element and sets any person-related attributes on the element - Different from updates, which use the class names we set here: */ - setElement: function (el) { - var count = 0; - var classes = ["togetherjs-person", "togetherjs-person-status", - "togetherjs-person-name", "togetherjs-person-name-abbrev", - "togetherjs-person-bgcolor", "togetherjs-person-swatch", - "togetherjs-person-status", "togetherjs-person-role", - "togetherjs-person-url", "togetherjs-person-url-title", - "togetherjs-person-bordercolor"]; - classes.forEach(function (cls) { - var els = el.find("." + cls); - els.addClass(this.peer.className(cls + "-")); - count += els.length; - }, this); - if (! count) { - console.warn("setElement(", el, ") doesn't contain any person items"); - } - this.updateDisplay(el); - }, - - updateDisplay: deferForContainer(function (container) { - container = container || ui.container; - var abbrev = this.peer.name; - if (this.peer.isSelf) { - abbrev = "me"; - } - container.find("." + this.peer.className("togetherjs-person-name-")).text(this.peer.name || ""); - container.find("." + this.peer.className("togetherjs-person-name-abbrev-")).text(abbrev); - var avatarEl = container.find("." + this.peer.className("togetherjs-person-")); - if (this.peer.avatar) { - util.assertValidUrl(this.peer.avatar); - avatarEl.css({ - backgroundImage: "url(" + this.peer.avatar + ")" - }); - } - if (this.peer.idle == "inactive") { - avatarEl.addClass("togetherjs-person-inactive"); - } else { - avatarEl.removeClass("togetherjs-person-inactive"); - } - avatarEl.attr("title", this.peer.name); - if (this.peer.color) { - avatarEl.css({ - borderColor: this.peer.color - }); - avatarEl.find(".togetherjs-person-avatar-swatch").css({ - borderTopColor: this.peer.color, - borderRightColor: this.peer.color - }); - } - if (this.peer.color) { - var colors = container.find("." + this.peer.className("togetherjs-person-bgcolor-")); - colors.css({ - backgroundColor: this.peer.color - }); - colors = container.find("." + this.peer.className("togetherjs-person-bordercolor-")); - colors.css({ - borderColor: this.peer.color - }); - } - container.find("." + this.peer.className("togetherjs-person-role-")) - .text(this.peer.isCreator ? "Creator" : "Participant"); - var urlName = this.peer.title || ""; - if (this.peer.title) { - urlName += " ("; - } - urlName += util.truncateCommonDomain(this.peer.url, location.href); - if (this.peer.title) { - urlName += ")"; - } - container.find("." + this.peer.className("togetherjs-person-url-title-")) - .text(urlName); - var url = this.peer.url; - if (this.peer.urlHash) { - url += this.peer.urlHash; - } - container.find("." + this.peer.className("togetherjs-person-url-")) - .attr("href", url); - // FIXME: should have richer status: - container.find("." + this.peer.className("togetherjs-person-status-")) - .text(this.peer.idle == "active" ? "Active" : "Inactive"); - if (this.peer.isSelf) { - // FIXME: these could also have consistent/reliable class names: - var selfName = $(".togetherjs-self-name"); - selfName.each((function (index, el) { - el = $(el); - if (el.val() != this.peer.name) { - el.val(this.peer.name); - } - }).bind(this)); - $("#togetherjs-menu-avatar").attr("src", this.peer.avatar); - if (! this.peer.name) { - $("#togetherjs-menu .togetherjs-person-name-self").text(this.peer.defaultName); - } - } - if (this.peer.url != session.currentUrl()) { - container.find("." + this.peer.className("togetherjs-person-")) - .addClass("togetherjs-person-other-url"); - } else { - container.find("." + this.peer.className("togetherjs-person-")) - .removeClass("togetherjs-person-other-url"); - } - if (this.peer.following) { - if (this.followCheckbox) { - this.followCheckbox.prop("checked", true); - } - } else { - if (this.followCheckbox) { - this.followCheckbox.prop("checked", false); - } - } - // FIXME: add some style based on following? - updateChatParticipantList(); - this.updateFollow(); - }), - - update: function () { - if (! this.peer.isSelf) { - if (this.peer.status == "live") { - this.dock(); - } else { - this.undock(); - } - } - this.updateDisplay(); - this.updateUrlDisplay(); - }, - - updateUrlDisplay: function (force) { - var url = this.peer.url; - if ((! url) || (url == this._lastUpdateUrlDisplay && ! force)) { - return; - } - this._lastUpdateUrlDisplay = url; - var sameUrl = url == session.currentUrl(); - ui.chat.urlChange({ - peer: this.peer, - url: this.peer.url, - title: this.peer.title, - sameUrl: sameUrl - }); - }, - - urlNudge: function () { - // FIXME: do something more distinct here - this.updateUrlDisplay(true); - }, - - notifyJoined: function () { - ui.chat.joinedSession({ - peer: this.peer - }); - }, - - // when there are too many participants in the dock, consolidate the participants to one avatar, and on mouseOver, the dock expands down to reveal the rest of the participants - // if there are X users in the session - // then hide the users in the dock - // and shrink the size of the dock - // and if you rollover the dock, it expands and reveals the rest of the participants in the dock - - //if users hit X then show the participant button with the consol - - dock: deferForContainer(function () { - - var numberOfUsers = peers.getAllPeers().length; - - // collapse the Dock if too many users - function CollapsedDock() { - // decrease/reset dock height - $("#togetherjs-dock").css("height", 260); - //replace participant button - $("#togetherjs-dock-participants").replaceWith(""); - // new full participant window created on toggle - $("#togetherjs-participantlist-button").click(function () { - windowing.toggle("#togetherjs-participantlist"); - }); - } - - // FIXME: turned off for now - if( numberOfUsers >= 5 && false) { - CollapsedDock(); - } else { - // reset - - } - - - if (this.dockElement) { - return; - } - this.dockElement = templating.sub("dock-person", { - peer: this.peer - }); - this.dockElement.attr("id", this.peer.className("togetherjs-dock-element-")); - ui.container.find("#togetherjs-dock-participants").append(this.dockElement); - this.dockElement.find(".togetherjs-person").animateDockEntry(); - adjustDockSize(1); - this.detailElement = templating.sub("participant-window", { - peer: this.peer - }); - var followId = this.peer.className("togetherjs-person-status-follow-"); - this.detailElement.find('[for="togetherjs-person-status-follow"]').attr("for", followId); - this.detailElement.find('#togetherjs-person-status-follow').attr("id", followId); - this.detailElement.find(".togetherjs-follow").click(function () { - location.href = $(this).attr("href"); - }); - this.detailElement.find(".togetherjs-nudge").click((function () { - this.peer.nudge(); - }).bind(this)); - this.followCheckbox = this.detailElement.find("#" + followId); - this.followCheckbox.change(function () { - if (! this.checked) { - this.peer.unfollow(); - } - // Following doesn't happen until the window is closed - // FIXME: should we tell the user this? - }); - this.maybeHideDetailWindow = this.maybeHideDetailWindow.bind(this); - session.on("hide-window", this.maybeHideDetailWindow); - ui.container.append(this.detailElement); - this.dockElement.click((function () { - if (this.detailElement.is(":visible")) { - windowing.hide(this.detailElement); - } else { - windowing.show(this.detailElement, {bind: this.dockElement}); - this.scrollTo(); - this.cursor().element.animate({ - opacity:0.3 - }).animate({ - opacity:1 - }).animate({ - opacity:0.3 - }).animate({ - opacity:1 - }); - } - }).bind(this)); - this.updateFollow(); - }), - - undock: function () { - if (! this.dockElement) { - return; - } - this.dockElement.animateDockExit().promise().then((function () { - this.dockElement.remove(); - this.dockElement = null; - this.detailElement.remove(); - this.detailElement = null; - adjustDockSize(-1); - }).bind(this)); - }, - - scrollTo: function () { - if (this.peer.url != session.currentUrl()) { - return; - } - var pos = this.peer.scrollPosition; - if (! pos) { - console.warn("Peer has no scroll position:", this.peer); - return; - } - pos = elementFinder.pixelForPosition(pos); - $("html, body").easeTo(pos); - }, - - updateFollow: function () { - if (! this.peer.url) { - return; - } - if (! this.detailElement) { - return; - } - var same = this.detailElement.find(".togetherjs-same-url"); - var different = this.detailElement.find(".togetherjs-different-url"); - if (this.peer.url == session.currentUrl()) { - same.show(); - different.hide(); - } else { - same.hide(); - different.show(); - } - }, - - maybeHideDetailWindow: function (windows) { - if (this.detailElement && windows[0] && windows[0][0] === this.detailElement[0]) { - if (this.followCheckbox[0].checked) { - this.peer.follow(); - } else { - this.peer.unfollow(); - } - } - }, - - dockClick: function () { - // FIXME: scroll to person - }, - - cursor: function () { - return require("cursor").getClient(this.peer.id); - }, - - destroy: function () { - // FIXME: should I get rid of the dockElement? - session.off("hide-window", this.maybeHideDetailWindow); - } - }); - - function updateChatParticipantList() { - var live = peers.getAllPeers(true); - if (live.length) { - ui.displayToggle("#togetherjs-chat-participants"); - $("#togetherjs-chat-participant-list").text( - live.map(function (p) {return p.name;}).join(", ")); - } else { - ui.displayToggle("#togetherjs-chat-no-participants"); - } - } - - function inviteHubUrl() { - var base = TogetherJS.config.get("inviteFromRoom"); - assert(base); - return util.makeUrlAbsolute(base, session.hubUrl()); - } - - var inRefresh = false; - - function refreshInvite() { - if (inRefresh) { - return; - } - inRefresh = true; - require(["who"], function (who) { - var def = who.getList(inviteHubUrl()); - function addUser(user, before) { - var item = templating.sub("invite-user-item", {peer: user}); - item.attr("data-clientid", user.id); - if (before) { - item.insertBefore(before); - } else { - $("#togetherjs-invite-users").append(item); - } - item.click(function() { - invite(user.clientId); - }); - } - function refresh(users, finished) { - var sorted = []; - for (var id in users) { - if (users.hasOwnProperty(id)) { - sorted.push(users[id]); - } - } - sorted.sort(function (a, b) { - return a.name < b.name ? -1 : 1; - }); - var pos = 0; - ui.container.find("#togetherjs-invite-users .togetherjs-menu-item").each(function () { - var $this = $(this); - if (finished && ! users[$this.attr("data-clientid")]) { - $this.remove(); - return; - } - if (pos >= sorted.length) { - return; - } - while (pos < sorted.length && $this.attr("data-clientid") !== sorted[pos].id) { - addUser(sorted[pos], $this); - pos++; - } - while (pos < sorted.length && $this.attr("data-clientid") == sorted[pos].id) { - pos++; - } - }); - for (var i=pos; i TOO_FAR_APART; - } - - session.on("close", unsetListeners); - - function unsetListeners() { - var videos = $('video'); - listeners.forEach(function (event) { - videos.off(event.name, event.listener); - }); - listeners = []; - } - - - session.hub.on('video-timeupdate', function (msg) { - var element = $findElement(msg.location); - var oldTime = element.prop('currentTime'); - var newTime = msg.position; - - //to help throttle uneccesary position changes - if(areTooFarApart(oldTime, newTime)){ - setTime(element, msg.position); - } - }); - - MIRRORED_EVENTS.forEach( function (eventName) { - session.hub.on("video-"+eventName, function (msg) { - var element = $findElement(msg.location); - - setTime(element, msg.position); - - element.trigger(eventName, {silent: true}); - }); - }); - - //Currently does not discriminate between visible and invisible videos - function $findElement(location) { - return $(elementFinder.findElement(location)); - } - - function setTime(video, time) { - video.prop('currentTime', time); - } - -}); diff --git a/togetherjs/visibilityApi.js b/togetherjs/visibilityApi.js deleted file mode 100644 index 6d4b6ce94..000000000 --- a/togetherjs/visibilityApi.js +++ /dev/null @@ -1,46 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/* Loading this module will cause, when TogetherJS is active, the - session object to emit visibility-change with a `hidden` argument - whenever the visibility changes, on browsers where we can detect - it. - */ - -define(["util", "session"], function (util, session) { - var visibilityApi = util.Module("visibilityApi"); - var hidden; - var visibilityChange; - if (document.hidden !== undefined) { // Opera 12.10 and Firefox 18 and later support - hidden = "hidden"; - visibilityChange = "visibilitychange"; - } else if (document.mozHidden !== undefined) { - hidden = "mozHidden"; - visibilityChange = "mozvisibilitychange"; - } else if (document.msHidden !== undefined) { - hidden = "msHidden"; - visibilityChange = "msvisibilitychange"; - } else if (document.webkitHidden !== undefined) { - hidden = "webkitHidden"; - visibilityChange = "webkitvisibilitychange"; - } - - session.on("start", function () { - document.addEventListener(visibilityChange, change, false); - }); - - session.on("close", function () { - document.removeEventListener(visibilityChange, change, false); - }); - - function change() { - session.emit("visibility-change", document[hidden]); - } - - visibilityApi.hidden = function () { - return document[hidden]; - }; - - return visibilityApi; -}); diff --git a/togetherjs/walkthrough.js b/togetherjs/walkthrough.js deleted file mode 100644 index 9acd32236..000000000 --- a/togetherjs/walkthrough.js +++ /dev/null @@ -1,151 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -define(["util", "ui", "jquery", "windowing", "templates", "templating", "session", "peers"], function (util, ui, $, windowing, templates, templating, session, peers) { - var assert = util.assert; - var walkthrough = util.Module("walkthrough"); - var onHideAll = null; - var container = null; - - var slides = null; - - walkthrough.start = function (firstTime, doneCallback) { - if (! container) { - container = $(templates("walkthrough")); - container.hide(); - ui.container.append(container); - slides = container.find(".togetherjs-walkthrough-slide"); - slides.hide(); - var progress = $("#togetherjs-walkthrough-progress"); - slides.each(function (index) { - var bullet = templating.sub("walkthrough-slide-progress"); - progress.append(bullet); - bullet.click(function () { - show(index); - }); - }); - container.find("#togetherjs-walkthrough-previous").click(previous); - container.find("#togetherjs-walkthrough-next").click(next); - ui.prepareShareLink(container); - container.find(".togetherjs-self-name").bind("keyup", function (event) { - var val = $(event.target).val(); - peers.Self.update({name: val}); - }); - container.find(".togetherjs-swatch").click(function () { - var picker = $("#togetherjs-pick-color"); - if (picker.is(":visible")) { - picker.hide(); - return; - } - picker.show(); - picker.find(".togetherjs-swatch-active").removeClass("togetherjs-swatch-active"); - picker.find(".togetherjs-swatch[data-color=\"" + peers.Self.color + "\"]").addClass("togetherjs-swatch-active"); - var location = container.find(".togetherjs-swatch").offset(); - picker.css({ - top: location.top, - // The -7 comes out of thin air, but puts it in the right place: - left: location.left-7 - }); - }); - if (session.isClient) { - container.find(".togetherjs-if-creator").remove(); - container.find(".togetherjs-ifnot-creator").show(); - } else { - container.find(".togetherjs-if-creator").show(); - container.find(".togetherjs-ifnot-creator").remove(); - } - TogetherJS.config.track("siteName", function (value) { - value = value || document.title; - container.find(".togetherjs-site-name").text(value); - }); - ui.activateAvatarEdit(container, { - onSave: function () { - container.find("#togetherjs-avatar-when-saved").show(); - container.find("#togetherjs-avatar-when-unsaved").hide(); - }, - onPending: function () { - container.find("#togetherjs-avatar-when-saved").hide(); - container.find("#togetherjs-avatar-when-unsaved").show(); - } - }); - // This triggers substititions in the walkthrough: - peers.Self.update({}); - session.emit("new-element", container); - } - assert(typeof firstTime == "boolean", "You must provide a firstTime boolean parameter"); - if (firstTime) { - container.find(".togetherjs-walkthrough-firsttime").show(); - container.find(".togetherjs-walkthrough-not-firsttime").hide(); - } else { - container.find(".togetherjs-walkthrough-firsttime").hide(); - container.find(".togetherjs-walkthrough-not-firsttime").show(); - } - onHideAll = doneCallback; - show(0); - windowing.show(container); - }; - - function show(index) { - slides.hide(); - $(slides[index]).show(); - var bullets = container.find("#togetherjs-walkthrough-progress .togetherjs-walkthrough-slide-progress"); - bullets.removeClass("togetherjs-active"); - $(bullets[index]).addClass("togetherjs-active"); - var $next = $("#togetherjs-walkthrough-next").removeClass("togetherjs-disabled"); - var $previous = $("#togetherjs-walkthrough-previous").removeClass("togetherjs-disabled"); - if (index == slides.length - 1) { - $next.addClass("togetherjs-disabled"); - } else if (index === 0) { - $previous.addClass("togetherjs-disabled"); - } - } - - function previous() { - var index = getIndex(); - index--; - if (index < 0) { - index = 0; - } - show(index); - } - - function next() { - var index = getIndex(); - index++; - if (index >= slides.length) { - index = slides.length-1; - } - show(index); - } - - function getIndex() { - var active = slides.filter(":visible"); - if (! active.length) { - return 0; - } - for (var i=0; i"); - $canvas[0].height = session.AVATAR_SIZE; - $canvas[0].width = session.AVATAR_SIZE; - var context = $canvas[0].getContext("2d"); - context.arc(session.AVATAR_SIZE/2, session.AVATAR_SIZE/2, session.AVATAR_SIZE/2, 0, Math.PI*2); - context.closePath(); - context.clip(); - context.drawImage($video[0], (session.AVATAR_SIZE - width) / 2, 0, width, height); - savePicture($canvas[0].toDataURL("image/png")); - } - - $upload.on("change", function () { - var reader = new FileReader(); - reader.onload = function () { - // FIXME: I don't actually know it's JPEG, but it's probably a - // good enough guess: - var url = "data:image/jpeg;base64," + util.blobToBase64(this.result); - convertImage(url, function (result) { - savePicture(result); - }); - }; - reader.onerror = function () { - console.error("Error reading file:", this.error); - }; - reader.readAsArrayBuffer(this.files[0]); - }); - - function convertImage(imageUrl, callback) { - var $canvas = $(""); - $canvas[0].height = session.AVATAR_SIZE; - $canvas[0].width = session.AVATAR_SIZE; - var context = $canvas[0].getContext("2d"); - var img = new Image(); - img.src = imageUrl; - // Sometimes the DOM updates immediately to call - // naturalWidth/etc, and sometimes it doesn't; using setTimeout - // gives it a chance to catch up - setTimeout(function () { - var width = img.naturalWidth || img.width; - var height = img.naturalHeight || img.height; - width = width * (session.AVATAR_SIZE / height); - height = session.AVATAR_SIZE; - context.drawImage(img, 0, 0, width, height); - callback($canvas[0].toDataURL("image/png")); - }); - } - - }); - - /**************************************** - * RTC support - */ - - function audioButton(selector) { - ui.displayToggle(selector); - if (selector == "#togetherjs-audio-incoming") { - $("#togetherjs-audio-button").addClass("togetherjs-animated").addClass("togetherjs-color-alert"); - } else { - $("#togetherjs-audio-button").removeClass("togetherjs-animated").removeClass("togetherjs-color-alert"); - } - } - - session.on("ui-ready", function () { - $("#togetherjs-audio-button").click(function () { - if ($("#togetherjs-rtc-info").is(":visible")) { - windowing.hide(); - return; - } - if (session.RTCSupported) { - enableAudio(); - } else { - windowing.show("#togetherjs-rtc-not-supported"); - } - }); - - if (! session.RTCSupported) { - audioButton("#togetherjs-audio-unavailable"); - return; - } - audioButton("#togetherjs-audio-ready"); - - var audioStream = null; - var accepted = false; - var connected = false; - var $audio = $("#togetherjs-audio-element"); - var offerSent = null; - var offerReceived = null; - var offerDescription = false; - var answerSent = null; - var answerReceived = null; - var answerDescription = false; - var _connection = null; - var iceCandidate = null; - - function enableAudio() { - accepted = true; - storage.settings.get("dontShowRtcInfo").then(function (dontShow) { - if (! dontShow) { - windowing.show("#togetherjs-rtc-info"); - } - }); - if (! audioStream) { - startStreaming(connect); - return; - } - if (! connected) { - connect(); - } - toggleMute(); - } - - ui.container.find("#togetherjs-rtc-info .togetherjs-dont-show-again").change(function () { - storage.settings.set("dontShowRtcInfo", this.checked); - }); - - function error() { - console.warn.apply(console, arguments); - var s = ""; - for (var i=0; i= expected) { - close(); - } else { - def.notify(users); - } - } - } - console.log("users", users); - }; - channel.send({ - type: "who", - "server-echo": true, - clientId: null - }); - var timeout = setTimeout(function () { - close(); - }, MAX_RESPONSE_TIME); - function close() { - if (timeout) { - clearTimeout(timeout); - } - if (lateResponseTimeout) { - clearTimeout(lateResponseTimeout); - } - channel.close(); - def.resolve(users); - } - }); - }; - - who.invite = function (hubUrl, clientId) { - return util.Deferred(function (def) { - var channel = channels.WebSocketChannel(hubUrl); - var id = util.generateId(); - channel.onmessage = function (msg) { - if (msg.type == "invite" && msg.inviteId == id) { - channel.close(); - def.resolve(); - } - }; - var userInfo = session.makeHelloMessage(false); - delete userInfo.type; - userInfo.clientId = session.clientId; - channel.send({ - type: "invite", - inviteId: id, - url: session.shareUrl(), - userInfo: userInfo, - forClientId: clientId, - clientId: null, - "server-echo": true - }); - }); - }; - - who.ExternalPeer = util.Class({ - isSelf: false, - isExternal: true, - constructor: function (id, attrs) { - attrs = attrs || {}; - assert(id); - this.id = id; - this.identityId = attrs.identityId || null; - this.status = attrs.status || "live"; - this.idle = attrs.status || "active"; - this.name = attrs.name || null; - this.avatar = attrs.avatar || null; - this.color = attrs.color || "#00FF00"; - this.lastMessageDate = 0; - this.view = ui.PeerView(this); - }, - - className: function (prefix) { - prefix = prefix || ""; - return prefix + util.safeClassName(this.id); - } - - }); - - return who; -}); diff --git a/togetherjs/windowing.js b/togetherjs/windowing.js deleted file mode 100644 index ee02fc537..000000000 --- a/togetherjs/windowing.js +++ /dev/null @@ -1,216 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ -define(["jquery", "util", "peers", "session"], function ($, util, peers, session) { - var assert = util.assert; - var windowing = util.Module("windowing"); - var $window = $(window); - // This is also in togetherjs.less, under .togetherjs-animated - var ANIMATION_DURATION = 1000; - - /* Displays one window. A window must already exist. This hides other windows, and - positions the window according to its data-bound-to attributes */ - windowing.show = function (element, options) { - element = $(element); - options = options || {}; - options.bind = options.bind || element.attr("data-bind-to"); - var notification = element.hasClass("togetherjs-notification"); - var modal = element.hasClass("togetherjs-modal"); - if (options.bind) { - options.bind = $(options.bind); - } - windowing.hide(); - element.stop(); - element.show(); - // In addition to being hidden, the window can be faded out, which we want to undo: - element.css({opacity: "1"}); - if (options.bind) { - assert(! modal, "Binding does not currently work with modals"); - bind(element, options.bind); - } - if (notification) { - element.slideIn(); - } else if (! modal) { - element.popinWindow(); - } - if (modal) { - getModalBackground().show(); - modalEscape.bind(); - } - onClose = options.onClose || null; - session.emit("display-window", element.attr("id"), element); - }; - - var onClose = null; - - /* Moves a window to be attached to data-bind-to, e.g., the button - that opened the window. Or you can provide an element that it should bind to. */ - function bind(win, bound) { - if ($.browser.mobile) { - return; - } - win = $(win); - assert(bound.length, "Cannot find binding:", bound.selector, "from:", win.selector); - // FIXME: hardcoding - var ifacePos = "right"; - //var ifacePos = panelPosition(); - var boundPos = bound.offset(); - boundPos.height = bound.height(); - boundPos.width = bound.width(); - var windowHeight = $window.height(); - boundPos.top -= $window.scrollTop(); - boundPos.left -= $window.scrollLeft(); - // FIXME: I appear to have to add the padding to the width to get a "true" - // width. But it's still not entirely consistent. - var height = win.height() + 5; - var width = win.width() + 20; - var left, top; - if (ifacePos == "right") { - left = boundPos.left - 11 - width; - top = boundPos.top + (boundPos.height / 2) - (height / 2); - } else if (ifacePos == "left") { - left = boundPos.left + boundPos.width + 15; - top = boundPos.top + (boundPos.height / 2) - (height / 2); - } else if (ifacePos == "bottom") { - left = (boundPos.left + boundPos.width / 2) - (width / 2); - top = boundPos.top - 10 - height; - } - top = Math.min(windowHeight - 10 - height, Math.max(10, top)); - win.css({ - top: top + "px", - left: left + "px" - }); - if (win.hasClass("togetherjs-window")) { - $("#togetherjs-window-pointer-right, #togetherjs-window-pointer-left").hide(); - var pointer = $("#togetherjs-window-pointer-" + ifacePos); - pointer.show(); - if (ifacePos == "right") { - pointer.css({ - top: boundPos.top + Math.floor(boundPos.height / 2) + "px", - left: left + win.width() + 9 + "px" - }); - } else if (ifacePos == "left") { - pointer.css({ - top: boundPos.top + Math.floor(boundPos.height / 2) + "px", - left: (left - 5) + "px" - }); - } else { - console.warn("don't know how to deal with position:", ifacePos); - } - } - win.data("boundTo", bound.selector || "#" + bound.attr("id")); - bound.addClass("togetherjs-active"); - } - - session.on("resize", function () { - var win = $(".togetherjs-modal:visible, .togetherjs-window:visible"); - if (! win.length) { - return; - } - var boundTo = win.data("boundTo"); - if (! boundTo) { - return; - } - boundTo = $(boundTo); - bind(win, boundTo); - }); - - windowing.hide = function (els) { - // FIXME: also hide modals? - els = els || ".togetherjs-window, .togetherjs-modal, .togetherjs-notification"; - els = $(els); - els = els.filter(":visible"); - els.filter(":not(.togetherjs-notification)").hide(); - getModalBackground().hide(); - var windows = []; - els.each(function (index, element) { - element = $(element); - windows.push(element); - var bound = element.data("boundTo"); - if (! bound) { - return; - } - bound = $(bound); - bound.addClass("togetherjs-animated").addClass("togetherjs-color-pulse"); - setTimeout(function () { - bound.removeClass("togetherjs-color-pulse").removeClass("togetherjs-animated"); - }, ANIMATION_DURATION+10); - element.data("boundTo", null); - bound.removeClass("togetherjs-active"); - if (element.hasClass("togetherjs-notification")) { - element.fadeOut().promise().then(function () { - this.hide(); - }); - } - }); - $("#togetherjs-window-pointer-right, #togetherjs-window-pointer-left").hide(); - if (onClose) { - onClose(); - onClose = null; - } - if (windows.length) { - session.emit("hide-window", windows); - } - }; - - windowing.showNotification = function (element, options) { - element = $(element); - options = options || {}; - assert(false); - }; - - windowing.toggle = function (el) { - el = $(el); - if (el.is(":visible")) { - windowing.hide(el); - } else { - windowing.show(el); - } - }; - - function bindEvents(el) { - el.find(".togetherjs-close, .togetherjs-dismiss").click(function (event) { - var w = $(event.target).closest(".togetherjs-window, .togetherjs-modal, .togetherjs-notification"); - windowing.hide(w); - event.stopPropagation(); - return false; - }); - } - - function getModalBackground() { - if (getModalBackground.element) { - return getModalBackground.element; - } - var background = $("#togetherjs-modal-background"); - assert(background.length); - getModalBackground.element = background; - background.click(function () { - windowing.hide(); - }); - return background; - } - - var modalEscape = { - bind: function () { - $(document).keydown(modalEscape.onKeydown); - }, - unbind: function () { - $(document).unbind("keydown", modalEscape.onKeydown); - }, - onKeydown: function (event) { - if (event.which == 27) { - windowing.hide(); - } - } - }; - - session.on("close", function () { - modalEscape.unbind(); - }); - - session.on("new-element", function (el) { - bindEvents(el); - }); - - return windowing; -}); diff --git a/togetherjs/youtubeVideos.js b/togetherjs/youtubeVideos.js deleted file mode 100644 index 95d6a9ee1..000000000 --- a/togetherjs/youtubeVideos.js +++ /dev/null @@ -1,302 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http:// mozilla.org/MPL/2.0/. */ - -define(["jquery", "util", "session", "elementFinder"], -function ($, util, session, elementFinder) { - - // constant var to indicate whether two players are too far apart in sync - var TOO_FAR_APART = 3000; - // embedded youtube iframes - var youTubeIframes = []; - // youtube API load delay - var API_LOADING_DELAY = 2000; - - session.on("reinitialize", function () { - if (TogetherJS.config.get("youtube")) { - prepareYouTube(); - } - }); - - session.on("close", function () { - $(youTubeIframes).each(function (i, iframe) { - // detach players from iframes - $(iframe).removeData("togetherjs-player"); - $(iframe).removeData("dontPublish"); - $(iframe).removeData("currentVideoId"); - // disable iframeAPI - $(iframe).removeAttr("enablejsapi"); - // remove unique youtube iframe indicators - var id = $(iframe).attr("id") || ""; - if (id.indexOf("youtube-player") === 0) { - // An id we added - $(iframe).removeAttr("id"); - } - youTubeIframes = []; - }); - }); - - $(function() { - TogetherJS.config.track("youtube", function (track, previous) { - if (track && ! previous) { - prepareYouTube(); - // You can enable youtube dynamically, but can't turn it off: - TogetherJS.config.close("youtube"); - } - }); - }); - - var youtubeHooked = false; - - function prepareYouTube() { - // setup iframes first - setupYouTubeIframes(); - - // this function should be global so it can be called when API is loaded - if (!youtubeHooked) { - youtubeHooked = true; - window.onYouTubeIframeAPIReady = (function(oldf) { - return function() { - // YouTube API is ready - $(youTubeIframes).each(function (i, iframe) { - var player = new YT.Player(iframe.id, { // get the reference to the already existing iframe - events: { - 'onReady': insertPlayer, - 'onStateChange': publishPlayerStateChange - } - }); - }); - if (oldf) { - return oldf(); - } - }; - })(window.onYouTubeIframeAPIReady); - } - - if (window.YT === undefined) { - // load necessary API - // it calls onYouTubeIframeAPIReady automatically when the API finishes loading - var tag = document.createElement('script'); - tag.src = "https://www.youtube.com/iframe_api"; - var firstScriptTag = document.getElementsByTagName('script')[0]; - firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); - } else { - // manually invoke APIReady function when the API was already loaded by user - onYouTubeIframeAPIReady(); - } - - // give each youtube iframe a unique id and set its enablejsapi param to true - function setupYouTubeIframes() { - var iframes = $('iframe'); - iframes.each(function (i, iframe) { - // if the iframe's unique id is already set, skip it - // FIXME: what if the user manually sets an iframe's id (i.e. "#my-youtube")? - // maybe we should set iframes everytime togetherjs is reinitialized? - var osrc = $(iframe).attr("src"), src = osrc; - if ((src || "").indexOf("youtube") != -1 && !$(iframe).attr("id")) { - $(iframe).attr("id", "youtube-player"+i); - $(iframe).attr("enablejsapi", 1); - // we also need to add ?enablejsapi to the iframe src. - if (!/[?&]enablejsapi=1(&|$)/.test(src)) { - src += (/[?]/.test(src)) ? '&' : '?'; - src += 'enablejsapi=1'; - } - // the youtube API seems to be unhappy unless the URL starts - // with https - if (!/^https[:]\/\//.test(src)) { - src = 'https://' + src.replace(/^(\w+[:])?\/\//, ''); - } - if (src !== osrc) { - $(iframe).attr("src", src); - } - youTubeIframes[i] = iframe; - } - }); - } // iframes are ready - - function insertPlayer(event) { - // only when it is READY, attach a player to its iframe - var currentPlayer = event.target; - var currentIframe = currentPlayer.getIframe(); - // check if a player is already attached in case of being reinitialized - if (!$(currentIframe).data("togetherjs-player")) { - $(currentIframe).data("togetherjs-player", currentPlayer); - // initialize its dontPublish flag as well - $(currentIframe).data("dontPublish", false); - // store its current video's id - var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl()); - $(currentIframe).data("currentVideoId", currentVideoId); - } - } - } // end of prepareYouTube - - function publishPlayerStateChange(event) { - var target = event.target; - var currentIframe = target.getIframe(); - //var currentPlayer = $(currentIframe).data("togetherjs-player"); - var currentPlayer = target; - var currentTime = currentPlayer.getCurrentTime(); - //var currentTime = target.k.currentTime; - var iframeLocation = elementFinder.elementLocation(currentIframe); - - if ($(currentPlayer).data("seek")) { - $(currentPlayer).removeData("seek"); - return; - } - - // do not publish if playerState was changed by other users - if ($(currentIframe).data("dontPublish")) { - // make it false again so it can start publishing events of its own state changes - $(currentIframe).data("dontPublish", false); - return; - } - - // notify other people that I changed the player state - if (event.data == YT.PlayerState.PLAYING) { - - var currentVideoId = isDifferentVideoLoaded(currentIframe); - if (currentVideoId) { - // notify that I just loaded another video - publishDifferentVideoLoaded(iframeLocation, currentVideoId); - // update current video id - $(currentIframe).data("currentVideoId", currentVideoId); - } else { - session.send({ - type: "playerStateChange", - element: iframeLocation, - playerState: 1, - playerTime: currentTime - }); - } - } else if (event.data == YT.PlayerState.PAUSED) { - session.send({ - type: "playerStateChange", - element: iframeLocation, - playerState: 2, - playerTime: currentTime - }); - } else { - // do nothing when the state is buffering, cued, or ended - return; - } - } - - function publishDifferentVideoLoaded(iframeLocation, videoId) { - session.send({ - type: "differentVideoLoaded", - videoId: videoId, - element: iframeLocation - }); - } - - session.hub.on('playerStateChange', function (msg) { - var iframe = elementFinder.findElement(msg.element); - var player = $(iframe).data("togetherjs-player"); - var currentTime = player.getCurrentTime(); - var currentState = player.getPlayerState(); - - if (currentState != msg.playerState) { - $(iframe).data("dontPublish", true); - } - - if (msg.playerState == 1) { - player.playVideo(); - // seekTo() updates the video's time and plays it if it was already playing - // and pauses it if it was already paused - if (areTooFarApart(currentTime, msg.playerTime)) { - player.seekTo(msg.playerTime, true); - } - } else if (msg.playerState == 2) { - // When YouTube videos are advanced while playing, - // Chrome: pause -> pause -> play (onStateChange is called even when it is from pause to pause) - // FireFox: buffering -> play -> buffering -> play - // We must prevent advanced videos from going out of sync - player.pauseVideo(); - if (areTooFarApart(currentTime, msg.playerTime)) { - // "seek" flag will help supress publishing unwanted state changes - $(player).data("seek", true); - player.seekTo(msg.playerTime, true); - } - } - }); - - // if a late user joins a channel, synchronize his videos - session.hub.on('hello', function () { - // wait a couple seconds to make sure the late user has finished loading API - setTimeout(synchronizeVideosOfLateGuest, API_LOADING_DELAY); - }); - - session.hub.on('synchronizeVideosOfLateGuest', function (msg) { - // XXX can this message arrive before we're initialized? - var iframe = elementFinder.findElement(msg.element); - var player = $(iframe).data("togetherjs-player"); - // check if another video had been loaded to an existing iframe before I joined - var currentVideoId = getVideoIdFromUrl(player.getVideoUrl()); - if (msg.videoId != currentVideoId) { - $(iframe).data("currentVideoId", msg.videoId); - player.loadVideoById(msg.videoId, msg.playerTime, 'default'); - } else { - // if the video is only cued, I do not have to do anything to sync - if (msg.playerState != 5) { - player.seekTo(msg.playerTime, true).playVideo(); - } - } - }); - - session.hub.on('differentVideoLoaded', function (msg) { - // load a new video if the host has loaded one - var iframe = elementFinder.findElement(msg.element); - var player = $(iframe).data("togetherjs-player"); - player.loadVideoById(msg.videoId, 0, 'default'); - $(iframe).data("currentVideoId", msg.videoId); - - }); - - function synchronizeVideosOfLateGuest() { - youTubeIframes.forEach(function (iframe) { - var currentPlayer = $(iframe).data("togetherjs-player"); - var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl()); - var currentState = currentPlayer.getPlayerState(); - var currentTime = currentPlayer.getCurrentTime(); - var iframeLocation = elementFinder.elementLocation(iframe); - session.send({ - type: "synchronizeVideosOfLateGuest", - element: iframeLocation, - videoId: currentVideoId, - playerState: currentState, //this might be necessary later - playerTime: currentTime - }); - }); - } - - function isDifferentVideoLoaded(iframe) { - var lastVideoId = $(iframe).data("currentVideoId"); - var currentPlayer = $(iframe).data("togetherjs-player"); - var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl()); - - // since url forms of iframe src and player's video url are different, - // I have to compare the video ids - if (currentVideoId != lastVideoId) { - return currentVideoId; - } else { - return false; - } - } - - // parses videoId from the url returned by getVideoUrl function - function getVideoIdFromUrl(videoUrl) { - var videoId = videoUrl.split('v=')[1]; - //Chrome and Firefox have different positions for parameters - var ampersandIndex = videoId.indexOf('&'); - if (ampersandIndex != -1) { - videoId = videoId.substring(0, ampersandIndex); - } - return videoId; - } - - function areTooFarApart(myTime, theirTime) { - var secDiff = Math.abs(myTime - theirTime); - var milliDiff = secDiff * 1000; - return milliDiff > TOO_FAR_APART; - } -}); diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 000000000..cd84d51ae --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + // Unit tests import modules directly rather than the built bundle, so the + // same build-time substitutions build/build.mjs makes have to happen here. + define: { + __HUB_URL__: JSON.stringify("http://localhost:8787"), + __GIT_COMMIT__: JSON.stringify("test"), + __BASE_URL__: JSON.stringify("http://localhost:8099/dist"), + }, + test: { + include: ["tests/unit/**/*.test.js"], + environment: "jsdom", + }, +}); From 067974d5404678e6f7b8a08c347489a568f1518f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 21:56:52 +0000 Subject: [PATCH 2/5] Remove jQuery and replace its Deferreds with native promises MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit jQuery was the client's last vendored dependency and its promise primitive. Both are gone. src/dom/dom.js is a ~700-line stand-in providing the subset of the jQuery API the client actually called — nothing more. Rewriting ~500 call sites as querySelectorAll + loops would have ballooned ui.js and forms.js and invited transcription bugs, so the call sites are almost unchanged; only the import moved. It is covered by 43 unit tests pinning the behaviours those call sites depend on, including the surprising ones: attr() yielding undefined rather than null for a missing attribute, `return false` in a handler meaning preventDefault + stopPropagation, and .end() restoring the previous set. Two jQuery behaviours needed explicit support rather than deletion: - The non-standard pseudo-selectors. `:visible` is used by windowing.js and `:password` is the default value of the ignoreForms config, and neither is valid CSS, so they are translated before hitting querySelectorAll. - Progress notifications on deferreds. who.js reports users as it discovers them and ui.js refreshes the invite list from each notification, so util.Deferred keeps notify()/progress() on top of a native promise. Unlike a jQuery Deferred it reports unhandled rejections instead of swallowing them — which is how the silent start-up failure in the previous commit managed to hide. jqueryPlugins.js is replaced by src/dom/animate.js. Every animation there ran through jQuery `step:` callbacks tweening a fake `borderSpacing` property and writing four vendor-prefixed transforms per frame; they are Web Animations and CSS transitions now, and the typing indicator is a CSS keyframe animation that respects prefers-reduced-motion instead of a setInterval writing opacity. Dead code removed, all of it dead because jQuery 1.9 removed the API it depended on and jqueryPlugins.js only faked back $.browser.mobile: - startup.js branched on $.browser.msie, undefined since 1.9. - ui.js bound the dock anchor with .toggle(fn1, fn2), a signature removed in 1.9, so collapsing the dock had silently done nothing for years. Restored as an explicit toggle. - cursor.js's rotateCursorDown had no callers and its own comment said it no longer did anything. - jqueryPlugins.js tested `matchMedia("screen and (max-screen-width: 480px)")` — not a valid media feature, so it never matched. $.isMobile() uses `(pointer: coarse)`. Also: $.ajax in playback.js and $.each in forms.js became fetch and a normal loop; $(window).unload in recorder.js became pagehide. e2e coverage grew to cover what this change touched most: cursor propagation between peers, and opening the chat and share windows (which exercises the replaced animation code). The fixture now presents as a returning user, because the first-run walkthrough is a modal whose backdrop covers the dock. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HgRCPNvLVGLYVox8Va8uJh --- package-lock.json | 7 - package.json | 1 - src/core/session.js | 2 +- src/core/startup.js | 5 +- src/core/util.js | 81 +++- src/dom/animate.js | 176 ++++++++ src/dom/dom.js | 916 ++++++++++++++++++++++++++++++++++++++ src/dom/elementFinder.js | 2 +- src/dom/eventMaker.js | 2 +- src/dom/jqueryPlugins.js | 329 -------------- src/dom/linkify.js | 4 +- src/dom/templating.js | 2 +- src/playback.js | 27 +- src/recorder.js | 4 +- src/rtc/index.js | 2 +- src/styles/togetherjs.css | 33 ++ src/sync/cursor.js | 38 +- src/sync/forms.js | 8 +- src/sync/videos.js | 2 +- src/sync/youtube.js | 2 +- src/ui/chat.js | 2 +- src/ui/ui.js | 49 +- src/ui/walkthrough.js | 4 +- src/ui/windowing.js | 15 +- tests/e2e/fixtures.js | 3 + tests/e2e/session.spec.js | 37 ++ tests/unit/dom.test.js | 312 +++++++++++++ 27 files changed, 1625 insertions(+), 440 deletions(-) create mode 100644 src/dom/animate.js create mode 100644 src/dom/dom.js delete mode 100644 src/dom/jqueryPlugins.js create mode 100644 tests/unit/dom.test.js diff --git a/package-lock.json b/package-lock.json index f6c7aa4af..da1a669e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.5.0", "license": "MPL-2.0", "dependencies": { - "jquery": "^3.7.1", "tinycolor2": "^1.6.0" }, "devDependencies": { @@ -2922,12 +2921,6 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT" - }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", diff --git a/package.json b/package.json index b90a74ec6..4cfe74e39 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "dist" ], "dependencies": { - "jquery": "^3.7.1", "tinycolor2": "^1.6.0" }, "devDependencies": { diff --git a/src/core/session.js b/src/core/session.js index 900ba0124..20469f939 100644 --- a/src/core/session.js +++ b/src/core/session.js @@ -6,7 +6,7 @@ import TogetherJS from "./togetherjs.js"; import { provide, need } from "./registry.js"; import util from "./util.js"; import channels from "./channels.js"; -import $ from "jquery"; +import $ from "../dom/dom.js"; import storage from "./storage.js"; diff --git a/src/core/startup.js b/src/core/startup.js index c2f768bba..0372bbf7d 100644 --- a/src/core/startup.js +++ b/src/core/startup.js @@ -16,7 +16,7 @@ import TogetherJS from "./togetherjs.js"; import { provide, need } from "./registry.js"; import util from "./util.js"; -import $ from "jquery"; +import $ from "../dom/dom.js"; import windowing from "../ui/windowing.js"; import storage from "./storage.js"; @@ -65,9 +65,6 @@ var handlers = { session.close(); } }); - if ($.browser.msie) { - $("#togetherjs-browser-broken-is-ie").show(); - } }, browserUnsupported: function (next) { diff --git a/src/core/util.js b/src/core/util.js index 8b23aa174..63f5a1045 100644 --- a/src/core/util.js +++ b/src/core/util.js @@ -3,12 +3,75 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import TogetherJS from "./togetherjs.js"; -import $ from "jquery"; -import "../dom/jqueryPlugins.js"; +import $ from "../dom/dom.js"; var util = {}; -util.Deferred = $.Deferred; +/* A deferred built on native promises. + * + * This replaces jQuery's $.Deferred. Two of its features are actually used by + * the client and so are kept: the synchronous `Deferred(fn)` initializer form, + * and progress notifications (who.js reports users as it discovers them, and + * ui.js refreshes the invite list from each notification). Everything else + * delegates to a real promise, which — unlike a jQuery Deferred — reports + * unhandled rejections instead of swallowing them. + */ +util.Deferred = function Deferred(initializer) { + var resolveFn; + var rejectFn; + var progressListeners = []; + var promise = new Promise(function (resolve, reject) { + resolveFn = resolve; + rejectFn = reject; + }); + var def = { + resolve: function (value) { + resolveFn(value); + return def; + }, + reject: function (error) { + rejectFn(error); + return def; + }, + /* jQuery's resolveWith/rejectWith rebind `this` for the callbacks. No + caller in this client depends on that, so only the value carries over. */ + resolveWith: function (context, args) { + resolveFn(args && args[0]); + return def; + }, + rejectWith: function (context, args) { + rejectFn(args && args[0]); + return def; + }, + notify: function (value) { + progressListeners.forEach(function (listener) { + listener(value); + }); + return def; + }, + progress: function (listener) { + progressListeners.push(listener); + return def; + }, + then: function (onResolved, onRejected) { + return promise.then(onResolved, onRejected); + }, + catch: function (onRejected) { + return promise.catch(onRejected); + }, + finally: function (onFinally) { + return promise.finally(onFinally); + }, + promise: function () { + return def; + } + }; + if (initializer) { + initializer(def); + } + return def; +}; + TogetherJS.$ = $; /* A simple class pattern, use like: @@ -205,12 +268,12 @@ util.resolver = function (deferred, func) { throw e; } if (result && result.then) { - result.then(function () { - deferred.resolveWith(this, arguments); - }, function () { - deferred.rejectWith(this, arguments); + result.then(function (value) { + deferred.resolve(value); + }, function (error) { + deferred.reject(error); }); - // FIXME: doesn't pass progress through + // Note: progress notifications are not forwarded through a resolver. } else if (result === undefined) { deferred.resolve(); } else { @@ -234,7 +297,7 @@ util.makePromise = function (obj) { if (util.isPromise(obj)) { return obj; } else { - return $.Deferred(function (def) { + return util.Deferred(function (def) { def.resolve(obj); }); } diff --git a/src/dom/animate.js b/src/dom/animate.js new file mode 100644 index 000000000..79c4c1887 --- /dev/null +++ b/src/dom/animate.js @@ -0,0 +1,176 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* The UI's animations. + * + * These replace jqueryPlugins.js, which drove every animation through jQuery + * `step:` callbacks tweening a fake `borderSpacing` property and writing + * -webkit-/-moz-/-ms-/-o-transform by hand on each frame — a technique that + * predates CSS transitions. + * + * They are plain functions taking a DomList rather than $.fn plugins: the + * client has only a handful of call sites, and extending the DOM helper's + * prototype from another module is the jQuery-plugin pattern this rewrite is + * getting away from. + */ + +import $ from "./dom.js"; + +/** Wait for an element's animations to settle. */ +function finished(el) { + const node = el[0]; + if (!node || !node.getAnimations) return Promise.resolve(); + return Promise.all(node.getAnimations().map((a) => a.finished.catch(() => {}))); +} + +/* Slide a notification window in from the right. */ +export function slideIn(el) { + el.css({ opacity: 0, zIndex: 8888 }); + const node = el[0]; + if (!node) return Promise.resolve(); + const animation = node.animate( + [ + { transform: "translateX(74px)", opacity: 0 }, + { transform: "translateX(0)", opacity: 1 }, + ], + { duration: 200, easing: "ease-out", fill: "forwards" }, + ); + return animation.finished.then(() => { + el.css({ opacity: 1, zIndex: 9999 }); + animation.cancel(); + }); +} + +/* Pop a window out of its dock button, with a small overshoot. */ +export function popinWindow(el) { + el.css({ opacity: 1, zIndex: 8888 }); + const pointer = $("#togetherjs-window-pointer-right"); + + // The original skipped the bounce on mobile; a coarse pointer usually means + // a slower device, so keep that. + const frames = $.isMobile() + ? [{ transform: "translateX(74px)" }, { transform: "translateX(0)" }] + : [ + { transform: "translateX(74px)" }, + { transform: "translateX(-4px)", offset: 0.8 }, + { transform: "translateX(0)" }, + ]; + const options = { duration: 120, easing: "ease-out", fill: "forwards" }; + + const animations = []; + if (el[0]) animations.push(el[0].animate(frames, options)); + if (pointer[0]) { + pointer.css({ opacity: 1, zIndex: 8888 }); + animations.push(pointer[0].animate(frames, options)); + } + return Promise.all(animations.map((a) => a.finished.catch(() => {}))).then(() => { + for (const a of animations) { + // Leave the element at its natural position rather than holding the + // animation's forwards fill, which would win over later style changes. + try { + a.commitStyles(); + } catch { + /* not composited */ + } + a.cancel(); + } + el.css({ transform: "" }); + pointer.css({ transform: "" }); + }); +} + +/* Fade a notification away, flipping its bottom edge out. */ +export function fadeOutWindow(el) { + const node = el[0]; + if (!node) return Promise.resolve(); + const animation = node.animate( + [ + { transform: "perspective(600px) rotateX(0deg)", opacity: 1 }, + { transform: "perspective(600px) rotateX(-90deg)", opacity: 0.5 }, + ], + { duration: 500, easing: "linear", fill: "forwards" }, + ); + return animation.finished.then(() => { + animation.cancel(); + el.css({ transform: "", opacity: "" }); + }); +} + +/* Grow an avatar into the dock. */ +export function animateDockEntry(el) { + const node = el[0]; + if (!node) return Promise.resolve(); + const height = el.height(); + const width = el.width(); + const margin = parseInt(el.css("marginLeft"), 10) || 0; + + const animation = node.animate( + [ + { + marginLeft: margin + width / 2 + "px", + height: "0px", + width: "0px", + backgroundSize: "0 0", + }, + { + marginLeft: margin + "px", + height: height + "px", + width: width + "px", + backgroundSize: height + 4 + "px", + }, + ], + { duration: 600, easing: "ease-out" }, + ); + // No fill: the element should land back on its stylesheet values. + return animation.finished.catch(() => {}); +} + +/* Shrink an avatar out of the dock; the reverse of the above. */ +export function animateDockExit(el) { + const node = el[0]; + if (!node) return Promise.resolve(); + const height = el.height(); + const width = el.width(); + const margin = parseInt(el.css("marginLeft"), 10) || 0; + + const animation = node.animate( + [ + { + marginLeft: margin + "px", + height: height + "px", + width: width + "px", + backgroundSize: height + 4 + "px", + opacity: 1, + }, + { + marginLeft: margin + width / 2 + "px", + height: "0px", + width: "0px", + backgroundSize: "0 0", + opacity: 0, + }, + ], + { duration: 600, easing: "ease-in", fill: "forwards" }, + ); + return animation.finished.catch(() => {}); +} + +/* Smoothly scroll the page to a vertical position. */ +export function easeTo(y) { + window.scrollTo({ top: y, behavior: "smooth" }); + return Promise.resolve(); +} + +/* The three-dot "typing" indicator. + The original drove this from a setInterval writing opacity on each dot; it + is a CSS animation now, so it costs nothing while it runs. */ +export function animateKeyboard(el) { + el.addClass("togetherjs-typing-animating"); +} + +export function stopKeyboardAnimation(el) { + el.removeClass("togetherjs-typing-animating"); +} + +export { finished }; diff --git a/src/dom/dom.js b/src/dom/dom.js new file mode 100644 index 000000000..0445a26b5 --- /dev/null +++ b/src/dom/dom.js @@ -0,0 +1,916 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* A small jQuery stand-in. + * + * The client had ~500 jQuery call sites across ui.js, forms.js and friends. + * Rewriting each as querySelectorAll + loops would have ballooned those files + * and invited transcription bugs, so this provides the subset of the jQuery + * API the client actually used — nothing more. It is deliberately not a + * general-purpose library: every method here exists because something in + * src/ calls it. + * + * Differences from jQuery worth knowing: + * - No effects queue. .animate() is implemented on the Web Animations API + * and returns a promise-bearing object rather than queueing. + * - .data() stores values in a WeakMap, not in dataset, so non-string + * values round-trip (jQuery does the same). + * - Events are delegated through a single listener per (element, type), + * which is enough for the client's usage and keeps removal simple. + */ + +const ELEMENT_NODE = 1; + +/** Parse an HTML string into a list of nodes. */ +function parseHTML(html) { + const template = document.createElement("template"); + template.innerHTML = html.trim(); + return Array.from(template.content.childNodes).filter( + (n) => n.nodeType === ELEMENT_NODE || (n.nodeType === 3 && n.textContent.trim()), + ); +} + +/** Per-element arbitrary data, keyed like jQuery's .data(). */ +const dataStore = new WeakMap(); +/** Per-element event bookkeeping so .off() can find what .on() registered. */ +const eventStore = new WeakMap(); + +function getData(el) { + let d = dataStore.get(el); + if (!d) { + d = {}; + dataStore.set(el, d); + } + return d; +} + +/* jQuery writes unitless numbers for these; everything else gets "px". */ +const UNITLESS = new Set([ + "opacity", + "zIndex", + "zoom", + "fontWeight", + "lineHeight", + "order", + "flexGrow", + "flexShrink", + "columnCount", + "fillOpacity", + "strokeOpacity", +]); + +function camelCase(name) { + return name.replace(/-([a-z])/g, (_m, c) => c.toUpperCase()); +} + +function setStyle(el, name, value) { + const prop = camelCase(name); + if (typeof value === "number" && !UNITLESS.has(prop)) { + value = value + "px"; + } + if (prop in el.style) { + el.style[prop] = value; + } else { + el.style.setProperty(name, value); + } +} + +/* jQuery accepted a handful of pseudo-selectors that are not valid CSS. The + client relies on several of them — `:visible` in windowing.js, and + `:password` as the default value of the ignoreForms config — so they are + translated rather than dropped. */ +const INPUT_PSEUDOS = { + ":password": 'input[type="password"]', + ":text": 'input[type="text"]', + ":checkbox": 'input[type="checkbox"]', + ":radio": 'input[type="radio"]', + ":file": 'input[type="file"]', + ":submit": 'input[type="submit"], button[type="submit"]', + ":button": 'button, input[type="button"]', + ":input": "input, textarea, select, button", + ":selected": "option:checked", +}; + +function isVisible(node) { + if (!node || node.nodeType !== ELEMENT_NODE) return false; + return !!(node.offsetWidth || node.offsetHeight || node.getClientRects().length); +} + +/** Split a selector into a native part and any jQuery visibility pseudos. */ +function compileSelector(selector) { + let native = String(selector); + let visibility = null; + native = native.replace(/:visible\b/g, () => { + visibility = true; + return ""; + }); + native = native.replace(/:hidden\b/g, () => { + visibility = false; + return ""; + }); + for (const [pseudo, replacement] of Object.entries(INPUT_PSEUDOS)) { + if (native.includes(pseudo)) { + native = native.split(pseudo).join(replacement); + } + } + native = native.trim(); + // ":visible" alone leaves nothing to match on natively. + if (!native || native === ",") native = "*"; + return { native, visibility }; +} + +/** matches() that understands the jQuery pseudo-selectors above. */ +function matchesSelector(node, selector) { + if (!node || node.nodeType !== ELEMENT_NODE) return false; + const { native, visibility } = compileSelector(selector); + let ok; + try { + ok = node.matches(native); + } catch (e) { + console.warn("Bad selector:", selector, e); + return false; + } + if (ok && visibility !== null) { + ok = isVisible(node) === visibility; + } + return ok; +} + +function queryAll(root, selector) { + const { native, visibility } = compileSelector(selector); + let found; + try { + found = Array.from(root.querySelectorAll(native)); + } catch (e) { + console.warn("Bad selector:", selector, e); + return []; + } + if (visibility !== null) { + found = found.filter((node) => isVisible(node) === visibility); + } + return found; +} + +function flatten(input) { + if (input === null || input === undefined) return []; + if (input instanceof DomList) return input.nodes.slice(); + if (typeof input === "string") { + // A selector, or a chunk of markup. + return input.trim()[0] === "<" ? parseHTML(input) : queryAll(document, input); + } + if (input.nodeType || input === window) return [input]; + if (typeof input.length === "number") return Array.from(input); + return [input]; +} + +class DomList { + constructor(nodes, prevObject) { + this.nodes = nodes; + this.length = nodes.length; + // Backs .end(), which restores the previous set in a chain. + this.prevObject = prevObject || null; + // Indexed access: el[0] is used all over the client. + for (let i = 0; i < nodes.length; i++) { + this[i] = nodes[i]; + } + } + + /* ---- traversal ---- */ + + get(i) { + if (i === undefined) return this.nodes.slice(); + return i < 0 ? this.nodes[this.nodes.length + i] : this.nodes[i]; + } + + eq(i) { + const node = this.get(i); + return new DomList(node ? [node] : [], this); + } + + first() { + return this.eq(0); + } + + last() { + return this.eq(-1); + } + + toArray() { + return this.nodes.slice(); + } + + each(fn) { + this.nodes.forEach((node, i) => fn.call(node, i, node)); + return this; + } + + map(fn) { + return new DomList( + this.nodes.map((node, i) => fn.call(node, i, node)).filter((v) => v != null), + this, + ); + } + + find(selector) { + const out = []; + for (const node of this.nodes) { + if (!node.querySelectorAll) continue; + for (const found of queryAll(node, selector)) { + if (!out.includes(found)) out.push(found); + } + } + return new DomList(out, this); + } + + filter(selector) { + const test = + typeof selector === "function" + ? (node, i) => selector.call(node, i, node) + : (node) => matchesSelector(node, selector); + return new DomList(this.nodes.filter(test), this); + } + + not(selector) { + return new DomList( + this.nodes.filter((node) => !matchesSelector(node, selector)), + this, + ); + } + + is(selector) { + if (typeof selector === "function") { + return this.nodes.some((node, i) => selector.call(node, i, node)); + } + if (selector instanceof DomList) { + return this.nodes.some((node) => selector.nodes.includes(node)); + } + if (selector && selector.nodeType) { + return this.nodes.includes(selector); + } + return this.nodes.some((node) => matchesSelector(node, selector)); + } + + has(selector) { + return new DomList( + this.nodes.filter((node) => node.querySelector && node.querySelector(selector)), + this, + ); + } + + closest(selector) { + const out = []; + for (const node of this.nodes) { + const found = node.closest && node.closest(selector); + if (found && !out.includes(found)) out.push(found); + } + return new DomList(out, this); + } + + parent() { + const out = []; + for (const node of this.nodes) { + if (node.parentNode && !out.includes(node.parentNode)) out.push(node.parentNode); + } + return new DomList(out, this); + } + + parents(selector) { + const out = []; + for (const node of this.nodes) { + let p = node.parentNode; + while (p && p.nodeType === ELEMENT_NODE) { + if ((!selector || p.matches(selector)) && !out.includes(p)) out.push(p); + p = p.parentNode; + } + } + return new DomList(out, this); + } + + children(selector) { + const out = []; + for (const node of this.nodes) { + for (const child of node.children || []) { + if ((!selector || matchesSelector(child, selector)) && !out.includes(child)) { + out.push(child); + } + } + } + return new DomList(out, this); + } + + siblings(selector) { + const out = []; + for (const node of this.nodes) { + for (const sib of node.parentNode ? node.parentNode.children : []) { + if (sib !== node && (!selector || sib.matches(selector)) && !out.includes(sib)) { + out.push(sib); + } + } + } + return new DomList(out, this); + } + + next(selector) { + const out = []; + for (const node of this.nodes) { + const sib = node.nextElementSibling; + if (sib && (!selector || sib.matches(selector))) out.push(sib); + } + return new DomList(out, this); + } + + prev(selector) { + const out = []; + for (const node of this.nodes) { + const sib = node.previousElementSibling; + if (sib && (!selector || sib.matches(selector))) out.push(sib); + } + return new DomList(out, this); + } + + contents() { + const out = []; + for (const node of this.nodes) out.push(...node.childNodes); + return new DomList(out, this); + } + + add(other) { + const out = this.nodes.slice(); + for (const node of flatten(other)) { + if (!out.includes(node)) out.push(node); + } + return new DomList(out, this); + } + + index(target) { + if (target === undefined) { + const node = this.nodes[0]; + if (!node || !node.parentNode) return -1; + return Array.from(node.parentNode.children).indexOf(node); + } + return this.nodes.indexOf(flatten(target)[0]); + } + + /** Restore the set this chain was derived from (jQuery's .end()). */ + end() { + return this.prevObject || new DomList([]); + } + + /* ---- classes and attributes ---- */ + + addClass(names) { + const list = String(names).split(/\s+/).filter(Boolean); + for (const node of this.nodes) node.classList.add(...list); + return this; + } + + removeClass(names) { + if (names === undefined) { + for (const node of this.nodes) node.className = ""; + return this; + } + const list = String(names).split(/\s+/).filter(Boolean); + for (const node of this.nodes) node.classList.remove(...list); + return this; + } + + toggleClass(name, force) { + for (const node of this.nodes) node.classList.toggle(name, force); + return this; + } + + hasClass(name) { + return this.nodes.some((node) => node.classList && node.classList.contains(name)); + } + + attr(name, value) { + if (typeof name === "object") { + for (const node of this.nodes) { + for (const [k, v] of Object.entries(name)) node.setAttribute(k, v); + } + return this; + } + if (value === undefined) { + const node = this.nodes[0]; + if (!node || !node.getAttribute) return undefined; + const found = node.getAttribute(name); + // jQuery yields undefined, not null, for a missing attribute. + return found === null ? undefined : found; + } + for (const node of this.nodes) { + if (value === null) node.removeAttribute(name); + else node.setAttribute(name, value); + } + return this; + } + + removeAttr(name) { + for (const node of this.nodes) node.removeAttribute(name); + return this; + } + + prop(name, value) { + if (value === undefined) { + const node = this.nodes[0]; + return node ? node[name] : undefined; + } + for (const node of this.nodes) node[name] = value; + return this; + } + + data(key, value) { + if (key === undefined) { + return this.nodes[0] ? getData(this.nodes[0]) : {}; + } + if (value === undefined) { + const node = this.nodes[0]; + if (!node) return undefined; + const store = getData(node); + if (key in store) return store[key]; + // Fall back to data-* attributes, as jQuery does. + const attr = node.getAttribute && node.getAttribute("data-" + key); + return attr === null || attr === undefined ? undefined : attr; + } + for (const node of this.nodes) getData(node)[key] = value; + return this; + } + + removeData(key) { + for (const node of this.nodes) delete getData(node)[key]; + return this; + } + + /* ---- content ---- */ + + text(value) { + if (value === undefined) { + return this.nodes.map((n) => n.textContent).join(""); + } + for (const node of this.nodes) node.textContent = value; + return this; + } + + html(value) { + if (value === undefined) { + return this.nodes[0] ? this.nodes[0].innerHTML : undefined; + } + for (const node of this.nodes) node.innerHTML = value; + return this; + } + + val(value) { + if (value === undefined) { + const node = this.nodes[0]; + if (!node) return undefined; + if (node.type === "checkbox" || node.type === "radio") return node.checked; + if (node.tagName === "SELECT" && node.multiple) { + return Array.from(node.selectedOptions).map((o) => o.value); + } + return node.value; + } + for (const node of this.nodes) { + if (node.type === "checkbox" || node.type === "radio") node.checked = !!value; + else node.value = value; + } + return this; + } + + /* ---- style and geometry ---- */ + + css(name, value) { + if (typeof name === "object") { + for (const node of this.nodes) { + for (const [k, v] of Object.entries(name)) setStyle(node, k, v); + } + return this; + } + if (value === undefined) { + const node = this.nodes[0]; + if (!node || node.nodeType !== ELEMENT_NODE) return undefined; + return getComputedStyle(node)[camelCase(name)]; + } + for (const node of this.nodes) setStyle(node, name, value); + return this; + } + + show() { + for (const node of this.nodes) { + if (node.nodeType !== ELEMENT_NODE) continue; + if (getComputedStyle(node).display === "none") node.style.display = ""; + // An inline display:none in the markup wins over the empty value above. + if (getComputedStyle(node).display === "none") node.style.display = "block"; + } + return this; + } + + hide() { + for (const node of this.nodes) { + if (node.style) node.style.display = "none"; + } + return this; + } + + width(value) { + if (value === undefined) { + const node = this.nodes[0]; + if (!node) return undefined; + if (node === window) return window.innerWidth; + // $(document).width() is the full scrollable width, as in jQuery. + if (node.nodeType === 9) { + return Math.max( + node.documentElement.scrollWidth, + node.documentElement.offsetWidth, + node.body ? node.body.scrollWidth : 0, + ); + } + return parseFloat(getComputedStyle(node).width) || node.offsetWidth || 0; + } + return this.css("width", value); + } + + height(value) { + if (value === undefined) { + const node = this.nodes[0]; + if (!node) return undefined; + if (node === window) return window.innerHeight; + if (node.nodeType === 9) { + return Math.max( + node.documentElement.scrollHeight, + node.documentElement.offsetHeight, + node.body ? node.body.scrollHeight : 0, + ); + } + return parseFloat(getComputedStyle(node).height) || node.offsetHeight || 0; + } + return this.css("height", value); + } + + outerWidth(includeMargin) { + const node = this.nodes[0]; + if (!node) return undefined; + if (node === window) return window.innerWidth; + let w = node.offsetWidth; + if (includeMargin) { + const s = getComputedStyle(node); + w += parseFloat(s.marginLeft) + parseFloat(s.marginRight); + } + return w; + } + + outerHeight(includeMargin) { + const node = this.nodes[0]; + if (!node) return undefined; + if (node === window) return window.innerHeight; + let h = node.offsetHeight; + if (includeMargin) { + const s = getComputedStyle(node); + h += parseFloat(s.marginTop) + parseFloat(s.marginBottom); + } + return h; + } + + offset() { + const node = this.nodes[0]; + if (!node || !node.getBoundingClientRect) return undefined; + const rect = node.getBoundingClientRect(); + return { top: rect.top + window.scrollY, left: rect.left + window.scrollX }; + } + + position() { + const node = this.nodes[0]; + if (!node) return undefined; + return { top: node.offsetTop, left: node.offsetLeft }; + } + + scrollTop(value) { + const node = this.nodes[0]; + if (value === undefined) { + if (!node) return undefined; + return node === window ? window.scrollY : node.scrollTop; + } + for (const n of this.nodes) { + if (n === window) window.scrollTo(window.scrollX, value); + else n.scrollTop = value; + } + return this; + } + + scrollLeft(value) { + const node = this.nodes[0]; + if (value === undefined) { + if (!node) return undefined; + return node === window ? window.scrollX : node.scrollLeft; + } + for (const n of this.nodes) { + if (n === window) window.scrollTo(value, window.scrollY); + else n.scrollLeft = value; + } + return this; + } + + /* ---- manipulation ---- */ + + append(...children) { + for (const node of this.nodes) { + for (const child of children) { + for (const c of flatten(child)) node.appendChild(c); + } + } + return this; + } + + prepend(...children) { + for (const node of this.nodes) { + for (const child of children) { + for (const c of flatten(child).reverse()) node.insertBefore(c, node.firstChild); + } + } + return this; + } + + appendTo(target) { + for (const parent of flatten(target)) { + for (const node of this.nodes) parent.appendChild(node); + } + return this; + } + + prependTo(target) { + for (const parent of flatten(target)) { + for (const node of this.nodes.slice().reverse()) parent.insertBefore(node, parent.firstChild); + } + return this; + } + + before(...content) { + for (const node of this.nodes) { + for (const item of content) { + for (const c of flatten(item)) node.parentNode.insertBefore(c, node); + } + } + return this; + } + + after(...content) { + for (const node of this.nodes) { + for (const item of content) { + for (const c of flatten(item).reverse()) { + node.parentNode.insertBefore(c, node.nextSibling); + } + } + } + return this; + } + + insertBefore(target) { + for (const ref of flatten(target)) { + for (const node of this.nodes) ref.parentNode.insertBefore(node, ref); + } + return this; + } + + insertAfter(target) { + for (const ref of flatten(target)) { + for (const node of this.nodes) ref.parentNode.insertBefore(node, ref.nextSibling); + } + return this; + } + + replaceWith(content) { + for (const node of this.nodes) { + const replacements = flatten(content); + for (const c of replacements) node.parentNode.insertBefore(c, node); + node.parentNode.removeChild(node); + } + return this; + } + + remove() { + for (const node of this.nodes) { + if (node.parentNode) node.parentNode.removeChild(node); + } + return this; + } + + detach() { + return this.remove(); + } + + empty() { + for (const node of this.nodes) { + while (node.firstChild) node.removeChild(node.firstChild); + } + return this; + } + + clone(withEvents) { + const copies = this.nodes.map((node) => node.cloneNode(true)); + if (withEvents) { + // The client only ever clones templates, which carry no handlers, so a + // deep event copy has never been needed. Say so rather than pretending. + console.warn("dom.clone(true) does not copy event handlers"); + } + return new DomList(copies, this); + } + + /* ---- events ---- */ + + on(types, selector, handler) { + if (typeof selector === "function") { + handler = selector; + selector = null; + } + for (const type of String(types).split(/\s+/).filter(Boolean)) { + for (const node of this.nodes) { + const listener = (event) => { + let target = event.target; + if (selector) { + target = target.closest && target.closest(selector); + if (!target || !node.contains(target)) return; + } + const result = handler.call(target, event); + // jQuery treats `return false` as preventDefault + stopPropagation. + if (result === false) { + event.preventDefault(); + event.stopPropagation(); + } + return result; + }; + let registry = eventStore.get(node); + if (!registry) { + registry = []; + eventStore.set(node, registry); + } + registry.push({ type, selector, handler, listener }); + node.addEventListener(type, listener, false); + } + } + return this; + } + + one(types, selector, handler) { + if (typeof selector === "function") { + handler = selector; + selector = null; + } + const self = this; + function once(event) { + self.off(types, once); + return handler.call(this, event); + } + return this.on(types, selector, once); + } + + off(types, handler) { + const typeList = types ? String(types).split(/\s+/).filter(Boolean) : null; + for (const node of this.nodes) { + const registry = eventStore.get(node); + if (!registry) continue; + for (let i = registry.length - 1; i >= 0; i--) { + const entry = registry[i]; + if (typeList && !typeList.includes(entry.type)) continue; + if (handler && entry.handler !== handler) continue; + node.removeEventListener(entry.type, entry.listener, false); + registry.splice(i, 1); + } + } + return this; + } + + trigger(type, detail) { + for (const node of this.nodes) { + // Native methods first, so .trigger("click") activates a real click. + if (typeof node[type] === "function" && !detail) { + node[type](); + continue; + } + node.dispatchEvent(new CustomEvent(type, { bubbles: true, cancelable: true, detail })); + } + return this; + } + + /* ---- animation ---- + Backed by the Web Animations API. The client's animations were written + against jQuery's queue, but nothing depends on queueing — only on being + told when an animation finishes, which .promise() provides. */ + + animate(properties, options) { + const opts = typeof options === "number" ? { duration: options } : options || {}; + const duration = opts.duration === undefined ? 400 : opts.duration; + const animations = []; + for (const node of this.nodes) { + const from = {}; + const to = {}; + for (const [prop, value] of Object.entries(properties)) { + const key = camelCase(prop); + from[key] = getComputedStyle(node)[key]; + to[key] = typeof value === "number" && !UNITLESS.has(key) ? value + "px" : value; + } + const animation = node.animate([from, to], { + duration, + easing: opts.easing === "linear" ? "linear" : "ease", + fill: "forwards", + }); + animation.addEventListener("finish", () => { + // Commit the end state so it survives the animation being discarded. + for (const [k, v] of Object.entries(to)) node.style[k] = v; + try { + animation.cancel(); + } catch { + /* already gone */ + } + if (opts.complete) opts.complete.call(node); + }); + animations.push(animation); + } + const done = Promise.all(animations.map((a) => a.finished.catch(() => {}))); + const result = new DomList(this.nodes, this.prevObject); + result.promise = () => done; + return result; + } + + stop() { + for (const node of this.nodes) { + for (const animation of node.getAnimations ? node.getAnimations() : []) { + animation.cancel(); + } + } + return this; + } + + fadeOut(duration, complete) { + return this.animate({ opacity: 0 }, { duration: duration || 400, complete }); + } + + fadeIn(duration, complete) { + return this.animate({ opacity: 1 }, { duration: duration || 400, complete }); + } + + /* ---- shorthands the client uses ---- */ + + focus() { + if (this.nodes[0] && this.nodes[0].focus) this.nodes[0].focus(); + return this; + } + + blur() { + if (this.nodes[0] && this.nodes[0].blur) this.nodes[0].blur(); + return this; + } +} + +/* Event shorthands. With a handler they bind; with no argument they fire. + Firing goes through .trigger(), which prefers a same-named native method — + so .select() on an input selects its text, as jQuery's did. */ +for (const type of [ + "click", + "dblclick", + "change", + "submit", + "keydown", + "keyup", + "keypress", + "input", + "scroll", + "select", + "mousedown", + "mouseup", + "mousemove", + "mouseover", + "mouseout", + "resize", + "error", +]) { + DomList.prototype[type] = function (handler) { + return handler ? this.on(type, handler) : this.trigger(type); + }; +} + +/** $(selectorOrNodeOrHtml, [context]) — plus $(fn) for DOM-ready. */ +function $(input, context) { + if (typeof input === "function") { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", () => input($)); + } else { + // Match jQuery: ready callbacks are always asynchronous. + Promise.resolve().then(() => input($)); + } + return new DomList([]); + } + if (context) { + return new DomList(flatten(context)).find(input); + } + return new DomList(flatten(input)); +} + +$.parseHTML = parseHTML; +$.matches = matchesSelector; +$.DomList = DomList; + +/** True when the browser is most likely a touch-first device. */ +$.isMobile = () => + window.matchMedia("(pointer: coarse)").matches || window.innerWidth <= 480; + +export default $; +export { DomList, parseHTML }; diff --git a/src/dom/elementFinder.js b/src/dom/elementFinder.js index 84f8e567f..f13c8d631 100644 --- a/src/dom/elementFinder.js +++ b/src/dom/elementFinder.js @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import util from "../core/util.js"; -import $ from "jquery"; +import $ from "./dom.js"; var elementFinder = util.Module("elementFinder"); var assert = util.assert; diff --git a/src/dom/eventMaker.js b/src/dom/eventMaker.js index 74f418a18..1bbd59404 100644 --- a/src/dom/eventMaker.js +++ b/src/dom/eventMaker.js @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -import $ from "jquery"; +import $ from "./dom.js"; import util from "../core/util.js"; var eventMaker = util.Module("eventMaker"); diff --git a/src/dom/jqueryPlugins.js b/src/dom/jqueryPlugins.js deleted file mode 100644 index 9d8e30875..000000000 --- a/src/dom/jqueryPlugins.js +++ /dev/null @@ -1,329 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import $ from "jquery"; - -// This isn't really a "module" since it just patches jQuery itself - -// FIX ME Animations TO DO -// walkthrough animations go here -// animate participant cursor and box popping in when they enter the session -// animate participant cursor and box popping out when they leave the session -// animate the participant cursor -> rotate down when they're down the page -$.fn.rotateCursorDown = function () { - $('svg').animate({borderSpacing: -150, opacity: 1}, { - step: function(now, fx) { - if (fx.prop == "borderSpacing") { - $(this).css('-webkit-transform', 'rotate('+now+'deg)') - .css('-moz-transform', 'rotate('+now+'deg)') - .css('-ms-transform', 'rotate('+now+'deg)') - .css('-o-transform', 'rotate('+now+'deg)') - .css('transform', 'rotate('+now+'deg)'); - } else { - $(this).css(fx.prop, now); - } - }, - duration: 500 - }, 'linear').promise().then(function () { - this.css('-webkit-transform', ''); - this.css('-moz-transform', ''); - this.css('-ms-transform', ''); - this.css('-o-transform', ''); - this.css('transform', ''); - this.css("opacity", ""); - }); -}; - -// animate the participant cursor -> rotate up when they're on the same frame as the user -$.fn.rotateCursorDown = function () { - $('.togetherjs-cursor svg').animate({borderSpacing: 0, opacity: 1}, { - step: function(now, fx) { - if (fx.prop == "borderSpacing") { - $(this).css('-webkit-transform', 'rotate('+now+'deg)') - .css('-moz-transform', 'rotate('+now+'deg)') - .css('-ms-transform', 'rotate('+now+'deg)') - .css('-o-transform', 'rotate('+now+'deg)') - .css('transform', 'rotate('+now+'deg)'); - } else { - $(this).css(fx.prop, now); - } - }, - duration: 500 - }, 'linear').promise().then(function () { - this.css('-webkit-transform', ''); - this.css('-moz-transform', ''); - this.css('-ms-transform', ''); - this.css('-o-transform', ''); - this.css('transform', ''); - this.css("opacity", ""); - }); -}; - -// Move notification when another notification slides in // - - -/* Pop in window from dock button: */ -$.fn.popinWindow = function () { - - //mobile popout window with no animation - if($.browser.mobile) { - - //starting position - this.css({ - left: "0px", - opacity: 1, - "zIndex": 8888 - }); - - //starting position for arrow - $('#togetherjs-window-pointer-right').css({ - left: "+=74px", - opacity: 1, - "zIndex": 8888 - }); - - //animate arrow out - $('#togetherjs-window-pointer-right').animate({ - opacity: 1, - left: "-=78px" - }, { - duration:60, easing:"linear" - }); - $('#togetherjs-window-pointer-right').queue(); - - //bounce arrow back - $('#togetherjs-window-pointer-right').animate({ - left:'+=4px' - }, { - duration:60, easing:"linear" - }); - - //animate window out - this.animate({ - opacity: 1, - left: "0px" - }, { - duration:60, easing:"linear" - }); - this.queue(); - - //bounce window back - this.animate({ - left:'0px' - }, { - duration:60, easing:"linear" - }); - } - - else { - - //starting position - this.css({ - left: "+=74px", - opacity: 1, - "zIndex": 8888 - }); - - //starting position for arrow - $('#togetherjs-window-pointer-right').css({ - left: "+=74px", - opacity: 1, - "zIndex": 8888 - }); - - //animate arrow out - $('#togetherjs-window-pointer-right').animate({ - opacity: 1, - left: "-=78px" - }, { - duration:60, easing:"linear" - }); - $('#togetherjs-window-pointer-right').queue(); - - //bounce arrow back - $('#togetherjs-window-pointer-right').animate({ - left:'+=4px' - }, { - duration:60, easing:"linear" - }); - - //animate window out - this.animate({ - opacity: 1, - left: "-=78px" - }, { - duration:60, easing:"linear" - }); - this.queue(); - - //bounce window back - this.animate({ - left:'+=4px' - }, { - duration:60, easing:"linear" - }); - - } - -}; - -/* Slide in notification window: */ -$.fn.slideIn = function () { - this.css({ - //top: "240px", - left: "+=74px", - opacity: 0, - "zIndex": 8888 - }); - return this.animate({ - "left": "-=74px", - opacity: 1, - "zIndex": 9999 - }, "fast"); -}; - -/* Used to fade away notification windows + flip the bottom of them out: */ -$.fn.fadeOut = function () { - this.animate({borderSpacing: -90, opacity: 0.5}, { - step: function(now, fx) { - if (fx.prop == "borderSpacing") { - $(this).css('-webkit-transform', 'perspective( 600px ) rotateX('+now+'deg)') - .css('-moz-transform', 'perspective( 600px ) rotateX('+now+'deg)') - .css('-ms-transform', 'perspective( 600px ) rotateX('+now+'deg)') - .css('-o-transform', 'perspective( 600px ) rotateX('+now+'deg)') - .css('transform', 'perspective( 600px ) rotateX('+now+'deg)'); - } else { - $(this).css(fx.prop, now); - } - }, - duration: 500 - }, 'linear').promise().then(function () { - this.css('-webkit-transform', ''); - this.css('-moz-transform', ''); - this.css('-ms-transform', ''); - this.css('-o-transform', ''); - this.css('transform', ''); - this.css("opacity", ""); - }); - return this; -}; - -/* used when user goes down to participant cursor location on screen */ -$.fn.easeTo = function (y) { - return this.animate({ - scrollTop: y - }, { - duration: 400, - easing: "swing" - }); -}; - -// avatar animate in -$.fn.animateDockEntry = function () { - var height = this.height(); - var width = this.width(); - var backgroundSize = height + 4; - var margin = parseInt(this.css("marginLeft"), 10); - - // set starting position CSS for avatar - this.css({ - marginLeft: margin + width/2, - height: 0, - width: 0, - backgroundSize: "0 0" - }); - - var self = this; - - //then animate avatar to the actual dimensions, and reset the values - this.animate({ - marginLeft: margin, - height: height, - width: width, - backgroundSize: backgroundSize - }, { - duration: 600 - }).promise().then(function () { - self.css({ - marginLeft: "", - height: "", - width: "", - backgroundSize: "" - }); - }); - return this; -}; - -// avatar animate out, reverse of above -$.fn.animateDockExit = function () { - - // get the current avatar dimenensions - var height = this.height(); - var width = this.width(); - var backgroundSize = height + 4; - var margin = parseInt(this.css("marginLeft"), 10); - - //then animate avatar to shrink to nothing, and reset the values again - // FIXME this needs to animate from the CENTER - this.animate({ - marginLeft: margin + width/2, - height: 0, - width: 0, - backgroundSize: "0 0", - opacity: 0 - }, 600 ); - - return this; - -}; - -$.fn.animateCursorEntry = function () { - // Make the cursor bubble pop in -}; - -// keyboard typing animation -$.fn.animateKeyboard = function () { - var one = this.find(".togetherjs-typing-ellipse-one"); - var two = this.find(".togetherjs-typing-ellipse-two"); - var three = this.find(".togetherjs-typing-ellipse-three"); - var count = -1; - var run = (function () { - count = (count+1) % 4; - if (count === 0) { - one.css("opacity", 0.5); - two.css("opacity", 0.5); - three.css("opacity", 0.5); - } else if (count == 1) { - one.css("opacity", 1); - } else if (count == 2) { - two.css("opacity", 1); - } else { // count==3 - three.css("opacity", 1); - } - }).bind(this); - run(); - var interval = setInterval(run, 300); - this.data("animateKeyboard", interval); -}; - -$.fn.stopKeyboardAnimation = function () { - clearTimeout(this.data("animateKeyboard")); - this.data("animateKeyboard", null); -}; - -// FIXME: not sure if this is legit, but at least the modern mobile devices we -// care about should have this defined: -if (! $.browser) { - $.browser = {}; -} -$.browser.mobile = window.orientation !== undefined; -if (navigator.userAgent.search(/mobile/i) != -1) { - // FIXME: At least on the Firefox OS simulator I need this - $.browser.mobile = true; -} - -if ($.browser.mobile && window.matchMedia && ! window.matchMedia("screen and (max-screen-width: 480px)").matches) { - // FIXME: for Firefox OS simulator really: - document.body.className += " togetherjs-mobile-browser"; -} diff --git a/src/dom/linkify.js b/src/dom/linkify.js index 9f139aafb..d42f3dc12 100644 --- a/src/dom/linkify.js +++ b/src/dom/linkify.js @@ -4,7 +4,9 @@ /* Finds any links in the text of an element (or its children) and turns them into anchors (with target=_blank) */ function linkify(el) { - if (el.jquery) { + // Accepts a DomList (which was a jQuery object before the rewrite) or a + // bare element. + if (el && !el.nodeType && el[0]) { el = el[0]; } el.normalize(); diff --git a/src/dom/templating.js b/src/dom/templating.js index f00bc80f4..b782d216e 100644 --- a/src/dom/templating.js +++ b/src/dom/templating.js @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -import $ from "jquery"; +import $ from "./dom.js"; import util from "../core/util.js"; import peers from "../core/peers.js"; import windowing from "../ui/windowing.js"; diff --git a/src/playback.js b/src/playback.js index 3c8ace36a..18191f645 100644 --- a/src/playback.js +++ b/src/playback.js @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -import $ from "jquery"; +import $ from "./dom/dom.js"; import util from "./core/util.js"; import session from "./core/session.js"; import storage from "./core/storage.js"; @@ -17,7 +17,7 @@ var ALWAYS_REPLAY = { playback.getLogs = function (url) { if (url.search(/^local:/) === 0) { - return $.Deferred(function (def) { + return util.Deferred(function (def) { storage.get("recording." + url.substr("local:".length)).then(function (logs) { if (! logs) { def.resolve(null); @@ -30,18 +30,17 @@ playback.getLogs = function (url) { }); }); } - return $.Deferred(function (def) { - $.ajax({ - url: url, - dataType: "text" - }).then( - function (logs) { - logs = parseLogs(logs); - def.resolve(logs); - }, - function (error) { - def.reject(error); - }); + return util.Deferred(function (def) { + fetch(url).then(function (resp) { + if (! resp.ok) { + throw new Error("Could not fetch logs: " + resp.status + " " + resp.statusText); + } + return resp.text(); + }).then(function (logs) { + def.resolve(parseLogs(logs)); + }, function (error) { + def.reject(error); + }); }); }; diff --git a/src/recorder.js b/src/recorder.js index 83e72f337..547cda55f 100644 --- a/src/recorder.js +++ b/src/recorder.js @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import TogetherJS from "./core/togetherjs.js"; -import $ from "jquery"; +import $ from "./dom/dom.js"; import util from "./core/util.js"; import channels from "./core/channels.js"; @@ -102,7 +102,7 @@ recorder.logMessage = function (msg) { $record.val($record.val() + msg + "\n\n"); }; -$(window).unload(function () { +$(window).on("pagehide", function () { channel.send({ type: "bye", clientId: clientId diff --git a/src/rtc/index.js b/src/rtc/index.js index 370d79497..d14fed415 100644 --- a/src/rtc/index.js +++ b/src/rtc/index.js @@ -4,7 +4,7 @@ // WebRTC support -- Note that this relies on parts of the interface code that usually goes in ui.js -import $ from "jquery"; +import $ from "../dom/dom.js"; import util from "../core/util.js"; import session from "../core/session.js"; import ui from "../ui/ui.js"; diff --git a/src/styles/togetherjs.css b/src/styles/togetherjs.css index b414b784e..133bb5689 100644 --- a/src/styles/togetherjs.css +++ b/src/styles/togetherjs.css @@ -2103,3 +2103,36 @@ section.togetherjs-buttons .togetherjs-same-url { } /* We set this class on the element when that last media query won't work but we detect in Javascript that the client should be treated as mobile */ + +/* Typing indicator. + The three dots used to be driven from JS by a setInterval writing opacity + on each one (jqueryPlugins.js's animateKeyboard); it is a CSS animation + now, toggled by the togetherjs-typing-animating class. */ +.togetherjs .togetherjs-cursor-typing span[class^="togetherjs-typing-ellipse-"] { + opacity: 0.5; +} + +.togetherjs .togetherjs-typing-animating span[class^="togetherjs-typing-ellipse-"] { + animation: togetherjs-typing-dot 1.2s infinite steps(1, end); +} + +.togetherjs .togetherjs-typing-animating .togetherjs-typing-ellipse-two { + animation-delay: 0.3s; +} + +.togetherjs .togetherjs-typing-animating .togetherjs-typing-ellipse-three { + animation-delay: 0.6s; +} + +@keyframes togetherjs-typing-dot { + 0% { opacity: 1; } + 75% { opacity: 1; } + 100% { opacity: 0.5; } +} + +@media (prefers-reduced-motion: reduce) { + .togetherjs .togetherjs-typing-animating span[class^="togetherjs-typing-ellipse-"] { + animation: none; + opacity: 1; + } +} diff --git a/src/sync/cursor.js b/src/sync/cursor.js index e9d2e6ed4..78d1a2f03 100644 --- a/src/sync/cursor.js +++ b/src/sync/cursor.js @@ -6,7 +6,8 @@ import { provide } from "../core/registry.js"; import TogetherJS from "../core/togetherjs.js"; -import $ from "jquery"; +import $ from "../dom/dom.js"; +import { animateKeyboard, stopKeyboardAnimation } from "../dom/animate.js"; import ui from "../ui/ui.js"; import util from "../core/util.js"; import session from "../core/session.js"; @@ -48,7 +49,7 @@ var Cursor = util.Class({ this.updatePeer(peers.getPeer(clientId)); this.lastTop = this.lastLeft = null; $(document.body).append(this.element); - this.element.animateCursorEntry(); + this.keydownTimeout = null; this.clearKeydown = this.clearKeydown.bind(this); this.atOtherUrl = false; @@ -139,31 +140,6 @@ var Cursor = util.Class({ this.element.hide(); }, - // place Cursor rotate function down here FIXME: this doesnt do anything anymore. This is in the CSS as an animation - rotateCursorDown: function(){ - var e = $(this.element).find('svg'); - e.animate({borderSpacing: -150, opacity: 1}, { - step: function(now, fx) { - if (fx.prop == "borderSpacing") { - e.css('-webkit-transform', 'rotate('+now+'deg)') - .css('-moz-transform', 'rotate('+now+'deg)') - .css('-ms-transform', 'rotate('+now+'deg)') - .css('-o-transform', 'rotate('+now+'deg)') - .css('transform', 'rotate('+now+'deg)'); - } else { - e.css(fx.prop, now); - } - }, - duration: 500 - }, 'linear').promise().then(function () { - e.css('-webkit-transform', '') - .css('-moz-transform', '') - .css('-ms-transform', '') - .css('-o-transform', '') - .css('transform', '') - .css("opacity", ""); - }); - }, setPosition: function (top, left) { var wTop = $(window).scrollTop(); @@ -196,14 +172,14 @@ var Cursor = util.Class({ if (this.keydownTimeout) { clearTimeout(this.keydownTimeout); } else { - this.element.find(".togetherjs-cursor-typing").show().animateKeyboard(); + animateKeyboard(this.element.find(".togetherjs-cursor-typing").show()); } this.keydownTimeout = setTimeout(this.clearKeydown, this.KEYDOWN_WAIT_TIME); }, clearKeydown: function () { this.keydownTimeout = null; - this.element.find(".togetherjs-cursor-typing").hide().stopKeyboardAnimation(); + stopKeyboardAnimation(this.element.find(".togetherjs-cursor-typing").hide()); }, _destroy: function () { @@ -403,10 +379,10 @@ session.on("close", function () { Cursor.forEach(function (c, clientId) { Cursor.destroy(clientId); }); - $(document).unbind("mousemove", mousemove); + $(document).off("mousemove", mousemove); document.removeEventListener("click", documentClick, true); document.removeEventListener("keydown", documentKeydown, true); - $(window).unbind("scroll", scroll); + $(window).off("scroll", scroll); }); session.hub.on("hello", function (msg) { diff --git a/src/sync/forms.js b/src/sync/forms.js index 6fd933dc1..90733bb1f 100644 --- a/src/sync/forms.js +++ b/src/sync/forms.js @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ import TogetherJS from "../core/togetherjs.js"; -import $ from "jquery"; +import $ from "../dom/dom.js"; import util from "../core/util.js"; import session from "../core/session.js"; import elementFinder from "../dom/elementFinder.js"; @@ -442,9 +442,9 @@ function buildTrackers() { util.forEachAttr(editTrackers, function (TrackerClass) { var els = TrackerClass.scan(); if (els) { - $.each(els, function () { - var tracker = new TrackerClass(this); - $(this).data("togetherjsHistory", ot.SimpleHistory(session.clientId, tracker.getContent(), 1)); + $(els).each(function (index, el) { + var tracker = new TrackerClass(el); + $(el).data("togetherjsHistory", ot.SimpleHistory(session.clientId, tracker.getContent(), 1)); liveTrackers.push(tracker); }); } diff --git a/src/sync/videos.js b/src/sync/videos.js index 11650d574..81998e921 100644 --- a/src/sync/videos.js +++ b/src/sync/videos.js @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -import $ from "jquery"; +import $ from "../dom/dom.js"; import util from "../core/util.js"; import session from "../core/session.js"; import elementFinder from "../dom/elementFinder.js"; diff --git a/src/sync/youtube.js b/src/sync/youtube.js index 541304c9b..0b7f198b3 100644 --- a/src/sync/youtube.js +++ b/src/sync/youtube.js @@ -3,7 +3,7 @@ * You can obtain one at http:// mozilla.org/MPL/2.0/. */ import TogetherJS from "../core/togetherjs.js"; -import $ from "jquery"; +import $ from "../dom/dom.js"; import util from "../core/util.js"; import session from "../core/session.js"; import elementFinder from "../dom/elementFinder.js"; diff --git a/src/ui/chat.js b/src/ui/chat.js index ee85ecbd6..78c82eb5a 100644 --- a/src/ui/chat.js +++ b/src/ui/chat.js @@ -4,7 +4,7 @@ /*jshint evil:true */ import TogetherJS from "../core/togetherjs.js"; import { provide } from "../core/registry.js"; -import $ from "jquery"; +import $ from "../dom/dom.js"; import util from "../core/util.js"; import session from "../core/session.js"; import ui from "./ui.js"; diff --git a/src/ui/ui.js b/src/ui/ui.js index fa16b2596..3c82a2191 100644 --- a/src/ui/ui.js +++ b/src/ui/ui.js @@ -4,7 +4,8 @@ import TogetherJS from "../core/togetherjs.js"; import { provide, need } from "../core/registry.js"; -import $ from "jquery"; +import $ from "../dom/dom.js"; +import { animateDockEntry, animateDockExit, easeTo } from "../dom/animate.js"; import util from "../core/util.js"; import session from "../core/session.js"; import templates from "../templates/templates.js"; @@ -223,7 +224,7 @@ ui.activateUI = function () { var container = ui.container; //create the overlay - if($.browser.mobile) { + if($.isMobile()) { // $("body").append( "\x3cdiv class='overlay' style='position: absolute; top: 0; left: 0; background-color: rgba(0,0,0,0); width: 120%; height: 100%; z-index: 1000; margin: -10px'>\x3c/div>" ); } @@ -240,7 +241,7 @@ ui.activateUI = function () { // The chat input element: var input = container.find("#togetherjs-chat-input"); - input.bind("keydown", function (event) { + input.on("keydown", function (event) { if (event.which == 13 && !event.shiftKey) { // Enter without Shift pressed submitChat(); return false; @@ -253,7 +254,7 @@ ui.activateUI = function () { function submitChat() { var val = input.val(); - if ($.trim(val)) { + if (util.trim(val)) { input.val(""); // triggering the event manually to avoid the addition of newline character to the textarea: input.trigger("input").trigger("propertychange"); @@ -324,15 +325,15 @@ ui.activateUI = function () { startPos = null; } } - $(document).bind("mousemove", mousemove); + $(document).on("mousemove", mousemove); // If you don't turn selection off it will still select text, and show a // text selection cursor: - $(document).bind("selectstart", selectoff); + $(document).on("selectstart", selectoff); // FIXME: it seems like sometimes we lose the mouseup event, and it's as though // the mouse is stuck down: $(document).one("mouseup", function () { - $(document).unbind("mousemove", mousemove); - $(document).unbind("selectstart", selectoff); + $(document).off("mousemove", mousemove); + $(document).off("selectstart", selectoff); }); return false; }); @@ -414,7 +415,7 @@ ui.activateUI = function () { } // Setting the anchor button + dock mobile actions - if($.browser.mobile) { + if($.isMobile()) { // toggle the audio button $("#togetherjs-audio-button").click(function () { @@ -439,10 +440,16 @@ ui.activateUI = function () { var src = "/images/togetherjs-logo-close.png"; $("#togetherjs-dock-anchor #togetherjs-dock-anchor-horizontal img").attr("src", src); - $("#togetherjs-dock-anchor").toggle(function() { - closeDock(); - },function(){ + // Was $(...).toggle(fn1, fn2), a signature jQuery removed in 1.9 — so + // this handler had silently done nothing for years. + var dockOpen = true; + $("#togetherjs-dock-anchor").click(function () { + dockOpen = ! dockOpen; + if (dockOpen) { openDock(); + } else { + closeDock(); + } }); } @@ -451,7 +458,7 @@ ui.activateUI = function () { }); $("#togetherjs-profile-button").click(function (event) { - if ($.browser.mobile) { + if ($.isMobile()) { windowing.show("#togetherjs-menu-window"); return false; } @@ -487,7 +494,7 @@ ui.activateUI = function () { $("#togetherjs-edit-name-window input").focus(); }); - $("#togetherjs-menu .togetherjs-self-name").bind("keyup change", function (event) { + $("#togetherjs-menu .togetherjs-self-name").on("keyup change", function (event) { console.log("alrighty", event); if (event.which == 13) { ui.displayToggle("#togetherjs-self-name-display"); @@ -562,7 +569,7 @@ ui.activateUI = function () { session.on("display-window", function (id, element) { if (id == "togetherjs-chat") { - if (! $.browser.mobile) { + if (! $.isMobile()) { $("#togetherjs-chat-input").focus(); } } else if (id == "togetherjs-share") { @@ -742,7 +749,7 @@ function showMenu(event) { assert(el.length); el.show(); bindMenu(); - $(document).bind("click", maybeHideMenu); + $(document).on("click", maybeHideMenu); } function bindMenu() { @@ -785,7 +792,7 @@ function toggleMenu() { function hideMenu() { var el = $("#togetherjs-menu"); el.hide(); - $(document).unbind("click", maybeHideMenu); + $(document).off("click", maybeHideMenu); ui.displayToggle("#togetherjs-self-name-display"); $("#togetherjs-pick-color").hide(); } @@ -836,7 +843,7 @@ function updateShareLink() { session.on("close", function () { - if($.browser.mobile) { + if($.isMobile()) { // remove bg overlay //$(".overlay").remove(); @@ -1276,7 +1283,7 @@ ui.PeerView = util.Class({ }); this.dockElement.attr("id", this.peer.className("togetherjs-dock-element-")); ui.container.find("#togetherjs-dock-participants").append(this.dockElement); - this.dockElement.find(".togetherjs-person").animateDockEntry(); + animateDockEntry(this.dockElement.find(".togetherjs-person")); adjustDockSize(1); this.detailElement = templating.sub("participant-window", { peer: this.peer @@ -1325,7 +1332,7 @@ ui.PeerView = util.Class({ if (! this.dockElement) { return; } - this.dockElement.animateDockExit().promise().then((function () { + animateDockExit(this.dockElement).then((function () { this.dockElement.remove(); this.dockElement = null; this.detailElement.remove(); @@ -1344,7 +1351,7 @@ ui.PeerView = util.Class({ return; } pos = elementFinder.pixelForPosition(pos); - $("html, body").easeTo(pos); + easeTo(pos); }, updateFollow: function () { diff --git a/src/ui/walkthrough.js b/src/ui/walkthrough.js index f1391c80b..3b8a6f5f8 100644 --- a/src/ui/walkthrough.js +++ b/src/ui/walkthrough.js @@ -6,7 +6,7 @@ import { provide } from "../core/registry.js"; import TogetherJS from "../core/togetherjs.js"; import util from "../core/util.js"; import ui from "./ui.js"; -import $ from "jquery"; +import $ from "../dom/dom.js"; import windowing from "./windowing.js"; import templates from "../templates/templates.js"; import templating from "../dom/templating.js"; @@ -38,7 +38,7 @@ walkthrough.start = function (firstTime, doneCallback) { container.find("#togetherjs-walkthrough-previous").click(previous); container.find("#togetherjs-walkthrough-next").click(next); ui.prepareShareLink(container); - container.find(".togetherjs-self-name").bind("keyup", function (event) { + container.find(".togetherjs-self-name").on("keyup", function (event) { var val = $(event.target).val(); peers.Self.update({name: val}); }); diff --git a/src/ui/windowing.js b/src/ui/windowing.js index 6723c48c9..d9f4f098f 100644 --- a/src/ui/windowing.js +++ b/src/ui/windowing.js @@ -2,7 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ import { provide } from "../core/registry.js"; -import $ from "jquery"; +import $ from "../dom/dom.js"; +import { slideIn, popinWindow, fadeOutWindow } from "../dom/animate.js"; import util from "../core/util.js"; import peers from "../core/peers.js"; import session from "../core/session.js"; @@ -34,9 +35,9 @@ windowing.show = function (element, options) { bind(element, options.bind); } if (notification) { - element.slideIn(); + slideIn(element); } else if (! modal) { - element.popinWindow(); + popinWindow(element); } if (modal) { getModalBackground().show(); @@ -51,7 +52,7 @@ var onClose = null; /* Moves a window to be attached to data-bind-to, e.g., the button that opened the window. Or you can provide an element that it should bind to. */ function bind(win, bound) { - if ($.browser.mobile) { + if ($.isMobile()) { return; } win = $(win); @@ -143,8 +144,8 @@ windowing.hide = function (els) { element.data("boundTo", null); bound.removeClass("togetherjs-active"); if (element.hasClass("togetherjs-notification")) { - element.fadeOut().promise().then(function () { - this.hide(); + fadeOutWindow(element).then(function () { + element.hide(); }); } }); @@ -200,7 +201,7 @@ var modalEscape = { $(document).keydown(modalEscape.onKeydown); }, unbind: function () { - $(document).unbind("keydown", modalEscape.onKeydown); + $(document).off("keydown", modalEscape.onKeydown); }, onKeydown: function (event) { if (event.which == 27) { diff --git a/tests/e2e/fixtures.js b/tests/e2e/fixtures.js index 5da049bf9..64bf41740 100644 --- a/tests/e2e/fixtures.js +++ b/tests/e2e/fixtures.js @@ -92,6 +92,9 @@ export async function openClient(browser, hub, { url } = {}) { // Keep the modal flow out of the way of assertions. window.TogetherJSConfig_suppressJoinConfirmation = true; window.TogetherJSConfig_suppressInvite = true; + // The first-run walkthrough is a modal and its backdrop covers the dock, + // so present as a returning user unless a test says otherwise. + localStorage.setItem("togetherjs.settings.seenIntroDialog", "true"); }); const target = url || `/examples/index.html?hub=${encodeURIComponent(hub.url)}`; await page.goto(target); diff --git a/tests/e2e/session.spec.js b/tests/e2e/session.spec.js index e3f5b1fe0..16b04f1fe 100644 --- a/tests/e2e/session.spec.js +++ b/tests/e2e/session.spec.js @@ -88,3 +88,40 @@ test("closing a session leaves the other peer with no peers", async ({ browser, await a.context().close(); await b.context().close(); }); + +test("cursor positions propagate to the other peer", async ({ browser, hub, statics }) => { + const a = await openClient(browser, hub); + const shareUrl = await startSession(a); + const b = await openClient(browser, hub, { url: shareUrl }); + await b.waitForFunction(() => window.TogetherJS.running); + await expect.poll(() => peerCount(a)).toBe(1); + + await a.mouse.move(300, 200); + await a.mouse.move(320, 220); + + // B should render a cursor element for A. + await expect.poll(() => b.locator(".togetherjs-cursor").count()).toBeGreaterThan(0); + + await a.context().close(); + await b.context().close(); +}); + +test("the dock, chat pane and share window open", async ({ browser, hub, statics }) => { + const page = await openClient(browser, hub); + await startSession(page); + + // Exercises windowing.js + the animation module that replaced the jQuery + // plugins: each of these is shown by popinWindow()/slideIn(). + await page.click("#togetherjs-chat-button"); + await expect(page.locator("#togetherjs-chat")).toBeVisible(); + + await page.click("#togetherjs-share-button"); + await expect(page.locator("#togetherjs-share")).toBeVisible(); + // interface.html carries both a mobile and a desktop share input. + await expect(page.locator("#togetherjs-share .togetherjs-share-link").first()).toHaveValue( + /togetherjs=/, + ); + + expect(page.errors).toEqual([]); + await page.context().close(); +}); diff --git a/tests/unit/dom.test.js b/tests/unit/dom.test.js new file mode 100644 index 000000000..a11e19b35 --- /dev/null +++ b/tests/unit/dom.test.js @@ -0,0 +1,312 @@ +/* The DOM helper replaces jQuery across the whole client, so it carries the + weight of ~500 call sites. These tests pin the behaviours those call sites + depend on — especially the ones where jQuery's semantics are surprising + (attr returning undefined, return-false handlers, .end(), .data()). */ + +import { describe, it, expect, beforeEach, vi } from "vitest"; +import $ from "../../src/dom/dom.js"; + +const FIXTURE = ` +
+

first

+

second deep

+
  • a
  • b
  • c
+ + + + +
+`; + +beforeEach(() => { + document.body.innerHTML = FIXTURE; +}); + +describe("selection", () => { + it("selects by CSS selector", () => { + expect($("#root").length).toBe(1); + expect($("li").length).toBe(3); + }); + + it("wraps an element", () => { + expect($(document.getElementById("root"))[0].id).toBe("root"); + }); + + it("parses markup", () => { + const el = $("hi"); + expect(el.length).toBe(1); + expect(el[0].className).toBe("made"); + }); + + it("scopes with a context argument", () => { + expect($("li", "#list").length).toBe(3); + }); + + it("gives an empty list for no match", () => { + expect($("#nothing").length).toBe(0); + expect($("#nothing")[0]).toBeUndefined(); + }); + + it("supports indexed access and .get()", () => { + expect($("li").get(0).textContent).toBe("a"); + expect($("li").get(-1).textContent).toBe("c"); + expect($("li")[1].textContent).toBe("b"); + expect($("li").get().length).toBe(3); + }); +}); + +describe("traversal", () => { + it("finds descendants without duplicates", () => { + expect($("#root").find("p").length).toBe(2); + expect($("p").find("span").length).toBe(1); + }); + + it("restores the previous set with .end()", () => { + const result = $("#root").find("p").end(); + expect(result[0].id).toBe("root"); + }); + + it("filters by selector and by function", () => { + expect($("p").filter(".one").length).toBe(1); + expect($("li").filter((i) => i > 0).length).toBe(2); + }); + + it("tests membership with .is()", () => { + expect($("#root").is(".a")).toBe(true); + expect($("#root").is(".zzz")).toBe(false); + }); + + it("reports visibility with :visible and :hidden", () => { + // jsdom has no layout, so offsetWidth is always 0; only the inline + // display:none case is meaningfully testable here. + expect($("#hidden").is(":hidden")).toBe(true); + }); + + it("walks up with .closest() and .parent()", () => { + expect($("#nested").closest("p")[0].className).toBe("two"); + expect($("#nested").parent()[0].className).toBe("two"); + }); + + it("collects children and siblings", () => { + expect($("#list").children().length).toBe(3); + expect($("#list").children().first().siblings().length).toBe(2); + }); + + it("unions with .add() and skips duplicates", () => { + expect($(".one").add(".two").length).toBe(2); + expect($(".one").add(".one").length).toBe(1); + }); +}); + +describe("classes and attributes", () => { + it("adds, removes and tests classes", () => { + const el = $("#root"); + el.addClass("c d"); + expect(el.hasClass("c")).toBe(true); + el.removeClass("c"); + expect(el.hasClass("c")).toBe(false); + el.toggleClass("e"); + expect(el.hasClass("e")).toBe(true); + }); + + it("returns undefined, not null, for a missing attribute", () => { + // The client relies on this: `if (el.attr("data-toggles"))`. + expect($("#root").attr("data-nope")).toBeUndefined(); + }); + + it("reads and writes attributes", () => { + $("#root").attr("data-x", "1"); + expect($("#root").attr("data-x")).toBe("1"); + $("#root").attr({ "data-y": "2", "data-z": "3" }); + expect($("#root").attr("data-z")).toBe("3"); + }); + + it("removes an attribute when set to null", () => { + $("#root").attr("data-x", "1").attr("data-x", null); + expect($("#root").attr("data-x")).toBeUndefined(); + }); + + it("reads and writes properties", () => { + expect($("#check").prop("checked")).toBe(true); + $("#check").prop("checked", false); + expect($("#check")[0].checked).toBe(false); + }); + + it("stores arbitrary values with .data()", () => { + const value = { nested: true }; + $("#root").data("thing", value); + // Non-string values must round-trip identically. + expect($("#root").data("thing")).toBe(value); + $("#root").removeData("thing"); + expect($("#root").data("thing")).toBeUndefined(); + }); + + it("falls back to data-* attributes", () => { + $("#root").attr("data-from-markup", "yes"); + expect($("#root").data("from-markup")).toBe("yes"); + }); +}); + +describe("content", () => { + it("reads and writes text", () => { + expect($(".one").text()).toBe("first"); + $(".one").text("changed"); + expect($(".one")[0].textContent).toBe("changed"); + }); + + it("concatenates text across a set", () => { + expect($("#list").find("li").text()).toBe("abc"); + }); + + it("reads and writes values", () => { + expect($("#text-input").val()).toBe("hello"); + $("#text-input").val("bye"); + expect($("#text-input")[0].value).toBe("bye"); + }); + + it("treats checkbox values as checked state", () => { + expect($("#check").val()).toBe(true); + $("#check").val(false); + expect($("#check")[0].checked).toBe(false); + }); +}); + +describe("manipulation", () => { + it("appends and removes", () => { + $("#list").append("
  • d
  • "); + expect($("#list li").length).toBe(4); + $("#list li").last().remove(); + expect($("#list li").length).toBe(3); + }); + + it("prepends in order", () => { + $("#list").prepend("
  • z
  • "); + expect($("#list li").first().text()).toBe("z"); + }); + + it("inserts before a reference node", () => { + $("
  • new
  • ").insertBefore($("#list li").eq(1)); + expect( + $("#list li") + .toArray() + .map((el) => el.textContent), + ).toEqual(["a", "new", "b", "c"]); + }); + + it("empties a container", () => { + $("#list").empty(); + expect($("#list").children().length).toBe(0); + }); + + it("clones deeply without sharing nodes", () => { + const copy = $("#list").clone(); + expect(copy[0]).not.toBe($("#list")[0]); + expect(copy.find("li").length).toBe(3); + }); + + it("replaces a node", () => { + $(".one").replaceWith("

    x

    "); + expect($(".one").length).toBe(0); + expect($(".replaced").length).toBe(1); + }); +}); + +describe("style", () => { + it("adds px to numeric values but not unitless properties", () => { + $("#root").css("width", 100); + expect($("#root")[0].style.width).toBe("100px"); + $("#root").css("opacity", 0.5); + expect($("#root")[0].style.opacity).toBe("0.5"); + }); + + it("accepts an object and camelCases hyphenated names", () => { + $("#root").css({ "background-color": "red", zIndex: 5 }); + expect($("#root")[0].style.backgroundColor).toBe("red"); + expect($("#root")[0].style.zIndex).toBe("5"); + }); + + it("hides and shows", () => { + const el = $(".one"); + el.hide(); + expect(el[0].style.display).toBe("none"); + el.show(); + expect(el[0].style.display).not.toBe("none"); + }); +}); + +describe("events", () => { + it("binds and fires", () => { + const spy = vi.fn(); + $("#root").on("click", spy); + $("#root")[0].click(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it("unbinds with .off()", () => { + const spy = vi.fn(); + const el = $("#root"); + el.on("click", spy); + el.off("click", spy); + el[0].click(); + expect(spy).not.toHaveBeenCalled(); + }); + + it("unbinds every handler for a type when none is named", () => { + const a = vi.fn(); + const b = vi.fn(); + const el = $("#root"); + el.on("click", a).on("click", b); + el.off("click"); + el[0].click(); + expect(a).not.toHaveBeenCalled(); + expect(b).not.toHaveBeenCalled(); + }); + + it("delegates to descendants matching a selector", () => { + const spy = vi.fn(); + $("#list").on("click", "li", spy); + $("#list li")[1].click(); + expect(spy).toHaveBeenCalledTimes(1); + // The handler's `this` is the delegate target, not the bound element. + expect(spy.mock.instances[0].textContent).toBe("b"); + }); + + it("treats a false return as preventDefault + stopPropagation", () => { + const outer = vi.fn(); + $("#root").on("click", outer); + $(".one").on("click", () => false); + $(".one")[0].click(); + expect(outer).not.toHaveBeenCalled(); + }); + + it("binds multiple types at once", () => { + const spy = vi.fn(); + $("#root").on("click keydown", spy); + $("#root")[0].click(); + $("#root")[0].dispatchEvent(new Event("keydown")); + expect(spy).toHaveBeenCalledTimes(2); + }); + + it("fires once with .one()", () => { + const spy = vi.fn(); + $("#root").one("click", spy); + $("#root")[0].click(); + $("#root")[0].click(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it("supports the .click(fn) shorthand and .click() to fire", () => { + const spy = vi.fn(); + $("#root").click(spy); + $("#root").click(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it("runs a $(fn) ready callback asynchronously", async () => { + const spy = vi.fn(); + $(spy); + expect(spy).not.toHaveBeenCalled(); + await Promise.resolve(); + expect(spy).toHaveBeenCalled(); + }); +}); From 1053e74746dd54fb7080a2b48e5e93cb8b119446 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 22:37:24 +0000 Subject: [PATCH 3/5] Rebuild voice as an audio and video mesh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old webrtc.js could not work with more than two people, and was fragile with two. It kept a single module-level RTCPeerConnection for the entire room and broadcast its offers to everyone, so a third participant made every client fight over the same connection. ICE candidates went into a scalar that each new candidate overwrote, and any arriving before setRemoteDescription were dropped. abort() discarded the connection without close(). toggleMute() was an empty stub, so the muted state was unreachable and there was no way to hang up. src/rtc/ replaces it: media.js microphone and camera, acquired independently connection.js one peer connection, using perfect negotiation mesh.js the set of connections, and the signaling ui.js dock buttons and video tiles Signaling is addressed. rtc-description and rtc-ice carry a `to`, which session.js filters on; rtc-offer/rtc-answer/rtc-abort are gone, since perfect negotiation needs one description message and no abort channel. The filtering is client-side: hub-worker's room.ts is deliberately a blind relay that inspects nothing, and teaching it message types would couple client and server versions to save bandwidth on a mesh that is capped at six peers anyway. This is addressing, not privacy — every client still receives every frame, exactly as before. Getting three peers to connect reliably took four fixes, each a real defect: - Both sides opened a connection to each other, so the two objects could end up negotiating against each other's discarded counterpart. Perfect negotiation resolves colliding offers on one connection; it cannot merge two. The peer with the lower clientId now opens, and the other creates its connection when the offer arrives. - The answering side also added transceivers, which fires negotiationneeded, so it offered back at the moment it was about to answer — turning every single connection setup into a collision. It now stays quiet until it has applied the first remote description. - connectTo() awaits the ICE configuration, so two callers racing for the same peer built two connections and one was orphaned in the map, never negotiating. Attempts are now shared and carry an epoch, so a creation that was in flight when the peer was reset discards itself. - Candidates arriving while a connection was still being created were dropped. They are buffered per peer. A peer that offers before the other side has joined the call gets no answer, because a client that has not joined cannot respond. mesh.start() therefore announces with rtc-join, and a peer parked in have-local-offer with no remote description rebuilds that connection. Only that case: resetting anything not yet "connected" also kills connections that are merely still gathering, and with three peers announcing joins nothing ever settles. Behaviour that did not exist before: mute (track.enabled, no renegotiation, broadcast as rtc-state so peers can show a badge — a disabled audio track still sends silence, so there is no other way to tell), camera on/off mid-call via replaceTrack, hang-up, ICE restart on failure, device pickers, and video tiles. Turning the camera off stops the track rather than disabling it, so the hardware capture light goes out. Configuration gains enableVideo (off by default), iceServers, getIceServers for short-lived TURN credentials, and maxRtcPeers. Without TURN, symmetric-NAT peers still fail — but visibly, and the docs will say so rather than repeating the 2013 apology. session.RTCSupported now also requires a secure context: navigator.mediaDevices is undefined on http:// pages, where the old check passed and then threw inside getUserMedia. That case gets its own dialog, since no permission prompt fixes it. Also removed: the dead camera-avatar block bound to nine DOM ids that do not exist in interface.html, and the mobile-only handler in ui.js that bound a second click listener always claiming RTC was unsupported. Tested with seven end-to-end cases covering two- and three-peer calls, simultaneous joins (the glare the old code could not survive), enabling video mid-call without dropping audio, mute propagation, a peer leaving, and hang-up. They assert on getStats() packet counts, not just connectionState: ICE completing does not prove media is flowing. Chromium's fake media devices produce nothing in this container, so getUserMedia is backed by canvas and WebAudio tracks — real MediaStreamTracks that negotiate and flow normally. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HgRCPNvLVGLYVox8Va8uJh --- src/core/session.js | 8 + src/rtc/connection.js | 257 ++++++++++++++ src/rtc/index.js | 537 ++++-------------------------- src/rtc/media.js | 180 ++++++++++ src/rtc/mesh.js | 402 ++++++++++++++++++++++ src/rtc/ui.js | 366 ++++++++++++++++++++ src/styles/togetherjs.css | 101 ++++++ src/templates/interface.html | 75 ++++- src/ui/ui.js | 11 +- tests/e2e/fixtures.js | 56 ++++ tests/e2e/rtc.spec.js | 228 +++++++++++++ tests/unit/element-finder.test.js | 2 +- tests/unit/linkify.test.js | 2 +- 13 files changed, 1724 insertions(+), 501 deletions(-) create mode 100644 src/rtc/connection.js create mode 100644 src/rtc/media.js create mode 100644 src/rtc/mesh.js create mode 100644 src/rtc/ui.js create mode 100644 tests/e2e/rtc.spec.js diff --git a/src/core/session.js b/src/core/session.js index 20469f939..e5edb35c9 100644 --- a/src/core/session.js +++ b/src/core/session.js @@ -135,6 +135,14 @@ function openChannel() { console.warn("Got message without clientId, where clientId is required", msg); return; } + // The hub is a blind relay: it broadcasts every message to everyone in the + // room and understands none of them. Peer-to-peer messages (the WebRTC + // signaling) therefore carry an explicit recipient that we filter on here. + // Note this is addressing, not privacy — every client still receives the + // frame, exactly as before. + if (msg.to && msg.to !== session.clientId) { + return; + } if (msg.clientId) { msg.peer = peers.getPeer(msg.clientId, msg); } diff --git a/src/rtc/connection.js b/src/rtc/connection.js new file mode 100644 index 000000000..0bc3e7ee3 --- /dev/null +++ b/src/rtc/connection.js @@ -0,0 +1,257 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* One RTCPeerConnection to one remote peer. + * + * This implements the WebRTC spec's "perfect negotiation" pattern, which the + * old single-connection code approximated with an ad-hoc `rtc-abort` message + * and a 2-second watchdog. The pattern's whole point is that both sides can + * start negotiating at the same moment and still converge, without either + * having to be designated the caller up front. + * + * The three rules: + * 1. Politeness is decided deterministically and the two sides always + * disagree about who is polite. + * 2. onnegotiationneeded does setLocalDescription() with no arguments and + * lets the browser decide whether that is an offer or an answer. This is + * what makes turning the camera on mid-call Just Work. + * 3. On an offer collision, the impolite side ignores the incoming offer and + * the polite side rolls back to accept it. + */ + +import util from "../core/util.js"; + +var assert = util.assert; + +/* Reasons a connection reports itself as gone, so the mesh can decide whether + to retry or drop it. */ +export var FAILED = "failed"; +export var CLOSED = "closed"; + +export function PeerConnection(options) { + var peerId = options.peerId; + var selfId = options.selfId; + var send = options.send; + var onTrack = options.onTrack; + var onStateChange = options.onStateChange; + + assert(peerId && selfId, "PeerConnection needs both peer and self ids"); + + /* Both ends compute this independently and always disagree, which is what + breaks the tie when offers cross. Comparing the client ids is arbitrary + but stable, and every client already knows both of them. */ + var polite = selfId < peerId; + + /* Only one side opens the connection (see mesh.js). The answering side must + not create its own transceivers: doing so fires negotiationneeded and it + offers back at the very moment it is about to answer, turning every + single connection setup into a glare. It picks up the transceivers the + offer creates instead. */ + var initiator = !!options.initiator; + + var pc = new RTCPeerConnection(options.configuration); + var makingOffer = false; + var ignoreOffer = false; + /* Candidates that arrive before the remote description is set cannot be + added yet. The old code kept a single candidate in a scalar and let each + new one overwrite the last, which lost all but one and dropped any that + arrived early. */ + var pendingCandidates = []; + var closed = false; + var disconnectTimer = null; + + /* Both sides create the transceivers, so the m-line layout is symmetric and + fixed for the connection's lifetime: turning a camera on later then only + needs replaceTrack() and never reshapes the SDP. + Adding them fires negotiationneeded on both sides, though, and the + answering side must not act on that — offering back at the moment it is + about to answer turns every connection setup into an offer collision. + So it stays quiet until it has applied the first remote description. */ + var audioTransceiver = pc.addTransceiver("audio", { direction: "sendrecv" }); + var videoTransceiver = pc.addTransceiver("video", { direction: "sendrecv" }); + var mayNegotiate = initiator; + + pc.onnegotiationneeded = async function () { + if (!mayNegotiate) { + // Answering side, before the first offer arrives. Renegotiation after + // that point (a camera turning on, say) is allowed from either side. + return; + } + try { + makingOffer = true; + await pc.setLocalDescription(); + send({ type: "rtc-description", to: peerId, description: pc.localDescription.toJSON() }); + } catch (e) { + console.warn("TogetherJS RTC: negotiation failed for", peerId, e); + } finally { + makingOffer = false; + } + }; + + pc.onicecandidate = function (event) { + // A null candidate signals end-of-candidates; forward it as-is. + send({ + type: "rtc-ice", + to: peerId, + candidate: event.candidate ? event.candidate.toJSON() : null, + }); + }; + + pc.ontrack = function (event) { + if (onTrack) { + onTrack(event.track, event.streams[0], peerId); + } + }; + + pc.onconnectionstatechange = function () { + if (onStateChange) { + onStateChange(pc.connectionState, peerId); + } + }; + + pc.oniceconnectionstatechange = function () { + var state = pc.iceConnectionState; + if (state === "failed") { + // Only one side should restart, or the two restarts race each other. + if (!polite) { + pc.restartIce(); + } + return; + } + if (state === "disconnected") { + // "disconnected" often recovers by itself, so give it a moment before + // forcing an ICE restart. + clearTimeout(disconnectTimer); + disconnectTimer = setTimeout(function () { + if (pc.iceConnectionState === "disconnected" && !polite) { + pc.restartIce(); + } + }, 5000); + return; + } + clearTimeout(disconnectTimer); + }; + + async function drainCandidates() { + var queued = pendingCandidates; + pendingCandidates = []; + for (var i = 0; i < queued.length; i++) { + try { + await pc.addIceCandidate(queued[i]); + } catch (e) { + if (!ignoreOffer) { + console.warn("TogetherJS RTC: could not add queued candidate", e); + } + } + } + } + + var connection = { + peerId: peerId, + polite: polite, + pc: pc, + + /** Handle an incoming description (offer or answer) from this peer. */ + handleDescription: async function (description) { + if (closed) { + return; + } + // An offer arriving while we are mid-offer, or while the signaling state + // is anything but stable, is a collision. + var collision = + description.type === "offer" && (makingOffer || pc.signalingState !== "stable"); + ignoreOffer = !polite && collision; + if (ignoreOffer) { + // The polite peer will roll back and accept ours instead. + return; + } + try { + // setRemoteDescription performs the implicit rollback when we are the + // polite peer in a collision. + await pc.setRemoteDescription(description); + mayNegotiate = true; + await drainCandidates(); + if (description.type === "offer") { + await pc.setLocalDescription(); + send({ type: "rtc-description", to: peerId, description: pc.localDescription.toJSON() }); + } + ignoreOffer = false; + } catch (e) { + console.warn("TogetherJS RTC: could not apply description from", peerId, e); + } + }, + + /** Handle an incoming ICE candidate from this peer. */ + handleCandidate: async function (candidate) { + if (closed) { + return; + } + if (!pc.remoteDescription) { + pendingCandidates.push(candidate); + return; + } + try { + await pc.addIceCandidate(candidate); + } catch (e) { + // Expected when we deliberately ignored an offer; otherwise worth + // knowing about. + if (!ignoreOffer) { + console.warn("TogetherJS RTC: could not add candidate from", peerId, e); + } + } + }, + + /* The track setters can race a close(): the mesh resets a connection while + its tracks are still being attached. replaceTrack() throws on a closed + connection, so short-circuit rather than letting that surface. */ + + /** Send (or stop sending) a local audio track. */ + setAudioTrack: function (track) { + if (closed) { + return Promise.resolve(); + } + return audioTransceiver.sender.replaceTrack(track || null); + }, + + /** Send (or stop sending) a local video track. */ + setVideoTrack: function (track) { + if (closed) { + return Promise.resolve(); + } + return videoTransceiver.sender.replaceTrack(track || null); + }, + + get closed() { + return closed; + }, + + /** True when we offered and nothing ever came back — which happens when + the peer was not in the call yet and dropped our offer. */ + isUnanswered: function () { + return pc.signalingState === "have-local-offer" && !pc.remoteDescription; + }, + + get connectionState() { + return pc.connectionState; + }, + + close: function () { + if (closed) { + return; + } + closed = true; + clearTimeout(disconnectTimer); + pc.onnegotiationneeded = null; + pc.onicecandidate = null; + pc.ontrack = null; + pc.onconnectionstatechange = null; + pc.oniceconnectionstatechange = null; + // The old abort() dropped the connection object without ever calling + // close(), leaking the underlying transport. + pc.close(); + }, + }; + + return connection; +} diff --git a/src/rtc/index.js b/src/rtc/index.js index d14fed415..56b642d37 100644 --- a/src/rtc/index.js +++ b/src/rtc/index.js @@ -2,503 +2,80 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -// WebRTC support -- Note that this relies on parts of the interface code that usually goes in ui.js +/* Audio and video calling. + * + * This replaces the old webrtc.js, which held a single RTCPeerConnection for + * the whole room and broadcast its offers to everyone, so any third + * participant broke the call. See ./mesh.js for the per-peer connections and + * ./connection.js for the negotiation. + * + * The module is split as: + * media.js the local microphone and camera + * connection.js one peer connection, with perfect negotiation + * mesh.js the set of connections, and the signaling + * ui.js the dock buttons and video tiles + */ -import $ from "../dom/dom.js"; import util from "../core/util.js"; import session from "../core/session.js"; -import ui from "../ui/ui.js"; -import peers from "../core/peers.js"; -import storage from "../core/storage.js"; -import windowing from "../ui/windowing.js"; +import { provide } from "../core/registry.js"; +import media, { mediaSupported } from "./media.js"; +import mesh from "./mesh.js"; +import "./ui.js"; var webrtc = util.Module("webrtc"); -var assert = util.assert; - -session.RTCSupported = !!window.RTCPeerConnection; - -// Passed to createOffer(); createAnswer() needs no equivalent since the -// answerer's directions are dictated by the received offer. -var offerOptions = { - offerToReceiveAudio: true, - offerToReceiveVideo: false -}; - -function makePeerConnection() { - return new RTCPeerConnection({ - iceServers: [{urls: "stun:stun.l.google.com:19302"}] - }); -} - -function getUserMedia(options, success, failure) { - failure = failure || function (error) { - console.error("Error in getUserMedia:", error); - }; - navigator.mediaDevices.getUserMedia(options).then(success, failure); -} - -/**************************************** - * getUserMedia Avatar support - */ - -session.on("ui-ready", function () { - $("#togetherjs-self-avatar").click(function () { - var avatar = peers.Self.avatar; - if (avatar) { - $preview.attr("src", avatar); - } - ui.displayToggle("#togetherjs-avatar-edit"); - }); - if (! session.RTCSupported) { - $("#togetherjs-avatar-edit-rtc").hide(); - } - - var avatarData = null; - var $preview = $("#togetherjs-self-avatar-preview"); - var $accept = $("#togetherjs-self-avatar-accept"); - var $cancel = $("#togetherjs-self-avatar-cancel"); - var $takePic = $("#togetherjs-avatar-use-camera"); - var $video = $("#togetherjs-avatar-video"); - var $upload = $("#togetherjs-avatar-upload"); - - $takePic.click(function () { - if (! streaming) { - startStreaming(); - return; - } - takePicture(); - }); - - function savePicture(dataUrl) { - avatarData = dataUrl; - $preview.attr("src", avatarData); - $accept.attr("disabled", null); - } - - $accept.click(function () { - peers.Self.update({avatar: avatarData}); - ui.displayToggle("#togetherjs-no-avatar-edit"); - // FIXME: these probably shouldn't be two elements: - $("#togetherjs-participants-other").show(); - $accept.attr("disabled", "1"); - }); - $cancel.click(function () { - ui.displayToggle("#togetherjs-no-avatar-edit"); - // FIXME: like above: - $("#togetherjs-participants-other").show(); - }); - - var streaming = false; - function startStreaming() { - getUserMedia({ - video: true, - audio: false - }, - function(stream) { - streaming = true; - $video[0].srcObject = stream; - $video[0].play(); - }, - function(err) { - // FIXME: should pop up help or something in the case of a user - // cancel - console.error("getUserMedia error:", err); - } - ); - } - - function takePicture() { - assert(streaming); - var height = $video[0].videoHeight; - var width = $video[0].videoWidth; - width = width * (session.AVATAR_SIZE / height); - height = session.AVATAR_SIZE; - var $canvas = $(""); - $canvas[0].height = session.AVATAR_SIZE; - $canvas[0].width = session.AVATAR_SIZE; - var context = $canvas[0].getContext("2d"); - context.arc(session.AVATAR_SIZE/2, session.AVATAR_SIZE/2, session.AVATAR_SIZE/2, 0, Math.PI*2); - context.closePath(); - context.clip(); - context.drawImage($video[0], (session.AVATAR_SIZE - width) / 2, 0, width, height); - savePicture($canvas[0].toDataURL("image/png")); - } - - $upload.on("change", function () { - var reader = new FileReader(); - reader.onload = function () { - // FIXME: I don't actually know it's JPEG, but it's probably a - // good enough guess: - var url = "data:image/jpeg;base64," + util.blobToBase64(this.result); - convertImage(url, function (result) { - savePicture(result); - }); - }; - reader.onerror = function () { - console.error("Error reading file:", this.error); - }; - reader.readAsArrayBuffer(this.files[0]); - }); - - function convertImage(imageUrl, callback) { - var $canvas = $(""); - $canvas[0].height = session.AVATAR_SIZE; - $canvas[0].width = session.AVATAR_SIZE; - var context = $canvas[0].getContext("2d"); - var img = new Image(); - img.src = imageUrl; - // Sometimes the DOM updates immediately to call - // naturalWidth/etc, and sometimes it doesn't; using setTimeout - // gives it a chance to catch up - setTimeout(function () { - var width = img.naturalWidth || img.width; - var height = img.naturalHeight || img.height; - width = width * (session.AVATAR_SIZE / height); - height = session.AVATAR_SIZE; - context.drawImage(img, 0, 0, width, height); - callback($canvas[0].toDataURL("image/png")); - }); - } +/* Announced to peers in the hello message and stored on each Peer, so we never + offer to a client that cannot answer. Requires both the API and a secure + context: navigator.mediaDevices is undefined on http:// pages, where the old + check (`!!window.RTCPeerConnection`) passed and then failed inside + getUserMedia. */ +session.RTCSupported = !!window.RTCPeerConnection && mediaSupported(); +session.on("prepare-hello", function (msg) { + msg.rtcSupported = session.RTCSupported; }); -/**************************************** - * RTC support - */ +webrtc.media = media; +webrtc.mesh = mesh; -function audioButton(selector) { - ui.displayToggle(selector); - if (selector == "#togetherjs-audio-incoming") { - $("#togetherjs-audio-button").addClass("togetherjs-animated").addClass("togetherjs-color-alert"); - } else { - $("#togetherjs-audio-button").removeClass("togetherjs-animated").removeClass("togetherjs-color-alert"); - } -} - -session.on("ui-ready", function () { - $("#togetherjs-audio-button").click(function () { - if ($("#togetherjs-rtc-info").is(":visible")) { - windowing.hide(); - return; - } - if (session.RTCSupported) { - enableAudio(); - } else { - windowing.show("#togetherjs-rtc-not-supported"); - } - }); - - if (! session.RTCSupported) { - audioButton("#togetherjs-audio-unavailable"); - return; - } - audioButton("#togetherjs-audio-ready"); - - var audioStream = null; - var accepted = false; - var connected = false; - var $audio = $("#togetherjs-audio-element"); - var offerSent = null; - var offerReceived = null; - var offerDescription = false; - var answerSent = null; - var answerReceived = null; - var answerDescription = false; - var _connection = null; - var iceCandidate = null; - - function enableAudio() { - accepted = true; - storage.settings.get("dontShowRtcInfo").then(function (dontShow) { - if (! dontShow) { - windowing.show("#togetherjs-rtc-info"); - } - }); - if (! audioStream) { - startStreaming(connect); - return; - } - if (! connected) { - connect(); - } - toggleMute(); - } - - ui.container.find("#togetherjs-rtc-info .togetherjs-dont-show-again").change(function () { - storage.settings.set("dontShowRtcInfo", this.checked); - }); - - function error() { - console.warn.apply(console, arguments); - var s = ""; - for (var i=0; i= max; +} + +/* Exactly one side of each pair opens the connection. + * + * Both sides calling connectTo() means both build an RTCPeerConnection and + * offer, and the two objects can end up negotiating against each other's + * discarded counterpart — which shows up as a connection that completes its + * SDP exchange and then never starts ICE. Perfect negotiation resolves + * colliding offers on *one* connection; it cannot merge two. + * + * So the peer with the lower clientId opens; the other waits and creates its + * connection when the offer arrives. Perfect negotiation still earns its keep + * for renegotiation later, when either side may turn a camera on. */ +function isInitiatorFor(peerId) { + return session.clientId < peerId; +} + +/** Peers that support RTC, are still live, and are not us. */ +function eligiblePeers() { + return peers.getAllPeers(true).filter(function (peer) { + return !peer.isSelf && peer.rtcSupported; + }); +} + +function connectTo(peerId) { + if (connections.has(peerId) || peerId === session.clientId) { + return Promise.resolve(connections.get(peerId) || null); + } + var inFlight = connecting.get(peerId); + if (inFlight) { + return inFlight; + } + if (capacityReached()) { + mesh.emit("capacity", peerId); + console.warn( + "TogetherJS RTC: refusing connection to", + peerId, + "— maxRtcPeers reached. A mesh past this size needs an SFU.", + ); + return Promise.resolve(null); + } + var epoch = epochs.get(peerId) || 0; + var promise = createConnection(peerId, epoch).finally(function () { + // Only clear our own entry: a reset may already have replaced it. + if (connecting.get(peerId) === promise) { + connecting.delete(peerId); + } + }); + connecting.set(peerId, promise); + return promise; +} + +async function createConnection(peerId, epoch) { + var configuration = await iceConfiguration(); + if ((epochs.get(peerId) || 0) !== epoch) { + // The peer was reset while we awaited the config; a newer attempt owns + // this slot now. + return connections.get(peerId) || null; + } + + var connection = PeerConnection({ + peerId: peerId, + selfId: session.clientId, + initiator: isInitiatorFor(peerId), + configuration: configuration, + send: function (msg) { + session.send(msg); + }, + onTrack: function (track, stream, fromId) { + mesh.emit("track", { track: track, stream: stream, peerId: fromId }); + }, + onStateChange: function (state, fromId) { + mesh.emit("connection-state", { peerId: fromId, state: state }); + if (state === "failed" || state === "closed") { + // Leave "disconnected" alone: connection.js gives ICE a chance to + // recover before anything is torn down. + mesh.disconnectFrom(fromId); + } + }, + }); + if ((epochs.get(peerId) || 0) !== epoch) { + connection.close(); + return connections.get(peerId) || null; + } + connections.set(peerId, connection); + + var queued = earlyCandidates.get(peerId); + if (queued) { + earlyCandidates.delete(peerId); + queued.forEach(function (candidate) { + connection.handleCandidate(candidate); + }); + } + + // Attach whatever we are already sending. Adding the tracks fires + // onnegotiationneeded, which starts the handshake. + await applyLocalTracks(connection); + return connection; +} + +async function applyLocalTracks(connection) { + if (connection.closed) { + return; + } + await connection.setAudioTrack(media.get("audio")); + await connection.setVideoTrack(media.get("video")); +} + +/** Push the current local tracks onto every open connection. */ +mesh.syncLocalTracks = async function () { + var updates = []; + connections.forEach(function (connection) { + updates.push(applyLocalTracks(connection)); + }); + await Promise.all(updates); + mesh.broadcastState(); +}; + +/** Tell peers what we are sending, so they can show mute/camera badges. An + audio track that is merely disabled still sends silence, so this is the + only way for them to know. */ +mesh.broadcastState = function () { + if (!active) { + return; + } + var state = media.state(); + session.send({ type: "rtc-state", audio: state.audio, video: state.video }); +}; + +/** Open connections to every eligible peer. + * + * Announcing the join first matters. Perfect negotiation only converges if no + * signaling is dropped, but a peer that has not joined the call cannot answer + * an offer — so a caller who offered too early would sit in have-local-offer + * forever. The rtc-join broadcast tells peers already in the call to discard + * any half-open connection to us and start again from a clean state. */ +mesh.start = async function () { + active = true; + // Announce first: peers already in the call use this both to know we can + // answer now, and to start the connection when they are the initiator. + session.send({ type: "rtc-join" }); + var targets = eligiblePeers(); + for (var i = 0; i < targets.length; i++) { + var peerId = targets[i].id; + resetIfUnanswered(peerId); + if (isInitiatorFor(peerId)) { + await connectTo(peerId); + } + // Otherwise they will offer us, prompted by the rtc-join above. + } + mesh.broadcastState(); +}; + +/* Drop a connection only if its offer was never answered. + * + * There is exactly one situation this exists for: we offered while the peer + * had not joined the call yet, so they dropped the offer and we are parked in + * have-local-offer with no remote description, forever. That connection has + * to be rebuilt. + * + * Anything else must be left alone. Resetting on "not yet connected" also + * kills connections that are merely still gathering candidates, and with + * three peers each announcing a join, connections get torn down mid-handshake + * over and over and never settle. */ +function resetIfUnanswered(peerId) { + var connection = connections.get(peerId); + if (connection && connection.isUnanswered()) { + mesh.disconnectFrom(peerId); + } +} + +mesh.disconnectFrom = function (peerId) { + bumpEpoch(peerId); + connecting.delete(peerId); + earlyCandidates.delete(peerId); + var connection = connections.get(peerId); + if (!connection) { + return; + } + connection.close(); + connections.delete(peerId); + peerState.delete(peerId); + mesh.emit("peer-state", { peerId: peerId, gone: true }); +}; + +/** Tear the whole mesh down and stop the local devices. */ +mesh.stop = function () { + active = false; + connections.forEach(function (connection) { + connection.close(); + }); + connections.forEach(function (connection, peerId) { + bumpEpoch(peerId); + }); + connections.clear(); + connecting.clear(); + earlyCandidates.clear(); + peerState.clear(); + media.stopAll(); + mesh.emit("peer-state", { all: true, gone: true }); +}; + +mesh.isActive = function () { + return active; +}; + +mesh.peerIds = function () { + return Array.from(connections.keys()); +}; + +mesh.stateFor = function (peerId) { + return peerState.get(peerId) || null; +}; + +mesh.size = function () { + return connections.size; +}; + +/* For the end-to-end tests, which assert on connectionState and getStats(). + Not part of the supported API. */ +mesh._connectionFor = function (peerId) { + return connections.get(peerId) || null; +}; + +/**************************************** + * Signaling + */ + +/* Someone joined the call. If we are in it too, reset our connection to them + and negotiate afresh; both sides may now offer at once, which is exactly the + collision perfect negotiation exists to resolve. */ +session.hub.on("rtc-join", function (msg) { + if (msg.clientId === session.clientId) { + return; + } + if (!active) { + // The UI pulses the button to show an incoming call; answering runs start(). + mesh.emit("peer-state", { peerId: msg.clientId, calling: true }); + return; + } + resetIfUnanswered(msg.clientId); + if (isInitiatorFor(msg.clientId)) { + connectTo(msg.clientId).then(function () { + mesh.broadcastState(); + }); + } else { + mesh.broadcastState(); + } +}); + +session.hub.on("rtc-description", function (msg) { + if (msg.clientId === session.clientId) { + return; + } + var connection = connections.get(msg.clientId); + if (!connection) { + if (!active) { + // We have not joined, so we cannot answer. The peer will re-offer when + // we send our own rtc-join. + mesh.emit("peer-state", { peerId: msg.clientId, calling: true }); + return; + } + connectTo(msg.clientId).then(function (created) { + if (created) { + created.handleDescription(msg.description); + } + }); + return; + } + connection.handleDescription(msg.description); +}); + +session.hub.on("rtc-ice", function (msg) { + if (msg.clientId === session.clientId) { + return; + } + var connection = connections.get(msg.clientId); + if (connection) { + connection.handleCandidate(msg.candidate); + return; + } + if (!active) { + return; + } + // The connection is probably still being built; hold on to this. + if (!earlyCandidates.has(msg.clientId)) { + earlyCandidates.set(msg.clientId, []); + } + earlyCandidates.get(msg.clientId).push(msg.candidate); +}); + +session.hub.on("rtc-state", function (msg) { + if (msg.clientId === session.clientId) { + return; + } + peerState.set(msg.clientId, { audio: !!msg.audio, video: !!msg.video }); + mesh.emit("peer-state", { peerId: msg.clientId, audio: !!msg.audio, video: !!msg.video }); +}); + +/* A peer that reloads or navigates away tears down its side without us seeing + a state change, so treat a fresh hello as a reset for that peer. */ +/* A peer that reloads or navigates away tears down its side without us seeing + a state change, so a fresh hello resets that peer. They will send their own + rtc-join if they rejoin the call. */ +session.hub.on("hello", function (msg) { + if (msg.clientId === session.clientId) { + return; + } + mesh.disconnectFrom(msg.clientId); +}); + +/* peers.js has no dedicated "bye" event; leaving is a status change. */ +peers.on("status-updated", function (peer) { + if (peer.status !== "live" && !peer.isSelf) { + mesh.disconnectFrom(peer.id); + } +}); + +session.on("close", function () { + mesh.stop(); +}); + +media.on("change", function () { + if (active) { + mesh.syncLocalTracks(); + } +}); + +export default mesh; diff --git a/src/rtc/ui.js b/src/rtc/ui.js new file mode 100644 index 000000000..8ddd9a421 --- /dev/null +++ b/src/rtc/ui.js @@ -0,0 +1,366 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* The call's user interface: the dock buttons and the video panel. + * + * Audio playback is deliberately kept out of the video tiles — one hidden + *
    @@ -265,11 +264,16 @@ {{ gettext('Activate your browser microphone near your URL bar above.') }}

    - {{ gettext('Talking on your microphone through your web browser is an experimental feature.') }} -

    -

    - {{ gettext('Read more about Audio Chat') }} {{ gettext('here') }}. + {{ gettext('Click the microphone button again to mute yourself.') }}

    + +
    @@ -277,10 +281,61 @@ {{ gettext('Don\'t show again.') }} +
    + + + + + + + + +
    {{ gettext('Audio Chat') }}
    -

    {{ gettext('Audio chat requires you to use a newer browser!') }}

    -

    - {{ gettext('Live audio chat requires a newer (or different) browser than you\'re using.') }} -

    - {{ gettext('See this pagefor more information and a list of supported browsers.') }} + {{ gettext('Live audio and video chat require a newer (or different) browser than you\'re using.') }}

    @@ -392,8 +443,6 @@
    - - diff --git a/src/ui/ui.js b/src/ui/ui.js index 3c82a2191..8576c370a 100644 --- a/src/ui/ui.js +++ b/src/ui/ui.js @@ -196,11 +196,15 @@ ui.prepareUI = function () { }); TogetherJS.config.track("disableWebRTC", function (hide, previous) { + var buttons = "#togetherjs-audio-button, #togetherjs-video-button"; if (hide && ! previous) { - ui.container.find("#togetherjs-audio-button").hide(); + ui.container.find(buttons).hide(); adjustDockSize(-1); } else if ((! hide) && previous) { ui.container.find("#togetherjs-audio-button").show(); + if (TogetherJS.config.get("enableVideo")) { + ui.container.find("#togetherjs-video-button").show(); + } adjustDockSize(1); } }); @@ -417,11 +421,6 @@ ui.activateUI = function () { // Setting the anchor button + dock mobile actions if($.isMobile()) { - // toggle the audio button - $("#togetherjs-audio-button").click(function () { - windowing.toggle("#togetherjs-rtc-not-supported"); - }); - // toggle the profile button $("#togetherjs-profile-button").click(function () { windowing.toggle("#togetherjs-menu-window"); diff --git a/tests/e2e/fixtures.js b/tests/e2e/fixtures.js index 64bf41740..71dfc1127 100644 --- a/tests/e2e/fixtures.js +++ b/tests/e2e/fixtures.js @@ -64,6 +64,58 @@ export const test = base.extend({ export const expect = base.expect; +/* Chromium's --use-fake-device-for-media-capture produces no devices in this + container (enumerateDevices returns an empty list in every headless mode), + so getUserMedia is backed by tracks synthesised in the page instead. + + These are real MediaStreamTracks — a canvas capture and a WebAudio + destination — so they negotiate, encode and flow over an RTCPeerConnection + exactly like camera and microphone tracks. Only the device layer is + substituted; everything the mesh does is exercised for real. */ +function installSyntheticMedia() { + function videoTrack() { + const canvas = Object.assign(document.createElement("canvas"), { + width: 320, + height: 240, + }); + const ctx = canvas.getContext("2d"); + let frame = 0; + // Keep painting: a static canvas produces no frames after the first. + setInterval(() => { + frame++; + ctx.fillStyle = `hsl(${frame % 360}, 70%, 50%)`; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }, 100); + return canvas.captureStream(10).getVideoTracks()[0]; + } + + function audioTrack() { + const ctx = new AudioContext(); + const oscillator = ctx.createOscillator(); + const destination = ctx.createMediaStreamDestination(); + oscillator.connect(destination); + oscillator.start(); + return destination.stream.getAudioTracks()[0]; + } + + const fakeDevices = [ + { deviceId: "fake-audio", kind: "audioinput", label: "Fake microphone", groupId: "fake" }, + { deviceId: "fake-video", kind: "videoinput", label: "Fake camera", groupId: "fake" }, + ]; + + if (!navigator.mediaDevices) { + navigator.mediaDevices = {}; + } + navigator.mediaDevices.getUserMedia = async (constraints = {}) => { + const tracks = []; + if (constraints.audio) tracks.push(audioTrack()); + if (constraints.video) tracks.push(videoTrack()); + if (!tracks.length) throw new DOMException("No constraints", "NotFoundError"); + return new MediaStream(tracks); + }; + navigator.mediaDevices.enumerateDevices = async () => fakeDevices; +} + /** Open a fresh browser context pointed at the example page. */ export async function openClient(browser, hub, { url } = {}) { const context = await browser.newContext(); @@ -95,7 +147,11 @@ export async function openClient(browser, hub, { url } = {}) { // The first-run walkthrough is a modal and its backdrop covers the dock, // so present as a returning user unless a test says otherwise. localStorage.setItem("togetherjs.settings.seenIntroDialog", "true"); + // util.testExpose() only publishes internals when this object already + // exists, so it has to be created before the bundle evaluates. + window.TogetherJSTestSpy = {}; }); + await page.addInitScript(installSyntheticMedia); const target = url || `/examples/index.html?hub=${encodeURIComponent(hub.url)}`; await page.goto(target); return page; diff --git a/tests/e2e/rtc.spec.js b/tests/e2e/rtc.spec.js new file mode 100644 index 000000000..17e4598d5 --- /dev/null +++ b/tests/e2e/rtc.spec.js @@ -0,0 +1,228 @@ +/* The audio/video mesh. + * + * Chromium runs with --use-fake-device-for-media-capture and + * --use-fake-ui-for-media-stream (see playwright.config.js), so getUserMedia + * resolves with a synthetic camera and microphone and no permission prompt. + * That makes the whole negotiation testable headlessly. + * + * The tests assert on getStats() rather than only on connectionState: ICE + * completing does not prove media is flowing. + */ + +import { test, expect, openClient, startSession, peerCount } from "./fixtures.js"; + +/** Join the call from this page. */ +function joinAudio(page) { + return page.evaluate(() => window.TogetherJSTestSpy.rtc.startAudio()); +} + +/** Every peer connection's state, as this page sees it. */ +function connectionStates(page) { + return page.evaluate(() => { + const mesh = window.TogetherJSTestSpy.rtc.mesh; + return mesh.peerIds().map((id) => ({ id, state: mesh.stateFor(id) })); + }); +} + +function meshSize(page) { + return page.evaluate(() => window.TogetherJSTestSpy.rtc.mesh.size()); +} + +/** True once every connection this page holds reports "connected". */ +function allConnected(page) { + return page.evaluate(() => { + const mesh = window.TogetherJSTestSpy.rtc.mesh; + const ids = mesh.peerIds(); + if (!ids.length) return false; + return ids.every((id) => { + const conn = mesh._connectionFor(id); + return conn && conn.connectionState === "connected"; + }); + }); +} + +/** Total inbound RTP packets across every connection — proof media flows. */ +function inboundPackets(page) { + return page.evaluate(async () => { + const mesh = window.TogetherJSTestSpy.rtc.mesh; + let total = 0; + for (const id of mesh.peerIds()) { + const conn = mesh._connectionFor(id); + if (!conn) continue; + const stats = await conn.pc.getStats(); + stats.forEach((report) => { + if (report.type === "inbound-rtp" && typeof report.packetsReceived === "number") { + total += report.packetsReceived; + } + }); + } + return total; + }); +} + +async function twoPeerCall(browser, hub) { + const a = await openClient(browser, hub); + const shareUrl = await startSession(a); + const b = await openClient(browser, hub, { url: shareUrl }); + await b.waitForFunction(() => window.TogetherJS.running); + await expect.poll(() => peerCount(a)).toBe(1); + return { a, b }; +} + +test("two peers connect and audio flows both ways", async ({ browser, hub, statics }) => { + const { a, b } = await twoPeerCall(browser, hub); + + await joinAudio(a); + await joinAudio(b); + + await expect.poll(() => meshSize(a), { timeout: 20000 }).toBe(1); + await expect.poll(() => meshSize(b), { timeout: 20000 }).toBe(1); + await expect.poll(() => allConnected(a), { timeout: 20000 }).toBe(true); + await expect.poll(() => allConnected(b), { timeout: 20000 }).toBe(true); + + // ICE completing is not the same as media arriving. + await expect.poll(() => inboundPackets(a), { timeout: 20000 }).toBeGreaterThan(0); + await expect.poll(() => inboundPackets(b), { timeout: 20000 }).toBeGreaterThan(0); + + await a.context().close(); + await b.context().close(); +}); + +test("three peers form a full mesh", async ({ browser, hub, statics }) => { + // The case the previous implementation could not handle at all: it held one + // connection for the whole room and broadcast its offers to everyone. + const a = await openClient(browser, hub); + const shareUrl = await startSession(a); + const b = await openClient(browser, hub, { url: shareUrl }); + const c = await openClient(browser, hub, { url: shareUrl }); + await b.waitForFunction(() => window.TogetherJS.running); + await c.waitForFunction(() => window.TogetherJS.running); + await expect.poll(() => peerCount(a)).toBe(2); + + await joinAudio(a); + await joinAudio(b); + await joinAudio(c); + + for (const page of [a, b, c]) { + await expect.poll(() => meshSize(page), { timeout: 25000 }).toBe(2); + await expect.poll(() => allConnected(page), { timeout: 25000 }).toBe(true); + } + + await a.context().close(); + await b.context().close(); + await c.context().close(); +}); + +test("simultaneous joins still converge (offer glare)", async ({ browser, hub, statics }) => { + const { a, b } = await twoPeerCall(browser, hub); + + // Both sides start negotiating in the same tick. Without perfect + // negotiation the two offers collide and the old rtc-abort logic thrashes; + // here the impolite peer ignores the incoming offer and the polite one + // rolls back. + await Promise.all([joinAudio(a), joinAudio(b)]); + + await expect.poll(() => allConnected(a), { timeout: 25000 }).toBe(true); + await expect.poll(() => allConnected(b), { timeout: 25000 }).toBe(true); + + await a.context().close(); + await b.context().close(); +}); + +test("enabling video mid-call does not drop the connection", async ({ browser, hub, statics }) => { + const { a, b } = await twoPeerCall(browser, hub); + await joinAudio(a); + await joinAudio(b); + await expect.poll(() => allConnected(a), { timeout: 20000 }).toBe(true); + + await a.evaluate(() => window.TogetherJSTestSpy.rtc.startVideo()); + + // B should receive a video track and render a tile for A... + await expect.poll(() => b.locator(".togetherjs-video-tile[data-togetherjs-peer]").count(), { + timeout: 20000, + }).toBeGreaterThan(0); + + // ...without the audio connection being torn down and rebuilt. + expect(await allConnected(a)).toBe(true); + expect(await allConnected(b)).toBe(true); + + await a.context().close(); + await b.context().close(); +}); + +test("muting is visible to the other peer and keeps the connection", async ({ + browser, + hub, + statics, +}) => { + const { a, b } = await twoPeerCall(browser, hub); + await joinAudio(a); + await joinAudio(b); + await expect.poll(() => allConnected(b), { timeout: 20000 }).toBe(true); + + await a.evaluate(() => window.TogetherJSTestSpy.rtc.toggleMute()); + + await expect + .poll(() => b.evaluate(() => { + const mesh = window.TogetherJSTestSpy.rtc.mesh; + const id = mesh.peerIds()[0]; + const state = mesh.stateFor(id); + return state ? state.audio : null; + }), { timeout: 15000 }) + .toBe(false); + + // Muting flips track.enabled; it must not renegotiate or drop the call. + expect(await allConnected(b)).toBe(true); + + await a.context().close(); + await b.context().close(); +}); + +test("a peer leaving is torn down without disturbing the others", async ({ + browser, + hub, + statics, +}) => { + const a = await openClient(browser, hub); + const shareUrl = await startSession(a); + const b = await openClient(browser, hub, { url: shareUrl }); + const c = await openClient(browser, hub, { url: shareUrl }); + await b.waitForFunction(() => window.TogetherJS.running); + await c.waitForFunction(() => window.TogetherJS.running); + await expect.poll(() => peerCount(a)).toBe(2); + + await joinAudio(a); + await joinAudio(b); + await joinAudio(c); + await expect.poll(() => meshSize(a), { timeout: 25000 }).toBe(2); + + await c.evaluate(() => window.TogetherJS()); + + await expect.poll(() => meshSize(a), { timeout: 20000 }).toBe(1); + await expect.poll(() => meshSize(b), { timeout: 20000 }).toBe(1); + await expect.poll(() => allConnected(a), { timeout: 20000 }).toBe(true); + + await a.context().close(); + await b.context().close(); +}); + +test("hanging up closes every connection and releases the devices", async ({ + browser, + hub, + statics, +}) => { + const { a, b } = await twoPeerCall(browser, hub); + await joinAudio(a); + await joinAudio(b); + await expect.poll(() => allConnected(a), { timeout: 20000 }).toBe(true); + + await a.evaluate(() => window.TogetherJSTestSpy.rtc.hangup()); + + expect(await meshSize(a)).toBe(0); + expect( + await a.evaluate(() => window.TogetherJSTestSpy.rtc.media.hasAudio()), + ).toBe(false); + + await a.context().close(); + await b.context().close(); +}); diff --git a/tests/unit/element-finder.test.js b/tests/unit/element-finder.test.js index d7eb5803c..e60cd8770 100644 --- a/tests/unit/element-finder.test.js +++ b/tests/unit/element-finder.test.js @@ -6,7 +6,7 @@ on the wrong node. */ import { describe, it, expect, beforeEach } from "vitest"; -import $ from "jquery"; +import $ from "../../src/dom/dom.js"; import elementFinder from "../../src/dom/elementFinder.js"; const FIXTURE = ` diff --git a/tests/unit/linkify.test.js b/tests/unit/linkify.test.js index ae32c0488..19a0c60ba 100644 --- a/tests/unit/linkify.test.js +++ b/tests/unit/linkify.test.js @@ -1,7 +1,7 @@ /* Ported from togetherjs/tests/test_linkify.js */ import { describe, it, expect } from "vitest"; -import $ from "jquery"; +import $ from "../../src/dom/dom.js"; import linkify from "../../src/dom/linkify.js"; // linkify() takes a jQuery object or an element and returns the raw element. From 56dadeaa71e816ecfb4c68213b8e2b94d1c3ac2a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 22:40:56 +0000 Subject: [PATCH 4/5] Update the docs for the rewritten client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WebRTC documentation was thirteen years out of date: it told readers they needed Firefox Nightly, that support was coming "sometime in 2013", and apologised for a missing TURN server by linking a 2013 issue. It now describes what the client actually does — joining and muting, the camera button, why the page has to be served over https, how to supply your own TURN servers (both static and minted-per-session), and why calls are capped at six people. The new configuration keys are documented: enableVideo, iceServers, getIceServers and maxRtcPeers. README build instructions replace the Grunt ones, and src/README.md replaces the old per-module list. It leads with the two conventions that are not obvious from reading any single file: core/togetherjs.js is imported rather than read off window, and configuration must be read lazily because the bundle evaluates before the host page has configured anything. Deleted module-descriptions.json, which nothing had read since the docco task went away. The Playwright config now finds a browser in both places: it uses the Chromium this dev container pins when that path exists, and otherwise lets Playwright resolve the one `playwright install` fetched — so the suite runs in CI without further configuration. Note: a GitHub Actions workflow to run lint/build/unit/e2e on Node 22 is *not* included, because this app lacks permission to push .github/workflows/. Travis is gone, so there is currently no CI; adding a workflow is a one-file follow-up. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HgRCPNvLVGLYVox8Va8uJh --- README.md | 78 +++++++++--------- playwright.config.js | 19 +++-- site/docs/index.md | 56 ++++++++++--- src/README.md | 149 ++++++++++++++++++++++------------- src/module-descriptions.json | 32 -------- 5 files changed, 189 insertions(+), 145 deletions(-) delete mode 100644 src/module-descriptions.json diff --git a/README.md b/README.md index 237cda154..7a91daa6c 100644 --- a/README.md +++ b/README.md @@ -32,76 +32,70 @@ Setting up a development environment TogetherJS has two main pieces: -* The [server](https://github.com/mozilla/togetherjs/blob/develop/hub/server.js), which echos messages back and forth between users. The server doesn't do much, you may gaze upon its incredibly boring [history](https://github.com/mozilla/togetherjs/commits/develop/hub/server.js). +* The hub in [`hub-worker/`](hub-worker/), which echoes messages back and forth between users. It doesn't do much: it broadcasts whatever it receives to everyone else in the room, and understands none of it. -* The client in [`togetherjs/`](https://github.com/mozilla/togetherjs/tree/develop/togetherjs) which does all the real work. +* The client in [`src/`](src/), which does all the real work. There is no shared hub server anymore, so you'll need to host your own (see "Hosting the Hub Server" in `site/docs/index.md`). The recommended way is [`hub-worker/`](hub-worker/), a Cloudflare Workers + Durable Objects port of the relay logic that runs on Cloudflare's free plan. Note if you include TogetherJS on an https site, you must use an https/wss hub server (Cloudflare Workers handle this automatically). -The files need to be lightly "built": we use [LESS](http://lesscss.org/) for styles, and a couple files are generated. To develop you need to build the library using [Grunt](http://gruntjs.com/). - -To build a copy of the library, check out TogetherJS: - -```sh -$ git clone git://github.com/mozilla/togetherjs.git -$ cd togetherjs -``` - -Then [install npm](http://nodejs.org/download/) and run: +The client is a set of ES modules under `src/`, bundled with +[esbuild](https://esbuild.github.io/). To build it, install +[Node](https://nodejs.org/) 20 or newer and run: ```sh $ npm install -$ npm install -g grunt-cli +$ npm run build ``` -This will install a bunch of stuff, most of which is only used for development. The only "server" dependency is [WebSocket-Node](https://github.com/Worlize/WebSocket-Node) (and if you use our hub then you don't need to worry about the server). By default everything is installed locally, i.e., in `node_modules/`. This works just fine, but it is useful to install the `grunt` command-line program globally, which `npm install -g grunt-cli` does. +That writes `dist/`: -Now you can build TogetherJS, like: +* `togetherjs.js` — the whole client, ready to drop into a page with + `` +* `togetherjs.min.js` — the same thing, minified, with a source map +* `togetherjs.esm.js` — an ES module entry, for `import { TogetherJS } from "togetherjs"` +* `togetherjs.css`, `images/` — the stylesheet and assets +* `recorder.js`, `walkabout.js` — separate bundles, loaded on demand -```sh -$ grunt build buildsite --no-hardlink -``` - -This will create a copy of the entire `togetherjs.com` site in `build/`. You'll need to setup a local web server of your own pointed to the `build/` directory. To start a server on port 8080, run: +To develop, run a watching build with a static server: ```sh -$ node devserver.js +$ npm run dev ``` -If you want to develop with TogetherJS you probably want the files built continually. To do this use: +Then open `examples/index.html`. It expects a hub at `http://localhost:8787` +(run `npx wrangler dev` inside `hub-worker/`), or you can point it elsewhere +with `?hub=http://host:port`. + +The hub URL baked into a build comes from the `HUB_URL` environment variable: ```sh -$ grunt devwatch +$ HUB_URL=https://hub.example.com npm run build ``` -This will rebuild when changes are detected. Note that Grunt is configured to create [hard links](http://en.wikipedia.org/wiki/Hard_link) instead of copying so that most changes you make to files in `togetherjs/` don't need to be rebuilt to show up in `build/togetherjs/`. `--no-hardlink` turns this behavior off. +`BASE_URL` does the same for the URL the client's own assets are served from; +leave it unset and the client works out where it was loaded from. -You may wish to create a static copy of the TogetherJS client to distribute and use on your website. To do this run: +Testing +------- + +Unit tests use [Vitest](https://vitest.dev/) and live in `tests/unit/`: ```sh -$ grunt build --base-url https://myapp.com --no-hardlink --dest static-myapp +$ npm test ``` -Then `static-myapp/togetherjs.js` and `static-myapp/togetherjs-min.js` will be in place, and the rest of the code will be under `static-myapp/togetherjs/`. You would deploy these on your server. +End-to-end tests use [Playwright](https://playwright.dev/) and live in +`tests/e2e/`. They drive two or three real browser contexts through a session +against an in-process stand-in for the hub, covering cursors, chat, form sync +and the audio/video mesh: -Running a local server ----------------------- -You'll need to run your own hub server (see "Hosting the Hub Server" in -`site/docs/index.md` and [`hub-worker/`](hub-worker/)). If you make changes to -the hub and want to point a local build at it, set the HUB_URL environment -variable when building. For example: -``` -$ HUB_URL=http://localhost:8080 grunt devwatch +```sh +$ npm run test:e2e ``` -Testing -------- - -Tests are in `togetherjs/tests/` -- these are [doctest.js](http://doctestjs.org/) tests. To actually run the tests build togetherjs, serve it up, and go to `http://localhost:PORT/togetherjs/tests/` -- from there the tests are linked to from the top of the page. The actual tests are `*.js` files in `togetherjs/tests/`, generally `test_*.js` for unit-style tests, and `func_*.js` for functional tests. - -The "Manual testing" link is something that lets you simulate different conditions in TogetherJS without setting up a second browser/client. +Lint with `npm run lint` and format with `npm run format`. -There is unfortunately no automated runner for these tests. It might be nice if [Karma](http://karma-runner.github.io/) could be setup with doctest.js in general, but so far that isn't done. +`examples/manual/` holds pages for poking at particular behaviours by hand. License ------- diff --git a/playwright.config.js b/playwright.config.js index ad6487de8..8d8f677dd 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,4 +1,12 @@ import { defineConfig } from "@playwright/test"; +import fs from "node:fs"; + +/* This dev container ships a Chromium that Playwright does not manage, so + point at it when it is there and let Playwright resolve its own browser + otherwise (CI, or a normal checkout after `playwright install`). */ +const PINNED_CHROMIUM = + process.env.CHROMIUM_PATH || "/opt/pw-browsers/chromium-1194/chrome-linux/chrome"; +const executablePath = fs.existsSync(PINNED_CHROMIUM) ? PINNED_CHROMIUM : undefined; export default defineConfig({ testDir: "./tests/e2e", @@ -15,17 +23,16 @@ export default defineConfig({ { name: "chromium", use: { - // Chromium is preinstalled in this environment; never run - // `playwright install`. launchOptions: { - executablePath: - process.env.CHROMIUM_PATH || "/opt/pw-browsers/chromium-1194/chrome-linux/chrome", + executablePath, args: [ // The sandbox may route through an HTTPS proxy that would // intercept the loopback hub connection. "--no-proxy-server", - // Synthetic camera/mic so getUserMedia resolves headlessly, and - // no permission prompt to click through. + // Fake devices and auto-granted permission, so getUserMedia + // resolves headlessly. Note this container produces no devices + // even with the flag set, so tests/e2e/fixtures.js also installs + // canvas/WebAudio-backed tracks; keep both. "--use-fake-device-for-media-capture", "--use-fake-ui-for-media-stream", "--autoplay-policy=no-user-gesture-required", diff --git a/site/docs/index.md b/site/docs/index.md index 77924c156..23a6abeb4 100644 --- a/site/docs/index.md +++ b/site/docs/index.md @@ -51,7 +51,7 @@ In this section we'll describe the general way that TogetherJS works, without di The core of TogetherJS is the **hub**: this is a server that everyone in a session connects to, and it echos messages to all the participants using Web Sockets. This server does not rewrite the messages or do much of anything besides **pass the messages between the participants**. -[WebRTC](http://www.webrtc.org/) is available for **audio chat**, but is not otherwise used. We are often asked about this, as WebRTC offers data channels that allow browsers to send data directly to other browsers without a server. Unfortunately you still need a server to establish the connection (the connection strings to connect browsers are quite unwieldy), it only supports one-to-one connections, and that support is limited to only some browsers and browser versions. Also establishing the connection is significantly slower than Web Sockets. But maybe someday. +[WebRTC](https://webrtc.org/) is used for **audio and video chat**, but not for anything else. We are often asked about this, as WebRTC offers data channels that let browsers send data directly to each other. You still need a server to introduce the two browsers, though, and the hub is already there and already fast; a data channel would add a second path to maintain without removing the first. So application messages continue to go over the Web Socket. Everything that TogetherJS does is based on these messages being passed between browsers. It doesn't require that everyone be on the same page, all it requires is that everyone in the session know what hub URL to connect to (the URL is essentially the session name). People *can* be on different sites, but the session URL is stored in [sessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window.sessionStorage) which is local to one domain (and because we use sessionStorage instead of localStorage, it is local to one tab). We don't have any techniques implemented to share sessions across multiple sites, but the only barrier is this local storage of session information. @@ -129,7 +129,19 @@ The other way to set a variable *after* TogetherJS is loaded is `TogetherJS.conf When true (default false), TogetherJS treats the entire URL, including the hash, as the identifier of the page; i.e., if you one person is on `http://example.com/#view1` and another person is at `http://example.com/#view2` then these two people are considered to be at completely different URLs. You'd want to use this in single-page apps where being at the same base URL doesn't mean the two people are looking at the same thing. `TogetherJSConfig_disableWebRTC`: - Disables/removes the button to do audio chat via WebRTC. + Disables/removes the microphone and camera buttons. + +`TogetherJSConfig_enableVideo`: + When true (default false), a camera button appears next to the microphone button and participants can share video. It is off by default because camera access is a bigger ask than the microphone, and because an existing embed should not sprout a camera button when it upgrades. + +`TogetherJSConfig_iceServers`: + An array of [RTCIceServer](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers) objects used for audio/video connections. Defaults to a public STUN server. See "About audio and video chat" below for why you may want to supply a TURN server here. + +`TogetherJSConfig_getIceServers`: + An `async () => RTCIceServer[]` function, called once per connection. Prefer this over `iceServers` when your TURN credentials are short-lived: a static array cannot express a credential that expires. + +`TogetherJSConfig_maxRtcPeers`: + How many people may be in a call at once (default 6). Everyone sends their audio and video directly to everyone else, so the bandwidth each participant needs grows with the size of the call. Past this limit new connections are refused rather than degrading the call for everyone. `TogetherJSConfig_youtube`: If true, then YouTube videos will be synchronized (i.e., when one person plays or pauses a video, it will play for all people). This will also load up the YouTube iframe API. @@ -166,17 +178,41 @@ The button you add to your site to start TogetherJS will typically look like thi TogetherJS sessions are connected to the domain you start them on (specifically the [origin](http://tools.ietf.org/html/rfc6454)). So if part of your site is on another domain, people won't be able to talk across those domains. Even a page that is on https when another is on http will cause the session to be lost. We might make this work sometime, but if it's an issue to you please give us [feedback](https://docs.google.com/forms/d/1lVE7JyRo_tjakN0mLG1Cd9X9vseBX9wci153z9JcNEs/viewform). -## About Audio Chat and WebRTC +## About audio and video chat + +Live audio and video chat are based on [WebRTC](https://webrtc.org/), which every current browser supports. -The live audio chat is based on [WebRTC](http://www.webrtc.org/). This is a very new technology, built into some new browsers. +Click the microphone button to join the call, and click it again to mute yourself. If you set `TogetherJSConfig_enableVideo`, a camera button appears alongside it; peers who turn their camera on show up as tiles in a video panel. Audio plays whether or not that panel is open. "Leave call" in the microphone popup hangs up and releases the microphone and camera. -To enable WebRTC both you and your collaborator need a new browser. Right now, [Firefox Nightly](http://nightly.mozilla.org/) is supported, and we believe that the newest release of Chrome should work. +### It needs a secure page -Sometime in 2013 support for this should be available in new (non-experimental) versions of Firefox, Chrome, and both Firefox and Chrome for Android. +Browsers only grant access to the microphone and camera on a secure origin. If your page is served over plain `http`, the buttons will tell you so — no permission prompt will help, the API is simply not there. `localhost` counts as secure, so local development works. -To see a summary of outstanding issues that we know of with audio chat see [this page](https://github.com/mozilla/togetherjs/issues?labels=rtc&milestone=&page=1&state=open). +### It needs TURN on some networks -Note that audio chat will not work between some networks. These networks require a [TURN server](http://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT) which unfortunately we do not have allocated (and full support for TURN has not landed in some browsers). Unfortunately when the network makes chat impossible, chat will simply not work – we don't receive an error, and can't tell you why chat is not working. See [#327](https://github.com/mozilla/togetherjs/issues/327) for progress. +Everyone in a call connects directly to everyone else. Some networks — symmetric NATs, and many corporate firewalls — will not allow that, and the connection needs a [TURN server](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT) to relay it. TogetherJS does not run one, and the default configuration only includes a STUN server, which is enough for most home networks but not all of them. + +If your users are on networks where calls fail to connect, supply your own: + +```js +TogetherJSConfig_iceServers = [ + {urls: "stun:stun.l.google.com:19302"}, + {urls: "turn:turn.example.com:3478", username: "…", credential: "…"} +]; +``` + +TURN credentials are usually short-lived, so if yours are minted per session use the async hook instead: + +```js +TogetherJSConfig_getIceServers = async () => { + const resp = await fetch("/my-turn-credentials"); + return resp.json(); +}; +``` + +### How many people + +The call is a full mesh: each participant sends their audio and video to every other participant separately, so the bandwidth each person needs grows with the number of people. That is fine for a handful and poor beyond it, so calls are capped at `TogetherJSConfig_maxRtcPeers` (default 6). Supporting larger calls would mean routing media through a server (an SFU), which TogetherJS does not do. ## Extending TogetherJS @@ -432,9 +468,7 @@ The bare minimum that we've identified for TogetherJS is [WebSocket support](htt We recommend the most recent release of [Firefox](http://www.mozilla.org/en-US/firefox/new/) or [Chrome](https://www.google.com/intl/en/chrome/browser/). -If you want to have [WebRTC support](https://github.com/mozilla/togetherjs/wiki/About-Audio-Chat-and-WebRTC) and are using Firefox, as of April 2013 this requires [Firefox Nightly](http://nightly.mozilla.org/) (this support will be moving towards beta and release in the coming months). - -We haven't done much testing on mobile (yet!) and cannot recommend anything there. +Audio and video chat work in any current browser, but need the page to be served over https (see "About audio and video chat" above). #### Internet Explorer diff --git a/src/README.md b/src/README.md index 36e381140..b6a90eb70 100644 --- a/src/README.md +++ b/src/README.md @@ -1,57 +1,98 @@ TogetherJS client ================= -This is all the files for the TogetherJS client. -An overview of the modules: - -- `libs/`: contains external libraries, sometimes as [git subtree inclusions](https://github.com/apenwarr/git-subtree) and sometimes just copied in. - -- `analytics.js`: a little library for handling Google Analytics opt-in support - -- `channels.js`: abstraction over WebSockets and other communication methods (like `postMessage`). Buffers output while the connection is opening, handles JSON encoding/decoding. - -- `chat.js`: handles the chat code, including logging old chat messages. Doesn't actually include the chat UI, which is in `ui.js` - -- `cursor.js`: handles the shared cursors, both displaying and capturing events. Also handles clicks. This *does* include the relevant UI. - -- `elementFinder.js`: this generates a description/locator/path for any element, and finds elements based on those paths. It generates something similar to a CSS selector. It also includes a function to determine what elements should be ignored (generally TogetherJS's own elements). - -- `eventMaker.js`: this creates artificial events, like a fake click event. - -- `forms.js`: handles synchronization of forms, including CodeMirror and ACE support. - -- `jqueryPlugins.js`: some plugins for jQuery; doesn't export anything. - -- `linkify.js`: detects and adds links to plain text. - -- `ot.js`: operational transformation support: what keeps big chunks of text in sync when multiple people are simultaneously editing those fields. - -- `peers.js`: handles the objects representing the peers and oneself. - -- `playback.js`: handles the magic `/playback` command that plays recordings. - -- `randomutil.js`: some functions/methods for random numbers, really just for testing. - -- `recorder.js`: this is used by `recorder.html`, which is a kind of alternate mini-client used to record sessions when you put `/record` in the chat box. - -- `session.js`: probably the most important and most core module in the system. This sets up the channels, routes messages, tracks peers, and is used for some communication (like `session.on("ui-ready")` - which is actually signalled by `ui.js` but is fired on the session module). - -- `startup.js`: handles the logic of what to display when TogetherJS is first started up (including warning messages, introductory stuff, the share link, confirmation of joining the session) - -- `storage.js`: an abstraction of per-tab and client storage. Mostly uses `localStorage` (or `sessionStorage`), but designed so it could use an async backed someday, perhaps. - -- `templates.js`: this is generated dynamically, and includes the `*.html` content as inlined strings. Basically just a container for these strings. - -- `templating.js`: handles creating nodes based on DOM templates. Does some substitution based on specific class names. - -- `togetherjs.js`: this is the bootstrap code. It is included on all pages, defines the `TogetherJS` variable, and handles configuration and initial loading. - -- `ui.js`: this has most of the UI. It loads the UI and binds most of the methods. It's a jumble of UI stuff. `ui.activateUI()` is the most important function. - -- `util.js`: several bits of abstract support code are in here. It doesn't depend on other things, and has fairly abstract general-purpose code. It includes a pattern for creating classes, assertions, events. - -- `walkthrough.js`: implements the walkthrough help. - -- `webrtc.js`: handles the live audio chat and avatar editing. - -- `windowing.js`: handles creating the different windows, notifications, and modal windows. +The client is a set of ES modules bundled by `build/build.mjs` (esbuild). The +entry point is `index.js`; everything else is grouped by what it does. + +Two conventions are worth knowing before reading any of it: + +- **`core/togetherjs.js` is imported, not global.** It defines the `TogetherJS` + object and assigns `window.TogetherJS`, but every module that uses it imports + it explicitly. Under RequireJS the load order made the global safe to read at + module scope; in a single bundle it is not. +- **Configuration is read lazily.** The bundle evaluates when the `