From 01e9fdf772cb6d2d3da0f93d51957c5ea9c9179e Mon Sep 17 00:00:00 2001 From: Kevin Dangoor Date: Thu, 18 Nov 2010 23:13:56 -0500 Subject: [PATCH] editor.html now boots with a bit of the plugin system --- demo/boot.js | 125 ++++++ demo/demo_startup.js | 209 +++++++++ editor.html | 206 +-------- plugins/pilot/lib/console.js | 76 ++++ plugins/pilot/lib/index.js | 147 +++++++ plugins/pilot/lib/plugin_manager.js | 149 +++++++ plugins/pilot/lib/promise.js | 264 +++++++++++ plugins/pilot/lib/proxy.js | 82 ++++ plugins/pilot/lib/stacktrace.js | 332 ++++++++++++++ plugins/pilot/lib/util.js | 659 ++++++++++++++++++++++++++++ 10 files changed, 2063 insertions(+), 186 deletions(-) create mode 100644 demo/boot.js create mode 100644 demo/demo_startup.js create mode 100644 plugins/pilot/lib/console.js create mode 100644 plugins/pilot/lib/index.js create mode 100644 plugins/pilot/lib/plugin_manager.js create mode 100644 plugins/pilot/lib/promise.js create mode 100644 plugins/pilot/lib/proxy.js create mode 100644 plugins/pilot/lib/stacktrace.js create mode 100644 plugins/pilot/lib/util.js diff --git a/demo/boot.js b/demo/boot.js new file mode 100644 index 00000000..d21535de --- /dev/null +++ b/demo/boot.js @@ -0,0 +1,125 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Kevin Dangoor (kdangoor@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +// TODO: Yuck! A global function +var setupPlugins = function(config, callback) { + config = config || {}; + if (!config.pluginDirs) { + config.pluginDirs = {}; + } + // config.pluginDirs["../lib"] = { + // packages: ["ace"] + // }; + config.pluginDirs["../plugins"] = { + packages: ["pilot"] + }; + + var knownPlugins = []; + + var pluginPackageInfo = { + "../lib": [ + { + name: "ace", + lib: "." + } + ] + }; + + var paths = {}; + var i; + var location; + + // we need to ensure that the core plugin directory is loaded first + var pluginDirs = []; + var pluginDir; + for (pluginDir in config.pluginDirs) { + pluginDirs.push(pluginDir); + } + pluginDirs.sort(function(a, b) { + if (a == "../plugins") { + return -1; + } else if (b == "../plugins") { + return 1; + } else if (a < b) { + return -1; + } else if (b < a) { + return 1; + } else { + return 0; + } + }); + + // set up RequireJS to know that our plugins all have a main module called "index" + for (var dirNum = 0; dirNum < pluginDirs.length; dirNum++) { + pluginDir = pluginDirs[dirNum]; + var dirInfo = config.pluginDirs[pluginDir]; + if (dirInfo.packages) { + location = pluginPackageInfo[pluginDir]; + if (location === undefined) { + pluginPackageInfo[pluginDir] = location = []; + } + var packages = dirInfo.packages; + for (i = 0; i < packages.length; i++) { + location.push({ + name: packages[i], + main: "index" + }); + knownPlugins.push(packages[i]); + } + } + if (dirInfo.singleFiles) { + for (i = 0; i < dirInfo.singleFiles.length; i++) { + var pluginName = dirInfo.singleFiles[i]; + paths[pluginName] = pluginDir + "/" + pluginName; + knownPlugins.push(pluginName); + } + } + } + + require({ + packagePaths: pluginPackageInfo, + paths: paths + }); + require(["pilot/plugin_manager"], function() { + var pluginsModule = require("pilot/plugin_manager"); + var catalog = pluginsModule.catalog; + catalog.registerPlugins(knownPlugins); + if (callback) { + callback(pluginsModule); + } + }); +}; diff --git a/demo/demo_startup.js b/demo/demo_startup.js new file mode 100644 index 00000000..177b5e06 --- /dev/null +++ b/demo/demo_startup.js @@ -0,0 +1,209 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * Kevin Dangoor (kdangoor@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + + +define(function(require, exports, module) { + +exports.launch = function() { + + var eventMod = require("ace/lib/event"); + var editorMod = require("ace/editor"); + var renderMod = require("ace/virtual_renderer"); + var theme = require("ace/theme/textmate"); + var docMod = require("ace/document"); + var jsMod = require("ace/mode/javascript"); + var cssMod = require("ace/mode/css"); + var htmlMod = require("ace/mode/html"); + var xmlMod = require("ace/mode/xml"); + var textMod = require("ace/mode/text"); + var undoMod = require("ace/undomanager"); + + var event = eventMod.event; + var Editor = editorMod.Editor; + var Renderer = renderMod.VirtualRenderer; + var Document = docMod.Document; + var JavaScriptMode = jsMod.JavaScript; + var CssMode = cssMod.Css; + var HtmlMode = htmlMod.Html; + var XmlMode = xmlMod.Xml; + var TextMode = textMod.Text; + var UndoManager = undoMod.UndoManager; + + var docs = {} + + docs.js = new Document(document.getElementById("jstext").innerHTML); + docs.js.setMode(new JavaScriptMode()); + docs.js.setUndoManager(new UndoManager()); + + docs.css = new Document(document.getElementById("csstext").innerHTML); + docs.css.setMode(new CssMode()); + docs.css.setUndoManager(new UndoManager()); + + docs.html = new Document(document.getElementById("htmltext").innerHTML); + docs.html.setMode(new HtmlMode()); + docs.html.setUndoManager(new UndoManager()); + + var docEl = document.getElementById("doc"); + + function onDocChange() { + var doc = getDoc(); + editor.setDocument(doc); + + var mode = doc.getMode(); + if (mode instanceof JavaScriptMode) { + modeEl.value = "javascript" + } + else if (mode instanceof CssMode) { + modeEl.value = "css" + } + else if (mode instanceof HtmlMode) { + modeEl.value = "html" + } + else if (mode instanceof XmlMode) { + modeEl.value = "xml" + } + else { + modeEl.value = "text" + } + + editor.focus(); + } + docEl.onchange = onDocChange; + + function getDoc() { + return docs[docEl.value]; + } + + var modeEl = document.getElementById("mode"); + modeEl.onchange = function() { + editor.getDocument().setMode(modes[modeEl.value] || modes.text); + }; + + var modes = { + text: new TextMode(), + xml: new XmlMode(), + html: new HtmlMode(), + css: new CssMode(), + javascript: new JavaScriptMode() + }; + + function getMode() { + return modes[modeEl.value]; + } + + var themeEl = document.getElementById("theme"); + themeEl.onchange = function() { + editor.setTheme(themeEl.value); + }; + + var selectEl = document.getElementById("select_style"); + selectEl.onchange = function() { + if (selectEl.checked) { + editor.setSelectionStyle("line"); + } else { + editor.setSelectionStyle("text"); + } + }; + + var activeEl = document.getElementById("highlight_active"); + activeEl.onchange = function() { + editor.setHighlightActiveLine(!!activeEl.checked); + }; + + var container = document.getElementById("editor"); + var editor = new Editor(new Renderer(container, theme)); + onDocChange(); + + window.jump = function() { + var jump = document.getElementById("jump") + var cursor = editor.getCursorPosition() + var pos = editor.renderer.textToScreenCoordinates(cursor.row, cursor.column); + jump.style.left = pos.pageX + "px"; + jump.style.top = pos.pageY + "px"; + jump.style.display = "block"; + } + + function onResize() { + container.style.width = (document.documentElement.clientWidth - 4) + "px"; + container.style.height = (document.documentElement.clientHeight - 55 - 4) + "px"; + editor.resize(); + }; + + window.onresize = onResize; + onResize(); + + event.addListener(container, "dragover", function(e) { + return event.preventDefault(e); + }); + + event.addListener(container, "drop", function(e) { + try { + var file = e.dataTransfer.files[0]; + } catch(e) { + return event.stopEvent(); + } + + if (window.FileReader) { + var reader = new FileReader(); + reader.onload = function(e) { + editor.getSelection().selectAll(); + + var mode = "text"; + if (/^.*\.js$/i.test(file.name)) { + mode = "javascript"; + } else if (/^.*\.xml$/i.test(file.name)) { + mode = "xml"; + } else if (/^.*\.html$/i.test(file.name)) { + mode = "html"; + } else if (/^.*\.css$/i.test(file.name)) { + mode = "css"; + } + + editor.onTextInput(reader.result); + + modeEl.value = mode; + editor.getDocument().setMode(modes[mode]); + } + reader.readAsText(file); + } + + return event.preventDefault(e); + }); +}; + +}); diff --git a/editor.html b/editor.html index c3e382ef..1c6703bf 100644 --- a/editor.html +++ b/editor.html @@ -48,7 +48,27 @@ } + + +
@@ -131,191 +151,5 @@ - - \ No newline at end of file diff --git a/plugins/pilot/lib/console.js b/plugins/pilot/lib/console.js new file mode 100644 index 00000000..b1fe5190 --- /dev/null +++ b/plugins/pilot/lib/console.js @@ -0,0 +1,76 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker (jwalker@mozilla.com) + * Patrick Walton (pwalton@mozilla.com) + * Julian Viereck (jviereck@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +define(function(require, exports, module) { + +/** + * This object represents a "safe console" object that forwards debugging + * messages appropriately without creating a dependency on Firebug in Firefox. + */ + +var noop = function() {}; + +// These are the functions that are available in Chrome 4/5, Safari 4 +// and Firefox 3.6. Don't add to this list without checking browser support +var NAMES = [ + "assert", "count", "debug", "dir", "dirxml", "error", "group", "groupEnd", + "info", "log", "profile", "profileEnd", "time", "timeEnd", "trace", "warn" +]; + +if (typeof(window) === 'undefined') { + // We're in a web worker. Forward to the main thread so the messages + // will show up. + NAMES.forEach(function(name) { + exports[name] = function() { + var args = Array.prototype.slice.call(arguments); + var msg = { op: 'log', method: name, args: args }; + postMessage(JSON.stringify(msg)); + }; + }); +} else { + // For each of the console functions, copy them if they exist, stub if not + NAMES.forEach(function(name) { + if (window.console && window.console[name]) { + exports[name] = window.console[name].bind(window.console); + } else { + exports[name] = noop; + } + }); +} + +}); diff --git a/plugins/pilot/lib/index.js b/plugins/pilot/lib/index.js new file mode 100644 index 00000000..995afb8a --- /dev/null +++ b/plugins/pilot/lib/index.js @@ -0,0 +1,147 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Kevin Dangoor (kdangoor@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { + +exports.startup = function(data, reason) { + // Narwhal's shim for ES5 defineProperty + // ES5 15.2.3.6 + if (!Object.defineProperty) { + Object.defineProperty = function(object, property, descriptor) { + var has = Object.prototype.hasOwnProperty; + if (typeof descriptor == "object" && object.__defineGetter__) { + if (has.call(descriptor, "value")) { + if (!object.__lookupGetter__(property) && !object.__lookupSetter__(property)) { + // data property defined and no pre-existing accessors + object[property] = descriptor.value; + } + if (has.call(descriptor, "get") || has.call(descriptor, "set")) { + // descriptor has a value property but accessor already exists + throw new TypeError("Object doesn't support this action"); + } + } + // fail silently if "writable", "enumerable", or "configurable" + // are requested but not supported + /* + // alternate approach: + if ( // can't implement these features; allow false but not true + !(has.call(descriptor, "writable") ? descriptor.writable : true) || + !(has.call(descriptor, "enumerable") ? descriptor.enumerable : true) || + !(has.call(descriptor, "configurable") ? descriptor.configurable : true) + ) + throw new RangeError( + "This implementation of Object.defineProperty does not " + + "support configurable, enumerable, or writable." + ); + */ + else if (typeof descriptor.get == "function") { + object.__defineGetter__(property, descriptor.get); + } + if (typeof descriptor.set == "function") { + object.__defineSetter__(property, descriptor.set); + } + } + return object; + }; + } + + // ES5 15.2.3.7 + if (!Object.defineProperties) { + Object.defineProperties = function(object, properties) { + for (var property in properties) { + if (Object.prototype.hasOwnProperty.call(properties, property)) { + Object.defineProperty(object, property, properties[property]); + } + } + return object; + }; + } + + + + /** + * Array detector. + * Firefox 3.5 and Safari 4 have this already. Chrome 4 however ... + * Note to Dojo - your isArray is still broken: instanceof doesn't work with + * Arrays taken from a different frame/window. + */ + if (!Array.isArray) { + Array.isArray = function(data) { + return data && Object.prototype.toString.call(data) === "[object Array]"; + }; + } + + /** + * Retrieves the list of keys on an object. + */ + if (!Object.keys) { + Object.keys = function(obj) { + var k, ret = []; + for (k in obj) { + if (obj.hasOwnProperty(k)) { + ret.push(k); + } + } + return ret; + }; + } + + if (!Function.prototype.bind) { + // From Narwhal + Function.prototype.bind = function () { + var args = Array.prototype.slice.call(arguments); + var self = this; + var bound = function () { + return self.call.apply( + self, + args.concat( + Array.prototype.slice.call(arguments) + ) + ); + }; + bound.name = this.name; + bound.displayName = this.displayName; + bound.length = this.length; + bound.unbound = self; + return bound; + }; + } + + exports.globalsLoaded = true; +}; + +}); \ No newline at end of file diff --git a/plugins/pilot/lib/plugin_manager.js b/plugins/pilot/lib/plugin_manager.js new file mode 100644 index 00000000..75fceb61 --- /dev/null +++ b/plugins/pilot/lib/plugin_manager.js @@ -0,0 +1,149 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Kevin Dangoor (kdangoor@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { + +var Promise = require("pilot/promise").Promise; + +exports.REASONS = { + APP_STARTUP: 1, + APP_SHUTDOWN: 2, + PLUGIN_ENABLE: 3, + PLUGIN_DISABLE: 4, + PLUGIN_INSTALL: 5, + PLUGIN_UNINSTALL: 6, + PLUGIN_UPGRADE: 7, + PLUGIN_DOWNGRADE: 8 +}; + +exports.Plugin = function(name) { + this.name = name; + this.status = this.INSTALLED; +}; + +exports.Plugin.prototype = { + /** + * constants for the state + */ + NEW: 0, + INSTALLED: 1, + STARTED: 2, + SHUTDOWN: 3, + + install: function(data, reason) { + var pr = new Promise(); + if (this.status > this.NEW) { + pr.resolve(this); + return pr; + } + require([this.name], function(pluginModule) { + if (pluginModule.install) { + pluginModule.install(data, reason); + } + this.status = this.INSTALLED; + pr.resolve(this); + }.bind(this)); + return pr; + }, + + startup: function(data, reason) { + var pr = new Promise(); + if (this.status != this.INSTALLED) { + pr.resolve(this); + return pr; + } + require([this.name], function(pluginModule) { + if (pluginModule.startup) { + pluginModule.startup(data, reason); + } + this.status = this.STARTED; + pr.resolve(this); + }.bind(this)); + return pr; + }, + + shutdown: function(data, reason) { + if (this.status != this.STARTED) { + return; + } + pluginModule = require(this.name); + if (pluginModule.shutdown) { + pluginModule.shutdown(data, reason); + } + } +}; + +exports.PluginCatalog = function() { + this.plugins = {}; +}; + +exports.PluginCatalog.prototype = { + registerPlugins: function(pluginList) { + pluginList.forEach(function(pluginName) { + var plugin = this.plugins[pluginName]; + if (plugin === undefined) { + plugin = new exports.Plugin(pluginName); + this.plugins[pluginName] = plugin; + } + }.bind(this)); + }, + + startupPlugins: function(data, reason) { + var startupPromises = []; + for (var pluginName in this.plugins) { + var plugin = this.plugins[pluginName]; + startupPromises.push(plugin.startup(data, reason)); + } + return Promise.group(startupPromises); + } +}; + +exports.catalog = new exports.PluginCatalog(); + +// TODO the code below is temporary to bootstrap while setting up the new command system +var PluginManager = { + commands : {}, + + registerCommand : function(name, command) { + this.commands[name] = command; + } +}; + +exports.PluginManager = PluginManager; + + +}); diff --git a/plugins/pilot/lib/promise.js b/plugins/pilot/lib/promise.js new file mode 100644 index 00000000..8bd9f971 --- /dev/null +++ b/plugins/pilot/lib/promise.js @@ -0,0 +1,264 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Joe Walker (jwalker@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { + +var console = require("pilot/console"); +var Trace = require('pilot/stacktrace').Trace; + +/** + * A promise can be in one of 2 states. + * The ERROR and SUCCESS states are terminal, the PENDING state is the only + * start state. + */ +var ERROR = -1; +var PENDING = 0; +var SUCCESS = 1; + +/** + * We give promises and ID so we can track which are outstanding + */ +var _nextId = 0; + +/** + * Debugging help if 2 things try to complete the same promise. + * This can be slow (especially on chrome due to the stack trace unwinding) so + * we should leave this turned off in normal use. + */ +var _traceCompletion = false; + +/** + * Outstanding promises. Handy list for debugging only. + */ +var _outstanding = []; + +/** + * Recently resolved promises. Also for debugging only. + */ +var _recent = []; + +/** + * Create an unfulfilled promise + */ +Promise = function () { + this._status = PENDING; + this._value = undefined; + this._onSuccessHandlers = []; + this._onErrorHandlers = []; + + // Debugging help + this._id = _nextId++; + //this._createTrace = new Trace(new Error()); + _outstanding[this._id] = this; +}; + +/** + * Yeay for RTTI. + */ +Promise.prototype.isPromise = true; + +/** + * Have we either been resolve()ed or reject()ed? + */ +Promise.prototype.isComplete = function() { + return this._status != PENDING; +}; + +/** + * Have we resolve()ed? + */ +Promise.prototype.isResolved = function() { + return this._status == SUCCESS; +}; + +/** + * Have we reject()ed? + */ +Promise.prototype.isRejected = function() { + return this._status == ERROR; +}; + +/** + * Take the specified action of fulfillment of a promise, and (optionally) + * a different action on promise rejection. + */ +Promise.prototype.then = function(onSuccess, onError) { + if (typeof onSuccess === 'function') { + if (this._status === SUCCESS) { + onSuccess.call(null, this._value); + } else if (this._status === PENDING) { + this._onSuccessHandlers.push(onSuccess); + } + } + + if (typeof onError === 'function') { + if (this._status === ERROR) { + onError.call(null, this._value); + } else if (this._status === PENDING) { + this._onErrorHandlers.push(onError); + } + } + + return this; +}; + +/** + * Like then() except that rather than returning this we return + * a promise which + */ +Promise.prototype.chainPromise = function(onSuccess) { + var chain = new Promise(); + chain._chainedFrom = this; + this.then(function(data) { + try { + chain.resolve(onSuccess(data)); + } catch (ex) { + chain.reject(ex); + } + }, function(ex) { + chain.reject(ex); + }); + return chain; +}; + +/** + * Supply the fulfillment of a promise + */ +Promise.prototype.resolve = function(data) { + return this._complete(this._onSuccessHandlers, SUCCESS, data, 'resolve'); +}; + +/** + * Renege on a promise + */ +Promise.prototype.reject = function(data) { + return this._complete(this._onErrorHandlers, ERROR, data, 'reject'); +}; + +/** + * Internal method to be called on resolve() or reject(). + * @private + */ +Promise.prototype._complete = function(list, status, data, name) { + // Complain if we've already been completed + if (this._status != PENDING) { + console.group('Promise already closed'); + console.error('Attempted ' + name + '() with ', data); + console.error('Previous status = ', this._status, + ', previous value = ', this._value); + console.trace(); + + if (this._completeTrace) { + console.error('Trace of previous completion:'); + this._completeTrace.log(5); + } + console.groupEnd(); + return this; + } + + if (_traceCompletion) { + this._completeTrace = new Trace(new Error()); + } + + this._status = status; + this._value = data; + + // Call all the handlers, and then delete them + list.forEach(function(handler) { + handler.call(null, this._value); + }, this); + this._onSuccessHandlers.length = 0; + this._onErrorHandlers.length = 0; + + // Remove the given {promise} from the _outstanding list, and add it to the + // _recent list, pruning more than 20 recent promises from that list. + delete _outstanding[this._id]; + _recent.push(this); + while (_recent.length > 20) { + _recent.shift(); + } + + return this; +}; + +/** + * Takes an array of promises and returns a promise that that is fulfilled once + * all the promises in the array are fulfilled + * @param group The array of promises + * @return the promise that is fulfilled when all the array is fulfilled + */ +Promise.group = function(promiseList) { + if (!(promiseList instanceof Array)) { + promiseList = Array.prototype.slice.call(arguments); + } + + // If the original array has nothing in it, return now to avoid waiting + if (promiseList.length === 0) { + return new Promise().resolve([]); + } + + var groupPromise = new Promise(); + var results = []; + var fulfilled = 0; + + var onSuccessFactory = function(index) { + return function(data) { + results[index] = data; + fulfilled++; + // If the group has already failed, silently drop extra results + if (groupPromise._status !== ERROR) { + if (fulfilled === promiseList.length) { + groupPromise.resolve(results); + } + } + }; + }; + + promiseList.forEach(function(promise, index) { + var onSuccess = onSuccessFactory(index); + var onError = groupPromise.reject.bind(groupPromise); + promise.then(onSuccess, onError); + }); + + return groupPromise; +}; + +exports.Promise = Promise; +exports._outstanding = _outstanding; +exports._recent = _recent; + +}); diff --git a/plugins/pilot/lib/proxy.js b/plugins/pilot/lib/proxy.js new file mode 100644 index 00000000..c2675dd0 --- /dev/null +++ b/plugins/pilot/lib/proxy.js @@ -0,0 +1,82 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Julian Viereck (jviereck@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ +define(function(require, exports, module) { + +var Promise = require('pilot/promise').Promise; + +exports.xhr = function(method, url, async, beforeSendCallback) { + var pr = new Promise(); + + if (!skywriter.proxy || !skywriter.proxy.xhr) { + var req = new XMLHttpRequest(); + req.onreadystatechange = function() { + if (req.readyState !== 4) { + return; + } + + var status = req.status; + if (status !== 0 && status !== 200) { + var error = new Error(req.responseText + ' (Status ' + req.status + ")"); + error.xhr = req; + pr.reject(error); + return; + } + + pr.resolve(req.responseText); + }.bind(this); + + req.open("GET", url, async); + if (beforeSendCallback) { + beforeSendCallback(req); + } + req.send(); + } else { + skywriter.proxy.xhr.call(this, method, url, async, beforeSendCallback, pr); + } + + return pr; +}; + +exports.Worker = function(url) { + if (!skywriter.proxy || !skywriter.proxy.worker) { + return new Worker(url); + } else { + return new skywriter.proxy.worker(url); + } +}; + +}); diff --git a/plugins/pilot/lib/stacktrace.js b/plugins/pilot/lib/stacktrace.js new file mode 100644 index 00000000..961d88cc --- /dev/null +++ b/plugins/pilot/lib/stacktrace.js @@ -0,0 +1,332 @@ +define(function(require, exports, module) { + +var util = require("pilot/util"); +var console = require('pilot/console'); + +// Changed to suit the specific needs of running within Skywriter + +// Domain Public by Eric Wendelin http://eriwen.com/ (2008) +// Luke Smith http://lucassmith.name/ (2008) +// Loic Dachary (2008) +// Johan Euphrosine (2008) +// Øyvind Sean Kinsey http://kinsey.no/blog +// +// Information and discussions +// http://jspoker.pokersource.info/skin/test-printstacktrace.html +// http://eriwen.com/javascript/js-stack-trace/ +// http://eriwen.com/javascript/stacktrace-update/ +// http://pastie.org/253058 +// http://browsershots.org/http://jspoker.pokersource.info/skin/test-printstacktrace.html +// + +// +// guessFunctionNameFromLines comes from firebug +// +// Software License Agreement (BSD License) +// +// Copyright (c) 2007, Parakey Inc. +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the +// following disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// * Neither the name of Parakey Inc. nor the names of its +// contributors may be used to endorse or promote products +// derived from this software without specific prior +// written permission of Parakey Inc. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +/** + * Different browsers create stack traces in different ways. + * Feature Browser detection baby ;). + */ +var mode = (function() { + + // We use SC's browser detection here to avoid the "break on error" + // functionality provided by Firebug. Firebug tries to do the right + // thing here and break, but it happens every time you load the page. + // bug 554105 + if (util.isMozilla) { + return 'firefox'; + } else if (util.isOpera) { + return 'opera'; + } else if (util.isSafari) { + return 'other'; + } + + // SC doesn't do any detection of Chrome at this time. + + // this is the original feature detection code that is used as a + // fallback. + try { + (0)(); + } catch (e) { + if (e.arguments) { + return 'chrome'; + } + if (e.stack) { + return 'firefox'; + } + if (window.opera && !('stacktrace' in e)) { //Opera 9- + return 'opera'; + } + } + return 'other'; +})(); + +/** + * + */ +function stringifyArguments(args) { + for (var i = 0; i < args.length; ++i) { + var argument = args[i]; + if (typeof argument == 'object') { + args[i] = '#object'; + } else if (typeof argument == 'function') { + args[i] = '#function'; + } else if (typeof argument == 'string') { + args[i] = '"' + argument + '"'; + } + } + return args.join(','); +} + +/** + * Extract a stack trace from the format emitted by each browser. + */ +var decoders = { + chrome: function(e) { + var stack = e.stack; + if (!stack) { + console.log(e); + return []; + } + return stack.replace(/^.*?\n/, ''). + replace(/^.*?\n/, ''). + replace(/^.*?\n/, ''). + replace(/^[^\(]+?[\n$]/gm, ''). + replace(/^\s+at\s+/gm, ''). + replace(/^Object.\s*\(/gm, '{anonymous}()@'). + split('\n'); + }, + + firefox: function(e) { + var stack = e.stack; + if (!stack) { + console.log(e); + return []; + } + // stack = stack.replace(/^.*?\n/, ''); + stack = stack.replace(/(?:\n@:0)?\s+$/m, ''); + stack = stack.replace(/^\(/gm, '{anonymous}('); + return stack.split('\n'); + }, + + // Opera 7.x and 8.x only! + opera: function(e) { + var lines = e.message.split('\n'), ANON = '{anonymous}', + lineRE = /Line\s+(\d+).*?script\s+(http\S+)(?:.*?in\s+function\s+(\S+))?/i, i, j, len; + + for (i = 4, j = 0, len = lines.length; i < len; i += 2) { + if (lineRE.test(lines[i])) { + lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) + + ' -- ' + + lines[i + 1].replace(/^\s+/, ''); + } + } + + lines.splice(j, lines.length - j); + return lines; + }, + + // Safari, Opera 9+, IE, and others + other: function(curr) { + var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], j = 0, fn, args; + + var maxStackSize = 10; + while (curr && stack.length < maxStackSize) { + fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON; + args = Array.prototype.slice.call(curr['arguments']); + stack[j++] = fn + '(' + stringifyArguments(args) + ')'; + + //Opera bug: if curr.caller does not exist, Opera returns curr (WTF) + if (curr === curr.caller && window.opera) { + //TODO: check for same arguments if possible + break; + } + curr = curr.caller; + } + return stack; + } +}; + +/** + * + */ +function NameGuesser() { +} + +NameGuesser.prototype = { + + sourceCache: {}, + + ajax: function(url) { + var req = this.createXMLHTTPObject(); + if (!req) { + return; + } + req.open('GET', url, false); + req.setRequestHeader('User-Agent', 'XMLHTTP/1.0'); + req.send(''); + return req.responseText; + }, + + createXMLHTTPObject: function() { + // Try XHR methods in order and store XHR factory + var xmlhttp, XMLHttpFactories = [ + function() { + return new XMLHttpRequest(); + }, function() { + return new ActiveXObject('Msxml2.XMLHTTP'); + }, function() { + return new ActiveXObject('Msxml3.XMLHTTP'); + }, function() { + return new ActiveXObject('Microsoft.XMLHTTP'); + } + ]; + for (var i = 0; i < XMLHttpFactories.length; i++) { + try { + xmlhttp = XMLHttpFactories[i](); + // Use memoization to cache the factory + this.createXMLHTTPObject = XMLHttpFactories[i]; + return xmlhttp; + } catch (e) {} + } + }, + + getSource: function(url) { + if (!(url in this.sourceCache)) { + this.sourceCache[url] = this.ajax(url).split('\n'); + } + return this.sourceCache[url]; + }, + + guessFunctions: function(stack) { + for (var i = 0; i < stack.length; ++i) { + var reStack = /{anonymous}\(.*\)@(\w+:\/\/([-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/; + var frame = stack[i], m = reStack.exec(frame); + if (m) { + var file = m[1], lineno = m[4]; //m[7] is character position in Chrome + if (file && lineno) { + var functionName = this.guessFunctionName(file, lineno); + stack[i] = frame.replace('{anonymous}', functionName); + } + } + } + return stack; + }, + + guessFunctionName: function(url, lineNo) { + try { + return this.guessFunctionNameFromLines(lineNo, this.getSource(url)); + } catch (e) { + return 'getSource failed with url: ' + url + ', exception: ' + e.toString(); + } + }, + + guessFunctionNameFromLines: function(lineNo, source) { + var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/; + var reGuessFunction = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/; + // Walk backwards from the first line in the function until we find the line which + // matches the pattern above, which is the function definition + var line = '', maxLines = 10; + for (var i = 0; i < maxLines; ++i) { + line = source[lineNo - i] + line; + if (line !== undefined) { + var m = reGuessFunction.exec(line); + if (m) { + return m[1]; + } + else { + m = reFunctionArgNames.exec(line); + } + if (m && m[1]) { + return m[1]; + } + } + } + return '(?)'; + } +}; + +var guesser = new NameGuesser(); + +var frameIgnorePatterns = [ + /http:\/\/localhost:4020\/sproutcore.js:/ +]; + +exports.ignoreFramesMatching = function(regex) { + frameIgnorePatterns.push(regex); +}; + +/** + * Create a stack trace from an exception + * @param ex {Error} The error to create a stacktrace from (optional) + * @param guess {Boolean} If we should try to resolve the names of anonymous functions + */ +exports.Trace = function Trace(ex, guess) { + this._ex = ex; + this._stack = decoders[mode](ex); + + if (guess) { + this._stack = guesser.guessFunctions(this._stack); + } +}; + +/** + * Log to the console a number of lines (default all of them) + * @param lines {number} Maximum number of lines to wrote to console + */ +exports.Trace.prototype.log = function(lines) { + if (lines <= 0) { + // You aren't going to have more lines in your stack trace than this + // and it still fits in a 32bit integer + lines = 999999999; + } + + var printed = 0; + for (var i = 0; i < this._stack.length && printed < lines; i++) { + var frame = this._stack[i]; + var display = true; + frameIgnorePatterns.forEach(function(regex) { + if (regex.test(frame)) { + display = false; + } + }); + if (display) { + console.debug(frame); + printed++; + } + } +}; + +}); diff --git a/plugins/pilot/lib/util.js b/plugins/pilot/lib/util.js new file mode 100644 index 00000000..5a138a20 --- /dev/null +++ b/plugins/pilot/lib/util.js @@ -0,0 +1,659 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Mozilla Skywriter. + * + * The Initial Developer of the Original Code is + * Mozilla. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Skywriter Team (skywriter@mozilla.com) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { + +/** + * Create an object representing a de-serialized query section of a URL. + * Query keys with multiple values are returned in an array. + *

Example: The input "foo=bar&foo=baz&thinger=%20spaces%20=blah&zonk=blarg&" + * Produces the output object: + *

{
+ *   foo: [ "bar", "baz" ],
+ *   thinger: " spaces =blah",
+ *   zonk: "blarg"
+ * }
+ * 
+ *

Note that spaces and other urlencoded entities are correctly handled + * @see dojo.queryToObject() + * While dojo.queryToObject() is mainly for URL query strings, this version + * allows to specify a separator character + */ +exports.queryToObject = function(str, seperator) { + var ret = {}; + var qp = str.split(seperator || "&"); + var dec = decodeURIComponent; + qp.forEach(function(item) { + if (item.length) { + var parts = item.split("="); + var name = dec(parts.shift()); + var val = dec(parts.join("=")); + if (exports.isString(ret[name])){ + ret[name] = [ret[name]]; + } + if (Array.isArray(ret[name])){ + ret[name].push(val); + } else { + ret[name] = val; + } + } + }); + return ret; +}; + +/** + * Takes a name/value mapping object and returns a string representing a + * URL-encoded version of that object for use in a GET request + *

For example, given the input: + * { blah: "blah", multi: [ "thud", "thonk" ] } + * The following string would be returned: + * "blah=blah&multi=thud&multi=thonk" + * @param map {Object} The object to convert + * @return {string} A URL-encoded version of the input + */ +exports.objectToQuery = function(map) { + // FIXME: need to implement encodeAscii!! + var enc = encodeURIComponent; + var pairs = []; + var backstop = {}; + for (var name in map) { + var value = map[name]; + if (value != backstop[name]) { + var assign = enc(name) + "="; + if (value.isArray) { + for (var i = 0; i < value.length; i++) { + pairs.push(assign + enc(value[i])); + } + } else { + pairs.push(assign + enc(value)); + } + } + } + return pairs.join("&"); +}; + +/** + * Holds the count to keep a unique value for setTimeout + * @private See rateLimit() + */ +var nextRateLimitId = 0; + +/** + * Holds the timeouts so they can be cleared later + * @private See rateLimit() + */ +var rateLimitTimeouts = {}; + +/** + * Delay calling some function to check that it's not called again inside a + * maxRate. The real function is called after maxRate ms unless the return + * value of this function is called before, in which case the clock is restarted + */ +exports.rateLimit = function(maxRate, scope, func) { + if (maxRate) { + var rateLimitId = nextRateLimitId++; + + return function() { + if (rateLimitTimeouts[rateLimitId]) { + clearTimeout(rateLimitTimeouts[rateLimitId]); + } + + rateLimitTimeouts[rateLimitId] = setTimeout(function() { + func.apply(scope, arguments); + delete rateLimitTimeouts[rateLimitId]; + }, maxRate); + }; + } +}; + +/** + * Return true if it is a String + */ +exports.isString = function(it) { + return (typeof it == "string" || it instanceof String); +}; + +/** + * Returns true if it is a Boolean. + */ +exports.isBoolean = function(it) { + return (typeof it == 'boolean'); +}; + +/** + * Returns true if it is a Number. + */ +exports.isNumber = function(it) { + return (typeof it == 'number' && isFinite(it)); +}; + +/** + * Hack copied from dojo. + */ +exports.isObject = function(it) { + return it !== undefined && + (it === null || typeof it == "object" || + Array.isArray(it) || exports.isFunction(it)); +}; + +/** + * Is the passed object a function? + * From dojo.isFunction() + */ +exports.isFunction = (function() { + var _isFunction = function(it) { + var t = typeof it; // must evaluate separately due to bizarre Opera bug. See #8937 + //Firefox thinks object HTML element is a function, so test for nodeType. + return it && (t == "function" || it instanceof Function) && !it.nodeType; // Boolean + }; + + return exports.isSafari ? + // only slow this down w/ gratuitious casting in Safari (not WebKit) + function(/*anything*/ it) { + if (typeof it == "function" && it == "[object NodeList]") { + return false; + } + return _isFunction(it); // Boolean + } : _isFunction; +})(); + +/** + * A la Prototype endsWith(). Takes a regex excluding the '$' end marker + */ +exports.endsWith = function(str, end) { + if (!str) { + return false; + } + return str.match(new RegExp(end + "$")); +}; + +/** + * A la Prototype include(). + */ +exports.include = function(array, item) { + return array.indexOf(item) > -1; +}; + +/** + * Like include, but useful when you're checking for a specific + * property on each object in the list... + * + * Returns null if the item is not in the list, otherwise + * returns the index of the item. + */ +exports.indexOfProperty = function(array, propertyName, item) { + for (var i = 0; i < array.length; i++) { + if (array[i][propertyName] == item) { + return i; + } + } + return null; +}; + +/** + * A la Prototype last(). + */ +exports.last = function(array) { + if (Array.isArray(array)) { + return array[array.length - 1]; + } +}; + +/** + * Knock off any undefined items from the end of an array + */ +exports.shrinkArray = function(array) { + var newArray = []; + + var stillAtBeginning = true; + array.reverse().forEach(function(item) { + if (stillAtBeginning && item === undefined) { + return; + } + + stillAtBeginning = false; + + newArray.push(item); + }); + + return newArray.reverse(); +}; + +/** + * Create an array + * @param number The size of the new array to create + * @param character The item to put in the array, defaults to ' ' + */ +exports.makeArray = function(number, character) { + if (number < 1) { + return []; // give us a normal number please! + } + if (!character){character = ' ';} + + var newArray = []; + for (var i = 0; i < number; i++) { + newArray.push(character); + } + return newArray; +}; + +/** + * Repeat a string a given number of times. + * @param string String to repeat + * @param repeat Number of times to repeat + */ +exports.repeatString = function(string, repeat) { + var newstring = ''; + + for (var i = 0; i < repeat; i++) { + newstring += string; + } + + return newstring; +}; + +/** + * Given a row, find the number of leading spaces. + * E.g. an array with the string " aposjd" would return 2 + * @param row The row to hunt through + */ +exports.leadingSpaces = function(row) { + var numspaces = 0; + for (var i = 0; i < row.length; i++) { + if (row[i] == ' ' || row[i] == '' || row[i] === undefined) { + numspaces++; + } else { + return numspaces; + } + } + return numspaces; +}; + +/** + * Given a row, find the number of leading tabs. + * E.g. an array with the string " aposjd" would return 2 + * @param row The row to hunt through + */ +exports.leadingTabs = function(row) { + var numtabs = 0; + for (var i = 0; i < row.length; i++) { + if (row[i] == ' ' || row[i] == '' || row[i] === undefined) { + numtabs++; + } else { + return numtabs; + } + } + return numtabs; +}; + +/** + * Given a row, extract a copy of the leading spaces or tabs. + * E.g. an array with the string " aposjd" would return an array with the + * string " ". + * @param row The row to hunt through + */ +exports.leadingWhitespace = function(row) { + var leading = []; + for (var i = 0; i < row.length; i++) { + if (row[i] == ' ' || row[i] == ' ' || row[i] == '' || row[i] === undefined) { + leading.push(row[i]); + } else { + return leading; + } + } + return leading; +}; + +/** + * Given a camelCaseWord convert to "Camel Case Word" + */ +exports.englishFromCamel = function(camel) { + camel.replace(/([A-Z])/g, function(str) { + return " " + str.toLowerCase(); + }).trim(); +}; + +/** + * I hate doing this, but we need some way to determine if the user is on a Mac + * The reason is that users have different expectations of their key combinations. + * + * Take copy as an example, Mac people expect to use CMD or APPLE + C + * Windows folks expect to use CTRL + C + */ +exports.OS = { + LINUX: 'LINUX', + MAC: 'MAC', + WINDOWS: 'WINDOWS' +}; + +var ua = navigator.userAgent; +var av = navigator.appVersion; + +/** Is the user using a browser that identifies itself as Linux */ +exports.isLinux = av.indexOf("Linux") >= 0; + +/** Is the user using a browser that identifies itself as Windows */ +exports.isWindows = av.indexOf("Win") >= 0; + +/** Is the user using a browser that identifies itself as WebKit */ +exports.isWebKit = parseFloat(ua.split("WebKit/")[1]) || undefined; + +/** Is the user using a browser that identifies itself as Chrome */ +exports.isChrome = parseFloat(ua.split("Chrome/")[1]) || undefined; + +/** Is the user using a browser that identifies itself as Mac OS */ +exports.isMac = av.indexOf("Macintosh") >= 0; + +/* Is this Firefox or related? */ +exports.isMozilla = av.indexOf('Gecko/') >= 0; + +if (ua.indexOf("AdobeAIR") >= 0) { + exports.isAIR = 1; +} + +/** + * Is the user using a browser that identifies itself as Safari + * See also: + * - http://developer.apple.com/internet/safari/faq.html#anchor2 + * - http://developer.apple.com/internet/safari/uamatrix.html + */ +var index = Math.max(av.indexOf("WebKit"), av.indexOf("Safari"), 0); +if (index && !exports.isChrome) { + // try to grab the explicit Safari version first. If we don't get + // one, look for less than 419.3 as the indication that we're on something + // "Safari 2-ish". + exports.isSafari = parseFloat(av.split("Version/")[1]); + if (!exports.isSafari || parseFloat(av.substr(index + 7)) <= 419.3) { + exports.isSafari = 2; + } +} + +if (ua.indexOf("Gecko") >= 0 && !exports.isWebKit) { + exports.isMozilla = parseFloat(av); +} + +/** + * Return a exports.OS constant + */ +exports.getOS = function() { + if (exports.isMac) { + return exports.OS['MAC']; + } else if (exports.isLinux) { + return exports.OS['LINUX']; + } else { + return exports.OS['WINDOWS']; + } +}; + +/** Returns true if the DOM element "b" is inside the element "a". */ +if (typeof(document) !== 'undefined' && document.compareDocumentPosition) { + exports.contains = function(a, b) { + return a.compareDocumentPosition(b) & 16; + }; +} else { + exports.contains = function(a, b) { + return a !== b && (a.contains ? a.contains(b) : true); + }; +} + +/** + * Prevents propagation and clobbers the default action of the passed event + */ +exports.stopEvent = function(ev) { + ev.preventDefault(); + ev.stopPropagation(); +}; + +/** + * Create a random password of the given length (default 16 chars) + */ +exports.randomPassword = function(length) { + length = length || 16; + var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + var pass = ""; + for (var x = 0; x < length; x++) { + var charIndex = Math.floor(Math.random() * chars.length); + pass += chars.charAt(charIndex); + } + return pass; +}; + +/** + * Is the passed object free of members, i.e. are there any enumerable + * properties which the objects claims as it's own using hasOwnProperty() + */ +exports.isEmpty = function(object) { + for (var x in object) { + if (object.hasOwnProperty(x)) { + return false; + } + } + return true; +}; + +/** + * Does the name of a project indicate that it is owned by someone else + * TODO: This is a major hack. We really should have a File object that include + * separate owner information. + */ +exports.isMyProject = function(project) { + return project.indexOf("+") == -1; +}; + +/** + * Format a date as dd MMM yyyy + */ +exports.formatDate = function (date) { + if (!date) { + return "Unknown"; + } + return date.getDate() + " " + + exports.formatDate.shortMonths[date.getMonth()] + " " + + date.getFullYear(); +}; + +/** + * Month data for exports.formatDate + */ +exports.formatDate.shortMonths = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; + +/** + * Add a CSS class to the list of classes on the given node + */ +exports.addClass = function(node, className) { + var parts = className.split(/\s+/); + var cls = " " + node.className + " "; + for (var i = 0, len = parts.length, c; i < len; ++i) { + c = parts[i]; + if (c && cls.indexOf(" " + c + " ") < 0) { + cls += c + " "; + } + } + node.className = cls.trim(); +}; + +/** + * Remove a CSS class from the list of classes on the given node + */ +exports.removeClass = function(node, className) { + var cls; + if (className !== undefined) { + var parts = className.split(/\s+/); + cls = " " + node.className + " "; + for (var i = 0, len = parts.length; i < len; ++i) { + cls = cls.replace(" " + parts[i] + " ", " "); + } + cls = cls.trim(); + } else { + cls = ""; + } + if (node.className != cls) { + node.className = cls; + } +}; + +/** + * Add or remove a CSS class from the list of classes on the given node + * depending on the value of include + */ +exports.setClass = function(node, className, include) { + if (include) { + exports.addClass(node, className); + } else { + exports.removeClass(node, className); + } +}; + +/** + * Is the passed object either null or undefined (using ===) + */ +exports.none = function(obj) { + return obj === null || obj === undefined; +}; + +/** + * Creates a clone of the passed object. This function can take just about + * any type of object and create a clone of it, including primitive values + * (which are not actually cloned because they are immutable). + * If the passed object implements the clone() method, then this function + * will simply call that method and return the result. + * + * @param object {Object} the object to clone + * @param deep {Boolean} do a deep clone? + * @returns {Object} the cloned object + */ +exports.clone = function(object, deep) { + if (Array.isArray(object) && !deep) { + return object.slice(); + } + + if (typeof object === 'object' || Array.isArray(object)) { + if (object === null) { + return null; + } + + var reply = (Array.isArray(object) ? [] : {}); + for (var key in object) { + if (deep && (typeof object[key] === 'object' + || Array.isArray(object[key]))) { + reply[key] = exports.clone(object[key], true); + } else { + reply[key] = object[key]; + } + } + return reply; + } + + if (object && typeof(object.clone) === 'function') { + return object.clone(); + } + + // That leaves numbers, booleans, undefined. Doesn't it? + return object; +}; + + +/** + * Helper method for extending one object with another + * Copies all properties from source to target. Returns the extended target + * object. + * Taken from John Resig, http://ejohn.org/blog/javascript-getters-and-setters/. + */ +exports.mixin = function(a, b) { + for (var i in b) { + var g = b.__lookupGetter__(i); + var s = b.__lookupSetter__(i); + + if (g || s) { + if (g) { + a.__defineGetter__(i, g); + } + if (s) { + a.__defineSetter__(i, s); + } + } else { + a[i] = b[i]; + } + } + + return a; +}; + +/** + * Basically taken from Sproutcore. + * Replaces the count items from idx with objects. + */ +exports.replace = function(arr, idx, amt, objects) { + return arr.slice(0, idx).concat(objects).concat(arr.slice(idx + amt)); +}; + +/** + * Return true if the two frames match. You can also pass only points or sizes. + * @param r1 {Rect} the first rect + * @param r2 {Rect} the second rect + * @param delta {Float} an optional delta that allows for rects that do not match exactly. Defaults to 0.1 + * @returns {Boolean} true if rects match + */ +exports.rectsEqual = function(r1, r2, delta) { + if (!r1 || !r2) { + return r1 == r2; + } + + if (!delta && delta !== 0) { + delta = 0.1; + } + + if ((r1.y != r2.y) && (Math.abs(r1.y - r2.y) > delta)) { + return false; + } + + if ((r1.x != r2.x) && (Math.abs(r1.x - r2.x) > delta)) { + return false; + } + + if ((r1.width != r2.width) && (Math.abs(r1.width - r2.width) > delta)) { + return false; + } + + if ((r1.height != r2.height) && (Math.abs(r1.height - r2.height) > delta)) { + return false; + } + + return true; +}; + +});