From 8b0d9586b23eb166fafb064e75c4956021d73ca1 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Tue, 31 Aug 2010 21:09:13 -0400 Subject: Merge testing. --HG-- rename : common/content/base.js => common/modules/base.jsm rename : common/content/services.js => common/modules/services.jsm rename : common/content/style.js => common/modules/styles.jsm rename : common/content/template.js => common/modules/template.jsm rename : common/content/util.js => common/modules/util.jsm --- common/content/autocommands.js | 14 +- common/content/base.js | 624 ----------------------------- common/content/bookmarks.js | 272 +++---------- common/content/buffer.js | 88 ++--- common/content/commandline.js | 99 ++--- common/content/commands.js | 234 ++++++----- common/content/completion.js | 15 +- common/content/configbase.js | 16 +- common/content/dactyl-overlay.js | 82 ++-- common/content/dactyl.js | 120 +++--- common/content/editor.js | 14 +- common/content/events.js | 13 +- common/content/finder.js | 84 ++-- common/content/hints.js | 2 - common/content/history.js | 4 +- common/content/io.js | 366 ++--------------- common/content/javascript.js | 17 +- common/content/mappings.js | 20 +- common/content/marks.js | 20 +- common/content/modes.js | 4 - common/content/modules.js | 90 +++-- common/content/options.js | 59 +-- common/content/quickmarks.js | 2 - common/content/sanitizer.js | 13 +- common/content/services.js | 129 ------ common/content/style.js | 832 --------------------------------------- common/content/tabs.js | 11 +- common/content/template.js | 309 --------------- common/content/util.js | 715 --------------------------------- common/modules/base.jsm | 798 +++++++++++++++++++++++++++++++++++++ common/modules/bookmarkcache.jsm | 162 ++++++++ common/modules/highlight.jsm | 241 ++++++++++++ common/modules/services.jsm | 133 +++++++ common/modules/storage.jsm | 535 +++++++++++++++++-------- common/modules/styles.jsm | 373 ++++++++++++++++++ common/modules/template.jsm | 311 +++++++++++++++ common/modules/util.jsm | 815 ++++++++++++++++++++++++++++++++++++++ 37 files changed, 3800 insertions(+), 3836 deletions(-) delete mode 100644 common/content/base.js delete mode 100644 common/content/services.js delete mode 100644 common/content/style.js delete mode 100644 common/content/template.js delete mode 100644 common/content/util.js create mode 100644 common/modules/base.jsm create mode 100644 common/modules/bookmarkcache.jsm create mode 100644 common/modules/highlight.jsm create mode 100644 common/modules/services.jsm create mode 100644 common/modules/styles.jsm create mode 100644 common/modules/template.jsm create mode 100644 common/modules/util.jsm (limited to 'common') diff --git a/common/content/autocommands.js b/common/content/autocommands.js index 899019f7..97232f9b 100755 --- a/common/content/autocommands.js +++ b/common/content/autocommands.js @@ -14,8 +14,6 @@ const AutoCommand = Struct("event", "pattern", "command"); * @instance autocommands */ const AutoCommands = Module("autocommands", { - requires: ["config"], - init: function () { this._store = []; }, @@ -83,7 +81,7 @@ const AutoCommands = Module("autocommands", { } }); - let list = template.commandOutput(commandline.command, + commandline.commandOutput( @@ -101,8 +99,6 @@ const AutoCommands = Module("autocommands", { )) }
----- Auto Commands -----
); - - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); }, /** @@ -137,7 +133,6 @@ const AutoCommands = Module("autocommands", { autoCmd.command.call(autoCmd, args); } catch (e) { - dactyl.reportError(e); dactyl.echoerr(e); } } @@ -203,7 +198,12 @@ const AutoCommands = Module("autocommands", { return args["-javascript"] ? completion.javascript(context) : completion.ex(context); }, literal: 2, - options: [[["-javascript", "-js"], commands.OPTION_NOARG]] + options: [ + { + names: ["-javascript", "-js"], + description: "Interperate the action as JavaScript code rather than an ex command", + } + ] }); [ diff --git a/common/content/base.js b/common/content/base.js deleted file mode 100644 index e4a58bbb..00000000 --- a/common/content/base.js +++ /dev/null @@ -1,624 +0,0 @@ -// Copyright (c) 2009 by Kris Maglione -// -// This work is licensed for reuse under an MIT license. Details are -// given in the LICENSE.txt file included with this file. -"use strict"; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -function allkeys(obj) { - let ret = {}; - try { - for (; obj; obj = obj.__proto__) { - services.get("debugger").wrapValue(obj).getProperties(ret, {}); - for (let prop in values(ret.value)) - yield prop.name.stringValue; - } - return; - } - catch (e) {} - - let __iterator__ = obj.__iterator__; - try { - if ("__iterator__" in obj) { - yield "__iterator__"; - delete obj.__iterator__; - } - for (let k in obj) - yield k; - } - finally { - if (__iterator__) - obj.__iterator__ = __iterator__; - } -} - -function debuggerProperties(obj) { - if (modules.services && services.get("debugger").isOn) { - let ret = {}; - services.get("debugger").wrapValue(obj).getProperties(ret, {}); - return ret.value; - } -} - -if (!Object.keys) - Object.keys = function keys(obj) [k for (k in obj) if (obj.hasOwnProperty(k))]; - -if (!Object.getOwnPropertyNames) - Object.getOwnPropertyNames = function getOwnPropertyNames(obj) { - let res = debuggerProperties(obj); - if (res) - return [prop.name.stringValue for (prop in values(res))]; - return Object.keys(obj); - } - -function properties(obj, prototypes) { - let orig = obj; - let seen = {}; - for (; obj; obj = prototypes && obj.__proto__) - for (let key in values(Object.getOwnPropertyNames(obj))) - if (!prototypes || !set.add(seen, key) && obj != orig) - yield key -} - -function values(obj) { - for (var k in obj) - if (obj.hasOwnProperty(k)) - yield obj[k]; -} -function foreach(iter, fn, self) { - for (let val in iter) - fn.call(self, val); -} - -function dict(ary) { - var obj = {}; - for (var i = 0; i < ary.length; i++) { - var val = ary[i]; - obj[val[0]] = val[1]; - } - return obj; -} - -function set(ary) { - let obj = {}; - if (ary) - for (var i = 0; i < ary.length; i++) - obj[ary[i]] = true; - return obj; -} -set.add = function (set, key) { - let res = this.has(set, key); - set[key] = true; - return res; -} -set.has = function (set, key) Object.prototype.hasOwnProperty.call(set, key); -set.remove = function (set, key) { delete set[key]; } - -function iter(obj) { - if (obj instanceof Ci.nsISimpleEnumerator) - return (function () { - while (obj.hasMoreElements()) - yield obj.getNext(); - })(); - if (isinstance(obj, [Ci.nsIStringEnumerator, Ci.nsIUTF8StringEnumerator])) - return (function () { - while (obj.hasMore()) - yield obj.getNext(); - })(); - if (isinstance(obj, Ci.nsIDOMNodeIterator)) - return (function () { - try { - while (true) - yield obj.nextNode(); - } - catch (e) {} - })(); - if (isinstance(obj, [Ci.nsIDOMHTMLCollection, Ci.nsIDOMNodeList])) - return array.iteritems(obj); - if (obj instanceof Ci.nsIDOMNamedNodeMap) - return (function () { - for (let i = 0; i < obj.length; i++) - yield [obj.name, obj]; - })(); - return Iterator(obj); -} - -function issubclass(targ, src) { - return src === targ || - targ && typeof targ === "function" && targ.prototype instanceof src; -} - -function isinstance(targ, src) { - const types = { - boolean: Boolean, - string: String, - function: Function, - number: Number - } - src = Array.concat(src); - for (var i = 0; i < src.length; i++) { - if (targ instanceof src[i]) - return true; - if (typeof src[i] == "string") - return Object.prototype.toString(targ) == "[object " + src[i] + "]"; - var type = types[typeof targ]; - if (type && issubclass(src[i], type)) - return true; - } - return false; -} - -function isobject(obj) { - return typeof obj === "object" && obj != null; -} - -/** - * Returns true if and only if its sole argument is an - * instance of the builtin Array type. The array may come from - * any window, frame, namespace, or execution context, which - * is not the case when using (obj instanceof Array). - */ -function isarray(val) { - return Object.prototype.toString.call(val) == "[object Array]"; -} - -/** - * Returns true if and only if its sole argument is an - * instance of the builtin Generator type. This includes - * functions containing the 'yield' statement and generator - * statements such as (x for (x in obj)). - */ -function isgenerator(val) { - return Object.prototype.toString.call(val) == "[object Generator]"; -} - -/** - * Returns true if and only if its sole argument is a String, - * as defined by the builtin type. May be constructed via - * String(foo) or new String(foo) from any window, frame, - * namespace, or execution context, which is not the case when - * using (obj instanceof String) or (typeof obj == "string"). - */ -function isstring(val) { - return Object.prototype.toString.call(val) == "[object String]"; -} - -/** - * Returns true if and only if its sole argument may be called - * as a function. This includes classes and function objects. - */ -function callable(val) { - return typeof val === "function"; -} - -function call(fn) { - fn.apply(arguments[1], Array.slice(arguments, 2)); - return fn; -} - -function memoize(obj, key, getter) { - obj.__defineGetter__(key, function () { - delete obj[key]; - return obj[key] = getter(obj, key); - }); -} - -/** - * Curries a function to the given number of arguments. Each - * call of the resulting function returns a new function. When - * a call does not contain enough arguments to satisfy the - * required number, the resulting function is another curried - * function with previous arguments accumulated. - * - * function foo(a, b, c) [a, b, c].join(" "); - * curry(foo)(1, 2, 3) -> "1 2 3"; - * curry(foo)(4)(5, 6) -> "4 5 6"; - * curry(foo)(4)(8)(9) -> "7 8 9"; - * - * @param {function} fn The function to curry. - * @param {integer} length The number of arguments expected. - * @default fn.length - * @optional - * @param {object} self The 'this' value for the returned function. When - * omitted, the value of 'this' from the first call to the function is - * preserved. - * @optional - */ -function curry(fn, length, self, acc) { - if (length == null) - length = fn.length; - if (length == 0) - return fn; - - // Close over function with 'this' - function close(self, fn) function () fn.apply(self, Array.slice(arguments)); - - if (acc == null) - acc = []; - - return function () { - let args = acc.concat(Array.slice(arguments)); - - // The curried result should preserve 'this' - if (arguments.length == 0) - return close(self || this, arguments.callee); - - if (args.length >= length) - return fn.apply(self || this, args); - - return curry(fn, length, self || this, args); - }; -} - -/** - * Updates an object with the properties of another object. Getters - * and setters are copied as expected. Moreover, any function - * properties receive new 'supercall' and 'superapply' properties, - * which will call the identically named function in target's - * prototype. - * - * let a = { foo: function (arg) "bar " + arg } - * let b = { __proto__: a } - * update(b, { foo: function () arguments.callee.supercall(this, "baz") }); - * - * a.foo("foo") -> "bar foo" - * b.foo() -> "bar baz" - * - * @param {Object} target The object to update. - * @param {Object} src The source object from which to update target. - * May be provided multiple times. - * @returns {Object} Returns its updated first argument. - */ -function update(target) { - for (let i = 1; i < arguments.length; i++) { - let src = arguments[i]; - Object.getOwnPropertyNames(src || {}).forEach(function (k) { - var get = src.__lookupGetter__(k), - set = src.__lookupSetter__(k); - if (!get && !set) { - var v = src[k]; - target[k] = v; - if (target.__proto__ && callable(v)) { - v.superapply = function (self, args) { - return target.__proto__[k].apply(self, args); - }; - v.supercall = function (self) { - return v.superapply(self, Array.slice(arguments, 1)); - }; - } - } - if (get) - target.__defineGetter__(k, get); - if (set) - target.__defineSetter__(k, set); - }); - } - return target; -} - -/** - * Extends a subclass with a superclass. The subclass's - * prototype is replaced with a new object, which inherits - * from the super class's prototype, {@see update}d with the - * members of 'overrides'. - * - * @param {function} subclass - * @param {function} superclass - * @param {Object} overrides @optional - */ -function extend(subclass, superclass, overrides) { - subclass.prototype = { __proto__: superclass.prototype }; - update(subclass.prototype, overrides); - - subclass.superclass = superclass.prototype; - subclass.prototype.constructor = subclass; - subclass.prototype.__class__ = subclass; - - if (superclass.prototype.constructor === Object.prototype.constructor) - superclass.prototype.constructor = superclass; -} - -/** - * @constructor Class - * - * Constructs a new Class. Arguments marked as optional must be - * either entirely elided, or they must have the exact type - * specified. - * - * @param {string} name The class's as it will appear when toString - * is called, as well as in stack traces. - * @optional - * @param {function} base The base class for this module. May be any - * callable object. - * @optional - * @default Class - * @param {Object} prototype The prototype for instances of this - * object. The object itself is copied and not used as a prototype - * directly. - * @param {Object} classProperties The class properties for the new - * module constructor. More than one may be provided. - * @optional - * - * @returns {function} The constructor for the resulting class. - */ -function Class() { - function constructor() { - let self = { - __proto__: Constructor.prototype, - constructor: Constructor, - get closure() { - delete this.closure; - function closure(fn) function () fn.apply(self, arguments); - for (let k in this) - if (!this.__lookupGetter__(k) && callable(this[k])) - closure[k] = closure(self[k]); - return this.closure = closure; - } - }; - var res = self.init.apply(self, arguments); - return res !== undefined ? res : self; - } - - var args = Array.slice(arguments); - if (isstring(args[0])) - var name = args.shift(); - var superclass = Class; - if (callable(args[0])) - superclass = args.shift(); - - var Constructor = eval("(function " + (name || superclass.name).replace(/\W/g, "_") + - String.substr(constructor, 20) + ")"); - Constructor.__proto__ = superclass; - Constructor.name = name || superclass.name; - - if (!("init" in superclass.prototype)) { - var superc = superclass; - superclass = function Shim() {}; - extend(superclass, superc, { - init: superc - }); - } - - extend(Constructor, superclass, args[0]); - update(Constructor, args[1]); - args = args.slice(2); - Array.forEach(args, function (obj) { - if (callable(obj)) - obj = obj.prototype; - update(Constructor.prototype, obj); - }); - return Constructor; -} -Class.toString = function () "[class " + this.constructor.name + "]", -Class.prototype = { - /** - * Initializes new instances of this class. Called automatically - * when new instances are created. - */ - init: function () {}, - - toString: function () "[instance " + this.constructor.name + "]", - - /** - * Exactly like {@see nsIDOMWindow#setTimeout}, except that it - * preserves the value of 'this' on invocation of 'callback'. - * - * @param {function} callback The function to call after 'timeout' - * @param {number} timeout The timeout, in seconds, to wait - * before calling 'callback'. - * @returns {integer} The ID of this timeout, to be passed to - * {@see nsIDOMWindow#clearTimeout}. - */ - setTimeout: function (callback, timeout) { - const self = this; - let notify = { notify: function notify(timer) { callback.call(self) } }; - let timer = services.create("timer"); - timer.initWithCallback(notify, timeout, timer.TYPE_ONE_SHOT); - return timer; - } -}; - -/** - * @class Struct - * - * Creates a new Struct constructor, used for creating objects with - * a fixed set of named members. Each argument should be the name of - * a member in the resulting objects. These names will correspond to - * the arguments passed to the resultant constructor. Instances of - * the new struct may be treated vary much like arrays, and provide - * many of the same methods. - * - * const Point = Struct("x", "y", "z"); - * let p1 = Point(x, y, z); - * - * @returns {function} The constructor for the new Struct. - */ -function Struct() { - let args = Array.slice(arguments); - const Struct = Class("Struct", StructBase, { - length: args.length, - members: args - }); - args.forEach(function (name, i) { - Struct.prototype.__defineGetter__(name, function () this[i]); - Struct.prototype.__defineSetter__(name, function (val) { this[i] = val; }); - }); - return Struct; -} -const StructBase = Class("StructBase", { - init: function () { - for (let i = 0; i < arguments.length; i++) - if (arguments[i] != undefined) - this[i] = arguments[i]; - }, - - clone: function clone() this.constructor.apply(null, this.slice()), - - // Iterator over our named members - __iterator__: function () { - let self = this; - return ([k, self[k]] for (k in values(self.members))) - } -}, { - /** - * Sets a lazily constructed default value for a member of - * the struct. The value is constructed once, the first time - * it is accessed and memoized thereafter. - * - * @param {string} key The name of the member for which to - * provide the default value. - * @param {function} val The function which is to generate - * the default value. - */ - defaultValue: function (key, val) { - let i = this.prototype.members.indexOf(key); - this.prototype.__defineGetter__(i, function () (this[i] = val.call(this), this[i])); // Kludge for FF 3.0 - this.prototype.__defineSetter__(i, function (value) { - this.__defineGetter__(i, function () value); - this.__defineSetter__(i, function (val) { value = val; }); - }); - } -}); -// Add no-sideeffect array methods. Can't set new Array() as the prototype or -// get length() won't work. -for (let k in values(["concat", "every", "filter", "forEach", "indexOf", "join", "lastIndexOf", - "map", "reduce", "reduceRight", "reverse", "slice", "some", "sort"])) - StructBase.prototype[k] = Array.prototype[k]; - -/** - * Array utility methods. - */ -const array = Class("util.Array", Array, { - init: function (ary) { - if (isgenerator(ary)) - ary = [k for (k in ary)]; - else if (ary.length) - ary = Array.slice(ary); - - return { - __proto__: ary, - __iterator__: function () this.iteritems(), - __noSuchMethod__: function (meth, args) { - var res = array[meth].apply(null, [this.__proto__].concat(args)); - - if (array.isinstance(res)) - return array(res); - return res; - }, - toString: function () this.__proto__.toString(), - concat: function () this.__proto__.concat.apply(this.__proto__, arguments), - map: function () this.__noSuchMethod__("map", Array.slice(arguments)) - }; - } -}, { - isinstance: function isinstance(obj) { - return Object.prototype.toString.call(obj) == "[object Array]"; - }, - - /** - * Converts an array to an object. As in lisp, an assoc is an - * array of key-value pairs, which maps directly to an object, - * as such: - * [["a", "b"], ["c", "d"]] -> { a: "b", c: "d" } - * - * @param {Array[]} assoc - * @... {string} 0 - Key - * @... 1 - Value - */ - toObject: function toObject(assoc) { - let obj = {}; - assoc.forEach(function ([k, v]) { obj[k] = v; }); - return obj; - }, - - /** - * Compacts an array, removing all elements that are null or undefined: - * ["foo", null, "bar", undefined] -> ["foo", "bar"] - * - * @param {Array} ary - * @returns {Array} - */ - compact: function compact(ary) ary.filter(function (item) item != null), - - /** - * Flattens an array, such that all elements of the array are - * joined into a single array: - * [["foo", ["bar"]], ["baz"], "quux"] -> ["foo", ["bar"], "baz", "quux"] - * - * @param {Array} ary - * @returns {Array} - */ - flatten: function flatten(ary) ary.length ? Array.concat.apply([], ary) : [], - - /** - * Returns an Iterator for an array's values. - * - * @param {Array} ary - * @returns {Iterator(Object)} - */ - itervalues: function itervalues(ary) { - let length = ary.length; - for (let i = 0; i < length; i++) - yield ary[i]; - }, - - /** - * Returns an Iterator for an array's indices and values. - * - * @param {Array} ary - * @returns {Iterator([{number}, {Object}])} - */ - iteritems: function iteritems(ary) { - let length = ary.length; - for (let i = 0; i < length; i++) - yield [i, ary[i]]; - }, - - /** - * Filters out all duplicates from an array. If - * unsorted is false, the array is sorted before - * duplicates are removed. - * - * @param {Array} ary - * @param {boolean} unsorted - * @returns {Array} - */ - uniq: function uniq(ary, unsorted) { - let ret = []; - if (unsorted) { - for (let [, item] in Iterator(ary)) - if (ret.indexOf(item) == -1) - ret.push(item); - } - else { - for (let [, item] in Iterator(ary.sort())) { - if (item != last || !ret.length) - ret.push(item); - var last = item; - } - } - return ret; - }, - - /** - * Zips the contents of two arrays. The resulting array is twice the - * length of ary1, with any shortcomings of ary2 replaced with null - * strings. - * - * @param {Array} ary1 - * @param {Array} ary2 - * @returns {Array} - */ - zip: function zip(ary1, ary2) { - let res = [] - for(let [i, item] in Iterator(ary1)) - res.push(item, i in ary2 ? ary2[i] : ""); - return res; - } -}); - -// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/bookmarks.js b/common/content/bookmarks.js index fff84480..095b4c21 100644 --- a/common/content/bookmarks.js +++ b/common/content/bookmarks.js @@ -10,193 +10,13 @@ const DEFAULT_FAVICON = "chrome://mozapps/skin/places/defaultFavicon.png"; // also includes methods for dealing with keywords and search engines const Bookmarks = Module("bookmarks", { - requires: ["autocommands", "config", "dactyl", "storage", "services"], - init: function () { - const faviconService = services.get("favicon"); - const bookmarksService = services.get("bookmarks"); - const historyService = services.get("history"); - const tagging = PlacesUtils.tagging; - - this.getFavicon = getFavicon; - function getFavicon(uri) { - try { - return faviconService.getFaviconImageForPage(util.newURI(uri)).spec; - } - catch (e) { - return ""; - } - } - - // Fix for strange Firefox bug: - // Error: [Exception... "Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIObserverService.addObserver]" - // nsresult: "0x8000ffff (NS_ERROR_UNEXPECTED)" - // location: "JS frame :: file://~firefox/components/nsTaggingService.js :: anonymous :: line 89" - // data: no] - // Source file: file://~firefox/components/nsTaggingService.js - tagging.getTagsForURI(window.makeURI("http://mysterious.bug"), {}); - - const Bookmark = Struct("url", "title", "icon", "keyword", "tags", "id"); - const Keyword = Struct("keyword", "title", "icon", "url"); - Bookmark.defaultValue("icon", function () getFavicon(this.url)); - Bookmark.prototype.__defineGetter__("extra", function () [ - ["keyword", this.keyword, "Keyword"], - ["tags", this.tags.join(", "), "Tag"] - ].filter(function (item) item[1])); - - const storage = modules.storage; - function Cache(name, store) { - const rootFolders = [bookmarksService.toolbarFolder, bookmarksService.bookmarksMenuFolder, bookmarksService.unfiledBookmarksFolder]; - const sleep = dactyl.sleep; // Storage objects are global to all windows, 'dactyl' isn't. - - let bookmarks = []; - let self = this; - - this.__defineGetter__("name", function () name); - this.__defineGetter__("store", function () store); - this.__defineGetter__("bookmarks", function () this.load()); - - this.__defineGetter__("keywords", - function () [Keyword(k.keyword, k.title, k.icon, k.url) for ([, k] in Iterator(self.bookmarks)) if (k.keyword)]); - - this.__iterator__ = function () (val for ([, val] in Iterator(self.bookmarks))); - - function loadBookmark(node) { - if (node.uri == null) // How does this happen? - return false; - let uri = util.newURI(node.uri); - let keyword = bookmarksService.getKeywordForBookmark(node.itemId); - let tags = tagging.getTagsForURI(uri, {}) || []; - let bmark = Bookmark(node.uri, node.title, node.icon && node.icon.spec, keyword, tags, node.itemId); - - bookmarks.push(bmark); - return bmark; - } - - function readBookmark(id) { - return { - itemId: id, - uri: bookmarksService.getBookmarkURI(id).spec, - title: bookmarksService.getItemTitle(id) - }; - } - - function deleteBookmark(id) { - let length = bookmarks.length; - bookmarks = bookmarks.filter(function (item) item.id != id); - return bookmarks.length < length; - } - - this.findRoot = function findRoot(id) { - do { - var root = id; - id = bookmarksService.getFolderIdForItem(id); - } while (id != bookmarksService.placesRoot && id != root); - return root; - }; - - this.isBookmark = function (id) rootFolders.indexOf(self.findRoot(id)) >= 0; - - this.isRegularBookmark = function findRoot(id) { - do { - var root = id; - if (services.get("livemark") && services.get("livemark").isLivemark(id)) - return false; - id = bookmarksService.getFolderIdForItem(id); - } while (id != bookmarksService.placesRoot && id != root); - return rootFolders.indexOf(root) >= 0; - }; - - // since we don't use a threaded bookmark loading (by set preload) - // anymore, is this loading synchronization still needed? --mst - let loading = false; - this.load = function load() { - if (loading) { - while (loading) - sleep(10); - return bookmarks; - } - - // update our bookmark cache - bookmarks = []; - loading = true; - - let folders = rootFolders.slice(); - let query = historyService.getNewQuery(); - let options = historyService.getNewQueryOptions(); - while (folders.length > 0) { - query.setFolders(folders, 1); - folders.shift(); - let result = historyService.executeQuery(query, options); - let folder = result.root; - folder.containerOpen = true; - - // iterate over the immediate children of this folder - for (let i = 0; i < folder.childCount; i++) { - let node = folder.getChild(i); - if (node.type == node.RESULT_TYPE_FOLDER) // folder - folders.push(node.itemId); - else if (node.type == node.RESULT_TYPE_URI) // bookmark - loadBookmark(node); - } - - // close a container after using it! - folder.containerOpen = false; - } - this.__defineGetter__("bookmarks", function () bookmarks); - loading = false; - return bookmarks; - }; - - var observer = { - onBeginUpdateBatch: function onBeginUpdateBatch() {}, - onEndUpdateBatch: function onEndUpdateBatch() {}, - onItemVisited: function onItemVisited() {}, - onItemMoved: function onItemMoved() {}, - onItemAdded: function onItemAdded(itemId, folder, index) { - // dactyl.dump("onItemAdded(" + itemId + ", " + folder + ", " + index + ")\n"); - if (bookmarksService.getItemType(itemId) == bookmarksService.TYPE_BOOKMARK) { - if (self.isBookmark(itemId)) { - let bmark = loadBookmark(readBookmark(itemId)); - storage.fireEvent(name, "add", bmark); - } - } - }, - onItemRemoved: function onItemRemoved(itemId, folder, index) { - // dactyl.dump("onItemRemoved(" + itemId + ", " + folder + ", " + index + ")\n"); - if (deleteBookmark(itemId)) - storage.fireEvent(name, "remove", itemId); - }, - onItemChanged: function onItemChanged(itemId, property, isAnnotation, value) { - if (isAnnotation) - return; - // dactyl.dump("onItemChanged(" + itemId + ", " + property + ", " + value + ")\n"); - let bookmark = bookmarks.filter(function (item) item.id == itemId)[0]; - if (bookmark) { - if (property == "tags") - value = tagging.getTagsForURI(util.newURI(bookmark.url), {}); - if (property in bookmark) - bookmark[property] = value; - storage.fireEvent(name, "change", itemId); - } - }, - QueryInterface: function QueryInterface(iid) { - if (iid.equals(Ci.nsINavBookmarkObserver) || iid.equals(Ci.nsISupports)) - return this; - throw Cr.NS_ERROR_NO_INTERFACE; - } - }; - - bookmarksService.addObserver(observer, false); - } - let bookmarkObserver = function (key, event, arg) { if (event == "add") autocommands.trigger("BookmarkAdd", arg); statusline.updateUrl(); }; - this._cache = storage.newObject("bookmark-cache", Cache, { store: false }); storage.addObserver("bookmark-cache", bookmarkObserver, window); }, @@ -217,7 +37,7 @@ const Bookmarks = Module("bookmarks", { try { let uri = util.createURI(url); if (!force) { - for (let bmark in this._cache) { + for (let bmark in bookmarkcache) { if (bmark[0] == uri.spec) { var id = bmark[5]; if (title) @@ -268,7 +88,7 @@ const Bookmarks = Module("bookmarks", { isBookmarked: function isBookmarked(url) { try { return services.get("bookmarks").getBookmarkIdsForURI(makeURI(url), {}) - .some(this._cache.isRegularBookmark); + .some(bookmarkcache.isRegularBookmark); } catch (e) { return false; @@ -280,7 +100,7 @@ const Bookmarks = Module("bookmarks", { try { let uri = util.newURI(url); let bmarks = services.get("bookmarks").getBookmarkIdsForURI(uri, {}) - .filter(this._cache.isRegularBookmark); + .filter(bookmarkcache.isRegularBookmark); bmarks.forEach(services.get("bookmarks").removeItem); return bmarks.length; } @@ -350,7 +170,7 @@ const Bookmarks = Module("bookmarks", { // format of returned array: // [keyword, helptext, url] getKeywords: function getKeywords() { - return this._cache.keywords; + return bookmarkcache.keywords; }, // full search string including engine name as first word in @param text @@ -451,38 +271,53 @@ const Bookmarks = Module("bookmarks", { "Show jumplist", function () { let sh = history.session; - let list = template.jumps(sh.index, sh); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + commandline.commandOutput(template.jumps(sh.index, sh)); }, { argCount: "0" }); // TODO: Clean this up. - function tags(context, args) { - let filter = context.filter; - let have = filter.split(","); + const tags = { + names: ["-tags", "-T"], + description: "A comma-separated list of tags", + completer: function tags(context, args) { + let filter = context.filter; + let have = filter.split(","); - args.completeFilter = have.pop(); + args.completeFilter = have.pop(); - let prefix = filter.substr(0, filter.length - args.completeFilter.length); - let tags = array.uniq(util.Array.flatten([b.tags for ([k, b] in Iterator(this._cache.bookmarks))])); + let prefix = filter.substr(0, filter.length - args.completeFilter.length); + let tags = array.uniq(util.Array.flatten([b.tags for ([k, b] in Iterator(bookmarkcache.bookmarks))])); - return [[prefix + tag, tag] for ([i, tag] in Iterator(tags)) if (have.indexOf(tag) < 0)]; - } + return [[prefix + tag, tag] for ([i, tag] in Iterator(tags)) if (have.indexOf(tag) < 0)]; + }, + type: CommandOption.LIST + }; - function title(context, args) { - if (!args.bang) - return [[content.document.title, "Current Page Title"]]; - context.keys.text = "title"; - context.keys.description = "url"; - return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter }); - } + const title = { + names: ["-title", "-t"], + description: "Bookmark page title or description", + completer: function title(context, args) { + if (!args.bang) + return [[content.document.title, "Current Page Title"]]; + context.keys.text = "title"; + context.keys.description = "url"; + return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter }); + }, + type: CommandOption.STRING + }; - function keyword(context, args) { - if (!args.bang) - return []; - context.keys.text = "keyword"; - return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] }); - } + const keyword = { + names: ["-keyword", "-k"], + description: "Keyword by which this bookmark may be opened (:open {keyword})", + completer: function keyword(context, args) { + if (!args.bang) + return []; + context.keys.text = "keyword"; + return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] }); + }, + type: CommandOption.STRING, + validator: function (arg) /^\S+$/.test(arg) + }; commands.add(["bma[rk]"], "Add a bookmark", @@ -503,14 +338,13 @@ const Bookmarks = Module("bookmarks", { bang: true, completer: function (context, args) { if (!args.bang) { + context.title = ["Page URL"]; context.completions = [[content.document.documentURI, "Current Location"]]; return; } completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] }); }, - options: [[["-title", "-t"], commands.OPTION_STRING, null, title], - [["-tags", "-T"], commands.OPTION_LIST, null, tags], - [["-keyword", "-k"], commands.OPTION_STRING, function (arg) /\w/.test(arg)]] + options: [title, tags, keyword] }); commands.add(["bmarks"], @@ -525,22 +359,26 @@ const Bookmarks = Module("bookmarks", { context.filter = args.join(" "); completion.bookmark(context, args["-tags"]); }, - options: [[["-tags", "-T"], commands.OPTION_LIST, null, tags], - [["-max", "-m"], commands.OPTION_INT]] + options: [tags, + { + names: ["-max", "-m"], + description: "The maximum number of items to list or open", + type: CommandOption.INT + } + ] }); commands.add(["delbm[arks]"], "Delete a bookmark", function (args) { - if (args.bang) { + if (args.bang) commandline.input("This will delete all bookmarks. Would you like to continue? (yes/[no]) ", function (resp) { if (resp && resp.match(/^y(es)?$/i)) { - bookmarks._cache.bookmarks.forEach(function (bmark) { services.get("bookmarks").removeItem(bmark.id); }); + bookmarkcache.bookmarks.forEach(function (bmark) { services.get("bookmarks").removeItem(bmark.id); }); dactyl.echomsg("All bookmarks deleted", 1, commandline.FORCE_SINGLELINE); } }); - } else { let url = args.string || buffer.URL; let deletedCount = bookmarks.remove(url); @@ -621,9 +459,7 @@ const Bookmarks = Module("bookmarks", { if (v) context.filters.push(function (item) this._match(v, item[k])); } - // Need to make a copy because set completions() checks instanceof Array, - // and this may be an Array from another window. - context.completions = Array.slice(storage["bookmark-cache"].bookmarks); + context.completions = bookmarkcache.bookmarks; completion.urls(context, tags); }; diff --git a/common/content/buffer.js b/common/content/buffer.js index cb50f16f..ed050a00 100644 --- a/common/content/buffer.js +++ b/common/content/buffer.js @@ -8,8 +8,6 @@ /** @scope modules */ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm", modules); - const Point = Struct("x", "y"); /** @@ -19,8 +17,6 @@ const Point = Struct("x", "y"); * @instance buffer */ const Buffer = Module("buffer", { - requires: ["config", "util"], - init: function () { this.evaluateXPath = util.evaluateXPath; this.pageInfo = {}; @@ -146,10 +142,6 @@ const Buffer = Module("buffer", { }, destroy: function () { - try { - config.browser.removeProgressListener(this.progressListener); - } - catch (e) {} // Why? --djk }, _triggerLoadAutocmd: function _triggerLoadAutocmd(name, doc) { @@ -214,14 +206,10 @@ const Buffer = Module("buffer", { /** * @property {Object} The document loading progress listener. */ - progressListener: { - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIWebProgressListener, - Ci.nsIXULBrowserWindow - ]), - + progressListener: update({ __proto__: window.XULBrowserWindow }, { // XXX: function may later be needed to detect a canceled synchronous openURL() - onStateChange: function (webProgress, request, flags, status) { + onStateChange: function onStateChange(webProgress, request, flags, status) { + onStateChange.superapply(this, arguments); // STATE_IS_DOCUMENT | STATE_IS_WINDOW is important, because we also // receive statechange events for loading images and other parts of the web page if (flags & (Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | Ci.nsIWebProgressListener.STATE_IS_WINDOW)) { @@ -247,7 +235,8 @@ const Buffer = Module("buffer", { } }, // for notifying the user about secure web pages - onSecurityChange: function (webProgress, request, state) { + onSecurityChange: function onSecurityChange(webProgress, request, state) { + onSecurityChange.superapply(this, arguments); // TODO: do something useful with STATE_SECURE_MED and STATE_SECURE_LOW if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE) statusline.setClass("insecure"); @@ -258,14 +247,17 @@ const Buffer = Module("buffer", { else if (state & Ci.nsIWebProgressListener.STATE_SECURE_HIGH) statusline.setClass("secure"); }, - onStatusChange: function (webProgress, request, status, message) { + onStatusChange: function onStatusChange(webProgress, request, status, message) { + onStatusChange.superapply(this, arguments); statusline.updateUrl(message); }, - onProgressChange: function (webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) { + onProgressChange: function onProgressChange(webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) { + onProgressChange.superapply(this, arguments); statusline.updateProgress(curTotalProgress/maxTotalProgress); }, // happens when the users switches tabs - onLocationChange: function () { + onLocationChange: function onLocationChange() { + onLocationChange.superapply(this, arguments); statusline.updateUrl(); statusline.updateProgress(); @@ -279,10 +271,12 @@ const Buffer = Module("buffer", { }, 500); }, // called at the very end of a page load - asyncUpdateUI: function () { + asyncUpdateUI: function asyncUpdateUI() { + asyncUpdateUI.superapply(this, arguments); setTimeout(function () { statusline.updateUrl(); }, 100); }, - setOverLink: function (link, b) { + setOverLink: function setOverLink(link, b) { + setOverLink.superapply(this, arguments); let ssli = options["showstatuslinks"]; if (link && ssli) { if (ssli == 1) @@ -298,15 +292,7 @@ const Buffer = Module("buffer", { modes.show(); } }, - - // nsIXULBrowserWindow stubs - setJSDefaultStatus: function (status) {}, - setJSStatus: function (status) {}, - - // Stub for something else, presumably. Not in any documented - // interface. - onLinkIconAvailable: function () {} - }, + }), /** * @property {Array} The alternative style sheets for the current @@ -320,19 +306,6 @@ const Buffer = Module("buffer", { ); }, - /** - * @property {Array[Window]} All frames in the current buffer. - */ - get allFrames() { - let frames = []; - (function (frame) { - if (frame.document.body instanceof HTMLBodyElement) - frames.push(frame); - Array.forEach(frame.frames, arguments.callee); - })(window.content); - return frames; - }, - /** * @property {Object} A map of page info sections to their * content generating functions. @@ -447,6 +420,19 @@ const Buffer = Module("buffer", { this.pageInfo[option] = [func, title]; }, + /** + * Returns a list of all frames in the given current buffer. + */ + allFrames: function (win) { + let frames = []; + (function rec(frame) { + if (frame.document.body instanceof HTMLBodyElement) + frames.push(frame); + Array.forEach(frame.frames, rec); + })(win || window.content); + return frames; + }, + /** * Returns the currently selected word. If the selection is * null, it tries to guess the word that the caret is @@ -581,7 +567,7 @@ const Buffer = Module("buffer", { let ret = followFrame(window.content); if (!ret) // only loop through frames if the main content didn't match - ret = Array.some(buffer.allFrames.frames, followFrame); + ret = Array.some(buffer.allFrames().frames, followFrame); if (!ret) dactyl.beep(); @@ -805,7 +791,7 @@ const Buffer = Module("buffer", { return; count = Math.max(count, 1); - let frames = buffer.allFrames; + let frames = buffer.allFrames(); if (frames.length == 0) // currently top is always included return; @@ -1367,7 +1353,11 @@ const Buffer = Module("buffer", { }; }, events: function () { - /* + try { + config.browser.removeProgressListener(window.XULBrowserWindow); + } + catch (e) {} // Why? --djk + config.browser.addProgressListener(this.progressListener, Ci.nsIWebProgress.NOTIFY_ALL); window.XULBrowserWindow = this.progressListener; window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) @@ -1376,12 +1366,6 @@ const Buffer = Module("buffer", { .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIXULWindow) .XULBrowserWindow = this.progressListener; - */ - - try { - config.browser.addProgressListener(this.progressListener, Ci.nsIWebProgress.NOTIFY_ALL); - } - catch (e) {} // Why? --djk let appContent = document.getElementById("appcontent"); if (appContent) { diff --git a/common/content/commandline.js b/common/content/commandline.js index 974d7446..fc4c2075 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -15,8 +15,6 @@ * this class when the chrome is ready. */ const CommandLine = Module("commandline", { - requires: ["config", "dactyl", "modes", "services", "storage", "template", "util"], - init: function () { const self = this; @@ -95,7 +93,7 @@ const CommandLine = Module("commandline", { ////////////////////// TIMERS ////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ - this._statusTimer = new Timer(5, 100, function statusTell() { + this._statusTimer = Timer(5, 100, function statusTell() { if (self._completions == null) return; if (self._completions.selected == null) @@ -104,7 +102,7 @@ const CommandLine = Module("commandline", { statusline.updateProgress("match " + (self._completions.selected + 1) + " of " + self._completions.items.length); }); - this._autocompleteTimer = new Timer(200, 500, function autocompleteTell(tabPressed) { + this._autocompleteTimer = Timer(200, 500, function autocompleteTell(tabPressed) { if (!events.feedingKeys && self._completions && options.get("autocomplete").values.length) { self._completions.complete(true, false); self._completions.itemList.show(); @@ -114,8 +112,8 @@ const CommandLine = Module("commandline", { // This timer just prevents s from queueing up when the // system is under load (and, thus, giving us several minutes of // the completion list scrolling). Multiple presses are - // still processed normally, as the time is flushed on "keyup". - this._tabTimer = new Timer(0, 0, function tabTell(event) { + // still processed normally, as the timer is flushed on "keyup". + this._tabTimer = Timer(0, 0, function tabTell(event) { if (self._completions) self._completions.tab(event.shiftKey); }); @@ -463,6 +461,18 @@ const CommandLine = Module("commandline", { this._keepCommand = false; }, + /** + * Displays the multi-line output of a command, preceded by the last + * executed ex command string. + * + * @param {XML} xml The output as an E4X XML object. + */ + commandOutput: function (xml) { + XML.ignoreWhitespace = false; + XML.prettyPrinting = false; + this.echo(<>:{this.command}
{xml}, this.HIGHLIGHT_NORMAL, this.FORCE_MULTILINE); + }, + /** * Hides the command line, and shows any status messages that * are under it. @@ -492,7 +502,7 @@ const CommandLine = Module("commandline", { * commandline.FORCE_MULTILINE - Forces the message to appear in * the MOW. */ - echo: function echo(str, highlightGroup, flags) { + echo: requiresMainThread(function echo(str, highlightGroup, flags) { // dactyl.echo uses different order of flags as it omits the highlight group, change commandline.echo argument order? --mst if (this._silent) return; @@ -506,37 +516,34 @@ const CommandLine = Module("commandline", { services.get("windowWatcher").activeWindow.dactyl) return; - // The DOM isn't threadsafe. It must only be accessed from the main thread. - util.callInMainThread(function () { - if ((flags & this.DISALLOW_MULTILINE) && !this._outputContainer.collapsed) - return; + if ((flags & this.DISALLOW_MULTILINE) && !this._outputContainer.collapsed) + return; - let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE); - let action = this._echoLine; + let single = flags & (this.FORCE_SINGLELINE | this.DISALLOW_MULTILINE); + let action = this._echoLine; - // TODO: this is all a bit convoluted - clean up. - // assume that FORCE_MULTILINE output is fully styled - if (!(flags & this.FORCE_MULTILINE) && !single && (!this._outputContainer.collapsed || this.widgets.message.value == this._lastEcho)) { - highlightGroup += " Message"; - action = this._echoMultiline; - } + // TODO: this is all a bit convoluted - clean up. + // assume that FORCE_MULTILINE output is fully styled + if (!(flags & this.FORCE_MULTILINE) && !single && (!this._outputContainer.collapsed || this.widgets.message.value == this._lastEcho)) { + highlightGroup += " Message"; + action = this._echoMultiline; + } - if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || typeof str == "xml") && !(flags & this.FORCE_SINGLELINE)) - action = this._echoMultiline; + if ((flags & this.FORCE_MULTILINE) || (/\n/.test(str) || typeof str == "xml") && !(flags & this.FORCE_SINGLELINE)) + action = this._echoMultiline; - if (single) - this._lastEcho = null; - else { - if (this.widgets.message.value == this._lastEcho) - this._echoMultiline({this._lastEcho}, - this.widgets.message.getAttributeNS(NS.uri, "highlight")); - this._lastEcho = (action == this._echoLine) && str; - } + if (single) + this._lastEcho = null; + else { + if (this.widgets.message.value == this._lastEcho) + this._echoMultiline({this._lastEcho}, + this.widgets.message.getAttributeNS(NS.uri, "highlight")); + this._lastEcho = (action == this._echoLine) && str; + } - if (action) - action.call(this, str, highlightGroup, single); - }, this); - }, + if (action) + action.call(this, str, highlightGroup, single); + }), /** * Prompt the user. Sets modes.main to COMMAND_LINE, which the user may @@ -675,7 +682,7 @@ const CommandLine = Module("commandline", { modes.pop(); } } - else { // any other key + else { //this.resetCompletions(); } // allow this event to be handled by the host app @@ -1097,7 +1104,7 @@ const CommandLine = Module("commandline", { } let hist = this.store.get(this.index); - // user pressed DOWN when there is no newer this._history item + // user pressed DOWN when there is no newer history item if (!hist) hist = this.original; else @@ -1165,13 +1172,6 @@ const CommandLine = Module("commandline", { get wildtype() this.wildtypes[this.wildIndex] || "", - get type() ({ - list: this.wildmode.checkHas(this.wildtype, "list"), - longest: this.wildmode.checkHas(this.wildtype, "longest"), - first: this.wildmode.checkHas(this.wildtype, ""), - full: this.wildmode.checkHas(this.wildtype, "full") - }), - complete: function complete(show, tabPressed) { this.context.reset(); this.context.tabPressed = tabPressed; @@ -1181,6 +1181,9 @@ const CommandLine = Module("commandline", { this.wildIndex = 0; }, + haveType: function (type) + this.wildmode.checkHas(this.wildtype, type == "first" ? "" : type), + preview: function preview() { this.previewClear(); if (this.wildIndex < 0 || this.suffix || !this.items.length) @@ -1364,7 +1367,7 @@ const CommandLine = Module("commandline", { break; } - if (this.type.list) + if (this.haveType("list")) this.itemList.show(); this.wildIndex = Math.constrain(this.wildIndex + 1, 0, this.wildtypes.length - 1); @@ -1549,17 +1552,14 @@ const ItemList = Class("ItemList", { this._completionElements = []; var iframe = document.getElementById(id); - if (!iframe) { - dactyl.log("No iframe with id: " + id + " found, strange things may happen!"); // "The truth is out there..." -- djk - return; // XXX - } this._doc = iframe.contentDocument; + this._win = iframe.contentWindow; this._container = iframe.parentNode; this._doc.body.id = id + "-content"; this._doc.body.appendChild(this._doc.createTextNode("")); - this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where this._completions/MOW are shown at once, or ls=0. Should use :highlight. + this._doc.body.style.borderTop = "1px solid black"; // FIXME: For cases where completions/MOW are shown at once, or ls=0. Should use :highlight. this._gradient = template.gradient("GradientLeft", "GradientRight"); @@ -1578,7 +1578,8 @@ const ItemList = Class("ItemList", { if (this._container.collapsed) this._div.style.minWidth = document.getElementById("dactyl-commandline").scrollWidth + "px"; - this._minHeight = Math.max(this._minHeight, this._divNodes.completions.getBoundingClientRect().bottom); + this._minHeight = Math.max(this._minHeight, + this._win.scrollY + this._divNodes.completions.getBoundingClientRect().bottom); this._container.height = this._minHeight; if (this._container.collapsed) diff --git a/common/content/commands.js b/common/content/commands.js index 1cda7784..cb7cc3ae 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -11,6 +11,75 @@ // Do NOT create instances of this class yourself, use the helper method // commands.add() instead +/** + * A structure representing the options available for a command. + * + * @property {[string]} names An array of option names. The first name + * is the canonical option name. + * @property {number} type The option's value type. This is one of: + * (@link CommandOption.NOARG), + * (@link CommandOption.STRING), + * (@link CommandOption.BOOL), + * (@link CommandOption.INT), + * (@link CommandOption.FLOAT), + * (@link CommandOption.LIST), + * (@link CommandOption.ANY) + * @property {function} validator A validator function + * @property {function (CompletionContext, object)} completer A list of + * completions, or a completion function which will be passed a + * {@link CompletionContext} and an object like that returned by + * {@link commands.parseArgs} with the following additional keys: + * completeOpt - The name of the option currently being completed. + * @property {boolean} multiple Whether this option can be specified multiple times + * @property {string} description A description of the option + */ +const CommandOption = Struct("names", "type", "validator", "completer", "multiple", "description"); +CommandOption.defaultValue("description", function () ""); +CommandOption.defaultValue("type", function () CommandOption.NOARG); +CommandOption.defaultValue("multiple", function () false); +update(CommandOption, { + // FIXME: remove later, when our option handler is better + /** + * @property {number} The option argument is unspecified. Any argument + * is accepted and caller is responsible for parsing the return + * value. + * @final + */ + ANY: 0, + + /** + * @property {number} The option doesn't accept an argument. + * @final + */ + NOARG: 1, + /** + * @property {number} The option accepts a boolean argument. + * @final + */ + BOOL: 2, + /** + * @property {number} The option accepts a string argument. + * @final + */ + STRING: 3, + /** + * @property {number} The option accepts an integer argument. + * @final + */ + INT: 4, + /** + * @property {number} The option accepts a float argument. + * @final + */ + FLOAT: 5, + /** + * @property {number} The option accepts a string list argument. + * E.g. "foo,bar" + * @final + */ + LIST: 6 +}); + /** * A class representing Ex commands. Instances are created by * the {@link Commands} class. @@ -35,8 +104,6 @@ * @private */ const Command = Class("Command", { - requires: ["config"], - init: function (specs, description, action, extraInfo) { specs = Array.concat(specs); // XXX let parsedSpecs = Command.parseSpecs(specs); @@ -51,6 +118,8 @@ const Command = Class("Command", { if (extraInfo) update(this, extraInfo); + if (this.options) + this.options = this.options.map(CommandOption.fromArray, CommandOption); }, /** @@ -191,7 +260,7 @@ const Command = Class("Command", { * invocation which should be restored on subsequent @dactyl * startups. */ - serial: null, + serialize: null, /** * @property {boolean} When true, invocations of this command * may contain private data which should be purged from @@ -240,47 +309,6 @@ const Commands = Module("commands", { this._exMap = {}; }, - // FIXME: remove later, when our option handler is better - /** - * @property {number} The option argument is unspecified. Any argument - * is accepted and caller is responsible for parsing the return - * value. - * @final - */ - OPTION_ANY: 0, - - /** - * @property {number} The option doesn't accept an argument. - * @final - */ - OPTION_NOARG: 1, - /** - * @property {number} The option accepts a boolean argument. - * @final - */ - OPTION_BOOL: 2, - /** - * @property {number} The option accepts a string argument. - * @final - */ - OPTION_STRING: 3, - /** - * @property {number} The option accepts an integer argument. - * @final - */ - OPTION_INT: 4, - /** - * @property {number} The option accepts a float argument. - * @final - */ - OPTION_FLOAT: 5, - /** - * @property {number} The option accepts a string list argument. - * E.g. "foo,bar" - * @final - */ - OPTION_LIST: 6, - /** * @property Indicates that no count was specified for this * command invocation. @@ -442,29 +470,8 @@ const Commands = Module("commands", { * * @param {string} str The Ex command-line string to parse. E.g. * "-x=foo -opt=bar arg1 arg2" - * @param {Array} options The options accepted. These are specified as - * an array [names, type, validator, completions, multiple]. - * names - an array of option names. The first name is the - * canonical option name. - * type - the option's value type. This is one of: - * (@link Commands#OPTION_NOARG), - * (@link Commands#OPTION_STRING), - * (@link Commands#OPTION_BOOL), - * (@link Commands#OPTION_INT), - * (@link Commands#OPTION_FLOAT), - * (@link Commands#OPTION_LIST), - * (@link Commands#OPTION_ANY) - * validator - a validator function - * completer - a list of completions, or a completion function - * multiple - whether this option can be specified multiple times - * E.g. - * options = [[["-force"], OPTION_NOARG], - * [["-fullscreen", "-f"], OPTION_BOOL], - * [["-language"], OPTION_STRING, validateFunc, ["perl", "ruby"]], - * [["-speed"], OPTION_INT], - * [["-acceleration"], OPTION_FLOAT], - * [["-accessories"], OPTION_LIST, null, ["foo", "bar"], true], - * [["-other"], OPTION_ANY]]; + * @param {[CommandOption]} options The options accepted. These are specified + * as an array of {@link CommandOption} structures. * @param {string} argCount The number of arguments accepted. * "0": no arguments * "1": exactly one argument @@ -518,7 +525,7 @@ const Commands = Module("commands", { function matchOpts(arg) { // Push possible option matches into completions if (complete && !onlyArgumentsRemaining) - completeOpts = [[opt[0], opt[0][0]] for ([i, opt] in Iterator(options)) if (!(opt[0][0] in args))]; + completeOpts = options.filter(function (opt) opt.multiple || !(opt.names[0] in args)); } function resetCompletions() { completeOpts = null; @@ -562,14 +569,14 @@ const Commands = Module("commands", { var optname = ""; if (!onlyArgumentsRemaining) { for (let [, opt] in Iterator(options)) { - for (let [, optname] in Iterator(opt[0])) { + for (let [, optname] in Iterator(opt.names)) { if (sub.indexOf(optname) == 0) { invalid = false; arg = null; quote = null; count = 0; let sep = sub[optname.length]; - if (sep == "=" || /\s/.test(sep) && opt[1] != this.OPTION_NOARG) { + if (sep == "=" || /\s/.test(sep) && opt.type != CommandOption.NOARG) { [count, arg, quote, error] = getNextArg(sub.substr(optname.length + 1)); dactyl.assert(!error, error); @@ -595,7 +602,7 @@ const Commands = Module("commands", { args.completeFilter = arg; args.quote = Commands.complQuote[quote] || Commands.complQuote[""]; } - let type = Commands.argTypes[opt[1]]; + let type = Commands.argTypes[opt.type]; if (type && (!complete || arg != null)) { let orig = arg; arg = type.parse(arg); @@ -610,8 +617,8 @@ const Commands = Module("commands", { } // we have a validator function - if (typeof opt[2] == "function") { - if (opt[2].call(this, arg) == false) { + if (typeof opt.validator == "function") { + if (opt.validator.call(this, arg) == false) { echoerr("Invalid argument for option: " + optname); if (complete) complete.highlight(args.completeStart, count - 1, "SPELLCHECK"); @@ -620,11 +627,13 @@ const Commands = Module("commands", { } } + matchOpts(sub); + // option allowed multiple times - if (!!opt[4]) - args[opt[0][0]] = (args[opt[0][0]] || []).concat(arg); + if (opt.multiple) + args[opt.names[0]] = (args[opt.names[0]] || []).concat(arg); else - args[opt[0][0]] = opt[1] == this.OPTION_NOARG || arg; + args[opt.names[0]] = opt.type == this.OPTION_NOARG || arg; i += optname.length + count; if (i == str.length) @@ -683,17 +692,18 @@ const Commands = Module("commands", { if (complete) { if (args.completeOpt) { let opt = args.completeOpt; - let context = complete.fork(opt[0][0], args.completeStart); + let context = complete.fork(opt.names[0], args.completeStart); context.filter = args.completeFilter; - if (typeof opt[3] == "function") - var compl = opt[3](context, args); + if (typeof opt.completer == "function") + var compl = opt.completer(context, args); else - compl = opt[3] || []; - context.title = [opt[0][0]]; + compl = opt.completer || []; + context.title = [opt.names[0]]; context.quote = args.quote; context.completions = compl; } complete.advance(args.completeStart); + complete.keys = { text: "names", description: "description" }; complete.title = ["Options"]; if (completeOpts) complete.completions = completeOpts; @@ -1012,18 +1022,16 @@ const Commands = Module("commands", { // : No, array comprehensions are fine, generator statements aren't. --Kris let cmds = this._exCommands.filter(function (c) c.user && (!cmd || c.name.match("^" + cmd))); - if (cmds.length > 0) { - let str = template.tabular(["", "Name", "Args", "Range", "Complete", "Definition"], ["padding-right: 2em;"], - ([cmd.bang ? "!" : " ", - cmd.name, - cmd.argCount, - cmd.count ? "0c" : "", - completerToString(cmd.completer), - cmd.replacementText || "function () { ... }"] - for ([, cmd] in Iterator(cmds)))); - - commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } + if (cmds.length > 0) + commandline.commandOutput( + template.tabular(["", "Name", "Args", "Range", "Complete", "Definition"], ["padding-right: 2em;"]), + ([cmd.bang ? "!" : " ", + cmd.name, + cmd.argCount, + cmd.count ? "0c" : "", + completerToString(cmd.completer), + cmd.replacementText || "function () { ... }"] + for ([, cmd] in Iterator(cmds)))); else dactyl.echomsg("No user-defined commands found"); } @@ -1036,23 +1044,33 @@ const Commands = Module("commands", { completion.ex(context); }, options: [ - [["-nargs"], commands.OPTION_STRING, - function (arg) /^[01*?+]$/.test(arg), - [["0", "No arguments are allowed (default)"], - ["1", "One argument is allowed"], - ["*", "Zero or more arguments are allowed"], - ["?", "Zero or one argument is allowed"], - ["+", "One or more arguments is allowed"]]], - [["-bang"], commands.OPTION_NOARG], - [["-count"], commands.OPTION_NOARG], - [["-description"], commands.OPTION_STRING], - // TODO: "E180: invalid complete value: " + arg - [["-complete"], commands.OPTION_STRING, - function (arg) arg in completeOptionMap || /custom,\w+/.test(arg), - function (context) [[k, ""] for ([k, v] in Iterator(completeOptionMap))]] + { names: ["-bang"], description: "Command may be proceeded by a !" }, + { names: ["-count"], description: "Command may be preceeded by a count" }, + { + names: ["-description"], + description: "A user-visible description of the command", + type: CommandOption.STRING + }, { + // TODO: "E180: invalid complete value: " + arg + names: ["-complete"], + description: "The argument completion function", + completer: function (context) [[k, ""] for ([k, v] in Iterator(completeOptionMap))], + type: CommandOption.STRING, + validator: function (arg) arg in completeOptionMap || /custom,\w+/.test(arg), + }, { + names: ["-nargs"], + description: "The allowed number of arguments", + completer: [["0", "No arguments are allowed (default)"], + ["1", "One argument is allowed"], + ["*", "Zero or more arguments are allowed"], + ["?", "Zero or one argument is allowed"], + ["+", "One or more arguments are allowed"]], + type: CommandOption.STRING, + validator: function (arg) /^[01*?+]$/.test(arg) + }, ], literal: 1, - serial: function () [ { + serialize: function () [ { command: this.name, bang: true, options: util.Array.toObject( diff --git a/common/content/completion.js b/common/content/completion.js index 9a9268a0..1434b38e 100755 --- a/common/content/completion.js +++ b/common/content/completion.js @@ -297,7 +297,7 @@ const CompletionContext = Class("CompletionContext", { let [k, v] = i; let _k = "_" + k; if (typeof v == "string" && /^[.[]/.test(v)) - v = eval("(function (i) i" + v + ")"); + v = Function("i", "return i" + v); if (typeof v == "function") res.__defineGetter__(k, function () _k in this ? this[_k] : (this[_k] = v(this.item))); else @@ -325,7 +325,7 @@ const CompletionContext = Class("CompletionContext", { let lock = {}; this.cache.backgroundLock = lock; this.incomplete = true; - let thread = this.getCache("backgroundThread", dactyl.newThread); + let thread = this.getCache("backgroundThread", util.newThread); util.callAsync(thread, this, function () { if (this.cache.backgroundLock != lock) return; @@ -510,7 +510,7 @@ const CompletionContext = Class("CompletionContext", { if (!context.autoComplete && !context.tabPressed && context.editor) context.waitingForTab = true; else if (completer) - return completer.apply(self || this, [context].concat(Array.slice(arguments, arguments.callee.length))); + return completer.apply(self || this, [context].concat(Array.slice(arguments, fork.length))); if (completer) return null; @@ -668,14 +668,13 @@ const Completion = Module("completion", { context = context.contexts["/list"]; context.wait(); - let list = template.commandOutput(commandline.command, + commandline.commandOutput(
{ template.map(context.contextList.filter(function (c) c.hasItems), function (context) template.completionRow(context.title, "CompTitle") + template.map(context.items, function (item) context.createRow(item), null, 100)) }
); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); }, //////////////////////////////////////////////////////////////////////////////// @@ -765,13 +764,11 @@ const Completion = Module("completion", { commands.add(["contexts"], "List the completion contexts used during completion of an ex command", function (args) { - commandline.echo(template.commandOutput(commandline.command, + commandline.commandOutput(
{ template.completionRow(["Context", "Title"], "CompTitle") } { template.map(completion.contextList || [], function (item) template.completionRow(item, "CompItem")) } -
), - null, commandline.FORCE_MULTILINE); - + ); }, { argCount: "1", diff --git a/common/content/configbase.js b/common/content/configbase.js index 9005f122..5fb75d29 100644 --- a/common/content/configbase.js +++ b/common/content/configbase.js @@ -229,13 +229,13 @@ const ConfigBase = Class(ModuleBase, { HelpOptionalArg color: #6A97D4; HelpBody display: block; margin: 1em auto; max-width: 100ex; - HelpBorder,*,dactyl://help/* border-color: silver; border-width: 0px; border-style: solid; + HelpBorder,*,dactyl://help/* border-color: silver; border-width: 0px; border-style: solid; HelpCode display: block; white-space: pre; margin-left: 2em; font-family: Terminus, Fixed, monospace; HelpDefault margin-right: 1ex; white-space: pre; HelpDescription display: block; - HelpEm,html|em,dactyl://help/* font-weight: bold; font-style: normal; + HelpEm,html|em,dactyl://help/* font-weight: bold; font-style: normal; HelpEx display: inline-block; color: #527BBD; font-weight: bold; @@ -250,21 +250,21 @@ const ConfigBase = Class(ModuleBase, { HelpKey color: #102663; - HelpLink,html|a,dactyl://help/* text-decoration: none; + HelpLink,html|a,dactyl://help/* text-decoration: none; HelpLink[href]:hover text-decoration: underline; - HelpList,html|ul,dactyl://help/* display: block; list-style: outside disc; - HelpOrderedList,html|ol,dactyl://help/* display: block; list-style: outside decimal; - HelpListItem,html|li,dactyl://help/* display: list-item; + HelpList,html|ul,dactyl://help/* display: block; list-style: outside disc; + HelpOrderedList,html|ol,dactyl://help/* display: block; list-style: outside decimal; + HelpListItem,html|li,dactyl://help/* display: list-item; HelpNote color: red; font-weight: bold; HelpOpt color: #106326; HelpOptInfo display: inline-block; margin-bottom: 1ex; - HelpParagraph,html|p,dactyl://help/* display: block; margin: 1em 0em; + HelpParagraph,html|p,dactyl://help/* display: block; margin: 1em 0em; HelpParagraph:first-child margin-top: 0; - HelpSpec display: block; margin-left: -10em; float: left; clear: left; color: #527BBD; + HelpSpec display: block; margin-left: -10em; float: left; clear: left; color: #527BBD; margin-right: 2em; HelpString display: inline-block; color: green; font-weight: normal; vertical-align: text-top; HelpString::before content: '"'; diff --git a/common/content/dactyl-overlay.js b/common/content/dactyl-overlay.js index 32f23e9a..af0cc3be 100644 --- a/common/content/dactyl-overlay.js +++ b/common/content/dactyl-overlay.js @@ -1,4 +1,4 @@ -// Copyright (c) 2008-2008 Kris Maglione +// Copyright (c) 2008-2010 Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. @@ -11,55 +11,61 @@ modules.modules = modules; const loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Components.interfaces.mozIJSSubScriptLoader); - function load(script) { + .getService(Components.interfaces.mozIJSSubScriptLoader); + + modules.load = function load(script) { for (let [i, base] in Iterator(prefix)) { try { - loader.loadSubScript(base + script, modules); + loader.loadSubScript(base + script + ".js", modules); return; } catch (e) { - if (i + 1 < prefix.length) - continue; - if (Components.utils.reportError) - Components.utils.reportError(e); - dump("dactyl: Loading script " + script + ": " + e + "\n"); - dump(e.stack + "\n"); + if (e !== "Error opening input stream (invalid filename?)") + dump("dactyl: Trying: " + (base + script + ".js") + ": " + e + "\n" + e.stack); } } - } + try { + Components.utils.import("resource://dactyl/" + script + ".jsm", modules); + } + catch (e) { + dump("dactyl: Loading script " + script + ": " + e.result + " " + e + "\n"); + dump(Error().stack + "\n"); + } + }; let prefix = [BASE]; - ["base.js", - "modules.js", - "autocommands.js", - "buffer.js", - "commandline.js", - "commands.js", - "completion.js", - "configbase.js", - "config.js", - "dactyl.js", - "editor.js", - "events.js", - "finder.js", - "hints.js", - "io.js", - "javascript.js", - "mappings.js", - "marks.js", - "modes.js", - "options.js", - "services.js", - "statusline.js", - "style.js", - "template.js", - "util.js", - ].forEach(load); + ["base", + "modules", + "storage", + "util", + "autocommands", + "buffer", + "commandline", + "commands", + "completion", + "configbase", + "config", + "dactyl", + "editor", + "events", + "finder", + "highlight", + "hints", + "io", + "javascript", + "mappings", + "marks", + "modes", + "options", + "services", + "statusline", + "styles", + "template", + ].forEach(modules.load); prefix.unshift("chrome://" + modules.Config.prototype.name.toLowerCase() + "/content/"); - modules.Config.prototype.scripts.forEach(load); + modules.Config.prototype.scripts.forEach(modules.load); })(); // vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/dactyl.js b/common/content/dactyl.js index 320d6304..b56548b1 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -8,7 +8,9 @@ /** @scope modules */ -Cu.import("resource://gre/modules/XPCOMUtils.jsm", modules); +default xml namespace = XHTML; +XML.ignoreWhitespace = false; +XML.prettyPrinting = false; const plugins = { __proto__: modules }; const userContext = { __proto__: modules }; @@ -17,27 +19,6 @@ const EVAL_ERROR = "__dactyl_eval_error"; const EVAL_RESULT = "__dactyl_eval_result"; const EVAL_STRING = "__dactyl_eval_string"; -// Move elsewhere? -const Storage = Module("storage", { - requires: ["services"], - - init: function () { - Components.utils.import("resource://dactyl/storage.jsm", this); - modules.Timer = this.Timer; // Fix me, please. - - try { - let infoPath = services.create("file"); - infoPath.initWithPath(File.expandPath(IO.runtimePath.replace(/,.*/, ""))); - infoPath.append("info"); - infoPath.append(dactyl.profileName); - this.storage.infoPath = infoPath; - } - catch (e) {} - - return this.storage; - } -}); - const FailedAssertion = Class("FailedAssertion", Error, { init: function (message) { this.message = message; @@ -45,8 +26,6 @@ const FailedAssertion = Class("FailedAssertion", Error, { }); const Dactyl = Module("dactyl", { - requires: ["config", "services"], - init: function () { window.dactyl = this; window.liberator = this; @@ -61,8 +40,6 @@ const Dactyl = Module("dactyl", { // without explicitly selecting a profile. /** @property {string} The name of the current user profile. */ this.profileName = services.get("directory").get("ProfD", Ci.nsIFile).leafName.replace(/^.+?\./, ""); - - config.features.push(Dactyl.getPlatformFeature()); }, destroy: function () { @@ -139,29 +116,26 @@ const Dactyl = Module("dactyl", { * bell may be either audible or visual depending on the value of the * 'visualbell' option. */ - beep: function () { + beep: requiresMainThread(function () { // FIXME: popups clear the command line if (options["visualbell"]) { - util.callInMainThread(function () { - // flash the visual bell - let popup = document.getElementById("dactyl-visualbell"); - let win = config.visualbellWindow; - let rect = win.getBoundingClientRect(); - let width = rect.right - rect.left; - let height = rect.bottom - rect.top; - - // NOTE: this doesn't seem to work in FF3 with full box dimensions - popup.openPopup(win, "overlap", 1, 1, false, false); - popup.sizeTo(width - 2, height - 2); - setTimeout(function () { popup.hidePopup(); }, 20); - }); + // flash the visual bell + let popup = document.getElementById("dactyl-visualbell"); + let win = config.visualbellWindow; + let rect = win.getBoundingClientRect(); + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + + // NOTE: this doesn't seem to work in FF3 with full box dimensions + popup.openPopup(win, "overlap", 1, 1, false, false); + popup.sizeTo(width - 2, height - 2); + setTimeout(function () { popup.hidePopup(); }, 20); } else { let soundService = Cc["@mozilla.org/sound;1"].getService(Ci.nsISound); soundService.beep(); } - return false; // so you can do: if (...) return dactyl.beep(); - }, + }), /** * Prints a message to the console. If msg is an object it is @@ -217,9 +191,11 @@ const Dactyl = Module("dactyl", { echoerr: function (str, flags) { flags |= commandline.APPEND_TO_MESSAGES; + if (isinstance(str, ["Error", "Exception"])) + dactyl.reportError(str); if (typeof str == "object" && "echoerr" in str) str = str.echoerr; - else if (str instanceof Error) + else if (isinstance(str, ["Error"])) str = str.fileName + ":" + str.lineNumber + ": " + str; if (options["errorbells"]) @@ -504,7 +480,7 @@ const Dactyl = Module("dactyl", { // Process plugin help entries. XML.ignoreWhiteSpace = false; - XML.prettyPrinting = true; // Should be false, but ignoreWhiteSpace=false doesn't work correctly. This is the lesser evil. + XML.prettyPrinting = false; XML.prettyIndent = 4; let body = XML(); @@ -594,7 +570,7 @@ const Dactyl = Module("dactyl", { data.push(">"); if (node instanceof HTMLHeadElement) data.push(.toXMLString()); - Array.map(node.childNodes, arguments.callee); + Array.map(node.childNodes, fix); data.push(""); } break; @@ -1105,6 +1081,9 @@ const Dactyl = Module("dactyl", { dactyl.help(tag); } }, { + config: function () { + config.features.push(Dactyl.getPlatformFeature()); + }, // Only general options are added here, which are valid for all Dactyl extensions options: function () { @@ -1502,30 +1481,25 @@ const Dactyl = Module("dactyl", { extensions = extensions.filter(function (extension) extension.name.indexOf(args[0]) >= 0); extensions.sort(function (a, b) String.localeCompare(a.name, b.name)); - if (extensions.length > 0) { - let list = template.tabular( - ["Name", "Version", "Status", "Description"], [], - ([template.icon({ icon: e.iconURL }, e.name), - e.version, - (e.isActive ? enabled - : disabled) + - ((e.userDisabled || e.appDisabled) == !e.isActive ? XML() : - <> ({e.userDisabled || e.appDisabled - ? disabled - : enabled} - on restart) - ), - e.description] for ([, e] in Iterator(extensions))) - ); - - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } - else { - if (filter) - dactyl.echoerr("Exxx: No extension matching " + filter.quote()); - else - dactyl.echoerr("No extensions installed"); - } + if (extensions.length > 0) + commandline.commandOutput( + template.tabular(["Name", "Version", "Status", "Description"], [], + ([template.icon({ icon: e.iconURL }, e.name), + e.version, + (e.isActive ? enabled + : disabled) + + ((e.userDisabled || e.appDisabled) == !e.isActive ? XML() : + <> ({e.userDisabled || e.appDisabled + ? disabled + : enabled} + on restart) + ), + e.description] + for ([, e] in Iterator(extensions))))); + else if (filter) + dactyl.echoerr("Exxx: No extension matching " + filter.quote()); + else + dactyl.echoerr("No extensions installed"); }); }, { argCount: "?" }); @@ -1696,7 +1670,7 @@ const Dactyl = Module("dactyl", { else totalUnits = "msec"; - let str = template.commandOutput(commandline.command, + commandline.commandOutput( @@ -1705,7 +1679,6 @@ const Dactyl = Module("dactyl", {
Code execution summary
  Average time:{each.toFixed(2)}{eachUnits}
  Total time:{total.toFixed(2)}{totalUnits}
); - commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); } else { let beforeTime = Date.now(); @@ -1767,8 +1740,9 @@ const Dactyl = Module("dactyl", { if (args.bang) dactyl.open("about:"); else - dactyl.echo(template.commandOutput(commandline.command, - <>{config.name} {dactyl.version} running on:
{navigator.userAgent})); + commandline.commandOutput(<> + {config.name} {dactyl.version} running on:
{navigator.userAgent} + ); }, { argCount: "0", bang: true diff --git a/common/content/editor.js b/common/content/editor.js index ee77772f..6a2c7197 100644 --- a/common/content/editor.js +++ b/common/content/editor.js @@ -11,8 +11,6 @@ /** @instance editor */ const Editor = Module("editor", { - requires: ["config"], - init: function () { // store our last search with f, F, t or T // @@ -408,10 +406,10 @@ const Editor = Module("editor", { // blink the textbox after returning if (textBox) { let colors = [tmpBg, oldBg, tmpBg, oldBg]; - (function () { + (function next() { textBox.style.backgroundColor = colors.shift(); if (colors.length > 0) - setTimeout(arguments.callee, 100); + setTimeout(next, 100); })(); } @@ -577,10 +575,8 @@ const Editor = Module("editor", { dactyl.echo(mode + " " + lhs + " " + rhs, commandline.FORCE_SINGLELINE); // 2 spaces, 3 spaces } - else { - list = template.tabular(["", "LHS", "RHS"], [], list); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } + else + commandline.commandOutput(template.tabular(["", "LHS", "RHS"], [], list)); }, /** @@ -670,7 +666,7 @@ const Editor = Module("editor", { }, { completer: function (context, args) completion.abbreviation(context, args, mode), literal: 0, - serial: function () [ { + serialize: function () [ { command: this.name, arguments: [lhs], literalArg: abbr[1] diff --git a/common/content/events.js b/common/content/events.js index 574dd3f9..455bd0e0 100644 --- a/common/content/events.js +++ b/common/content/events.js @@ -12,8 +12,6 @@ * @instance events */ const Events = Module("events", { - requires: ["autocommands", "config"], - init: function () { const self = this; @@ -737,12 +735,10 @@ const Events = Module("events", { // TODO: Merge with onFocusChange onFocus: function (event) { - function hasHTMLDocument(win) win && win.document && win.document instanceof HTMLDocument - let elem = event.originalTarget; let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem; - if (hasHTMLDocument(win) && !buffer.focusAllowed(win) + if (Events.isContentNode(elem) && !buffer.focusAllowed(win) && isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement])) elem.blur(); }, @@ -1107,6 +1103,13 @@ const Events = Module("events", { editableInputs: set(["date", "datetime", "datetime-local", "email", "file", "month", "number", "password", "range", "search", "tel", "text", "time", "url", "week"]), + isContentNode: function (node) { + let win = (node.ownerDocument || node).defaultView; + for (; win; win = win.parent != win && win.parent) + if (win == window.content) + return true; + return false; + }, isInputElemFocused: function () { let elem = dactyl.focus; return elem instanceof HTMLInputElement && set.has(Events.editableInputs, elem.type) || diff --git a/common/content/finder.js b/common/content/finder.js index 463506ff..21650c83 100644 --- a/common/content/finder.js +++ b/common/content/finder.js @@ -8,8 +8,6 @@ /** @instance rangefinder */ const RangeFinder = Module("rangefinder", { - requires: ["config"], - init: function () { this.lastSearchPattern = ""; }, @@ -128,7 +126,7 @@ const RangeFinder = Module("rangefinder", { set rangeFind(val) buffer.localStore.rangeFind = val, /** - * Highlights all occurances of str in the buffer. + * Highlights all occurrences of str in the buffer. * * @param {string} str The string to highlight. */ @@ -146,6 +144,11 @@ const RangeFinder = Module("rangefinder", { } }, { }, { + modes: function () { + /* Must come before commandline. */ + modes.addMode("FIND_FORWARD", true); + modes.addMode("FIND_BACKWARD", true); + }, commandline: function () { // Event handlers for search - closure is needed commandline.registerCallback("change", modes.FIND_FORWARD, this.closure.onKeyPress); @@ -197,10 +200,6 @@ const RangeFinder = Module("rangefinder", { }); }, - modes: function () { - modes.addMode("FIND_FORWARD", true); - modes.addMode("FIND_BACKWARD", true); - }, options: function () { options.safeSetPref("accessibility.typeaheadfind.autostart", false); // The above should be sufficient, but: https://bugzilla.mozilla.org/show_bug.cgi?id=348187 @@ -256,8 +255,8 @@ const RangeFinder = Module("rangefinder", { * implementation will begin searching from the position of the * caret in the last active frame. This is contrary to the behavior * of the builtin component, which always starts a search from the - * begining of the first frame in the case of frameset documents, - * and cycles through all frames from begining to end. This makes it + * beginning of the first frame in the case of frameset documents, + * and cycles through all frames from beginning to end. This makes it * impossible to choose the starting point of a search for such * documents, and represents a major detriment to productivity where * large amounts of data are concerned (e.g., for API documents). @@ -302,8 +301,8 @@ const RangeFind = Class("RangeFind", { }, compareRanges: function (r1, r2) - this.backward ? r1.compareBoundaryPoints(Range.END_TO_START, r2) - : -r1.compareBoundaryPoints(Range.START_TO_END, r2), + this.backward ? r1.compareBoundaryPoints(r1.END_TO_START, r2) + : -r1.compareBoundaryPoints(r1.START_TO_END, r2), findRange: function (range) { let doc = range.startContainer.ownerDocument; @@ -331,7 +330,7 @@ const RangeFind = Class("RangeFind", { this.lastRange.commonAncestorContainer).snapshotItem(0); if (node) { node.focus(); - // Rehighlight collapsed selection + // Re-highlight collapsed selection this.selectedRange = this.lastRange; } }, @@ -358,7 +357,7 @@ const RangeFind = Class("RangeFind", { let string = this.lastString; for (let r in this.iter(string)) { let controller = this.range.selectionController; - for (let node=r.startContainer; node; node=node.parentNode) + for (let node = r.startContainer; node; node = node.parentNode) if (node instanceof Ci.nsIDOMNSEditableElement) { controller = node.editor.selectionController; break; @@ -370,11 +369,30 @@ const RangeFind = Class("RangeFind", { this.selections.push(sel); } this.highlighted = this.lastString; - this.selectedRange = this.lastRange; + if (this.lastRange) + this.selectedRange = this.lastRange; this.addListeners(); } }, + indexIter: function (private_) { + let idx = this.range.index; + if (this.backward) + var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)]; + else + var groups = [util.range(idx, this.ranges.length), util.range(0, idx + 1)]; + + for (let i in groups[0]) + yield i; + + if (!private_) { + this.wrapped = true; + this.lastRange = null; + for (let i in groups[1]) + yield i; + } + }, + iter: function (word) { let saved = ["range", "lastRange", "lastString"].map(function (s) [s, this[s]], this); try { @@ -382,7 +400,7 @@ const RangeFind = Class("RangeFind", { this.lastRange = null; this.lastString = word; var res; - while ((res = this.search(null, this.reverse, true))) + while (res = this.search(null, this.reverse, true)) yield res; } finally { @@ -504,25 +522,8 @@ const RangeFind = Class("RangeFind", { if (word == "") var range = this.startRange; - else { - function indices() { - let idx = this.range.index; - if (this.backward) - var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)]; - else - var groups = [util.range(idx, this.ranges.length), util.range(0, idx + 1)]; - - for (let i in groups[0]) - yield i; - - if (!private_) { - this.wrapped = true; - this.lastRange = null; - for (let i in groups[1]) - yield i; - } - } - for (let i in indices.call(this)) { + else + for (let i in this.indexIter(private_)) { if (!private_ && this.range.window != this.ranges[i].window && this.range.window != this.ranges[i].window.parent) { this.range.descroll(); this.range.deselect(); @@ -540,7 +541,6 @@ const RangeFind = Class("RangeFind", { if (range) break; } - } if (range) this.lastRange = range.cloneRange(); @@ -559,11 +559,11 @@ const RangeFind = Class("RangeFind", { }, addListeners: function () { - for (let range in values(this.ranges)) + for (let range in array.itervalues(this.ranges)) range.window.addEventListener("unload", this.closure.onUnload, true); }, purgeListeners: function () { - for (let range in values(this.ranges)) + for (let range in array.itervalues(this.ranges)) range.window.removeEventListener("unload", this.closure.onUnload, true); }, onUnload: function (event) { @@ -591,8 +591,8 @@ const RangeFind = Class("RangeFind", { }, intersects: function (range) - this.range.compareBoundaryPoints(Range.START_TO_END, range) >= 0 && - this.range.compareBoundaryPoints(Range.END_TO_START, range) <= 0, + this.range.compareBoundaryPoints(range.START_TO_END, range) >= 0 && + this.range.compareBoundaryPoints(range.END_TO_START, range) <= 0, save: function () { this.scroll = Point(this.window.pageXOffset, this.window.pageYOffset); @@ -625,8 +625,8 @@ const RangeFind = Class("RangeFind", { }), contains: function (range, r) - range.compareBoundaryPoints(Range.START_TO_END, r) >= 0 && - range.compareBoundaryPoints(Range.END_TO_START, r) <= 0, + range.compareBoundaryPoints(range.START_TO_END, r) >= 0 && + range.compareBoundaryPoints(range.END_TO_START, r) <= 0, endpoint: function (range, before) { range = range.cloneRange(); range.collapse(before); @@ -634,7 +634,7 @@ const RangeFind = Class("RangeFind", { }, equal: function (r1, r2) { try { - return !r1.compareBoundaryPoints(Range.START_TO_START, r2) && !r1.compareBoundaryPoints(Range.END_TO_END, r2) + return !r1.compareBoundaryPoints(r1.START_TO_START, r2) && !r1.compareBoundaryPoints(r1.END_TO_END, r2) } catch (e) {} return false; diff --git a/common/content/hints.js b/common/content/hints.js index 4ffe9ca7..2c6b8efc 100644 --- a/common/content/hints.js +++ b/common/content/hints.js @@ -10,8 +10,6 @@ /** @instance hints */ const Hints = Module("hints", { - requires: ["config"], - init: function () { this._hintMode = null; diff --git a/common/content/history.js b/common/content/history.js index abed5893..d8962567 100644 --- a/common/content/history.js +++ b/common/content/history.js @@ -7,8 +7,6 @@ "use strict"; const History = Module("history", { - requires: ["config"], - get format() bookmarks.format, get service() services.get("history"), @@ -196,7 +194,7 @@ const History = Module("history", { bang: true, completer: function (context) { context.quote = null; completion.history(context); }, // completer: function (filter) completion.history(filter) - options: [[["-max", "-m"], commands.OPTION_INT]] + options: [{ names: ["-max", "-m"], description: "The maximum number of items to list", type: CommandOption.INT }] }); }, completion: function () { diff --git a/common/content/io.js b/common/content/io.js index 51a00e37..ebe86d6a 100755 --- a/common/content/io.js +++ b/common/content/io.js @@ -10,311 +10,27 @@ /** @scope modules */ plugins.contexts = {}; -const Script = Class("Script", { - init: function (file) { - let self = plugins.contexts[file.path]; - if (self) { - if (self.onUnload) - self.onUnload(); - return self; - } - plugins.contexts[file.path] = this; - this.NAME = file.leafName.replace(/\..*/, "").replace(/-([a-z])/g, function (m, n1) n1.toUpperCase()); - this.PATH = file.path; - this.toString = this.toString; - this.__context__ = this; - this.__proto__ = plugins; - - // This belongs elsewhere - for (let [, dir] in Iterator(io.getRuntimeDirectories("plugin"))) { - if (dir.contains(file, false)) - plugins[this.NAME] = this; - } - return this; - } -}); - -/** - * @class File A class to wrap nsIFile objects and simplify operations - * thereon. - * - * @param {nsIFile|string} path Expanded according to {@link IO#expandPath} - * @param {boolean} checkPWD Whether to allow expansion relative to the - * current directory. @default true - */ -const File = Class("File", { - init: function (path, checkPWD) { - if (arguments.length < 2) - checkPWD = true; - - let file = services.create("file"); - - if (path instanceof Ci.nsIFile) - file = path; - else if (/file:\/\//.test(path)) - file = services.create("file:").getFileFromURLSpec(path); - else { - let expandedPath = File.expandPath(path); - - if (!File.isAbsolutePath(expandedPath) && checkPWD) - file = File.joinPaths(io.getCurrentDirectory().path, expandedPath); - else - file.initWithPath(expandedPath); - } - let self = XPCSafeJSObjectWrapper(file); - self.__proto__ = File.prototype; +function Script(file) { + let self = plugins.contexts[file.path]; + if (self) { + if (self.onUnload) + self.onUnload(); return self; - }, - - /** - * Iterates over the objects in this directory. - */ - iterDirectory: function () { - if (!this.isDirectory()) - throw Error("Not a directory"); - let entries = this.directoryEntries; - while (entries.hasMoreElements()) - yield File(entries.getNext().QueryInterface(Ci.nsIFile)); - }, - /** - * Returns the list of files in this directory. - * - * @param {boolean} sort Whether to sort the returned directory - * entries. - * @returns {nsIFile[]} - */ - readDirectory: function (sort) { - if (!this.isDirectory()) - throw Error("Not a directory"); - - let array = [e for (e in this.iterDirectory())]; - if (sort) - array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path)); - return array; - }, - - /** - * Reads this file's entire contents in "text" mode and returns the - * content as a string. - * - * @param {string} encoding The encoding from which to decode the file. - * @default options["fileencoding"] - * @returns {string} - */ - read: function (encoding) { - let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); - let icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); - - if (!encoding) - encoding = options["fileencoding"]; - - ifstream.init(this, -1, 0, 0); - icstream.init(ifstream, encoding, 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering - - let buffer = []; - let str = {}; - while (icstream.readString(4096, str) != 0) - buffer.push(str.value); - - icstream.close(); - ifstream.close(); - return buffer.join(""); - }, - - /** - * Writes the string buf to this file. - * - * @param {string} buf The file content. - * @param {string|number} mode The file access mode, a bitwise OR of - * the following flags: - * {@link #MODE_RDONLY}: 0x01 - * {@link #MODE_WRONLY}: 0x02 - * {@link #MODE_RDWR}: 0x04 - * {@link #MODE_CREATE}: 0x08 - * {@link #MODE_APPEND}: 0x10 - * {@link #MODE_TRUNCATE}: 0x20 - * {@link #MODE_SYNC}: 0x40 - * Alternatively, the following abbreviations may be used: - * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE} - * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND} - * @default ">" - * @param {number} perms The file mode bits of the created file. This - * is only used when creating a new file and does not change - * permissions if the file exists. - * @default 0644 - * @param {string} encoding The encoding to used to write the file. - * @default options["fileencoding"] - */ - write: function (buf, mode, perms, encoding) { - let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); - function getStream(defaultChar) { - let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); - stream.init(ofstream, encoding, 0, defaultChar); - return stream; - } - - if (!encoding) - encoding = options["fileencoding"]; - - if (mode == ">>") - mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND; - else if (!mode || mode == ">") - mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE; - - if (!perms) - perms = parseInt('0644', 8); - - ofstream.init(this, mode, perms, 0); - let ocstream = getStream(0); - try { - ocstream.writeString(buf); - } - catch (e) { - dactyl.dump(e); - if (e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) { - ocstream = getStream("?".charCodeAt(0)); - ocstream.writeString(buf); - return false; - } - else - throw e; - } - finally { - try { - ocstream.close(); - } - catch (e) {} - ofstream.close(); - } - return true; - } -}, { - /** - * @property {number} Open for reading only. - * @final - */ - MODE_RDONLY: 0x01, - - /** - * @property {number} Open for writing only. - * @final - */ - MODE_WRONLY: 0x02, - - /** - * @property {number} Open for reading and writing. - * @final - */ - MODE_RDWR: 0x04, - - /** - * @property {number} If the file does not exist, the file is created. - * If the file exists, this flag has no effect. - * @final - */ - MODE_CREATE: 0x08, - - /** - * @property {number} The file pointer is set to the end of the file - * prior to each write. - * @final - */ - MODE_APPEND: 0x10, - - /** - * @property {number} If the file exists, its length is truncated to 0. - * @final - */ - MODE_TRUNCATE: 0x20, - - /** - * @property {number} If set, each write will wait for both the file - * data and file status to be physically updated. - * @final - */ - MODE_SYNC: 0x40, - - /** - * @property {number} With MODE_CREATE, if the file does not exist, the - * file is created. If the file already exists, no action and NULL - * is returned. - * @final - */ - MODE_EXCL: 0x80, - - expandPathList: function (list) list.map(this.expandPath), - - expandPath: function (path, relative) { - - // expand any $ENV vars - this is naive but so is Vim and we like to be compatible - // TODO: Vim does not expand variables set to an empty string (and documents it). - // Kris reckons we shouldn't replicate this 'bug'. --djk - // TODO: should we be doing this for all paths? - function expand(path) path.replace( - !dactyl.has("Win32") ? /\$(\w+)\b|\${(\w+)}/g - : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g, - function (m, n1, n2, n3) services.get("environment").get(n1 || n2 || n3) || m - ); - path = expand(path); - - // expand ~ - // Yuck. - if (!relative && RegExp("~(?:$|[/" + util.escapeRegex(IO.PATH_SEP) + "])").test(path)) { - // Try $HOME first, on all systems - let home = services.get("environment").get("HOME"); - - // Windows has its own idiosyncratic $HOME variables. - if (!home && dactyl.has("Win32")) - home = services.get("environment").get("USERPROFILE") || - services.get("environment").get("HOMEDRIVE") + services.get("environment").get("HOMEPATH"); - - path = home + path.substr(1); - } - - // TODO: Vim expands paths twice, once before checking for ~, once - // after, but doesn't document it. Is this just a bug? --Kris - path = expand(path); - return path.replace("/", IO.PATH_SEP, "g"); - }, - - getPathsFromPathList: function (list) { - if (!list) - return []; - // empty list item means the current directory - return list.replace(/,$/, "").split(",") - .map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir); - }, - - replacePathSep: function (path) path.replace("/", IO.PATH_SEP, "g"), - - joinPaths: function (head, tail) { - let path = this(head); - try { - path.appendRelativePath(this.expandPath(tail, true)); // FIXME: should only expand env vars and normalise path separators - // TODO: This code breaks the external editor at least in ubuntu - // because /usr/bin/gvim becomes /usr/bin/vim.gnome normalized and for - // some strange reason it will start without a gui then (which is not - // optimal if you don't start firefox from a terminal ;) - // Why do we need this code? - // if (path.exists() && path.normalize) - // path.normalize(); - } - catch (e) { - return { exists: function () false, __noSuchMethod__: function () { throw e; } }; - } - return path; - }, - - isAbsolutePath: function (path) { - try { - services.create("file").initWithPath(path); - return true; - } - catch (e) { - return false; - } + } + self = { __proto__: plugins }; + plugins.contexts[file.path] = self; + self.NAME = file.leafName.replace(/\..*/, "").replace(/-([a-z])/g, function (m, n1) n1.toUpperCase()); + self.PATH = file.path; + self.__context__ = self; + self.__proto__ = plugins; + + // This belongs elsewhere + for (let [, dir] in Iterator(io.getRuntimeDirectories("plugin"))) { + if (dir.contains(file, false)) + plugins[self.NAME] = self; } -}); + return self; +} // TODO: why are we passing around strings rather than file objects? /** @@ -322,8 +38,6 @@ const File = Class("File", { * @instance io */ const IO = Module("io", { - requires: ["config", "services"], - init: function () { this._processDir = services.get("directory").get("CurWorkD", Ci.nsIFile); this._cwd = this._processDir; @@ -363,7 +77,10 @@ const IO = Module("io", { * @property {function} File class. * @final */ - File: File, + File: Class("File", File, { + init: function init(path, checkCWD) + init.supercall(this, path, (arguments.length < 2 || checkCWD) && io.getCurrentDirectory()) + }), /** * @property {Object} The current file sourcing context. As a file is @@ -423,7 +140,7 @@ const IO = Module("io", { if (newDir == "-") [this._cwd, this._oldcwd] = [this._oldcwd, this.getCurrentDirectory()]; else { - let dir = File(newDir); + let dir = io.File(newDir); if (!dir.exists() || !dir.isDirectory()) { dactyl.echoerr("E344: Can't find directory " + dir.path.quote()); @@ -489,7 +206,7 @@ const IO = Module("io", { file.append(config.tempFile); file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt('0600', 8)); - return File(file); + return io.File(file); }, /** @@ -507,7 +224,7 @@ const IO = Module("io", { let file; if (File.isAbsolutePath(program)) - file = File(program, true); + file = io.File(program, true); else { let dirs = services.get("environment").get("PATH").split(dactyl.has("Win32") ? ";" : ":"); // Windows tries the CWD first TODO: desirable? @@ -600,7 +317,7 @@ lookup: dactyl.dump("sourcing " + filename); let time = Date.now(); try { - var file = File(filename); + var file = io.File(filename); this.sourcing = { file: file.path, line: 0 @@ -803,12 +520,7 @@ lookup: /** * @property {string} The current platform's path seperator. */ - get PATH_SEP() { - delete this.PATH_SEP; - let f = services.get("directory").get("CurProcD", Ci.nsIFile); - f.append("foo"); - return this.PATH_SEP = f.path.substr(f.parent.path.length, 1); - } + PATH_SEP: File.PATH_SEP }, { commands: function () { commands.add(["cd", "chd[ir]"], @@ -876,13 +588,13 @@ lookup: dactyl.assert(args.length <= 1, "E172: Only one file name allowed"); let filename = args[0] || io.getRCFile(null, true).path; - let file = File(filename); + let file = io.File(filename); dactyl.assert(!file.exists() || args.bang, "E189: " + filename.quote() + " exists (add ! to override)"); // TODO: Use a set/specifiable list here: - let lines = [cmd.serial().map(commands.commandToString) for (cmd in commands) if (cmd.serial)]; + let lines = [cmd.serialize().map(commands.commandToString) for (cmd in commands) if (cmd.serialize)]; lines = util.Array.flatten(lines); // source a user .pentadactylrc file @@ -922,10 +634,9 @@ lookup: commands.add(["scrip[tnames]"], "List all sourced script names", function () { - let list = template.tabular(["", "Filename"], ["text-align: right; padding-right: 1em;"], - ([i + 1, file] for ([i, file] in Iterator(io._scriptNames)))); // TODO: add colon and remove column titles for pedantic Vim compatibility? - - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + commandline.commandOutput( + template.tabular(["", "Filename"], ["text-align: right; padding-right: 1em;"], + ([i + 1, file] for ([i, file] in Iterator(io._scriptNames))))); // TODO: add colon and remove column titles for pedantic Vim compatibility? }, { argCount: "0" }); @@ -967,8 +678,7 @@ lookup: let output = io.system(arg); commandline.command = "!" + arg; - commandline.echo(template.commandOutput(commandline.command, - {output})); + commandline.commandOutput({output}); autocommands.trigger("ShellCmdPost", {}); }, { @@ -1034,7 +744,7 @@ lookup: context.key = dir; context.generate = function generate_file() { try { - return File(dir).readDirectory(); + return io.File(dir).readDirectory(); } catch (e) {} return []; @@ -1065,7 +775,7 @@ lookup: }); }, javascript: function () { - JavaScript.setCompleter([this.File, File.expandPath], + JavaScript.setCompleter([File, File.expandPath], [function (context, obj, args) { context.quote[2] = ""; completion.file(context, true); @@ -1088,7 +798,9 @@ lookup: options.add(["fileencoding", "fenc"], "Sets the character encoding of read and written files", "string", "UTF-8", { - completer: function (context) completion.charset(context) + completer: function (context) completion.charset(context), + getter: function () File.defaultEncoding, + setter: function (value) (File.defaultEncoding = value) }); options.add(["cdpath", "cd"], "List of directories searched when executing :cd", diff --git a/common/content/javascript.js b/common/content/javascript.js index 91c18a72..ea730d2a 100644 --- a/common/content/javascript.js +++ b/common/content/javascript.js @@ -26,7 +26,6 @@ const JavaScript = Module("javascript", { // Some object members are only accessible as function calls getKey: function (obj, key) { try { - // if (!Object.prototype.__lookupGetter__.call(obj, key)) return obj[key]; } catch (e) {} @@ -35,20 +34,18 @@ const JavaScript = Module("javascript", { iter: function iter(obj, toplevel) { "use strict"; - const self = this; if (obj == null) return; - let orig = obj; - if(!options["jsdebugger"]) - function value(key) self.getKey(orig, key); - else { - let top = services.get("debugger").wrapValue(obj); - function value(key) top.getProperty(key).value.getWrappedValue(); + let seen = {}; + for (let key in properties(obj, !toplevel)) { + set.add(seen, key); + yield [key, this.getKey(obj, key)]; } - for (let key in properties(obj, !toplevel)) - yield [key, value(key)]; + for (let key in properties(this.getKey(obj, "wrappedJSObject"), !toplevel)) + if (!set.has(seen, key)) + yield [key, this.getKey(obj, key)]; }, objectKeys: function objectKeys(obj, toplevel) { diff --git a/common/content/mappings.js b/common/content/mappings.js index eb595c05..da646b9b 100644 --- a/common/content/mappings.js +++ b/common/content/mappings.js @@ -35,6 +35,7 @@ const Map = Class("Map", { this.modes = modes; this.names = keys.map(events.canonicalKeys); + this.name = this.names[0]; this.action = action; this.description = description; @@ -122,8 +123,6 @@ const Map = Class("Map", { * @instance mappings */ const Mappings = Module("mappings", { - requires: ["modes"], - init: function () { this._main = []; // default mappings this._user = []; // user created mappings @@ -177,9 +176,9 @@ const Mappings = Module("mappings", { // Return all mappings present in all @modes _mappingsIterator: function (modes, stack) { modes = modes.slice(); - return (map for ([i, map] in Iterator(stack[modes.shift()])) + return (map for ([i, map] in Iterator(stack[modes.shift()].sort(function (m1, m2) String.localeCompare(m1.name, m2.name)))) if (modes.every(function (mode) stack[mode].some( - function (m) m.rhs == map.rhs && m.names[0] == map.names[0])))) + function (mapping) m.rhs == map.rhs && m.name == map.name)))) }, // NOTE: just normal mode for now @@ -363,11 +362,10 @@ const Mappings = Module("mappings", { ; // TODO: Move this to an ItemList to show this automatically - if (list.*.length() == list.text().length()) { + if (list.*.length() == list.text().length()) dactyl.echomsg("No mapping found"); - return; - } - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + else + commandline.commandOutput(list); } }, { }, { @@ -413,11 +411,9 @@ const Mappings = Module("mappings", { const opts = { completer: function (context, args) completion.userMapping(context, args, modes), - options: [ - [["", ""], commands.OPTION_NOARG] - ], literal: 1, - serial: function () { + options: [{ names: ["", ""] }], + serialize: function () { let noremap = this.name.indexOf("noremap") > -1; return [ { diff --git a/common/content/marks.js b/common/content/marks.js index 2fd1f0b5..f6ee4755 100644 --- a/common/content/marks.js +++ b/common/content/marks.js @@ -11,8 +11,6 @@ * @instance marks */ const Marks = Module("marks", { - requires: ["config", "storage"], - init: function init() { this._localMarks = storage.newMap("local-marks", { store: true, privateData: true }); this._urlMarks = storage.newMap("url-marks", { store: true, privateData: true }); @@ -176,15 +174,15 @@ const Marks = Module("marks", { dactyl.assert(marks.length > 0, "E283: No marks matching " + filter.quote()); } - let list = template.tabular( - ["Mark", "Line", "Column", "File"], - ["", "text-align: right", "text-align: right", "color: green"], - ([mark[0], - Math.round(mark[1].position.x * 100) + "%", - Math.round(mark[1].position.y * 100) + "%", - mark[1].location] - for ([, mark] in Iterator(marks)))); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + commandline.commandOutput( + template.tabular( + ["Mark", "Line", "Column", "File"], + ["", "text-align: right", "text-align: right", "color: green"], + ([mark[0], + Math.round(mark[1].position.x * 100) + "%", + Math.round(mark[1].position.y * 100) + "%", + mark[1].location] + for ([, mark] in Iterator(marks))))); }, _onPageLoad: function _onPageLoad(event) { diff --git a/common/content/modes.js b/common/content/modes.js index fd7611b4..d9aa994b 100644 --- a/common/content/modes.js +++ b/common/content/modes.js @@ -9,8 +9,6 @@ /** @scope modules */ const Modes = Module("modes", { - requires: ["config", "util"], - init: function () { this._main = 1; // NORMAL this._extended = 0; // NONE @@ -49,8 +47,6 @@ const Modes = Module("modes", { this.addMode("MENU", true); // a popupmenu is active this.addMode("LINE", true); // linewise visual mode this.addMode("PROMPT", true); - - config.modes.forEach(function (mode) { this.addMode.apply(this, mode); }, this); }, _getModeMessage: function () { diff --git a/common/content/modules.js b/common/content/modules.js index b485157b..840f87c5 100644 --- a/common/content/modules.js +++ b/common/content/modules.js @@ -53,12 +53,17 @@ const ModuleBase = Class("ModuleBase", { * * @returns {function} The constructor for the resulting module. */ -function Module(name, prototype, classProperties, moduleInit) { +function Module(name) { + let args = Array.slice(arguments); + var base = ModuleBase; - if (callable(prototype)) - base = Array.splice(arguments, 1, 1)[0]; + if (callable(args[1])) + base = args.splice(1, 1)[0]; + let [, prototype, classProperties, moduleInit] = args; const module = Class(name, base, prototype, classProperties); + module.INIT = moduleInit || {}; + module.prototype.INIT = module.INIT; module.requires = prototype.requires || []; Module.list.push(module); Module.constructors[name] = module; @@ -67,67 +72,82 @@ function Module(name, prototype, classProperties, moduleInit) { Module.list = []; Module.constructors = {}; -window.addEventListener("load", function () { - window.removeEventListener("load", arguments.callee, false); +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad, false); + + Module.list.forEach(function(module) { + modules.__defineGetter__(module.name, function() { + delete modules[module.name]; + return load(module.name, null, Components.stack.caller); + }); + }); function dump(str) window.dump(String.replace(str, /\n?$/, "\n").replace(/^/m, Config.prototype.name.toLowerCase() + ": ")); const start = Date.now(); const deferredInit = { load: [] }; const seen = set(); - const loaded = []; + const loaded = set(["init"]); + + function init(module) { + function init(func) + function () func.call(module, dactyl, modules, window); + + set.add(loaded, module.constructor.name); + for (let [mod, func] in Iterator(module.INIT)) { + if (mod in loaded) + init(func)(); + else { + deferredInit[mod] = deferredInit[mod] || []; + deferredInit[mod].push(init(func)); + } + } + } + defmodule.modules.map(init); + + function load(module, prereq, frame) { + if (isstring(module)) { + if (!Module.constructors.hasOwnProperty(module)) + modules.load(module); + module = Module.constructors[module]; + } - function load(module, prereq) { try { - if (module.name in modules) + if (module.name in loaded) return; if (module.name in seen) throw Error("Module dependency loop."); set.add(seen, module.name); for (let dep in values(module.requires)) - load(Module.constructors[dep], module.name, dep); + load(Module.constructors[dep], module.name); dump("Load" + (isstring(prereq) ? " " + prereq + " dependency: " : ": ") + module.name); + if (frame && frame.filename) + dump(" from: " + frame.filename + ":" + frame.lineNumber); + + delete modules[module.name]; modules[module.name] = module(); - loaded.push(module.name); - - function init(mod, module) - function () module.INIT[mod].call(modules[module.name], modules[mod]); - for (let mod in values(loaded)) { - try { - if (mod in module.INIT) - init(mod, module)(); - delete module.INIT[mod]; - } - catch (e) { - if (modules.dactyl) - dactyl.reportError(e); - } - } - for (let mod in values(Object.keys(module.INIT))) { - deferredInit[mod] = deferredInit[mod] || []; - deferredInit[mod].push(init(mod, module)); - } + + init(modules[module.name]); for (let [, fn] in iter(deferredInit[module.name] || [])) fn(); } catch (e) { - dump("Loading " + (module && module.name) + ": " + e + "\n"); + dump("Loading " + (module && module.name) + ": " + e); if (e.stack) dump(e.stack); } + return modules[module.name]; } + Module.list.forEach(load); deferredInit["load"].forEach(call); - for (let module in values(Module.list)) - delete module.INIT; - - dump("Loaded in " + (Date.now() - start) + "ms\n"); + dump("Loaded in " + (Date.now() - start) + "ms"); }, false); -window.addEventListener("unload", function () { - window.removeEventListener("unload", arguments.callee, false); +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload, false); for (let [, mod] in iter(modules)) if (mod instanceof ModuleBase && "destroy" in mod) mod.destroy(); diff --git a/common/content/options.js b/common/content/options.js index d60a773b..8f28e7be 100644 --- a/common/content/options.js +++ b/common/content/options.js @@ -238,8 +238,8 @@ const Option = Class("Option", { * "string" - String, e.g., "Pentadactyl" * "charlist" - Character list, e.g., "rb" * "regexlist" - Regex list, e.g., "^foo,bar$" - * "stringmap" - String map, e.g., "key=v,foo=bar" - * "regexmap" - Regex map, e.g., "^key=v,foo$=bar" + * "stringmap" - String map, e.g., "key:v,foo:bar" + * "regexmap" - Regex map, e.g., "^key:v,foo$:bar" */ type: null, @@ -351,9 +351,9 @@ const Option = Class("Option", { boolean: function (value) value == "true" || value == true ? true : false, charlist: function (value) Array.slice(value), stringlist: function (value) (value === "") ? [] : value.split(","), - stringmap: function (value) array(v.split("=") for (v in values(value.split(",")))).toObject(), + stringmap: function (value) array(v.split(":") for (v in values(value.split(",")))).toObject(), regexlist: function (value) (value === "") ? [] : value.split(",").map(Option.parseRegex), - regexmap: function (value) value.split(",").map(function (v) v.split("=")) + regexmap: function (value) value.split(",").map(function (v) v.split(":")) .map(function ([k, v]) v != null ? Option.parseRegex(k, v) : Option.parseRegex('.?', k)) }, @@ -478,8 +478,6 @@ Option.ops["regexmap"] = Option.ops["stringlist"]; * @instance options */ const Options = Module("options", { - requires: ["config", "highlight", "storage"], - init: function () { this._optionHash = {}; this._prefContexts = []; @@ -661,8 +659,7 @@ const Options = Module("options", { } }; - let list = template.options("Options", opts()); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + commandline.commandOutput(template.options("Options", opts())); }, /** @@ -687,7 +684,7 @@ const Options = Module("options", { if (onlyNonDefault && !userValue || pref.indexOf(filter) == -1) continue; - value = options.getPref(pref); + let value = options.getPref(pref); let option = { isDefault: !userValue, @@ -701,8 +698,8 @@ const Options = Module("options", { } }; - let list = template.options(config.hostApplication + " Options", prefs()); - commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); + commandline.commandOutput( + template.options(config.hostApplication + " Options", prefs())); }, /** @@ -783,15 +780,7 @@ const Options = Module("options", { return this._loadPreference(name, forcedDefault); }, - /** - * Sets the preference name to value but warns the user - * if the value is changed from its default. - * - * @param {string} name The preference name. - * @param {value} value The new preference value. - */ - // FIXME: Well it used to. I'm looking at you mst! --djk - safeSetPref: function (name, value, message) { + _checkPrefSafe: function (name, message) { let curval = this._loadPreference(name, null, false); let defval = this._loadPreference(name, null, true); let saved = this._loadPreference(Options.SAVED + name); @@ -802,6 +791,30 @@ const Options = Module("options", { msg += " " + message; dactyl.echomsg(msg); } + }, + + /** + * Resets the preference name to value but warns the user + * if the value is changed from its default. + * + * @param {string} name The preference name. + * @param {value} value The new preference value. + */ + safeResetPref: function (name, message) { + this._checkPrefSafe(name, message); + this.resetPref(name); + this.resetPref(Options.SAVED + name); + }, + + /** + * Sets the preference name to value but warns the user + * if the value is changed from its default. + * + * @param {string} name The preference name. + * @param {value} value The new preference value. + */ + safeSetPref: function (name, value, message) { + this._checkPrefSafe(name, message); this._storePreference(name, value); this._storePreference(Options.SAVED + name, value); }, @@ -825,9 +838,7 @@ const Options = Module("options", { try { services.get("pref").clearUserPref(name); } - catch (e) { - // ignore - thrown if not a user set value - } + catch (e) {} // ignore - thrown if not a user set value }, /** @@ -1238,7 +1249,7 @@ const Options = Module("options", { completer: function (context, args) { return setCompleter(context, args); }, - serial: function () [ + serialize: function () [ { command: this.name, arguments: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name diff --git a/common/content/quickmarks.js b/common/content/quickmarks.js index fc44eb79..70dc1b92 100644 --- a/common/content/quickmarks.js +++ b/common/content/quickmarks.js @@ -12,8 +12,6 @@ * @instance quickmarks */ const QuickMarks = Module("quickmarks", { - requires: ["config", "storage"], - init: function () { this._qmarks = storage.newMap("quickmarks", { store: true }); }, diff --git a/common/content/sanitizer.js b/common/content/sanitizer.js index 68b5ed69..83d93aaa 100644 --- a/common/content/sanitizer.js +++ b/common/content/sanitizer.js @@ -15,8 +15,6 @@ // - finish 1.9.0 support if we're going to support sanitizing in Xulmus const Sanitizer = Module("sanitizer", { - requires: ["dactyl"], - init: function () { const self = this; dactyl.loadScript("chrome://browser/content/sanitize.js", Sanitizer); @@ -144,10 +142,13 @@ const Sanitizer = Module("sanitizer", { context.completions = options.get("sanitizeitems").completer(); }, options: [ - [["-timespan", "-t"], - commands.OPTION_INT, - function (arg) /^[0-4]$/.test(arg), - function () options.get("sanitizetimespan").completer()] + { + names: ["-timespan", "-t"], + description: "Timespan for which to sanitize items", + completer: function () options.get("sanitizetimespan").completer(), + type: CommandOption.INT, + validator: function (arg) /^[0-4]$/.test(arg) + } ] }); }, diff --git a/common/content/services.js b/common/content/services.js deleted file mode 100644 index 19cbfb09..00000000 --- a/common/content/services.js +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) 2008-2009 by Kris Maglione -// -// This work is licensed for reuse under an MIT license. Details are -// given in the LICENSE.txt file included with this file. -"use strict"; - -/** @scope modules */ - -/** - * Cached XPCOM services and classes. - * - * @constructor - */ -const Services = Module("services", { - init: function () { - this.classes = {}; - this.services = {}; - - this.add("appStartup", "@mozilla.org/toolkit/app-startup;1", Ci.nsIAppStartup); - this.add("autoCompleteSearch", "@mozilla.org/autocomplete/search;1?name=history", Ci.nsIAutoCompleteSearch); - this.add("bookmarks", "@mozilla.org/browser/nav-bookmarks-service;1", Ci.nsINavBookmarksService); - this.add("browserSearch", "@mozilla.org/browser/search-service;1", Ci.nsIBrowserSearchService); - this.add("cache", "@mozilla.org/network/cache-service;1", Ci.nsICacheService); - this.add("console", "@mozilla.org/consoleservice;1", Ci.nsIConsoleService); - this.add("dactyl:", "@mozilla.org/network/protocol;1?name=dactyl"); - this.add("debugger", "@mozilla.org/js/jsd/debugger-service;1", Ci.jsdIDebuggerService); - this.add("directory", "@mozilla.org/file/directory_service;1", Ci.nsIProperties); - this.add("downloadManager", "@mozilla.org/download-manager;1", Ci.nsIDownloadManager); - this.add("environment", "@mozilla.org/process/environment;1", Ci.nsIEnvironment); - this.add("extensionManager", "@mozilla.org/extensions/manager;1", Ci.nsIExtensionManager); - this.add("favicon", "@mozilla.org/browser/favicon-service;1", Ci.nsIFaviconService); - this.add("history", "@mozilla.org/browser/global-history;2", [Ci.nsIBrowserHistory, Ci.nsIGlobalHistory3, Ci.nsINavHistoryService]); - this.add("io", "@mozilla.org/network/io-service;1", Ci.nsIIOService); - this.add("json", "@mozilla.org/dom/json;1", Ci.nsIJSON, "createInstance"); - this.add("livemark", "@mozilla.org/browser/livemark-service;2", Ci.nsILivemarkService); - this.add("observer", "@mozilla.org/observer-service;1", Ci.nsIObserverService); - this.add("pref", "@mozilla.org/preferences-service;1", [Ci.nsIPrefBranch, Ci.nsIPrefBranch2, Ci.nsIPrefService]); - this.add("profile", "@mozilla.org/toolkit/profile-service;1", Ci.nsIToolkitProfileService); - this.add("runtime", "@mozilla.org/xre/runtime;1", [Ci.nsIXULAppInfo, Ci.nsIXULRuntime]); - this.add("rdf", "@mozilla.org/rdf/rdf-service;1", Ci.nsIRDFService); - this.add("sessionStore", "@mozilla.org/browser/sessionstore;1", Ci.nsISessionStore); - this.add("stylesheet", "@mozilla.org/content/style-sheet-service;1", Ci.nsIStyleSheetService); - this.add("subscriptLoader", "@mozilla.org/moz/jssubscript-loader;1", Ci.mozIJSSubScriptLoader); - this.add("tagging", "@mozilla.org/browser/tagging-service;1", Ci.nsITaggingService); - this.add("threadManager", "@mozilla.org/thread-manager;1", Ci.nsIThreadManager); - this.add("windowMediator", "@mozilla.org/appshell/window-mediator;1", Ci.nsIWindowMediator); - this.add("windowWatcher", "@mozilla.org/embedcomp/window-watcher;1", Ci.nsIWindowWatcher); - this.add("xulAppInfo", "@mozilla.org/xre/app-info;1", Ci.nsIXULAppInfo); - - this.addClass("file", "@mozilla.org/file/local;1", Ci.nsILocalFile); - this.addClass("file:", "@mozilla.org/network/protocol;1?name=file", Ci.nsIFileProtocolHandler); - this.addClass("find", "@mozilla.org/embedcomp/rangefind;1", Ci.nsIFind); - this.addClass("process", "@mozilla.org/process/util;1", Ci.nsIProcess); - this.addClass("timer", "@mozilla.org/timer;1", Ci.nsITimer); - this.addClass("xmlhttp", "@mozilla.org/xmlextras/xmlhttprequest;1", Ci.nsIXMLHttpRequest); - this.addClass("zipWriter", "@mozilla.org/zipwriter;1", Ci.nsIZipWriter); - - if (!this.get("extensionManager")) - Components.utils.import("resource://gre/modules/AddonManager.jsm", modules); - }, - - _create: function (classes, ifaces, meth) { - try { - let res = Cc[classes][meth || "getService"](); - if (!ifaces) - return res.wrappedJSObject; - ifaces = Array.concat(ifaces); - ifaces.forEach(function (iface) res.QueryInterface(iface)); - return res; - } - catch (e) { - // dactyl.log() is not defined at this time, so just dump any error - dump("Service creation failed for '" + classes + "': " + e + "\n"); - return null; - } - }, - - /** - * Adds a new XPCOM service to the cache. - * - * @param {string} name The service's cache key. - * @param {string} class The class's contract ID. - * @param {nsISupports|nsISupports[]} ifaces The interface or array of - * interfaces implemented by this service. - * @param {string} meth The name of the function used to instanciate - * the service. - */ - add: function (name, class_, ifaces, meth) { - const self = this; - this.services.__defineGetter__(name, function () { - delete this[name]; - return this[name] = self._create(class_, ifaces, meth); - }); - }, - - /** - * Adds a new XPCOM class to the cache. - * - * @param {string} name The class's cache key. - * @param {string} class The class's contract ID. - * @param {nsISupports|nsISupports[]} ifaces The interface or array of - * interfaces implemented by this class. - */ - addClass: function (name, class_, ifaces) { - const self = this; - return this.classes[name] = function () self._create(class_, ifaces, "createInstance"); - }, - - /** - * Returns the cached service with the specified name. - * - * @param {string} name The service's cache key. - */ - get: function (name) this.services[name], - - /** - * Returns a new instance of the cached class with the specified name. - * - * @param {string} name The class's cache key. - */ - create: function (name) this.classes[name]() -}, { - javascript: function (dactyl, modules) { - JavaScript.setCompleter(this.get, [function () services.services]); - JavaScript.setCompleter(this.create, [function () [[c, ""] for (c in services.classes)]]); - } -}); - -// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/style.js b/common/content/style.js deleted file mode 100644 index 778d1a5d..00000000 --- a/common/content/style.js +++ /dev/null @@ -1,832 +0,0 @@ -// Copyright (c) 2008-2009 by Kris Maglione -// -// This work is licensed for reuse under an MIT license. Details are -// given in the LICENSE.txt file included with this file. -"use strict"; - -/** @scope modules */ - -/** - * @constant - * @property {string} The default highlighting rules. They have the - * form: - * rule ::= selector space space+ css - * selector ::= group - * | group "," css-selector - * | group "," css-selector "," scope - * group ::= groupname - * | groupname css-selector - */ -// -Highlights.prototype.CSS = * font-family: monospace; padding: 1px; - CmdOutput white-space: pre; - - CompGroup - CompGroup:not(:first-of-type) margin-top: .5em; - CompTitle color: magenta; background: white; font-weight: bold; - CompTitle>* padding: 0 .5ex; - CompMsg font-style: italic; margin-left: 16px; - CompItem - CompItem[selected] background: yellow; - CompItem>* padding: 0 .5ex; - CompIcon width: 16px; min-width: 16px; display: inline-block; margin-right: .5ex; - CompIcon>img max-width: 16px; max-height: 16px; vertical-align: middle; - CompResult width: 45%; overflow: hidden; - CompDesc color: gray; width: 50%; - CompLess text-align: center; height: 0; line-height: .5ex; padding-top: 1ex; - CompLess::after content: "\2303" /* Unicode up arrowhead */ - CompMore text-align: center; height: .5ex; line-height: .5ex; margin-bottom: -.5ex; - CompMore::after content: "\2304" /* Unicode down arrowhead */ - - Gradient height: 1px; margin-bottom: -1px; margin-top: -1px; - GradientLeft background-color: magenta; - GradientRight background-color: white; - - Indicator color: blue; - Filter font-weight: bold; - - Keyword color: red; - Tag color: blue; - - LineNr color: orange; background: white; - Question color: green; background: white; font-weight: bold; - - StatusLine color: white; background: black; - StatusLineBroken color: black; background: #FFa0a0 /* light-red */ - StatusLineSecure color: black; background: #a0a0FF /* light-blue */ - StatusLineExtended color: black; background: #a0FFa0 /* light-green */ - - TabClose,.tab-close-button - TabIcon,.tab-icon - TabText,.tab-text - TabNumber font-weight: bold; margin: 0px; padding-right: .3ex; - TabIconNumber { - font-weight: bold; - color: white; - text-align: center; - text-shadow: black -1px 0 1px, black 0 1px 1px, black 1px 0 1px, black 0 -1px 1px; - } - - Title color: magenta; background: white; font-weight: bold; - URL text-decoration: none; color: green; background: inherit; - URL:hover text-decoration: underline; cursor: pointer; - - FrameIndicator,,* { - background-color: red; - opacity: 0.5; - z-index: 999; - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - } - - Bell border: none; background-color: black; - Hint,,* { - font-family: monospace; - font-size: 10px; - font-weight: bold; - color: white; - background-color: red; - border-color: ButtonShadow; - border-width: 0px; - border-style: solid; - padding: 0px 1px 0px 1px; - } - Hint::after,,* content: attr(number); - HintElem,,* background-color: yellow; color: black; - HintActive,,* background-color: #88FF00; color: black; - HintImage,,* opacity: .5; - - Help font-size: 8pt; line-height: 1.4em; font-family: -moz-fixed; - - HelpArg color: #6A97D4; - HelpOptionalArg color: #6A97D4; - - HelpBody display: block; margin: 1em auto; max-width: 100ex; - HelpBorder,*,dactyl://help/* border-color: silver; border-width: 0px; border-style: solid; - HelpCode display: block; white-space: pre; margin-left: 2em; font-family: Terminus, Fixed, monospace; - - HelpDefault margin-right: 1ex; white-space: pre; - - HelpDescription display: block; - HelpEm,html|em,dactyl://help/* font-weight: bold; font-style: normal; - - HelpEx display: inline-block; color: #527BBD; font-weight: bold; - - HelpExample display: block; margin: 1em 0; - HelpExample::before content: "Example: "; font-weight: bold; - - HelpInfo display: block; width: 20em; margin-left: auto; - HelpInfoLabel display: inline-block; width: 6em; color: magenta; font-weight: bold; vertical-align: text-top; - HelpInfoValue display: inline-block; width: 14em; text-decoration: none; vertical-align: text-top; - - HelpItem display: block; margin: 1em 1em 1em 10em; clear: both; - - HelpKey color: #102663; - - HelpLink,html|a,dactyl://help/* text-decoration: none; - HelpLink[href]:hover text-decoration: underline; - - HelpList,html|ul,dactyl://help/* display: block; list-style: outside disc; - HelpOrderedList,html|ol,dactyl://help/* display: block; list-style: outside decimal; - HelpListItem,html|li,dactyl://help/* display: list-item; - - HelpNote color: red; font-weight: bold; - - HelpOpt color: #106326; - HelpOptInfo display: inline-block; margin-bottom: 1ex; - - HelpParagraph,html|p,dactyl://help/* display: block; margin: 1em 0em; - HelpParagraph:first-child margin-top: 0; - HelpSpec display: block; margin-left: -10em; float: left; clear: left; color: #527BBD; - - HelpString display: inline-block; color: green; font-weight: normal; vertical-align: text-top; - HelpString::before content: '"'; - HelpString::after content: '"'; - HelpString[delim]::before content: attr(delim); - HelpString[delim]::after content: attr(delim); - - HelpHead,html|h1,dactyl://help/* { - display: block; - margin: 1em 0; - padding-bottom: .2ex; - border-bottom-width: 1px; - font-size: 2em; - font-weight: bold; - color: #527BBD; - clear: both; - } - HelpSubhead,html|h2,dactyl://help/* { - display: block; - margin: 1em 0; - padding-bottom: .2ex; - border-bottom-width: 1px; - font-size: 1.2em; - font-weight: bold; - color: #527BBD; - clear: both; - } - HelpSubsubhead,html|h3,dactyl://help/* { - display: block; - margin: 1em 0; - padding-bottom: .2ex; - font-size: 1.1em; - font-weight: bold; - color: #527BBD; - clear: both; - } - - HelpTOC - HelpTOC>ol ol margin-left: -1em; - - HelpTab,html|dl,dactyl://help/* display: table; width: 100%; margin: 1em 0; border-bottom-width: 1px; border-top-width: 1px; padding: .5ex 0; table-layout: fixed; - HelpTabColumn,html|column,dactyl://help/* display: table-column; - HelpTabColumn:first-child width: 25%; - HelpTabTitle,html|dt,dactyl://help/* display: table-cell; padding: .1ex 1ex; font-weight: bold; - HelpTabDescription,html|dd,dactyl://help/* display: table-cell; padding: .1ex 1ex; border-width: 0px; - HelpTabRow,html|dl>html|tr,dactyl://help/* display: table-row; - - HelpTag display: inline-block; color: #527BBD; margin-left: 1ex; font-size: 8pt; font-weight: bold; - HelpTags display: block; float: right; clear: right; - HelpTopic color: #102663; - HelpType margin-right: 2ex; - - HelpWarning color: red; font-weight: bold; - - Logo - - Search,,* { - font-size: inherit; - padding: 0; - color: black; - background-color: yellow; - } - ]]>.toString(); - -/** - * A class to manage highlighting rules. The parameters are the - * standard parameters for any {@link Storage} object. - * - * @author Kris Maglione - */ -function Highlights(name, store) { - let self = this; - let highlight = {}; - let styles = storage.styles; - - const Highlight = Struct("class", "selector", "filter", "default", "value", "base"); - Highlight.defaultValue("filter", function () - this.base ? this.base.filter : - ["chrome://dactyl/*", - "dactyl:*", - "file://*"].concat(config.styleableChrome).join(",")); - Highlight.defaultValue("selector", function () self.selector(this.class)); - Highlight.defaultValue("value", function () this.default); - Highlight.defaultValue("base", function () { - let base = this.class.match(/^(\w*)/)[0]; - return base != this.class && base in highlight ? highlight[base] : null; - }); - Highlight.prototype.toString = function () "Highlight(" + this.class + ")\n\t" + [k + ": " + util.escapeString(v || "undefined") for ([k, v] in this)].join("\n\t"); - - function keys() [k for ([k, v] in Iterator(highlight))].sort(); - - this.__iterator__ = function () (highlight[v] for ([k, v] in Iterator(keys()))); - - this.get = function (k) highlight[k]; - this.set = function (key, newStyle, force, append) { - let [, class_, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/); - - if (!(class_ in highlight)) - return "Unknown highlight keyword: " + class_; - - let style = highlight[key] || Highlight(key); - styles.removeSheet(true, style.selector); - - if (append) - newStyle = (style.value || "").replace(/;?\s*$/, "; " + newStyle); - if (/^\s*$/.test(newStyle)) - newStyle = null; - if (newStyle == null) { - if (style.default == null) { - delete highlight[style.class]; - styles.removeSheet(true, style.selector); - return null; - } - newStyle = style.default; - force = true; - } - - let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;") - .replace(";!important;", ";", "g"); // Seeming Spidermonkey bug - if (!/^\s*(?:!\s*important\s*)?;*\s*$/.test(css)) { - css = style.selector + " { " + css + " }"; - - let error = styles.addSheet(true, "highlight:" + style.class, style.filter, css, true); - if (error) - return error; - } - style.value = newStyle; - highlight[style.class] = style; - return null; - }; - - /** - * Gets a CSS selector given a highlight group. - * - * @param {string} class - */ - this.selector = function (class_) { - let [, hl, rest] = class_.match(/^(\w*)(.*)/); - let pattern = "[dactyl|highlight~=" + hl + "]" - if (highlight[hl] && highlight[hl].class != class_) - pattern = highlight[hl].selector; - return pattern + rest; - }; - - /** - * Clears all highlighting rules. Rules with default values are - * reset. - */ - this.clear = function () { - for (let [k, v] in Iterator(highlight)) - this.set(k, null, true); - }; - - /** - * Bulk loads new CSS rules. - * - * @param {string} css The rules to load. See {@link Highlights#css}. - */ - this.loadCSS = function (css) { - css.replace(/^(\s*\S*\s+)\{((?:.|\n)*?)\}\s*$/gm, function (_, _1, _2) _1 + " " + _2.replace(/\n\s*/g, " ")) - .split("\n").filter(function (s) /\S/.test(s)) - .forEach(function (style) { - style = Highlight.apply(Highlight, Array.slice(style.match(/^\s*((?:[^,\s]|\s\S)+)(?:,((?:[^,\s]|\s\S)+)?)?(?:,((?:[^,\s]|\s\S)+))?\s*(.*)$/), 1)); - if (/^[>+ ]/.test(style.selector)) - style.selector = self.selector(style.class) + style.selector; - - let old = highlight[style.class]; - highlight[style.class] = style; - if (old && old.value != old.default) - style.value = old.value; - }); - for (let [class_, hl] in Iterator(highlight)) { - if (hl.value == hl.default) - this.set(class_); - } - }; - this.loadCSS(this.CSS); -} - -/** - * Manages named and unnamed user style sheets, which apply to both - * chrome and content pages. The parameters are the standard - * parameters for any {@link Storage} object. - * - * @author Kris Maglione - */ -function Styles(name, store) { - // Can't reference dactyl or Components inside Styles -- - // they're members of the window object, which disappear - // with this window. - const self = this; - const util = modules.util; - const sleep = dactyl.sleep; - const storage = modules.storage; - const ios = services.get("io"); - const sss = Cc["@mozilla.org/content/style-sheet-service;1"].getService(Ci.nsIStyleSheetService); - const namespace = "@namespace html " + XHTML.uri.quote() + ";\n" + - "@namespace xul " + XUL.uri.quote() + ";\n" + - "@namespace dactyl " + NS.uri.quote() + ";\n"; - - const Sheet = Struct("name", "id", "sites", "css", "system", "agent"); - Sheet.prototype.__defineGetter__("fullCSS", function wrapCSS() { - let filter = this.sites; - let css = this.css; - if (filter[0] == "*") - return namespace + css; - let selectors = filter.map(function (part) (/[*]$/.test(part) ? "url-prefix" : - /[\/:]/.test(part) ? "url" - : "domain") - + '("' + part.replace(/"/g, "%22").replace(/[*]$/, "") + '")') - .join(", "); - return namespace + "/* Dactyl style #" + this.id + " */ @-moz-document " + selectors + "{\n" + css + "\n}\n"; - }); - Sheet.prototype.__defineGetter__("enabled", function () this._enabled); - Sheet.prototype.__defineSetter__("enabled", function (on) { - this._enabled = Boolean(on); - if (on) { - self.registerSheet(cssUri(this.fullCSS)); - if (this.agent) - self.registerSheet(cssUri(this.fullCSS), true); - } - else { - self.unregisterSheet(cssUri(this.fullCSS)); - self.unregisterSheet(cssUri(this.fullCSS), true); - } - }); - - let cssUri = function (css) "chrome-data:text/css," + window.encodeURI(css); - - let userSheets = []; - let systemSheets = []; - let userNames = {}; - let systemNames = {}; - - let id = 0; - - this.__iterator__ = function () Iterator(userSheets.concat(systemSheets)); - this.__defineGetter__("systemSheets", function () Iterator(systemSheets)); - this.__defineGetter__("userSheets", function () Iterator(userSheets)); - this.__defineGetter__("systemNames", function () Iterator(systemNames)); - this.__defineGetter__("userNames", function () Iterator(userNames)); - - /** - * Add a new style sheet. - * - * @param {boolean} system Declares whether this is a system or - * user sheet. System sheets are used internally by - * @dactyl. - * @param {string} name The name given to the style sheet by - * which it may be later referenced. - * @param {string} filter The sites to which this sheet will - * apply. Can be a domain name or a URL. Any URL ending in - * "*" is matched as a prefix. - * @param {string} css The CSS to be applied. - */ - this.addSheet = function (system, name, filter, css, agent) { - let sheets = system ? systemSheets : userSheets; - let names = system ? systemNames : userNames; - if (name && name in names) - this.removeSheet(system, name); - - let sheet = Sheet(name, id++, filter.split(",").filter(util.identity), String(css), null, system, agent); - - try { - sheet.enabled = true; - } - catch (e) { - return e.echoerr || e; - } - sheets.push(sheet); - - if (name) - names[name] = sheet; - return null; - }; - - /** - * Get a sheet with a given name or index. - * - * @param {boolean} system - * @param {string or number} sheet The sheet to retrieve. Strings indicate - * sheet names, while numbers indicate indices. - */ - this.get = function get(system, sheet) { - let sheets = system ? systemSheets : userSheets; - let names = system ? systemNames : userNames; - if (typeof sheet === "number") - return sheets[sheet]; - return names[sheet]; - }; - - /** - * Find sheets matching the parameters. See {@link #addSheet} - * for parameters. - * - * @param {boolean} system - * @param {string} name - * @param {string} filter - * @param {string} css - * @param {number} index - */ - this.findSheets = function (system, name, filter, css, index) { - let sheets = system ? systemSheets : userSheets; - let names = system ? systemNames : userNames; - - // Grossly inefficient. - let matches = [k for ([k, v] in Iterator(sheets))]; - if (index) - matches = String(index).split(",").filter(function (i) i in sheets); - if (name) - matches = matches.filter(function (i) sheets[i] == names[name]); - if (css) - matches = matches.filter(function (i) sheets[i].css == css); - if (filter) - matches = matches.filter(function (i) sheets[i].sites.indexOf(filter) >= 0); - return matches.map(function (i) sheets[i]); - }; - - /** - * Remove a style sheet. See {@link #addSheet} for parameters. - * In cases where filter is supplied, the given filters - * are removed from matching sheets. If any remain, the sheet is - * left in place. - * - * @param {boolean} system - * @param {string} name - * @param {string} filter - * @param {string} css - * @param {number} index - */ - this.removeSheet = function (system, name, filter, css, index) { - let self = this; - if (arguments.length == 1) { - var matches = [system]; - system = matches[0].system; - } - let sheets = system ? systemSheets : userSheets; - let names = system ? systemNames : userNames; - - if (filter && filter.indexOf(",") > -1) - return filter.split(",").reduce( - function (n, f) n + self.removeSheet(system, name, f, index), 0); - - if (filter == undefined) - filter = ""; - - if (!matches) - matches = this.findSheets(system, name, filter, css, index); - if (matches.length == 0) - return null; - - for (let [, sheet] in Iterator(matches.reverse())) { - sheet.enabled = false; - if (name) - delete names[name]; - if (sheets.indexOf(sheet) > -1) - sheets.splice(sheets.indexOf(sheet), 1); - - /* Re-add if we're only changing the site filter. */ - if (filter) { - let sites = sheet.sites.filter(function (f) f != filter); - if (sites.length) - this.addSheet(system, name, sites.join(","), css, sheet.agent); - } - } - return matches.length; - }; - - /** - * Register a user style sheet at the given URI. - * - * @param {string} uri The URI of the sheet to register. - * @param {boolean} agent If true, sheet is registered as an agent sheet. - * @param {boolean} reload Whether to reload any sheets that are - * already registered. - */ - this.registerSheet = function (uri, agent, reload) { - if (reload) - this.unregisterSheet(uri, agent); - uri = ios.newURI(uri, null, null); - if (reload || !sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET)) - sss.loadAndRegisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET); - }; - - /** - * Unregister a sheet at the given URI. - * - * @param {string} uri The URI of the sheet to unregister. - */ - this.unregisterSheet = function (uri, agent) { - uri = ios.newURI(uri, null, null); - if (sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET)) - sss.unregisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET); - }; -} - -Module("styles", { - requires: ["config", "dactyl", "storage", "util"], - - init: function () { - let (array = util.Array) { - update(Styles.prototype, { - get sites() array([v.sites for ([k, v] in this.userSheets)]).flatten().uniq().__proto__, - completeSite: function (context, content) { - context.anchored = false; - try { - context.fork("current", 0, this, function (context) { - context.title = ["Current Site"]; - context.completions = [ - [content.location.host, "Current Host"], - [content.location.href, "Current URL"] - ]; - }); - } - catch (e) {} - context.fork("others", 0, this, function (context) { - context.title = ["Site"]; - context.completions = [[s, ""] for ([, s] in Iterator(styles.sites))]; - }); - } - }); - } - return storage.newObject("styles", Styles, { store: false }); - } -}, { -}, { - commands: function () { - commands.add(["sty[le]"], - "Add or list user styles", - function (args) { - let [filter, css] = args; - let name = args["-name"]; - - if (!css) { - let list = Array.concat([i for (i in styles.userNames)], - [i for (i in styles.userSheets) if (!i[1].name)]); - let str = template.tabular(["", "Name", "Filter", "CSS"], - ["min-width: 1em; text-align: center; color: red; font-weight: bold;", - "padding: 0 1em 0 1ex; vertical-align: top;", - "padding: 0 1em 0 0; vertical-align: top;"], - ([sheet.enabled ? "" : "\u00d7", - key, - sheet.sites.join(","), - sheet.css] - for ([i, [key, sheet]] in Iterator(list)) - if ((!filter || sheet.sites.indexOf(filter) >= 0) && (!name || sheet.name == name)))); - commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - } - else { - if ("-append" in args) { - let sheet = styles.get(false, name); - if (sheet) { - filter = sheet.sites.concat(filter).join(","); - css = sheet.css + " " + css; - } - } - let err = styles.addSheet(false, name, filter, css); - if (err) - dactyl.echoerr(err); - } - }, - { - bang: true, - completer: function (context, args) { - let compl = []; - if (args.completeArg == 0) - styles.completeSite(context, content); - else if (args.completeArg == 1) { - let sheet = styles.get(false, args["-name"]); - if (sheet) - context.completions = [[sheet.css, "Current Value"]]; - } - }, - hereDoc: true, - literal: 1, - options: [[["-name", "-n"], commands.OPTION_STRING, null, function () [[k, v.css] for ([k, v] in Iterator(styles.userNames))]], - [["-append", "-a"], commands.OPTION_NOARG]], - serial: function () [ - { - command: this.name, - bang: true, - options: sty.name ? { "-name": sty.name } : {}, - arguments: [sty.sites.join(",")], - literalArg: sty.css - } for ([k, sty] in styles.userSheets) - ] - }); - - [ - { - name: ["stylee[nable]", "stye[nable]"], - desc: "Enable a user style sheet", - action: function (sheet) sheet.enabled = true, - filter: function (sheet) !sheet.enabled - }, - { - name: ["styled[isable]", "styd[isable]"], - desc: "Disable a user style sheet", - action: function (sheet) sheet.enabled = false, - filter: function (sheet) sheet.enabled - }, - { - name: ["stylet[oggle]", "styt[oggle]"], - desc: "Toggle a user style sheet", - action: function (sheet) sheet.enabled = !sheet.enabled - }, - { - name: ["dels[tyle]"], - desc: "Remove a user style sheet", - action: function (sheet) styles.removeSheet(sheet) - } - ].forEach(function (cmd) { - commands.add(cmd.name, cmd.desc, - function (args) { - styles.findSheets(false, args["-name"], args[0], args.literalArg, args["-index"]) - .forEach(cmd.action); - }, - { - completer: function (context) { context.completions = styles.sites.map(function (site) [site, ""]); }, - literal: 1, - options: [[["-index", "-i"], commands.OPTION_INT, null, - function (context) { - context.compare = CompletionContext.Sort.number; - return [[i, <>{sheet.sites.join(",")}: {sheet.css.replace("\n", "\\n")}] - for ([i, sheet] in styles.userSheets) - if (!cmd.filter || cmd.filter(sheet))]; - }], - [["-name", "-n"], commands.OPTION_STRING, null, - function () [[name, sheet.css] - for ([name, sheet] in Iterator(styles.userNames)) - if (!cmd.filter || cmd.filter(sheet))]]] - }); - }); - }, - completion: function () { - JavaScript.setCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]), - [ // Prototype: (system, name, filter, css, index) - null, - function (context, obj, args) args[0] ? this.systemNames : this.userNames, - function (context, obj, args) this.completeSite(context, content), - null, - function (context, obj, args) args[0] ? this.systemSheets : this.userSheets - ]); - } -}); - -Module("highlight", { - requires: ["config", "styles"], - - init: function () { - const self = storage.newObject("highlight", Highlights, { store: false }); - - if (self.CSS != Highlights.prototype.CSS) { - self.CSS = Highlights.prototype.CSS; - self.loadCSS(self.CSS); - } - return self; - } -}, { -}, { - commands: function () { - commands.add(["colo[rscheme]"], - "Load a color scheme", - function (args) { - let scheme = args[0]; - - if (scheme == "default") - highlight.clear(); - else - dactyl.assert(io.sourceFromRuntimePath(["colors/" + scheme + ".vimp"]), - "E185: Cannot find color scheme " + scheme); - autocommands.trigger("ColorScheme", { name: scheme }); - }, - { - argCount: "1", - completer: function (context) completion.colorScheme(context) - }); - - commands.add(["hi[ghlight]"], - "Set the style of certain display elements", - function (args) { - let style = ; - let clear = args[0] == "clear"; - if (clear) - args.shift(); - - let [key, css] = args; - dactyl.assert(!(clear && css), "E488: Trailing characters"); - - if (!css && !clear) { - // List matching keys - let str = template.tabular(["Key", "Sample", "CSS"], - ["padding: 0 1em 0 0; vertical-align: top", - "text-align: center"], - ([h.class, - XXX, - template.highlightRegexp(h.value, /\b[-\w]+(?=:)/g)] - for (h in highlight) - if (!key || h.class.indexOf(key) > -1))); - commandline.echo(str, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); - return; - } - if (!key && clear) - highlight.clear(); - else { - let error = highlight.set(key, css, clear, "-append" in args); - if (error) - dactyl.echoerr(error); - } - }, - { - // TODO: add this as a standard highlight completion function? - completer: function (context, args) { - // Complete a highlight group on :hi clear ... - if (args.completeArg > 0 && args[0] == "clear") - args.completeArg = args.completeArg > 1 ? -1 : 0; - - if (args.completeArg == 0) - context.completions = [[v.class, v.value] for (v in highlight)]; - else if (args.completeArg == 1) { - let hl = highlight.get(args[0]); - if (hl) - context.completions = [[hl.value, "Current Value"], [hl.default || "", "Default Value"]]; - } - }, - hereDoc: true, - literal: 1, - options: [[["-append", "-a"], commands.OPTION_NOARG]], - serial: function () [ - { - command: this.name, - arguments: [k], - literalArg: v - } - for ([k, v] in Iterator(highlight)) - if (v.value != v.default) - ] - }); - }, - completion: function () { - completion.colorScheme = function colorScheme(context) { - context.title = ["Color Scheme", "Runtime Path"]; - context.keys = { text: function (f) f.leafName.replace(/\.vimp$/, ""), description: ".parent.path" }; - context.completions = util.Array.flatten( - io.getRuntimeDirectories("colors").map( - function (dir) dir.readDirectory().filter( - function (file) /\.vimp$/.test(file.leafName)))) - - }; - - completion.highlightGroup = function highlightGroup(context) { - context.title = ["Highlight Group", "Value"]; - context.completions = [[v.class, v.value] for (v in highlight)]; - }; - } -}); - -// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/tabs.js b/common/content/tabs.js index a1b53949..efba231c 100644 --- a/common/content/tabs.js +++ b/common/content/tabs.js @@ -14,8 +14,6 @@ * @instance tabs */ const Tabs = Module("tabs", { - requires: ["config"], - init: function () { this._alternates = [config.tabbrowser.mCurrentTab, null]; @@ -187,7 +185,7 @@ const Tabs = Module("tabs", { this._groups = this._groups = iframe ? iframe.contentWindow : null; if (this._groups) while (!this._groups.TabItems) - dactyl.threadYield(false, true); + util.threadYield(false, true); return this._groups; }, @@ -196,14 +194,15 @@ const Tabs = Module("tabs", { * selected tab if index is not specified. This is a 0-based * index. * - * @param {number} index The index of the tab required. + * @param {number|Node} index The index of the tab required or the tab itself * @returns {Object} */ getTab: function (index) { + if (index instanceof Node) + return index; if (index != null) return config.tabbrowser.mTabs[index]; - else - return config.tabbrowser.mCurrentTab; + return config.tabbrowser.mCurrentTab; }, /** diff --git a/common/content/template.js b/common/content/template.js deleted file mode 100644 index ffe9ea1e..00000000 --- a/common/content/template.js +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright (c) 2008-2009 by Kris Maglione -// -// This work is licensed for reuse under an MIT license. Details are -// given in the LICENSE.txt file included with this file. -"use strict"; - -/** @scope modules */ - -const Template = Module("template", { - add: function add(a, b) a + b, - join: function join(c) function (a, b) a + c + b, - - map: function map(iter, func, sep, interruptable) { - if (iter.length) // FIXME: Kludge? - iter = array.itervalues(iter); - let ret = <>; - let n = 0; - for each (let i in Iterator(iter)) { - let val = func(i); - if (val == undefined) - continue; - if (sep && n++) - ret += sep; - if (interruptable && n % interruptable == 0) - util.threadYield(true, true); - ret += val; - } - return ret; - }, - - maybeXML: function maybeXML(xml) { - if (typeof xml == "xml") - return xml; - try { - return new XMLList(xml); - } - catch (e) {} - return <>{xml}; - }, - - completionRow: function completionRow(item, highlightGroup) { - if (typeof icon == "function") - icon = icon(); - - if (highlightGroup) { - var text = item[0] || ""; - var desc = item[1] || ""; - } - else { - var text = this.process[0].call(this, item, item.text); - var desc = this.process[1].call(this, item, item.description); - } - - // - return
- -
  • {text} 
  • -
  • {desc} 
  • -
    ; - //
    - }, - - bookmarkDescription: function (item, text) - <> - {text}  - { - !(item.extra && item.extra.length) ? "" : - - ({ - template.map(item.extra, function (e) - <>{e[0]}: {e[1]}, - <> /* Non-breaking space */) - }) - - } - , - - icon: function (item, text) { - return <>{item.icon ? : <>}{text} - }, - - filter: function (str) {str}, - - gradient: function (left, right) -
    -
    -
    -
    - - - { template.map(util.range(0, 100), function (i) - -
    ) } -
    -
    , - - // if "processStrings" is true, any passed strings will be surrounded by " and - // any line breaks are displayed as \n - highlight: function highlight(arg, processStrings, clip) { - // some objects like window.JSON or getBrowsers()._browsers need the try/catch - try { - let str = clip ? util.clip(String(arg), clip) : String(arg); - switch (arg == null ? "undefined" : typeof arg) { - case "number": - return {str}; - case "string": - if (processStrings) - str = str.quote(); - return {str}; - case "boolean": - return {str}; - case "function": - // Vim generally doesn't like /foo*/, because */ looks like a comment terminator. - // Using /foo*(:?)/ instead. - if (processStrings) - return {str.replace(/\{(.|\n)*(?:)/g, "{ ... }")}; - <>}; /* Vim */ - return <>{arg}; - case "undefined": - return {arg}; - case "object": - // for java packages value.toString() would crash so badly - // that we cannot even try/catch it - if (/^\[JavaPackage.*\]$/.test(arg)) - return <>[JavaPackage]; - if (processStrings && false) - str = template.highlightFilter(str, "\n", function () ^J); - return {str}; - case "xml": - return arg; - default: - return ]]>; - } - } - catch (e) { - return ]]>; - } - }, - - highlightFilter: function highlightFilter(str, filter, highlight) { - return this.highlightSubstrings(str, (function () { - if (filter.length == 0) - return; - let lcstr = String.toLowerCase(str); - let lcfilter = filter.toLowerCase(); - let start = 0; - while ((start = lcstr.indexOf(lcfilter, start)) > -1) { - yield [start, filter.length]; - start += filter.length; - } - })(), highlight || template.filter); - }, - - highlightRegexp: function highlightRegexp(str, re, highlight) { - return this.highlightSubstrings(str, (function () { - let res; - while ((res = re.exec(str)) && res[0].length) - yield [res.index, res[0].length]; - })(), highlight || template.filter); - }, - - highlightSubstrings: function highlightSubstrings(str, iter, highlight) { - if (typeof str == "xml") - return str; - if (str == "") - return <>{str}; - - str = String(str).replace(" ", "\u00a0"); - let s = <>; - let start = 0; - let n = 0; - for (let [i, length] in iter) { - if (n++ > 50) // Prevent infinite loops. - return s + <>{str.substr(start)}; - XML.ignoreWhitespace = false; - s += <>{str.substring(start, i)}; - s += highlight(str.substr(i, length)); - start = i + length; - } - return s + <>{str.substr(start)}; - }, - - highlightURL: function highlightURL(str, force) { - if (force || /^[a-zA-Z]+:\/\//.test(str)) - return {str}; - else - return str; - }, - - commandOutput: function generic(command, xml) { - return <>:{command}
    {xml}; - }, - - genericTable: function genericTable(items, format) { - completion.listCompleter(function (context) { - context.filterFunc = null; - if (format) - context.format = format; - context.completions = items; - }); - }, - - jumps: function jumps(index, elems) { - // - return this.commandOutput( - - - - - { - this.map(Iterator(elems), function ([idx, val]) - - - - - - ) - } -
    jumptitleURI
    {idx == index ? ">" : ""}{Math.abs(idx - index)}{val.title}{val.URI.spec}
    ); - //
    - }, - - options: function options(title, opts) { - // - return this.commandOutput( - - - - - { - this.map(opts, function (opt) - - - ) - } -
    --- {title} ---
    - {opt.pre}{opt.name}{opt.value} - {opt.isDefault || opt.default == null ? "" : (default: {opt.default})} -
    ); - //
    - }, - - table: function table(title, data, indent) { - let table = - // - - - - - { - this.map(data, function (datum) - - - - ) - } -
    {title}
    {datum[0]}{template.maybeXML(datum[1])}
    ; - //
    - if (table.tr.length() > 1) - return table; - return XML(); - }, - - tabular: function tabular(headings, style, iter) { - // TODO: This might be mind-bogglingly slow. We'll see. - // - return this.commandOutput( - - - { - this.map(headings, function (h) - ) - } - - { - this.map(iter, function (row) - - { - template.map(Iterator(row), function ([i, d]) - ) - } - ) - } -
    {h}
    {d}
    ); - //
    - }, - - usage: function usage(iter) { - // - return this.commandOutput( - - { - this.map(iter, function (item) - - - - ) - } -
    {item.name || item.names[0]}{item.description}
    ); - //
    - } -}); - -// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/content/util.js b/common/content/util.js deleted file mode 100644 index a0ae92c7..00000000 --- a/common/content/util.js +++ /dev/null @@ -1,715 +0,0 @@ -// Copyright (c) 2006-2008 by Martin Stubenschrott -// Copyright (c) 2007-2009 by Doug Kearns -// Copyright (c) 2008-2009 by Kris Maglione -// -// This work is licensed for reuse under an MIT license. Details are -// given in the LICENSE.txt file included with this file. -"use strict"; - -/** @scope modules */ - -const XHTML = Namespace("html", "http://www.w3.org/1999/xhtml"); -const XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); -const NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator"); -default xml namespace = XHTML; - -const Util = Module("util", { - init: function () { - this.Array = array; - }, - - get activeWindow() services.get("windowWatcher").activeWindow, - callInMainThread: function (callback, self) { - let mainThread = services.get("threadManager").mainThread; - if (!services.get("threadManager").isMainThread) - mainThread.dispatch({ run: callback.call(self) }, mainThread.DISPATCH_NORMAL); - else - callback.call(self); - }, - - /** - * Returns a shallow copy of obj. - * - * @param {Object} obj - * @returns {Object} - */ - cloneObject: function cloneObject(obj) { - if (isarray(obj)) - return obj.slice(); - let newObj = {}; - for (let [k, v] in Iterator(obj)) - newObj[k] = v; - return newObj; - }, - - /** - * Clips a string to a given length. If the input string is longer - * than length, an ellipsis is appended. - * - * @param {string} str The string to truncate. - * @param {number} length The length of the returned string. - * @returns {string} - */ - clip: function clip(str, length) { - return str.length <= length ? str : str.substr(0, length - 3) + "..."; - }, - - /** - * Compares two strings, case insensitively. Return values are as - * in String#localeCompare. - * - * @param {string} a - * @param {string} b - * @returns {number} - */ - compareIgnoreCase: function compareIgnoreCase(a, b) String.localeCompare(a.toLowerCase(), b.toLowerCase()), - - /** - * Returns an object representing a Node's computed CSS style. - * - * @param {Node} node - * @returns {Object} - */ - computedStyle: function computedStyle(node) { - while (node instanceof Ci.nsIDOMText && node.parentNode) - node = node.parentNode; - return node.ownerDocument.defaultView.getComputedStyle(node, null); - }, - - /** - * Copies a string to the system clipboard. If verbose is specified - * the copied string is also echoed to the command line. - * - * @param {string} str - * @param {boolean} verbose - */ - copyToClipboard: function copyToClipboard(str, verbose) { - const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); - clipboardHelper.copyString(str); - - if (verbose) - dactyl.echo("Yanked " + str, commandline.FORCE_SINGLELINE); - }, - - /** - * Converts any arbitrary string into an URI object. - * - * @param {string} str - * @returns {Object} - */ - // FIXME: newURI needed too? - createURI: function createURI(str) { - const fixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup); - return fixup.createFixupURI(str, fixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP); - }, - - /** - * Expands brace globbing patterns in a string. - * - * Example: - * "a{b,c}d" => ["abd", "acd"] - * - * @param {string} pattern The pattern to deglob. - * @returns [string] The resulting strings. - */ - debrace: function deglobBrace(pattern) { - function split(pattern, re, fn, dequote) { - let end = 0, match, res = []; - while (match = re.exec(pattern)) { - end = match.index + match[0].length; - res.push(match[1]); - if (fn) - fn(match); - } - res.push(pattern.substr(end)); - return res.map(function (s) util.dequote(s, dequote)); - } - let patterns = [], res = []; - let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy, - function (match) { - patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy, - null, ",{}")); - }, "{}"); - function rec(acc) { - if (acc.length == patterns.length) - res.push(util.Array.zip(substrings, acc).join("")); - else - for (let [, pattern] in Iterator(patterns[acc.length])) - rec(acc.concat(pattern)); - } - rec([]); - return res; - }, - - /** - * Removes certain backslash-quoted characters while leaving other - * backslash-quoting sequences untouched. - * - * @param {string} pattern The string to unquote. - * @param {string} chars The characters to unquote. - * @returns {string} - */ - dequote: function dequote(pattern, chars) - pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0), - - /** - * Converts HTML special characters in str to the equivalent HTML - * entities. - * - * @param {string} str - * @returns {string} - */ - escapeHTML: function escapeHTML(str) { - return str.replace(/&/g, "&").replace(/str. - * - * @param {string} str - * @returns {string} - */ - escapeRegex: function escapeRegex(str) { - return str.replace(/([\\{}()[\].?*+])/g, "\\$1"); - }, - - /** - * Escapes quotes, newline and tab characters in str. The returned - * string is delimited by delimiter or " if delimiter is not - * specified. {@see String#quote}. - * - * @param {string} str - * @param {string} delimiter - * @returns {string} - */ - escapeString: function escapeString(str, delimiter) { - if (delimiter == undefined) - delimiter = '"'; - return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter; - }, - - /** - * Evaluates an XPath expression in the current or provided - * document. It provides the xhtml, xhtml2 and dactyl XML - * namespaces. The result may be used as an iterator. - * - * @param {string} expression The XPath expression to evaluate. - * @param {Document} doc The document to evaluate the expression in. - * @default The current document. - * @param {Node} elem The context element. - * @default doc - * @param {boolean} asIterator Whether to return the results as an - * XPath iterator. - */ - evaluateXPath: function (expression, doc, elem, asIterator) { - if (!doc) - doc = content.document; - if (!elem) - elem = doc; - if (isarray(expression)) - expression = util.makeXPath(expression); - - let result = doc.evaluate(expression, elem, - function lookupNamespaceURI(prefix) { - return { - xul: XUL.uri, - xhtml: XHTML.uri, - xhtml2: "http://www.w3.org/2002/06/xhtml2", - dactyl: NS.uri - }[prefix] || null; - }, - asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE, - null - ); - - return { - __proto__: result, - __iterator__: asIterator - ? function () { let elem; while ((elem = this.iterateNext())) yield elem; } - : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); } - } - }, - - extend: function extend(dest) { - Array.slice(arguments, 1).filter(util.identity).forEach(function (src) { - for (let [k, v] in Iterator(src)) { - let get = src.__lookupGetter__(k), - set = src.__lookupSetter__(k); - if (!get && !set) - dest[k] = v; - if (get) - dest.__defineGetter__(k, get); - if (set) - dest.__defineSetter__(k, set); - } - }); - return dest; - }, - - /** - * Returns the selection controller for the given window. - * - * @param {Window} window - * @returns {nsISelectionController} - */ - selectionController: function (win) - win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell) - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsISelectionDisplay) - .QueryInterface(Ci.nsISelectionController), - - /** - * Converts bytes to a pretty printed data size string. - * - * @param {number} bytes The number of bytes. - * @param {string} decimalPlaces The number of decimal places to use if - * humanReadable is true. - * @param {boolean} humanReadable Use byte multiples. - * @returns {string} - */ - formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable) { - const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; - let unitIndex = 0; - let tmpNum = parseInt(bytes, 10) || 0; - let strNum = [tmpNum + ""]; - - if (humanReadable) { - while (tmpNum >= 1024) { - tmpNum /= 1024; - if (++unitIndex > (unitVal.length - 1)) - break; - } - - let decPower = Math.pow(10, decimalPlaces); - strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2); - - if (!strNum[1]) - strNum[1] = ""; - - while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces) - strNum[1] += "0"; - } - - for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000 - strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u); - - if (unitIndex) // decimalPlaces only when > Bytes - strNum[0] += "." + strNum[1]; - - return strNum[0] + " " + unitVal[unitIndex]; - }, - - /** - * Sends a synchronous or asynchronous HTTP request to url and - * returns the XMLHttpRequest object. If callback is specified the - * request is asynchronous and the callback is invoked with the - * object as its argument. - * - * @param {string} url - * @param {function(XMLHttpRequest)} callback - * @returns {XMLHttpRequest} - */ - httpGet: function httpGet(url, callback) { - try { - let xmlhttp = services.create("xmlhttp"); - xmlhttp.mozBackgroundRequest = true; - if (callback) { - xmlhttp.onreadystatechange = function () { - if (xmlhttp.readyState == 4) - callback(xmlhttp); - }; - } - xmlhttp.open("GET", url, !!callback); - xmlhttp.send(null); - return xmlhttp; - } - catch (e) { - dactyl.log("Error opening " + String.quote(url) + ": " + e, 1); - return null; - } - }, - - /** - * The identity function. - * - * @param {Object} k - * @returns {Object} - */ - identity: function identity(k) k, - - /** - * Returns the intersection of two rectangles. - * - * @param {Object} r1 - * @param {Object} r2 - * @returns {Object} - */ - intersection: function (r1, r2) ({ - get width() this.right - this.left, - get height() this.bottom - this.top, - left: Math.max(r1.left, r2.left), - right: Math.min(r1.right, r2.right), - top: Math.max(r1.top, r2.top), - bottom: Math.min(r1.bottom, r2.bottom) - }), - - /** - * Returns an XPath union expression constructed from the specified node - * tests. An expression is built with node tests for both the null and - * XHTML namespaces. See {@link Buffer#evaluateXPath}. - * - * @param nodes {Array(string)} - * @returns {string} - */ - makeXPath: function makeXPath(nodes) { - return util.Array(nodes).map(util.debrace).flatten() - .map(function (node) [node, "xhtml:" + node]).flatten() - .map(function (node) "//" + node).join(" | "); - }, - - /** - * Returns the array that results from applying func to each - * property of obj. - * - * @param {Object} obj - * @param {function} func - * @returns {Array} - */ - map: function map(obj, func) { - let ary = []; - for (let i in Iterator(obj)) - ary.push(func(i)); - return ary; - }, - - /** - * Memoize the lookup of a property in an object. - * - * @param {object} obj The object to alter. - * @param {string} key The name of the property to memoize. - * @param {function} getter A function of zero to two arguments which - * will return the property's value. obj is - * passed as the first argument, key as the - * second. - */ - memoize: memoize, - - /** - * Converts a URI string into a URI object. - * - * @param {string} uri - * @returns {nsIURI} - */ - // FIXME: createURI needed too? - newURI: function (uri) { - return services.get("io").newURI(uri, null, null); - }, - - /** - * Pretty print a JavaScript object. Use HTML markup to color certain items - * if color is true. - * - * @param {Object} object The object to pretty print. - * @param {boolean} color Whether the output should be colored. - * @returns {string} - */ - objectToString: function objectToString(object, color) { - // Use E4X literals so html is automatically quoted - // only when it's asked for. No one wants to see < - // on their console or :map :foo in their buffer - // when they expect :map :foo. - XML.prettyPrinting = false; - XML.ignoreWhitespace = false; - - if (object === null) - return "null\n"; - - if (typeof object != "object") - return false; - - const NAMESPACES = util.Array.toObject([ - [NS, 'dactyl'], - [XHTML, 'html'], - [XUL, 'xul'] - ]); - if (object instanceof Ci.nsIDOMElement) { - let elem = object; - if (elem.nodeType == elem.TEXT_NODE) - return elem.data; - function namespaced(node) { - var ns = NAMESPACES[node.namespaceURI]; - if (ns) - return ns + ":" + node.localName; - return node.localName.toLowerCase(); - } - try { - let tag = "<" + [namespaced(elem)].concat( - [namespaced(a) + "=" + template.highlight(a.value, true) - for ([i, a] in util.Array.iteritems(elem.attributes))]).join(" "); - - if (!elem.firstChild || /^\s*$/.test(elem.firstChild) && !elem.firstChild.nextSibling) - tag += '/>'; - else - tag += '>...'; - return tag; - } - catch (e) { - return {}.toString.call(elem); - } - } - - try { // for window.JSON - var obj = String(object); - } - catch (e) { - obj = "[Object]"; - } - obj = template.highlightFilter(util.clip(obj, 150), "\n", !color ? function () "^J" : function () ^J); - let string = <>{obj}::
    ; - - let keys = []; - try { // window.content often does not want to be queried with "var i in object" - let hasValue = !("__iterator__" in object); - /* - if (modules.isPrototypeOf(object)) { - object = Iterator(object); - hasValue = false; - } - */ - for (let i in object) { - let value = ]]>; - try { - value = object[i]; - } - catch (e) {} - if (!hasValue) { - if (isarray(i) && i.length == 2) - [i, value] = i; - else - var noVal = true; - } - - value = template.highlight(value, true, 150); - let key = {i}; - if (!isNaN(i)) - i = parseInt(i); - else if (/^[A-Z_]+$/.test(i)) - i = ""; - keys.push([i, <>{key}{noVal ? "" : <>: {value}}
    ]); - } - } - catch (e) {} - - function compare(a, b) { - if (!isNaN(a[0]) && !isNaN(b[0])) - return a[0] - b[0]; - return String.localeCompare(a[0], b[0]); - } - string += template.map(keys.sort(compare), function (f) f[1]); - return color ? string : [s for each (s in string)].join(""); - }, - - /** - * A generator that returns the values between start and end, - * in step increments. - * - * @param {number} start The interval's start value. - * @param {number} end The interval's end value. - * @param {boolean} step The value to step the range by. May be - * negative. @default 1 - * @returns {Iterator(Object)} - */ - range: function range(start, end, step) { - if (!step) - step = 1; - if (step > 0) { - for (; start < end; start += step) - yield start; - } - else { - while (start > end) - yield start += step; - } - }, - - /** - * An interruptible generator that returns all values between start - * and end. The thread yields every time milliseconds. - * - * @param {number} start The interval's start value. - * @param {number} end The interval's end value. - * @param {number} time The time in milliseconds between thread yields. - * @returns {Iterator(Object)} - */ - interruptibleRange: function interruptibleRange(start, end, time) { - let endTime = Date.now() + time; - while (start < end) { - if (Date.now() > endTime) { - util.threadYield(true, true); - endTime = Date.now() + time; - } - yield start++; - } - }, - - /** - * Reads a string from the system clipboard. - * - * This is same as Firefox's readFromClipboard function, but is needed for - * apps like Thunderbird which do not provide it. - * - * @returns {string} - */ - readFromClipboard: function readFromClipboard() { - let str; - - try { - const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); - const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); - - transferable.addDataFlavor("text/unicode"); - - if (clipboard.supportsSelectionClipboard()) - clipboard.getData(transferable, clipboard.kSelectionClipboard); - else - clipboard.getData(transferable, clipboard.kGlobalClipboard); - - let data = {}; - let dataLen = {}; - - transferable.getTransferData("text/unicode", data, dataLen); - - if (data) { - data = data.value.QueryInterface(Ci.nsISupportsString); - str = data.data.substring(0, dataLen.value / 2); - } - } - catch (e) {} - - return str; - }, - - /** - * Scrolls an element into view if and only if it's not already - * fully visible. - * - * @param {Node} elem The element to make visible. - */ - scrollIntoView: function scrollIntoView(elem) { - let win = elem.ownerDocument.defaultView; - let rect = elem.getBoundingClientRect(); - if (!(rect && rect.top < win.innerHeight && rect.bottom >= 0 && rect.left < win.innerWidth && rect.right >= 0)) - elem.scrollIntoView(); - }, - - sleep: function (delay) { - let mainThread = services.get("threadManager").mainThread; - - let end = Date.now() + delay; - while (Date.now() < end) - mainThread.processNextEvent(true); - return true; - }, - - /** - * Split a string on literal occurrences of a marker. - * - * Specifically this ignores occurrences preceded by a backslash, or - * contained within 'single' or "double" quotes. - * - * It assumes backslash escaping on strings, and will thus not count quotes - * that are preceded by a backslash or within other quotes as starting or - * ending quoted sections of the string. - * - * @param {string} str - * @param {RegExp} marker - */ - splitLiteral: function splitLiteral(str, marker) { - let results = []; - let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source); - let cont = true; - - while (cont) { - cont = false; - str = str.replace(resep, function (match, before) { - results.push(before); - cont = true; - return ""; - }); - } - - results.push(str); - return results; - }, - - threadYield: function (flush, interruptable) { - let mainThread = services.get("threadManager").mainThread; - /* FIXME */ - util.interrupted = false; - do { - mainThread.processNextEvent(!flush); - if (util.interrupted) - throw new Error("Interrupted"); - } - while (flush === true && mainThread.hasPendingEvents()); - }, - - /** - * Converts an E4X XML literal to a DOM node. - * - * @param {Node} node - * @param {Document} doc - * @param {Object} nodes If present, nodes with the "key" attribute are - * stored here, keyed to the value thereof. - * @returns {Node} - */ - xmlToDom: function xmlToDom(node, doc, nodes) { - XML.prettyPrinting = false; - if (node.length() != 1) { - let domnode = doc.createDocumentFragment(); - for each (let child in node) - domnode.appendChild(arguments.callee(child, doc, nodes)); - return domnode; - } - switch (node.nodeKind()) { - case "text": - return doc.createTextNode(String(node)); - case "element": - let domnode = doc.createElementNS(node.namespace(), node.localName()); - for each (let attr in node.@*) - domnode.setAttributeNS(attr.name() == "highlight" ? NS.uri : attr.namespace(), attr.name(), String(attr)); - for each (let child in node.*) - domnode.appendChild(arguments.callee(child, doc, nodes)); - if (nodes && node.@key) - nodes[node.@key] = domnode; - return domnode; - default: - return null; - } - } -}, { - Array: array -}); - -/** - * Math utility methods. - * @singleton - */ -var Math = { - __proto__: window.Math, - - /** - * Returns the specified value constrained to the range min - - * max. - * - * @param {number} value The value to constrain. - * @param {number} min The minimum constraint. - * @param {number} max The maximum constraint. - * @returns {number} - */ - constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max) -}; - -// vim: set fdm=marker sw=4 ts=4 et: diff --git a/common/modules/base.jsm b/common/modules/base.jsm new file mode 100644 index 00000000..20b22715 --- /dev/null +++ b/common/modules/base.jsm @@ -0,0 +1,798 @@ +// Copyright (c) 2009 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +let use = {}; +let loaded = {}; +let currentModule; +function defmodule(name, module, params) { + module.NAME = name; + module.EXPORTED_SYMBOLS = params.exports || []; + dump("defmodule " + name + "\n"); + for(let [, mod] in Iterator(params.require || [])) + require(module, mod); + + for(let [, mod] in Iterator(params.use || [])) + if (loaded.hasOwnProperty(mod)) + require(module, mod, "use"); + else { + use[mod] = use[mod] || []; + use[mod].push(module); + } + currentModule = module; +} +defmodule.modules = []; + +function endmodule() { + dump("endmodule " + currentModule.NAME + "\n"); + loaded[currentModule.NAME] = 1; + for(let [, mod] in Iterator(use[currentModule.NAME] || [])) + require(mod, currentModule.NAME, "use"); +} + +function require(obj, name, from) { + try { + dump((from || "require") + ": loading " + name + " into " + obj.NAME + "\n"); + Cu.import("resource://dactyl/" + name + ".jsm", obj); + } + catch (e) { + dump("loading " + String.quote("resource://dactyl/" + name + ".jsm") + "\n"); + dump(" " + e.fileName + ":" + e.lineNumber + ": " + e +"\n"); + } +} + +defmodule("base", this, { + // sed -n 's/^(const|function) ([a-zA-Z0-9_]+).*/ "\2",/p' base.jsm | sort | fmt + exports: [ + "Cc", "Ci", "Class", "Cr", "Cu", "Module", "Object", "Runnable", + "Struct", "StructBase", "Timer", "allkeys", "array", "call", + "callable", "curry", "debuggerProperties", "defmodule", "dict", + "endmodule", "extend", "foreach", "isarray", "isgenerator", + "isinstance", "isobject", "isstring", "issubclass", "iter", "memoize", + "properties", "requiresMainThread", "set", "update", "values", + ], + use: ["services"] +}); + +function Runnable(self, func, args) { + return { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRunnable]), + run: function () { func.apply(self, args || []); } + }; +} + +function allkeys(obj) { + let ret = {}; + try { + for (; obj; obj = obj.__proto__) { + services.get("debugger").wrapValue(obj).getProperties(ret, {}); + for (let prop in values(ret.value)) + yield prop.name.stringValue; + } + return; + } + catch (e) {} + + let __iterator__ = obj.__iterator__; + try { + if ("__iterator__" in obj) { + yield "__iterator__"; + delete obj.__iterator__; + } + for (let k in obj) + yield k; + } + finally { + if (__iterator__) + obj.__iterator__ = __iterator__; + } +} + +function debuggerProperties(obj) { + if (loaded.services && services.get("debugger").isOn) { + let ret = {}; + services.get("debugger").wrapValue(obj).getProperties(ret, {}); + return ret.value; + } +} + +if (!Object.keys) + Object.keys = function keys(obj) [k for (k in obj) if (obj.hasOwnProperty(k))]; + +if (!Object.getOwnPropertyNames) + Object.getOwnPropertyNames = function getOwnPropertyNames(obj) { + let res = debuggerProperties(obj); + if (res) + return [prop.name.stringValue for (prop in values(res))]; + return Object.keys(obj); + } + +function properties(obj, prototypes) { + let orig = obj; + let seen = {}; + for (; obj; obj = prototypes && obj.__proto__) { + try { + var iter = values(Object.getOwnPropertyNames(obj)); + } + catch (e) { + iter = (prop.name.stringValue for (prop in values(debuggerProperties(obj)))); + } + for (let key in iter) + if (!prototypes || !set.add(seen, key) && obj != orig) + yield key + } +} + +function values(obj) { + for (var k in obj) + if (obj.hasOwnProperty(k)) + yield obj[k]; +} +function foreach(iter, fn, self) { + for (let val in iter) + fn.call(self, val); +} + +function dict(ary) { + var obj = {}; + for (var i = 0; i < ary.length; i++) { + var val = ary[i]; + obj[val[0]] = val[1]; + } + return obj; +} + +function set(ary) { + let obj = {}; + if (ary) + for (var i = 0; i < ary.length; i++) + obj[ary[i]] = true; + return obj; +} +set.add = function (set, key) { + let res = this.has(set, key); + set[key] = true; + return res; +} +set.has = function (set, key) Object.prototype.hasOwnProperty.call(set, key); +set.remove = function (set, key) { delete set[key]; } + +function iter(obj) { + if (obj instanceof Ci.nsISimpleEnumerator) + return (function () { + while (obj.hasMoreElements()) + yield obj.getNext(); + })(); + if (isinstance(obj, [Ci.nsIStringEnumerator, Ci.nsIUTF8StringEnumerator])) + return (function () { + while (obj.hasMore()) + yield obj.getNext(); + })(); + if (isinstance(obj, Ci.nsIDOMNodeIterator)) + return (function () { + try { + while (true) + yield obj.nextNode(); + } + catch (e) {} + })(); + if (isinstance(obj, [Ci.nsIDOMHTMLCollection, Ci.nsIDOMNodeList])) + return array.iteritems(obj); + if (obj instanceof Ci.nsIDOMNamedNodeMap) + return (function () { + for (let i = 0; i < obj.length; i++) + yield [obj.name, obj]; + })(); + return Iterator(obj); +} + +function issubclass(targ, src) { + return src === targ || + targ && typeof targ === "function" && targ.prototype instanceof src; +} + +function isinstance(targ, src) { + const types = { + boolean: Boolean, + string: String, + function: Function, + number: Number + } + src = Array.concat(src); + for (var i = 0; i < src.length; i++) { + if (typeof src[i] == "string") { + if (Object.prototype.toString.call(targ) == "[object " + src[i] + "]") + return true; + } + else { + if (targ instanceof src[i]) + return true; + var type = types[typeof targ]; + if (type && issubclass(src[i], type)) + return true; + } + } + return false; +} + +function isobject(obj) { + return typeof obj === "object" && obj != null; +} + +/** + * Returns true if and only if its sole argument is an + * instance of the builtin Array type. The array may come from + * any window, frame, namespace, or execution context, which + * is not the case when using (obj instanceof Array). + */ +function isarray(val) { + return Object.prototype.toString.call(val) == "[object Array]"; +} + +/** + * Returns true if and only if its sole argument is an + * instance of the builtin Generator type. This includes + * functions containing the 'yield' statement and generator + * statements such as (x for (x in obj)). + */ +function isgenerator(val) { + return Object.prototype.toString.call(val) == "[object Generator]"; +} + +/** + * Returns true if and only if its sole argument is a String, + * as defined by the builtin type. May be constructed via + * String(foo) or new String(foo) from any window, frame, + * namespace, or execution context, which is not the case when + * using (obj instanceof String) or (typeof obj == "string"). + */ +function isstring(val) { + return Object.prototype.toString.call(val) == "[object String]"; +} + +/** + * Returns true if and only if its sole argument may be called + * as a function. This includes classes and function objects. + */ +function callable(val) { + return typeof val === "function"; +} + +function call(fn) { + fn.apply(arguments[1], Array.slice(arguments, 2)); + return fn; +} + +function memoize(obj, key, getter) { + obj.__defineGetter__(key, function () { + delete obj[key]; + return obj[key] = getter(obj, key); + }); +} + +/** + * Curries a function to the given number of arguments. Each + * call of the resulting function returns a new function. When + * a call does not contain enough arguments to satisfy the + * required number, the resulting function is another curried + * function with previous arguments accumulated. + * + * function foo(a, b, c) [a, b, c].join(" "); + * curry(foo)(1, 2, 3) -> "1 2 3"; + * curry(foo)(4)(5, 6) -> "4 5 6"; + * curry(foo)(4)(8)(9) -> "7 8 9"; + * + * @param {function} fn The function to curry. + * @param {integer} length The number of arguments expected. + * @default fn.length + * @optional + * @param {object} self The 'this' value for the returned function. When + * omitted, the value of 'this' from the first call to the function is + * preserved. + * @optional + */ +function curry(fn, length, self, acc) { + if (length == null) + length = fn.length; + if (length == 0) + return fn; + + // Close over function with 'this' + function close(self, fn) function () fn.apply(self, Array.slice(arguments)); + + if (acc == null) + acc = []; + + return function curried() { + let args = acc.concat(Array.slice(arguments)); + + // The curried result should preserve 'this' + if (arguments.length == 0) + return close(self || this, curried); + + if (args.length >= length) + return fn.apply(self || this, args); + + return curry(fn, length, self || this, args); + }; +} + +/** + * Wraps a function so that when called it will always run synchronously + * in the main thread. Return values are not preserved. + * + * @param {function} + * @returns {function} + */ +function requiresMainThread(callback) + function wrapper() { + let mainThread = services.get("threadManager").mainThread; + if (services.get("threadManager").isMainThread) + callback.apply(this, arguments); + else + mainThread.dispatch(Runnable(this, callback, arguments), mainThread.DISPATCH_NORMAL); + } + +/** + * Updates an object with the properties of another object. Getters + * and setters are copied as expected. Moreover, any function + * properties receive new 'supercall' and 'superapply' properties, + * which will call the identically named function in target's + * prototype. + * + * let a = { foo: function (arg) "bar " + arg } + * let b = { __proto__: a } + * update(b, { foo: function foo() foo.supercall(this, "baz") }); + * + * a.foo("foo") -> "bar foo" + * b.foo() -> "bar baz" + * + * @param {Object} target The object to update. + * @param {Object} src The source object from which to update target. + * May be provided multiple times. + * @returns {Object} Returns its updated first argument. + */ +function update(target) { + for (let i = 1; i < arguments.length; i++) { + let src = arguments[i]; + Object.getOwnPropertyNames(src || {}).forEach(function (k) { + var get = src.__lookupGetter__(k), + set = src.__lookupSetter__(k); + if (!get && !set) { + var v = src[k]; + target[k] = v; + if (target.__proto__ && callable(v)) { + v.superapply = function (self, args) { + return target.__proto__[k].apply(self, args); + }; + v.supercall = function (self) { + return v.superapply(self, Array.slice(arguments, 1)); + }; + } + } + if (get) + target.__defineGetter__(k, get); + if (set) + target.__defineSetter__(k, set); + }); + } + return target; +} + +/** + * Extends a subclass with a superclass. The subclass's + * prototype is replaced with a new object, which inherits + * from the super class's prototype, {@see update}d with the + * members of 'overrides'. + * + * @param {function} subclass + * @param {function} superclass + * @param {Object} overrides @optional + */ +function extend(subclass, superclass, overrides) { + subclass.prototype = { __proto__: superclass.prototype }; + update(subclass.prototype, overrides); + + subclass.superclass = superclass.prototype; + subclass.prototype.constructor = subclass; + subclass.prototype.__class__ = subclass; + + if (superclass.prototype.constructor === Object.prototype.constructor) + superclass.prototype.constructor = superclass; +} + +/** + * @constructor Class + * + * Constructs a new Class. Arguments marked as optional must be + * either entirely elided, or they must have the exact type + * specified. + * + * @param {string} name The class's as it will appear when toString + * is called, as well as in stack traces. + * @optional + * @param {function} base The base class for this module. May be any + * callable object. + * @optional + * @default Class + * @param {Object} prototype The prototype for instances of this + * object. The object itself is copied and not used as a prototype + * directly. + * @param {Object} classProperties The class properties for the new + * module constructor. More than one may be provided. + * @optional + * + * @returns {function} The constructor for the resulting class. + */ +function Class() { + function constructor() { + let self = { + __proto__: Constructor.prototype, + constructor: Constructor, + get closure() { + delete this.closure; + function closure(fn) function () fn.apply(self, arguments); + for (let k in this) + if (!this.__lookupGetter__(k) && callable(this[k])) + closure[k] = closure(self[k]); + return this.closure = closure; + } + }; + var res = self.init.apply(self, arguments); + return res !== undefined ? res : self; + } + + var args = Array.slice(arguments); + if (isstring(args[0])) + var name = args.shift(); + var superclass = Class; + if (callable(args[0])) + superclass = args.shift(); + + var Constructor = eval("(function " + (name || superclass.name).replace(/\W/g, "_") + + String.substr(constructor, 20) + ")"); + Constructor.__proto__ = superclass; + Constructor.name = name || superclass.name; + + if (!("init" in superclass.prototype)) { + var superc = superclass; + superclass = function Shim() {}; + extend(superclass, superc, { + init: superc + }); + } + + extend(Constructor, superclass, args[0]); + update(Constructor, args[1]); + args = args.slice(2); + Array.forEach(args, function (obj) { + if (callable(obj)) + obj = obj.prototype; + update(Constructor.prototype, obj); + }); + return Constructor; +} +if (Object.defineProperty) + Class.replaceProperty = function (obj, prop, value) { + Object.defineProperty(obj, prop, { configurable: true, enumerable: true, value: value, writable: true }); + return value; + }; +else + Class.replaceProperty = function (obj, prop, value) { + obj.__defineGetter__(prop, function () value); + obj.__defineSetter__(prop, function (val) { value = val; }); + return value; + }; +Class.toString = function () "[class " + this.name + "]"; +Class.prototype = { + /** + * Initializes new instances of this class. Called automatically + * when new instances are created. + */ + init: function () {}, + + toString: function () "[instance " + this.constructor.name + "]", + + /** + * Exactly like {@see nsIDOMWindow#setTimeout}, except that it + * preserves the value of 'this' on invocation of 'callback'. + * + * @param {function} callback The function to call after 'timeout' + * @param {number} timeout The timeout, in seconds, to wait + * before calling 'callback'. + * @returns {integer} The ID of this timeout, to be passed to + * {@see nsIDOMWindow#clearTimeout}. + */ + setTimeout: function (callback, timeout) { + const self = this; + let notify = { notify: function notify(timer) { callback.call(self) } }; + let timer = services.create("timer"); + timer.initWithCallback(notify, timeout, timer.TYPE_ONE_SHOT); + return timer; + } +}; + +/** + * Constructs a mew Module class and instantiates an instance into the current + * module global object. + * + * @param {string} name The name of the instance. + * @param {Object} prototype The instance prototype. + * @param {Object} classProperties Properties to be applied to the class constructor. + * @return {Class} + */ +function Module(name, prototype, classProperties, init) { + const module = Class(name, prototype, classProperties); + let instance = module(); + module.name = name.toLowerCase(); + instance.INIT = init || {}; + currentModule[module.name] = instance; + defmodule.modules.push(instance); + return module; +} + +/** + * @class Struct + * + * Creates a new Struct constructor, used for creating objects with + * a fixed set of named members. Each argument should be the name of + * a member in the resulting objects. These names will correspond to + * the arguments passed to the resultant constructor. Instances of + * the new struct may be treated vary much like arrays, and provide + * many of the same methods. + * + * const Point = Struct("x", "y", "z"); + * let p1 = Point(x, y, z); + * + * @returns {function} The constructor for the new Struct. + */ +function Struct() { + let args = Array.slice(arguments); + const Struct = Class("Struct", Struct_Base, { + length: args.length, + members: args + }); + args.forEach(function (name, i) { + Struct.prototype.__defineGetter__(name, function () this[i]); + Struct.prototype.__defineSetter__(name, function (val) { this[i] = val; }); + }); + return Struct; +} +let Struct_Base = Class("StructBase", Array, { + init: function () { + for (let i = 0; i < arguments.length; i++) + if (arguments[i] != undefined) + this[i] = arguments[i]; + }, + + clone: function clone() this.constructor.apply(null, this.slice()), + + // Iterator over our named members + __iterator__: function () { + let self = this; + return ([k, self[k]] for (k in values(self.members))) + } +}, { + fromArray: function (ary) { + ary.__proto__ = this.prototype; + return ary; + }, + + /** + * Sets a lazily constructed default value for a member of + * the struct. The value is constructed once, the first time + * it is accessed and memoized thereafter. + * + * @param {string} key The name of the member for which to + * provide the default value. + * @param {function} val The function which is to generate + * the default value. + */ + defaultValue: function (key, val) { + let i = this.prototype.members.indexOf(key); + this.prototype.__defineGetter__(i, function () (this[i] = val.call(this), this[i])); // Kludge for FF 3.0 + this.prototype.__defineSetter__(i, function (value) + Class.replaceProperty(this, i, value)); + } +}); + +const Timer = Class("Timer", { + init: function (minInterval, maxInterval, callback) { + this._timer = services.create("timer"); + this.callback = callback; + this.minInterval = minInterval; + this.maxInterval = maxInterval; + this.doneAt = 0; + this.latest = 0; + }, + + notify: function (timer) { + this._timer.cancel(); + this.latest = 0; + // minInterval is the time between the completion of the command and the next firing + this.doneAt = Date.now() + this.minInterval; + + try { + this.callback(this.arg); + } + finally { + this.doneAt = Date.now() + this.minInterval; + } + }, + + tell: function (arg) { + if (arguments.length > 0) + this.arg = arg; + + let now = Date.now(); + if (this.doneAt == -1) + this._timer.cancel(); + + let timeout = this.minInterval; + if (now > this.doneAt && this.doneAt > -1) + timeout = 0; + else if (this.latest) + timeout = Math.min(timeout, this.latest - now); + else + this.latest = now + this.maxInterval; + + this._timer.initWithCallback(this, Math.max(timeout, 0), this._timer.TYPE_ONE_SHOT); + this.doneAt = -1; + }, + + reset: function () { + this._timer.cancel(); + this.doneAt = 0; + }, + + flush: function () { + if (this.doneAt == -1) + this.notify(); + } +}); + +/** + * Array utility methods. + */ +const array = Class("util.Array", Array, { + init: function (ary) { + if (isgenerator(ary)) + ary = [k for (k in ary)]; + else if (ary.length) + ary = Array.slice(ary); + + return { + __proto__: ary, + __iterator__: function () this.iteritems(), + __noSuchMethod__: function (meth, args) { + var res = array[meth].apply(null, [this.__proto__].concat(args)); + + if (array.isinstance(res)) + return array(res); + return res; + }, + toString: function () this.__proto__.toString(), + concat: function () this.__proto__.concat.apply(this.__proto__, arguments), + map: function () this.__noSuchMethod__("map", Array.slice(arguments)) + }; + } +}, { + isinstance: function isinstance(obj) { + return Object.prototype.toString.call(obj) == "[object Array]"; + }, + + /** + * Converts an array to an object. As in lisp, an assoc is an + * array of key-value pairs, which maps directly to an object, + * as such: + * [["a", "b"], ["c", "d"]] -> { a: "b", c: "d" } + * + * @param {Array[]} assoc + * @... {string} 0 - Key + * @... 1 - Value + */ + toObject: function toObject(assoc) { + let obj = {}; + assoc.forEach(function ([k, v]) { obj[k] = v; }); + return obj; + }, + + /** + * Compacts an array, removing all elements that are null or undefined: + * ["foo", null, "bar", undefined] -> ["foo", "bar"] + * + * @param {Array} ary + * @returns {Array} + */ + compact: function compact(ary) ary.filter(function (item) item != null), + + /** + * Flattens an array, such that all elements of the array are + * joined into a single array: + * [["foo", ["bar"]], ["baz"], "quux"] -> ["foo", ["bar"], "baz", "quux"] + * + * @param {Array} ary + * @returns {Array} + */ + flatten: function flatten(ary) ary.length ? Array.concat.apply([], ary) : [], + + /** + * Returns an Iterator for an array's values. + * + * @param {Array} ary + * @returns {Iterator(Object)} + */ + itervalues: function itervalues(ary) { + let length = ary.length; + for (let i = 0; i < length; i++) + yield ary[i]; + }, + + /** + * Returns an Iterator for an array's indices and values. + * + * @param {Array} ary + * @returns {Iterator([{number}, {Object}])} + */ + iteritems: function iteritems(ary) { + let length = ary.length; + for (let i = 0; i < length; i++) + yield [i, ary[i]]; + }, + + /** + * Filters out all duplicates from an array. If + * unsorted is false, the array is sorted before + * duplicates are removed. + * + * @param {Array} ary + * @param {boolean} unsorted + * @returns {Array} + */ + uniq: function uniq(ary, unsorted) { + let ret = []; + if (unsorted) { + for (let [, item] in Iterator(ary)) + if (ret.indexOf(item) == -1) + ret.push(item); + } + else { + for (let [, item] in Iterator(ary.sort())) { + if (item != last || !ret.length) + ret.push(item); + var last = item; + } + } + return ret; + }, + + /** + * Zips the contents of two arrays. The resulting array is the length of + * ary1, with any shortcomings of ary2 replaced with null strings. + * + * @param {Array} ary1 + * @param {Array} ary2 + * @returns {Array} + */ + zip: function zip(ary1, ary2) { + let res = [] + for(let [i, item] in Iterator(ary1)) + res.push([item, i in ary2 ? ary2[i] : ""]); + return res; + } +}); + +endmodule(); + +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n");} + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/modules/bookmarkcache.jsm b/common/modules/bookmarkcache.jsm new file mode 100644 index 00000000..8fe022ee --- /dev/null +++ b/common/modules/bookmarkcache.jsm @@ -0,0 +1,162 @@ +// Copyright ©2008-2010 Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("bookmarkcache", this, { + exports: ["Bookmark", "BookmarkCache", "Keyword", "bookmarkcache"], + require: ["services", "util"] +}); + + +const Bookmark = Struct("url", "title", "icon", "keyword", "tags", "id"); +const Keyword = Struct("keyword", "title", "icon", "url"); +Bookmark.defaultValue("icon", function () BookmarkCache.getFavicon(this.url)); +Bookmark.prototype.__defineGetter__("extra", function () [ + ["keyword", this.keyword, "Keyword"], + ["tags", this.tags.join(", "), "Tag"] + ].filter(function (item) item[1])); + +const bookmarks = services.get("bookmarks"); +const history = services.get("history"); +const tagging = services.get("tagging"); +const name = "bookmark-cache"; + +const BookmarkCache = Module("BookmarkCache", { + init: function init() { + + bookmarks.addObserver(this, false); + }, + + __iterator__: function () (val for ([, val] in Iterator(self.bookmarks))), + + get bookmarks() Class.replaceProperty(this, "bookmarks", this.load()), + + rootFolders: ["toolbarFolder", "bookmarksMenuFolder", "unfiledBookmarksFolder"] + .map(function (s) bookmarks[s]), + + _deleteBookmark: function deleteBookmark(id) { + let length = bookmarks.length; + bookmarks = bookmarks.filter(function (item) item.id != id); + return bookmarks.length < length; + }, + + _loadBookmark: function loadBookmark(node) { + if (node.uri == null) // How does this happen? + return false; + let uri = util.newURI(node.uri); + let keyword = bookmarks.getKeywordForBookmark(node.itemId); + let tags = tagging.getTagsForURI(uri, {}) || []; + return Bookmark(node.uri, node.title, node.icon && node.icon.spec, keyword, tags, node.itemId); + }, + + readBookmark: function readBookmark(id) { + return { + itemId: id, + uri: bookmarks.getBookmarkURI(id).spec, + title: bookmarks.getItemTitle(id) + }; + }, + + findRoot: function findRoot(id) { + do { + var root = id; + id = bookmarks.getFolderIdForItem(id); + } while (id != bookmarks.placesRoot && id != root); + return root; + }, + + isBookmark: function (id) this.rootFolders.indexOf(this.findRoot(id)) >= 0, + + isRegularBookmark: function isRegularBookmark(id) { + do { + var root = id; + if (services.get("livemark") && services.get("livemark").isLivemark(id)) + return false; + id = bookmarks.getFolderIdForItem(id); + } while (id != bookmarks.placesRoot && id != root); + return this.rootFolders.indexOf(root) >= 0; + }, + + get keywords() [Keyword(k.keyword, k.title, k.icon, k.url) for ([, k] in Iterator(this.bookmarks)) if (k.keyword)], + + // Should be made thread safe. + load: function load() { + let bookmarks = []; + + let folders = this.rootFolders.slice(); + let query = history.getNewQuery(); + let options = history.getNewQueryOptions(); + while (folders.length > 0) { + query.setFolders(folders, 1); + folders.shift(); + let result = history.executeQuery(query, options); + let folder = result.root; + folder.containerOpen = true; + + // iterate over the immediate children of this folder + for (let i = 0; i < folder.childCount; i++) { + let node = folder.getChild(i); + if (node.type == node.RESULT_TYPE_FOLDER) // folder + folders.push(node.itemId); + else if (node.type == node.RESULT_TYPE_URI) // bookmark + bookmarks.push(this._loadBookmark(node)); + } + + // close a container after using it! + folder.containerOpen = false; + } + + return bookmarks; + }, + + onBeginUpdateBatch: function onBeginUpdateBatch() {}, + onEndUpdateBatch: function onEndUpdateBatch() {}, + onItemVisited: function onItemVisited() {}, + onItemMoved: function onItemMoved() {}, + onItemAdded: function onItemAdded(itemId, folder, index) { + if (bookmarks.getItemType(itemId) == bookmarks.TYPE_BOOKMARK) { + if (self.isBookmark(itemId)) { + let bmark = this._loadBookmark(this.readBookmark(itemId)); + this.bookmarks.push(bmark); + storage.fireEvent(name, "add", bmark); + } + } + }, + onItemRemoved: function onItemRemoved(itemId, folder, index) { + if (this._deleteBookmark(itemId)) + storage.fireEvent(name, "remove", itemId); + }, + onItemChanged: function onItemChanged(itemId, property, isAnnotation, value) { + if (isAnnotation) + return; + let bookmark = bookmarks.filter(function (item) item.id == itemId)[0]; + if (bookmark) { + if (property == "tags") + value = tagging.getTagsForURI(util.newURI(bookmark.url), {}); + if (property in bookmark) + bookmark[property] = value; + storage.fireEvent(name, "change", itemId); + } + }, + QueryInterface: function QueryInterface(iid) { + if (iid.equals(Ci.nsINavBookmarkObserver) || iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NO_INTERFACE; + } +}, { + getFavicon: function getFavicon(uri) { + try { + return service.get("favicon").getFaviconImageForPage(util.newURI(uri)).spec; + } + catch (e) { + return ""; + } + } +}); + +endmodule(); + +// vim: set fdm=marker sw=4 sts=4 et ft=javascript: diff --git a/common/modules/highlight.jsm b/common/modules/highlight.jsm new file mode 100644 index 00000000..fa184d0e --- /dev/null +++ b/common/modules/highlight.jsm @@ -0,0 +1,241 @@ +// Copyright (c) 2008-2010 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("highlight", this, { + exports: ["Highlight", "Highlights", "highlight"], + require: ["services", "styles"], + use: ["template"] +}); + +const Highlight = Struct("class", "selector", "filter", "default", "value", "base"); + +Highlight.defaultValue("filter", function () + this.base ? this.base.filter : + ["chrome://dactyl/*", + "dactyl:*", + "file://*"].concat(highlight.styleableChrome).join(",")); +Highlight.defaultValue("selector", function () highlight.selector(this.class)); +Highlight.defaultValue("value", function () this.default); +Highlight.defaultValue("base", function () { + let base = /^(\w*)/.exec(this.class)[0]; + return (base != this.class && base in highlight.highlight) ? highlight.highlight[base] : null; +}); +Highlight.prototype.toString = function () + "Highlight(" + this.class + ")\n\t" + + [k + ": " + String.quote(v) for ([k, v] in this)] + .join("\n\t"); + +/** + * A class to manage highlighting rules. The parameters are the + * standard parameters for any {@link Storage} object. + * + * @author Kris Maglione + */ +const Highlights = Module("Highlight", { + init: function () { + this.highlight = {}; + }, + + keys: function keys() Object.keys(this.highlight).sort(), + + __iterator__: function () values(this.highlight), + + get: function (k) this.highlight[k], + set: function (key, newStyle, force, append) { + let [, class_, selectors] = key.match(/^([a-zA-Z_-]+)(.*)/); + + if (!(class_ in this.highlight)) + return "Unknown highlight keyword: " + class_; + + let style = this.highlight[key] || Highlight(key); + styles.removeSheet(true, style.selector); + + if (append) + newStyle = (style.value || "").replace(/;?\s*$/, "; " + newStyle); + if (/^\s*$/.test(newStyle)) + newStyle = null; + if (newStyle == null) { + if (style.default == null) { + delete this.highlight[style.class]; + styles.removeSheet(true, style.selector); + return null; + } + newStyle = style.default; + force = true; + } + + let css = newStyle.replace(/(?:!\s*important\s*)?(?:;?\s*$|;)/g, "!important;") + .replace(";!important;", ";", "g"); // Seeming Spidermonkey bug + if (!/^\s*(?:!\s*important\s*)?;*\s*$/.test(css)) { + css = style.selector + " { " + css + " }"; + + let error = styles.addSheet(true, "highlight:" + style.class, style.filter, css, true); + if (error) + return error; + } + style.value = newStyle; + this.highlight[style.class] = style; + return null; + }, + + /** + * Gets a CSS selector given a highlight group. + * + * @param {string} class + */ + selector: function (class_) { + let [, hl, rest] = class_.match(/^(\w*)(.*)/); + let pattern = "[dactyl|highlight~=" + hl + "]" + if (this.highlight[hl] && this.highlight[hl].class != class_) + pattern = this.highlight[hl].selector; + return pattern + rest; + }, + + /** + * Clears all highlighting rules. Rules with default values are + * reset. + */ + clear: function () { + for (let [k, v] in Iterator(this.highlight)) + this.set(k, null, true); + }, + + /** + * Bulk loads new CSS rules. + * + * @param {string} css The rules to load. See {@link Highlights#css}. + */ + loadCSS: function (css) { + css.replace(/^(\s*\S*\s+)\{((?:.|\n)*?)\}\s*$/gm, function (_, _1, _2) _1 + " " + _2.replace(/\n\s*/g, " ")) + .split("\n").filter(function (s) /\S/.test(s)) + .forEach(function (style) { + style = Highlight.apply(Highlight, + Array.slice(style.match(/^\s*((?:[^,\s]|\s\S)+)(?:,((?:[^,\s]|\s\S)+)?)?(?:,((?:[^,\s]|\s\S)+))?\s*(.*)$/), + 1)); + if (/^[>+ ]/.test(style.selector)) + style.selector = this.selector(style.class) + style.selector; + + let old = this.highlight[style.class]; + this.highlight[style.class] = style; + if (old && old.value != old.default) + style.value = old.value; + }, this); + for (let [class_, hl] in Iterator(this.highlight)) + if (hl.value == hl.default) + this.set(class_); + } +}, { +}, { + commands: function (dactyl, modules) { + const commands = modules.commands; + commands.add(["colo[rscheme]"], + "Load a color scheme", + function (args) { + let scheme = args[0]; + + if (scheme == "default") + highlight.clear(); + else + dactyl.assert(modules.io.sourceFromRuntimePath(["colors/" + scheme + ".vimp"]), + "E185: Cannot find color scheme " + scheme); + modules.autocommands.trigger("ColorScheme", { name: scheme }); + }, + { + argCount: "1", + completer: function (context) completion.colorScheme(context) + }); + + commands.add(["hi[ghlight]"], + "Set the style of certain display elements", + function (args) { + let style = ; + let clear = args[0] == "clear"; + if (clear) + args.shift(); + + let [key, css] = args; + dactyl.assert(!(clear && css), "E488: Trailing characters"); + + if (!css && !clear) + modules.commandline.commandOutput( + template.tabular(["Key", "Sample", "CSS"], + ["padding: 0 1em 0 0; vertical-align: top", + "text-align: center"], + ([h.class, + XXX, + template.highlightRegexp(h.value, /\b[-\w]+(?=:)/g)] + for (h in highlight) + if (!key || h.class.indexOf(key) > -1)))); + else if (!key && clear) + highlight.clear(); + else { + let error = highlight.set(key, css, clear, "-append" in args); + if (error) + dactyl.echoerr(error); + } + }, + { + // TODO: add this as a standard highlight completion function? + completer: function (context, args) { + // Complete a highlight group on :hi clear ... + if (args.completeArg > 0 && args[0] == "clear") + args.completeArg = args.completeArg > 1 ? -1 : 0; + + if (args.completeArg == 0) + context.completions = [[v.class, v.value] for (v in highlight)]; + else if (args.completeArg == 1) { + let hl = highlight.get(args[0]); + if (hl) + context.completions = [[hl.value, "Current Value"], [hl.default || "", "Default Value"]]; + } + }, + hereDoc: true, + literal: 1, + options: [{ names: ["-append", "-a"], description: "Append new CSS to the existing value" }], + serialize: function () [ + { + command: this.name, + arguments: [k], + literalArg: v + } + for ([k, v] in Iterator(highlight)) + if (v.value != v.default) + ] + }); + }, + completion: function (dactyl, modules) { + const completion = modules.completion; + completion.colorScheme = function colorScheme(context) { + context.title = ["Color Scheme", "Runtime Path"]; + context.keys = { text: function (f) f.leafName.replace(/\.vimp$/, ""), description: ".parent.path" }; + context.completions = util.Array.flatten( + modules.io.getRuntimeDirectories("colors").map( + function (dir) dir.readDirectory().filter( + function (file) /\.vimp$/.test(file.leafName)))) + + }; + + completion.highlightGroup = function highlightGroup(context) { + context.title = ["Highlight Group", "Value"]; + context.completions = [[v.class, v.value] for (v in highlight)]; + }; + } +}); + +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);} + +endmodule(); + +// vim:se fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/modules/services.jsm b/common/modules/services.jsm new file mode 100644 index 00000000..afef4cd8 --- /dev/null +++ b/common/modules/services.jsm @@ -0,0 +1,133 @@ +// Copyright (c) 2008-2009 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("services", this, { + exports: ["Services", "services"] +}); + +const Services = Module("Services", { + init: function () { + this.classes = {}; + this.services = {}; + + this.add("appStartup", "@mozilla.org/toolkit/app-startup;1", Ci.nsIAppStartup); + this.add("autoCompleteSearch", "@mozilla.org/autocomplete/search;1?name=history", Ci.nsIAutoCompleteSearch); + this.add("bookmarks", "@mozilla.org/browser/nav-bookmarks-service;1", Ci.nsINavBookmarksService); + this.add("browserSearch", "@mozilla.org/browser/search-service;1", Ci.nsIBrowserSearchService); + this.add("cache", "@mozilla.org/network/cache-service;1", Ci.nsICacheService); + this.add("console", "@mozilla.org/consoleservice;1", Ci.nsIConsoleService); + this.add("dactyl:", "@mozilla.org/network/protocol;1?name=dactyl"); + this.add("debugger", "@mozilla.org/js/jsd/debugger-service;1", Ci.jsdIDebuggerService); + this.add("directory", "@mozilla.org/file/directory_service;1", Ci.nsIProperties); + this.add("downloadManager", "@mozilla.org/download-manager;1", Ci.nsIDownloadManager); + this.add("environment", "@mozilla.org/process/environment;1", Ci.nsIEnvironment); + this.add("extensionManager", "@mozilla.org/extensions/manager;1", Ci.nsIExtensionManager); + this.add("favicon", "@mozilla.org/browser/favicon-service;1", Ci.nsIFaviconService); + this.add("history", "@mozilla.org/browser/global-history;2", [Ci.nsIBrowserHistory, Ci.nsIGlobalHistory3, Ci.nsINavHistoryService]); + this.add("io", "@mozilla.org/network/io-service;1", Ci.nsIIOService); + this.add("json", "@mozilla.org/dom/json;1", Ci.nsIJSON, "createInstance"); + this.add("livemark", "@mozilla.org/browser/livemark-service;2", Ci.nsILivemarkService); + this.add("observer", "@mozilla.org/observer-service;1", Ci.nsIObserverService); + this.add("pref", "@mozilla.org/preferences-service;1", [Ci.nsIPrefBranch, Ci.nsIPrefBranch2, Ci.nsIPrefService]); + this.add("profile", "@mozilla.org/toolkit/profile-service;1", Ci.nsIToolkitProfileService); + this.add("runtime", "@mozilla.org/xre/runtime;1", [Ci.nsIXULAppInfo, Ci.nsIXULRuntime]); + this.add("rdf", "@mozilla.org/rdf/rdf-service;1", Ci.nsIRDFService); + this.add("sessionStore", "@mozilla.org/browser/sessionstore;1", Ci.nsISessionStore); + this.add("stylesheet", "@mozilla.org/content/style-sheet-service;1", Ci.nsIStyleSheetService); + this.add("subscriptLoader", "@mozilla.org/moz/jssubscript-loader;1", Ci.mozIJSSubScriptLoader); + this.add("tagging", "@mozilla.org/browser/tagging-service;1", Ci.nsITaggingService); + this.add("threadManager", "@mozilla.org/thread-manager;1", Ci.nsIThreadManager); + this.add("windowMediator", "@mozilla.org/appshell/window-mediator;1", Ci.nsIWindowMediator); + this.add("windowWatcher", "@mozilla.org/embedcomp/window-watcher;1", Ci.nsIWindowWatcher); + this.add("xulAppInfo", "@mozilla.org/xre/app-info;1", Ci.nsIXULAppInfo); + + this.addClass("file", "@mozilla.org/file/local;1", Ci.nsILocalFile); + this.addClass("file:", "@mozilla.org/network/protocol;1?name=file", Ci.nsIFileProtocolHandler); + this.addClass("find", "@mozilla.org/embedcomp/rangefind;1", Ci.nsIFind); + this.addClass("process", "@mozilla.org/process/util;1", Ci.nsIProcess); + this.addClass("timer", "@mozilla.org/timer;1", Ci.nsITimer); + this.addClass("xmlhttp", "@mozilla.org/xmlextras/xmlhttprequest;1", Ci.nsIXMLHttpRequest); + this.addClass("zipWriter", "@mozilla.org/zipwriter;1", Ci.nsIZipWriter); + }, + + _create: function (classes, ifaces, meth) { + try { + let res = Cc[classes][meth || "getService"](); + if (!ifaces) + return res.wrappedJSObject; + ifaces = Array.concat(ifaces); + ifaces.forEach(function (iface) res.QueryInterface(iface)); + return res; + } + catch (e) { + // dactyl.log() is not defined at this time, so just dump any error + dump("Service creation failed for '" + classes + "': " + e + "\n"); + return null; + } + }, + + /** + * Adds a new XPCOM service to the cache. + * + * @param {string} name The service's cache key. + * @param {string} class The class's contract ID. + * @param {nsISupports|nsISupports[]} ifaces The interface or array of + * interfaces implemented by this service. + * @param {string} meth The name of the function used to instanciate + * the service. + */ + add: function (name, class_, ifaces, meth) { + const self = this; + this.services.__defineGetter__(name, function () { + delete this[name]; + return this[name] = self._create(class_, ifaces, meth); + }); + }, + + /** + * Adds a new XPCOM class to the cache. + * + * @param {string} name The class's cache key. + * @param {string} class The class's contract ID. + * @param {nsISupports|nsISupports[]} ifaces The interface or array of + * interfaces implemented by this class. + */ + addClass: function (name, class_, ifaces) { + const self = this; + return this.classes[name] = function () self._create(class_, ifaces, "createInstance"); + }, + + /** + * Returns the cached service with the specified name. + * + * @param {string} name The service's cache key. + */ + get: function (name) this.services[name], + + /** + * Returns a new instance of the cached class with the specified name. + * + * @param {string} name The class's cache key. + */ + create: function (name) this.classes[name]() +}, { +}, { + init: function (dactyl, modules) { + if (!this.get("extensionManager")) + Components.utils.import("resource://gre/modules/AddonManager.jsm", modules); + }, + javascript: function (dactyl, modules) { + modules.JavaScript.setCompleter(this.get, [function () services.services]); + modules.JavaScript.setCompleter(this.create, [function () [[c, ""] for (c in services.classes)]]); + } +}); + +endmodule(); + +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n");} + +// vim: set fdm=marker sw=4 sts=4 et ft=javascript: diff --git a/common/modules/storage.jsm b/common/modules/storage.jsm index f0fd006e..4d09d380 100644 --- a/common/modules/storage.jsm +++ b/common/modules/storage.jsm @@ -21,107 +21,22 @@ }}} ***** END LICENSE BLOCK *****/ "use strict"; -var EXPORTED_SYMBOLS = ["storage", "Timer"]; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -// XXX: does not belong here -function Timer(minInterval, maxInterval, callback) { - let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this.doneAt = 0; - this.latest = 0; - this.notify = function (aTimer) { - timer.cancel(); - this.latest = 0; - // minInterval is the time between the completion of the command and the next firing - this.doneAt = Date.now() + minInterval; +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("storage", this, { + exports: ["File", "storage"], + require: ["services", "util"] +}); - try { - callback(this.arg); - } - finally { - this.doneAt = Date.now() + minInterval; - } - }; - this.tell = function (arg) { - if (arguments.length > 0) - this.arg = arg; - - let now = Date.now(); - if (this.doneAt == -1) - timer.cancel(); - - let timeout = minInterval; - if (now > this.doneAt && this.doneAt > -1) - timeout = 0; - else if (this.latest) - timeout = Math.min(timeout, this.latest - now); - else - this.latest = now + maxInterval; - - timer.initWithCallback(this, Math.max(timeout, 0), timer.TYPE_ONE_SHOT); - this.doneAt = -1; - }; - this.reset = function () { - timer.cancel(); - this.doneAt = 0; - }; - this.flush = function () { - if (this.doneAt == -1) - this.notify(); - }; -} +var prefService = services.get("pref").getBranch("extensions.dactyl.datastore."); + +const win32 = services.get("runtime").OS == "Win32"; function getFile(name) { let file = storage.infoPath.clone(); file.append(name); - return file; -} - -function readFile(file) { - let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); - let stream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); - - try { - fileStream.init(file, -1, 0, 0); - stream.init(fileStream, "UTF-8", 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering - - let hunks = []; - let res = {}; - while (stream.readString(4096, res) != 0) - hunks.push(res.value); - - stream.close(); - fileStream.close(); - - return hunks.join(""); - } - catch (e) {} -} - -function writeFile(file, data) { - if (!file.exists()) - file.create(file.NORMAL_FILE_TYPE, parseInt('0600', 8)); - - let fileStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); - let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); - - fileStream.init(file, 0x20 | 0x08 | 0x02, parseInt('0600', 8), 0); // PR_TRUNCATE | PR_CREATE | PR_WRITE - stream.init(fileStream, "UTF-8", 0, 0); - - stream.writeString(data); - - stream.close(); - fileStream.close(); + return File(file); } -var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); -var prefService = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService) - .getBranch("extensions.dactyl.datastore."); -var json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON); - function getCharPref(name) { try { return prefService.getComplexValue(name, Ci.nsISupportsString).data; @@ -140,9 +55,9 @@ function loadPref(name, store, type) { if (store) var pref = getCharPref(name); if (!pref && storage.infoPath) - var file = readFile(getFile(name)); + var file = getFile(name).read(); if (pref || file) - var result = json.decode(pref || file); + var result = services.get("json").decode(pref || file); if (pref) { prefService.clearUserPref(name); savePref({ name: name, store: true, serial: pref }); @@ -157,122 +72,110 @@ function savePref(obj) { if (obj.privateData && storage.privateMode) return; if (obj.store && storage.infoPath) - writeFile(getFile(obj.name), obj.serial); + getFile(obj.name).write(obj.serial); } -var prototype = { +const StoreBase = Class("StoreBase", { OPTIONS: ["privateData"], fireEvent: function (event, arg) { storage.fireEvent(this.name, event, arg); }, + get serial() services.get("json").encode(this._object), save: function () { savePref(this); }, - init: function (name, store, data, options) { + init: function (name, store, load, options) { + this._load = load; + this.__defineGetter__("store", function () store); this.__defineGetter__("name", function () name); for (let [k, v] in Iterator(options)) if (this.OPTIONS.indexOf(k) >= 0) this[k] = v; this.reload(); - } -}; - -function ObjectStore(name, store, load, options) { - var object = {}; - - this.reload = function reload() { - object = load() || {}; + }, + reload: function reload() { + this._object = this._load() || this._constructor(); this.fireEvent("change", null); - }; + } +}); - this.init.apply(this, arguments); - this.__defineGetter__("serial", function () json.encode(object)); +const ObjectStore = Class("ObjectStore", StoreBase, { + _constructor: Object, - this.set = function set(key, val) { - var defined = key in object; - var orig = object[key]; - object[key] = val; + set: function set(key, val) { + var defined = key in this._object; + var orig = this._object[key]; + this._object[key] = val; if (!defined) this.fireEvent("add", key); else if (orig != val) this.fireEvent("change", key); - }; + }, - this.remove = function remove(key) { - var ret = object[key]; - delete object[key]; + remove: function remove(key) { + var ret = this._object[key]; + delete this._object[key]; this.fireEvent("remove", key); return ret; - }; - - this.get = function get(val, default_) val in object ? object[val] : default_; + }, - this.clear = function () { - object = {}; - }; + get: function get(val, default_) val in this._object ? this._object[val] : default_, - this.__iterator__ = function () Iterator(object); -} -ObjectStore.prototype = prototype; + clear: function () { + this._object = {}; + }, -function ArrayStore(name, store, load, options) { - var array = []; + __iterator__: function () Iterator(this._object), +}); - this.reload = function reload() { - array = load() || []; - this.fireEvent("change", null); - }; +const ArrayStore = Class("ArrayStore", StoreBase, { + _constructor: Array, - this.init.apply(this, arguments); - this.__defineGetter__("serial", function () json.encode(array)); - this.__defineGetter__("length", function () array.length); + get length() this._object.length, - this.set = function set(index, value) { - var orig = array[index]; - array[index] = value; + set: function set(index, value) { + var orig = this._object[index]; + this._object[index] = value; this.fireEvent("change", index); - }; + }, - this.push = function push(value) { - array.push(value); - this.fireEvent("push", array.length); - }; + push: function push(value) { + this._object.push(value); + this.fireEvent("push", this._object.length); + }, - this.pop = function pop(value) { - var ret = array.pop(); - this.fireEvent("pop", array.length); + pop: function pop(value) { + var ret = this._object.pop(); + this.fireEvent("pop", this._object.length); return ret; - }; + }, - this.truncate = function truncate(length, fromEnd) { - var ret = array.length; - if (array.length > length) { + truncate: function truncate(length, fromEnd) { + var ret = this._object.length; + if (this._object.length > length) { if (fromEnd) - array.splice(0, array.length - length); - array.length = length; + this._object.splice(0, this._object.length - length); + this._object.length = length; this.fireEvent("truncate", length); } return ret; - }; + }, // XXX: Awkward. - this.mutate = function mutate(aFuncName) { - var funcName = aFuncName; - arguments[0] = array; - array = Array[funcName].apply(Array, arguments); + mutate: function mutate(funcName) { + var _funcName = funcName; + arguments[0] = this._object; + this._object = Array[_funcName].apply(Array, arguments); this.fireEvent("change", null); - }; + }, - this.get = function get(index) { - return index >= 0 ? array[index] : array[array.length + index]; - }; + get: function get(index) index >= 0 ? this._object[index] : this._object[this._object.length + index], - this.__iterator__ = function () Iterator(array); -} -ArrayStore.prototype = prototype; + __iterator__: function () Iterator(this._object), +}); var keys = {}; var observers = {}; var timers = {}; -var storage = { +const Storage = Module("Storage", { alwaysReload: {}, newObject: function newObject(key, constructor, params) { if (!(key in keys) || params.reload || this.alwaysReload[key]) { @@ -363,6 +266,304 @@ var storage = { this.load(key); return this._privateMode = Boolean(val); } -}; +}, { +}, { + init: function (dactyl, modules) { + let infoPath = services.create("file"); + infoPath.initWithPath(File.expandPath(modules.IO.runtimePath.replace(/,.*/, ""))); + infoPath.append("info"); + infoPath.append(dactyl.profileName); + storage.infoPath = infoPath; + } +}); + +/** + * @class File A class to wrap nsIFile objects and simplify operations + * thereon. + * + * @param {nsIFile|string} path Expanded according to {@link IO#expandPath} + * @param {boolean} checkPWD Whether to allow expansion relative to the + * current directory. @default true + */ +const File = Class("File", { + init: function (path, checkPWD) { + let file = services.create("file"); + + if (path instanceof Ci.nsIFile) + file = path; + else if (/file:\/\//.test(path)) + file = services.create("file:").getFileFromURLSpec(path); + else { + let expandedPath = File.expandPath(path); + + if (!File.isAbsolutePath(expandedPath) && checkPWD) + file = File.joinPaths(checkPWD, expandedPath); + else + file.initWithPath(expandedPath); + } + let self = XPCSafeJSObjectWrapper(file); + self.__proto__ = File.prototype; + return self; + }, + + /** + * Iterates over the objects in this directory. + */ + iterDirectory: function () { + if (!this.isDirectory()) + throw Error("Not a directory"); + let entries = this.directoryEntries; + while (entries.hasMoreElements()) + yield File(entries.getNext().QueryInterface(Ci.nsIFile)); + }, + + /** + * Reads this file's entire contents in "text" mode and returns the + * content as a string. + * + * @param {string} encoding The encoding from which to decode the file. + * @default options["fileencoding"] + * @returns {string} + */ + read: function (encoding) { + let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + let icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream); + + if (!encoding) + encoding = File.defaultEncoding; + + ifstream.init(this, -1, 0, 0); + icstream.init(ifstream, encoding, 4096, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); // 4096 bytes buffering + + let buffer = []; + let str = {}; + while (icstream.readString(4096, str) != 0) + buffer.push(str.value); + + icstream.close(); + ifstream.close(); + return buffer.join(""); + }, + + /** + * Returns the list of files in this directory. + * + * @param {boolean} sort Whether to sort the returned directory + * entries. + * @returns {nsIFile[]} + */ + readDirectory: function (sort) { + if (!this.isDirectory()) + throw Error("Not a directory"); + + let array = [e for (e in this.iterDirectory())]; + if (sort) + array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path)); + return array; + }, + + /** + * Writes the string buf to this file. + * + * @param {string} buf The file content. + * @param {string|number} mode The file access mode, a bitwise OR of + * the following flags: + * {@link #MODE_RDONLY}: 0x01 + * {@link #MODE_WRONLY}: 0x02 + * {@link #MODE_RDWR}: 0x04 + * {@link #MODE_CREATE}: 0x08 + * {@link #MODE_APPEND}: 0x10 + * {@link #MODE_TRUNCATE}: 0x20 + * {@link #MODE_SYNC}: 0x40 + * Alternatively, the following abbreviations may be used: + * ">" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_TRUNCATE} + * ">>" is equivalent to {@link #MODE_WRONLY} | {@link #MODE_CREATE} | {@link #MODE_APPEND} + * @default ">" + * @param {number} perms The file mode bits of the created file. This + * is only used when creating a new file and does not change + * permissions if the file exists. + * @default 0644 + * @param {string} encoding The encoding to used to write the file. + * @default options["fileencoding"] + */ + write: function (buf, mode, perms, encoding) { + let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); + function getStream(defaultChar) { + let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream); + stream.init(ofstream, encoding, 0, defaultChar); + return stream; + } + + if (!encoding) + encoding = File.defaultEncoding; + + if (mode == ">>") + mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND; + else if (!mode || mode == ">") + mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_TRUNCATE; + + if (!perms) + perms = parseInt('0644', 8); + + ofstream.init(this, mode, perms, 0); + let ocstream = getStream(0); + try { + ocstream.writeString(buf); + } + catch (e) { + if (e.result == Cr.NS_ERROR_LOSS_OF_SIGNIFICANT_DATA) { + ocstream = getStream("?".charCodeAt(0)); + ocstream.writeString(buf); + return false; + } + else + throw e; + } + finally { + try { + ocstream.close(); + } + catch (e) {} + ofstream.close(); + } + return true; + } +}, { + /** + * @property {number} Open for reading only. + * @final + */ + MODE_RDONLY: 0x01, + + /** + * @property {number} Open for writing only. + * @final + */ + MODE_WRONLY: 0x02, + + /** + * @property {number} Open for reading and writing. + * @final + */ + MODE_RDWR: 0x04, + + /** + * @property {number} If the file does not exist, the file is created. + * If the file exists, this flag has no effect. + * @final + */ + MODE_CREATE: 0x08, + + /** + * @property {number} The file pointer is set to the end of the file + * prior to each write. + * @final + */ + MODE_APPEND: 0x10, + + /** + * @property {number} If the file exists, its length is truncated to 0. + * @final + */ + MODE_TRUNCATE: 0x20, + + /** + * @property {number} If set, each write will wait for both the file + * data and file status to be physically updated. + * @final + */ + MODE_SYNC: 0x40, + + /** + * @property {number} With MODE_CREATE, if the file does not exist, the + * file is created. If the file already exists, no action and NULL + * is returned. + * @final + */ + MODE_EXCL: 0x80, + + /** + * @property {string} The current platform's path seperator. + */ + get PATH_SEP() { + delete this.PATH_SEP; + let f = services.get("directory").get("CurProcD", Ci.nsIFile); + f.append("foo"); + return this.PATH_SEP = f.path.substr(f.parent.path.length, 1); + }, + + defaultEncoding: "UTF-8", + + expandPath: function (path, relative) { + + // expand any $ENV vars - this is naive but so is Vim and we like to be compatible + // TODO: Vim does not expand variables set to an empty string (and documents it). + // Kris reckons we shouldn't replicate this 'bug'. --djk + // TODO: should we be doing this for all paths? + function expand(path) path.replace( + !win32 ? /\$(\w+)\b|\${(\w+)}/g + : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g, + function (m, n1, n2, n3) services.get("environment").get(n1 || n2 || n3) || m + ); + path = expand(path); + + // expand ~ + // Yuck. + if (!relative && RegExp("~(?:$|[/" + util.escapeRegex(File.PATH_SEP) + "])").test(path)) { + // Try $HOME first, on all systems + let home = services.get("environment").get("HOME"); + + // Windows has its own idiosyncratic $HOME variables. + if (!home && win32) + home = services.get("environment").get("USERPROFILE") || + services.get("environment").get("HOMEDRIVE") + services.get("environment").get("HOMEPATH"); + + path = home + path.substr(1); + } + + // TODO: Vim expands paths twice, once before checking for ~, once + // after, but doesn't document it. Is this just a bug? --Kris + path = expand(path); + return path.replace("/", File.PATH_SEP, "g"); + }, + + expandPathList: function (list) list.map(this.expandPath), + + getPathsFromPathList: function (list) { + if (!list) + return []; + // empty list item means the current directory + return list.replace(/,$/, "").split(",") + .map(function (dir) dir == "" ? io.getCurrentDirectory().path : dir); + }, + + isAbsolutePath: function (path) { + try { + services.create("file").initWithPath(path); + return true; + } + catch (e) { + return false; + } + }, + + joinPaths: function (head, tail) { + let path = this(head); + try { + // FIXME: should only expand env vars and normalise path separators + path.appendRelativePath(this.expandPath(tail, true)); + } + catch (e) { + return { exists: function () false, __noSuchMethod__: function () { throw e; } }; + } + return path; + }, + + replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g") +}); + +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n");} + +endmodule(); // vim: set fdm=marker sw=4 sts=4 et ft=javascript: diff --git a/common/modules/styles.jsm b/common/modules/styles.jsm new file mode 100644 index 00000000..8b8b1c1c --- /dev/null +++ b/common/modules/styles.jsm @@ -0,0 +1,373 @@ +// Copyright (c) 2008-2010 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("styles", this, { + exports: ["Style", "Styles", "styles"], + require: ["services", "util"] +}); + +const sss = services.get("stylesheet"); +function cssUri(css) "chrome-data:text/css," + encodeURI(css); +const namespace = "@namespace html " + XHTML.uri.quote() + ";\n" + + "@namespace xul " + XUL.uri.quote() + ";\n" + + "@namespace dactyl " + NS.uri.quote() + ";\n"; + +const Sheet = Struct("name", "id", "sites", "css", "system", "agent"); +Sheet.prototype.__defineGetter__("fullCSS", function wrapCSS() { + let filter = this.sites; + let css = this.css; + if (filter[0] == "*") + return namespace + css; + + let selectors = filter.map(function (part) + (/[*]$/.test(part) ? "url-prefix" : + /[\/:]/.test(part) ? "url" + : "domain") + + '("' + part.replace(/"/g, "%22").replace(/\*$/, "") + '")') + .join(", "); + return "/* Dactyl style #" + this.id + " */ " + namespace + " @-moz-document " + selectors + "{\n" + css + "\n}\n"; +}); +Sheet.prototype.__defineGetter__("enabled", function () this._enabled); +Sheet.prototype.__defineSetter__("enabled", function (on) { + this._enabled = Boolean(on); + let meth = on ? "registerSheet" : "unregisterSheet"; + + styles[meth](cssUri(this.fullCSS)); + if (this.agent) + styles[meth](cssUri(this.fullCSS), true); +}); + +/** + * Manages named and unnamed user style sheets, which apply to both + * chrome and content pages. The parameters are the standard + * parameters for any {@link Storage} object. + * + * @author Kris Maglione + */ +const Styles = Module("Styles", { + init: function() { + this._id = 0; + this.userSheets = []; + this.systemSheets = []; + this.userNames = {}; + this.systemNames = {}; + }, + + get sites() array(this.userSheets).map(function (s) s.sites).flatten().uniq().__proto__, + + __iterator__: function () Iterator(this.userSheets.concat(this.systemSheets)), + + /** + * Add a new style sheet. + * + * @param {boolean} system Declares whether this is a system or + * user sheet. System sheets are used internally by + * @dactyl. + * @param {string} name The name given to the style sheet by + * which it may be later referenced. + * @param {string} filter The sites to which this sheet will + * apply. Can be a domain name or a URL. Any URL ending in + * "*" is matched as a prefix. + * @param {string} css The CSS to be applied. + */ + addSheet: function addSheet(system, name, filter, css, agent) { + let sheets = system ? this.systemSheets : this.userSheets; + let names = system ? this.systemNames : this.userNames; + if (name && name in names) + this.removeSheet(system, name); + + let sheet = Sheet(name, this._id++, filter.split(",").filter(util.identity), String(css), null, system, agent); + + try { + sheet.enabled = true; + } + catch (e) { + return e.echoerr || e; + } + sheets.push(sheet); + + if (name) + names[name] = sheet; + return null; + }, + + /** + * Get a sheet with a given name or index. + * + * @param {boolean} system + * @param {string or number} sheet The sheet to retrieve. Strings indicate + * sheet names, while numbers indicate indices. + */ + get: function getget(system, sheet) { + let sheets = system ? this.systemSheets : this.userSheets; + let names = system ? this.systemNames : this.userNames; + if (typeof sheet === "number") + return sheets[sheet]; + return names[sheet]; + }, + + /** + * Find sheets matching the parameters. See {@link #addSheet} + * for parameters. + * + * @param {boolean} system + * @param {string} name + * @param {string} filter + * @param {string} css + * @param {number} index + */ + findSheets: function findSheets(system, name, filter, css, index) { + let sheets = system ? this.systemSheets : this.userSheets; + let names = system ? this.systemNames : this.userNames; + + // Grossly inefficient. + let matches = [k for ([k, v] in Iterator(sheets))]; + if (index) + matches = String(index).split(",").filter(function (i) i in sheets); + if (name) + matches = matches.filter(function (i) sheets[i] == names[name]); + if (css) + matches = matches.filter(function (i) sheets[i].css == css); + if (filter) + matches = matches.filter(function (i) sheets[i].sites.indexOf(filter) >= 0); + return matches.map(function (i) sheets[i]); + }, + + /** + * Remove a style sheet. See {@link #addSheet} for parameters. + * In cases where filter is supplied, the given filters + * are removed from matching sheets. If any remain, the sheet is + * left in place. + * + * @param {boolean} system + * @param {string} name + * @param {string} filter + * @param {string} css + * @param {number} index + */ + removeSheet: function removeSheet(system, name, filter, css, index) { + let self = this; + if (arguments.length == 1) { + var matches = [system]; + system = matches[0].system; + } + let sheets = system ? this.systemSheets : this.userSheets; + let names = system ? this.systemNames : this.userNames; + + if (filter && filter.indexOf(",") > -1) + return filter.split(",").reduce( + function (n, f) n + self.removeSheet(system, name, f, index), 0); + + if (filter == undefined) + filter = ""; + + if (!matches) + matches = this.findSheets(system, name, filter, css, index); + if (matches.length == 0) + return null; + + for (let [, sheet] in Iterator(matches.reverse())) { + sheet.enabled = false; + if (name) + delete names[name]; + if (sheets.indexOf(sheet) > -1) + sheets.splice(sheets.indexOf(sheet), 1); + + /* Re-add if we're only changing the site filter. */ + if (filter) { + let sites = sheet.sites.filter(function (f) f != filter); + if (sites.length) + this.addSheet(system, name, sites.join(","), css, sheet.agent); + } + } + return matches.length; + }, + + /** + * Register a user style sheet at the given URI. + * + * @param {string} url The URI of the sheet to register. + * @param {boolean} agent If true, sheet is registered as an agent sheet. + * @param {boolean} reload Whether to reload any sheets that are + * already registered. + */ + registerSheet: function registerSheet(url, agent, reload) { + let uri = services.get("io").newURI(url, null, null); + if (reload) + this.unregisterSheet(url, agent); + if (reload || !sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET)) + sss.loadAndRegisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET); + }, + + /** + * Unregister a sheet at the given URI. + * + * @param {string} url The URI of the sheet to unregister. + * @param {boolean} agent If true, sheet is registered as an agent sheet. + */ + unregisterSheet: function unregisterSheet(url, agent) { + let uri = services.get("io").newURI(url, null, null); + if (sss.sheetRegistered(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET)) + sss.unregisterSheet(uri, agent ? sss.AGENT_SHEET : sss.USER_SHEET); + }, +}, { + completeSite: function (context, content) { + context.anchored = false; + try { + context.fork("current", 0, this, function (context) { + context.title = ["Current Site"]; + context.completions = [ + [content.location.host, "Current Host"], + [content.location.href, "Current URL"] + ]; + }); + } + catch (e) {} + context.fork("others", 0, this, function (context) { + context.title = ["Site"]; + context.completions = [[s, ""] for ([, s] in Iterator(styles.sites))]; + }); + } +}, { + commands: function (dactyl, modules, window) { + const commands = modules.commands; + commands.add(["sty[le]"], + "Add or list user styles", + function (args) { + let [filter, css] = args; + let name = args["-name"]; + + if (!css) { + let list = Array.concat([i for (i in Iterator(styles.userNames))], + [i for (i in Iterator(styles.userSheets)) if (!i[1].name)]); + modules.commandline.commandOutput( + template.tabular(["", "Name", "Filter", "CSS"], + ["min-width: 1em; text-align: center; color: red; font-weight: bold;", + "padding: 0 1em 0 1ex; vertical-align: top;", + "padding: 0 1em 0 0; vertical-align: top;"], + ([sheet.enabled ? "" : "\u00d7", + key, + sheet.sites.join(","), + sheet.css] + for ([i, [key, sheet]] in Iterator(list)) + if ((!filter || sheet.sites.indexOf(filter) >= 0) && (!name || sheet.name == name))))); + } + else { + if ("-append" in args) { + let sheet = styles.get(false, name); + if (sheet) { + filter = sheet.sites.concat(filter).join(","); + css = sheet.css + " " + css; + } + } + let err = styles.addSheet(false, name, filter, css); + if (err) + dactyl.echoerr(err); + } + }, + { + bang: true, + completer: function (context, args) { + let compl = []; + if (args.completeArg == 0) + Styles.completeSite(context, window.content); + else if (args.completeArg == 1) { + let sheet = styles.get(false, args["-name"]); + if (sheet) + context.completions = [[sheet.css, "Current Value"]]; + } + }, + hereDoc: true, + literal: 1, + options: [ + { + names: ["-name", "-n"], + description: "The name of this stylesheet", + completer: function () [[k, v.css] for ([k, v] in Iterator(styles.userNames))], + type: modules.CommandOption.STRING + }, + { names: ["-append", "-a"], description: "Append site filter and css to an existing, matching sheet" } + ], + serialize: function () [ + { + command: this.name, + arguments: [sty.sites.join(",")], + bang: true, + literalArg: sty.css, + options: sty.name ? { "-name": sty.name } : {} + } for ([k, sty] in Iterator(styles.userSheets)) + ] + }); + + [ + { + name: ["stylee[nable]", "stye[nable]"], + desc: "Enable a user style sheet", + action: function (sheet) sheet.enabled = true, + filter: function (sheet) !sheet.enabled + }, + { + name: ["styled[isable]", "styd[isable]"], + desc: "Disable a user style sheet", + action: function (sheet) sheet.enabled = false, + filter: function (sheet) sheet.enabled + }, + { + name: ["stylet[oggle]", "styt[oggle]"], + desc: "Toggle a user style sheet", + action: function (sheet) sheet.enabled = !sheet.enabled + }, + { + name: ["dels[tyle]"], + desc: "Remove a user style sheet", + action: function (sheet) styles.removeSheet(sheet) + } + ].forEach(function (cmd) { + commands.add(cmd.name, cmd.desc, + function (args) { + styles.findSheets(false, args["-name"], args[0], args.literalArg, args["-index"]) + .forEach(cmd.action); + }, + { + completer: function (context) { context.completions = styles.sites.map(function (site) [site, ""]); }, + literal: 1, + options: [ + { + names: ["-index", "-i"], + type: modules.CommandOption.INT, + completer: function (context) { + context.compare = CompletionContext.Sort.number; + return [[i, <>{sheet.sites.join(",")}: {sheet.css.replace("\n", "\\n")}] + for ([i, sheet] in styles.userSheets) + if (!cmd.filter || cmd.filter(sheet))]; + }, + }, { + names: ["-name", "-n"], + type: modules.CommandOption.STRING, + completer: function () [[name, sheet.css] + for ([name, sheet] in Iterator(styles.userNames)) + if (!cmd.filter || cmd.filter(sheet))] + } + ] + }); + }); + }, + javascript: function (dactyl, modules, window) { + modules.JavaScript.setCompleter(["get", "addSheet", "removeSheet", "findSheets"].map(function (m) styles[m]), + [ // Prototype: (system, name, filter, css, index) + null, + function (context, obj, args) args[0] ? this.systemNames : this.userNames, + function (context, obj, args) Styles.completeSite(context, window.content), + null, + function (context, obj, args) args[0] ? this.systemSheets : this.userSheets + ]); + } +}); + +endmodule(); + +// vim:se fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/modules/template.jsm b/common/modules/template.jsm new file mode 100644 index 00000000..f444c184 --- /dev/null +++ b/common/modules/template.jsm @@ -0,0 +1,311 @@ +// Copyright (c) 2008-2009 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("template", this, { + exports: ["Template", "template"], + require: ["util"] +}); + +default xml namespace = XHTML; +XML.ignoreWhiteSpace = true; +XML.prettyPrinting = false; + +const Template = Module("Template", { + add: function add(a, b) a + b, + join: function join(c) function (a, b) a + c + b, + + map: function map(iter, func, sep, interruptable) { + if (iter.length) // FIXME: Kludge? + iter = array.itervalues(iter); + let ret = <>; + let n = 0; + for each (let i in Iterator(iter)) { + let val = func(i); + if (val == undefined) + continue; + if (sep && n++) + ret += sep; + if (interruptable && n % interruptable == 0) + util.threadYield(true, true); + ret += val; + } + return ret; + }, + + maybeXML: function maybeXML(xml) { + if (typeof xml == "xml") + return xml; + try { + return new XMLList(xml); + } + catch (e) {} + return <>{xml}; + }, + + bookmarkDescription: function (item, text) + <> + {text}  + { + !(item.extra && item.extra.length) ? "" : + + ({ + template.map(item.extra, function (e) + <>{e[0]}: {e[1]}, + <> /* Non-breaking space */) + }) + + } + , + + filter: function (str) {str}, + + completionRow: function completionRow(item, highlightGroup) { + if (typeof icon == "function") + icon = icon(); + + if (highlightGroup) { + var text = item[0] || ""; + var desc = item[1] || ""; + } + else { + var text = this.process[0].call(this, item, item.text); + var desc = this.process[1].call(this, item, item.description); + } + + // + return
    + +
  • {text} 
  • +
  • {desc} 
  • +
    ; + //
    + }, + + genericTable: function genericTable(items, format) { + completion.listCompleter(function (context) { + context.filterFunc = null; + if (format) + context.format = format; + context.completions = items; + }); + }, + + gradient: function (left, right) +
    +
    +
    +
    + + + { template.map(util.range(0, 100), function (i) + +
    ) } +
    +
    , + + // if "processStrings" is true, any passed strings will be surrounded by " and + // any line breaks are displayed as \n + highlight: function highlight(arg, processStrings, clip) { + // some objects like window.JSON or getBrowsers()._browsers need the try/catch + try { + let str = clip ? util.clip(String(arg), clip) : String(arg); + switch (arg == null ? "undefined" : typeof arg) { + case "number": + return {str}; + case "string": + if (processStrings) + str = str.quote(); + return {str}; + case "boolean": + return {str}; + case "function": + // Vim generally doesn't like /foo*/, because */ looks like a comment terminator. + // Using /foo*(:?)/ instead. + if (processStrings) + return {str.replace(/\{(.|\n)*(?:)/g, "{ ... }")}; + <>}; /* Vim */ + return <>{arg}; + case "undefined": + return {arg}; + case "object": + // for java packages value.toString() would crash so badly + // that we cannot even try/catch it + if (/^\[JavaPackage.*\]$/.test(arg)) + return <>[JavaPackage]; + if (processStrings && false) + str = template.highlightFilter(str, "\n", function () ^J); + return {str}; + case "xml": + return arg; + default: + return ]]>; + } + } + catch (e) { + return ]]>; + } + }, + + highlightFilter: function highlightFilter(str, filter, highlight) { + return this.highlightSubstrings(str, (function () { + if (filter.length == 0) + return; + let lcstr = String.toLowerCase(str); + let lcfilter = filter.toLowerCase(); + let start = 0; + while ((start = lcstr.indexOf(lcfilter, start)) > -1) { + yield [start, filter.length]; + start += filter.length; + } + })(), highlight || template.filter); + }, + + highlightRegexp: function highlightRegexp(str, re, highlight) { + return this.highlightSubstrings(str, (function () { + let res; + while ((res = re.exec(str)) && res[0].length) + yield [res.index, res[0].length]; + })(), highlight || template.filter); + }, + + highlightSubstrings: function highlightSubstrings(str, iter, highlight) { + if (typeof str == "xml") + return str; + if (str == "") + return <>{str}; + + str = String(str).replace(" ", "\u00a0"); + let s = <>; + let start = 0; + let n = 0; + for (let [i, length] in iter) { + if (n++ > 50) // Prevent infinite loops. + return s + <>{str.substr(start)}; + XML.ignoreWhitespace = false; + s += <>{str.substring(start, i)}; + s += highlight(str.substr(i, length)); + start = i + length; + } + return s + <>{str.substr(start)}; + }, + + highlightURL: function highlightURL(str, force) { + if (force || /^[a-zA-Z]+:\/\//.test(str)) + return {str}; + else + return str; + }, + + icon: function (item, text) <> + {item.icon ? : <>}{text} + , + + jumps: function jumps(index, elems) { + // + return + + + + { + this.map(Iterator(elems), function ([idx, val]) + + + + + + ) + } +
    jumptitleURI
    {idx == index ? ">" : ""}{Math.abs(idx - index)}{val.title}{val.URI.spec}
    ; + //
    + }, + + options: function options(title, opts) { + // + return + + + + { + this.map(opts, function (opt) + + + ) + } +
    --- {title} ---
    + {opt.pre}{opt.name}{opt.value} + {opt.isDefault || opt.default == null ? "" : (default: {opt.default})} +
    ; + //
    + }, + + table: function table(title, data, indent) { + let table = + // + + + + + { + this.map(data, function (datum) + + + + ) + } +
    {title}
    {datum[0]}{template.maybeXML(datum[1])}
    ; + //
    + if (table.tr.length() > 1) + return table; + return XML(); + }, + + tabular: function tabular(headings, style, iter) { + // TODO: This might be mind-bogglingly slow. We'll see. + // + return + + { + this.map(headings, function (h) + ) + } + + { + this.map(iter, function (row) + + { + template.map(Iterator(row), function ([i, d]) + ) + } + ) + } +
    {h}
    {d}
    ; + //
    + }, + + usage: function usage(iter) { + // + return + { + this.map(iter, function (item) + + + + ) + } +
    {item.name || item.names[0]}{item.description}
    ; + //
    + } +}); + +endmodule(); + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: diff --git a/common/modules/util.jsm b/common/modules/util.jsm new file mode 100644 index 00000000..5d854b44 --- /dev/null +++ b/common/modules/util.jsm @@ -0,0 +1,815 @@ +// Copyright (c) 2006-2008 by Martin Stubenschrott +// Copyright (c) 2007-2009 by Doug Kearns +// Copyright (c) 2008-2009 by Kris Maglione +// +// This work is licensed for reuse under an MIT license. Details are +// given in the LICENSE.txt file included with this file. +"use strict"; + +Components.utils.import("resource://dactyl/base.jsm"); +defmodule("util", this, { + exports: ["Math", "NS", "Util", "XHTML", "XUL", "util"], + require: ["services"], + use: ["template"] +}); + +const XHTML = Namespace("html", "http://www.w3.org/1999/xhtml"); +const XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +const NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator"); +default xml namespace = XHTML; + +const Util = Module("Util", { + init: function () { + this.Array = array; + }, + + get activeWindow() services.get("windowWatcher").activeWindow, + dactyl: { + __noSuchMethod__: function (meth, args) { + let win = util.activeWindow; + if(win && win.dactyl) + return win.dactyl[meth].apply(win.dactyl, args); + return null; + } + }, + + callInMainThread: function (callback, self) { + let mainThread = services.get("threadManager").mainThread; + if (services.get("threadManager").isMainThread) + callback.call(self); + else + mainThread.dispatch(Runnable(self, callback), mainThread.DISPATCH_NORMAL); + }, + + /** + * Calls a function asynchronously on a new thread. + * + * @param {nsIThread} thread The thread to call the function on. If no + * thread is specified a new one is created. + * @optional + * @param {Object} self The 'this' object used when executing the + * function. + * @param {function} func The function to execute. + * + */ + callAsync: function (thread, self, func) { + thread = thread || services.get("threadManager").newThread(0); + thread.dispatch(Runnable(self, func, Array.slice(arguments, 3)), thread.DISPATCH_NORMAL); + }, + + /** + * Calls a function synchronously on a new thread. + * + * NOTE: Be sure to call GUI related methods like alert() or dump() + * ONLY in the main thread. + * + * @param {nsIThread} thread The thread to call the function on. If no + * thread is specified a new one is created. + * @optional + * @param {function} func The function to execute. + */ + callInThread: function (thread, func) { + thread = thread || services.get("threadManager").newThread(0); + thread.dispatch(Runnable(null, func, Array.slice(arguments, 2)), thread.DISPATCH_SYNC); + }, + + /** + * Returns a shallow copy of obj. + * + * @param {Object} obj + * @returns {Object} + */ + cloneObject: function cloneObject(obj) { + if (isarray(obj)) + return obj.slice(); + let newObj = {}; + for (let [k, v] in Iterator(obj)) + newObj[k] = v; + return newObj; + }, + + /** + * Clips a string to a given length. If the input string is longer + * than length, an ellipsis is appended. + * + * @param {string} str The string to truncate. + * @param {number} length The length of the returned string. + * @returns {string} + */ + clip: function clip(str, length) { + return str.length <= length ? str : str.substr(0, length - 3) + "..."; + }, + + /** + * Compares two strings, case insensitively. Return values are as + * in String#localeCompare. + * + * @param {string} a + * @param {string} b + * @returns {number} + */ + compareIgnoreCase: function compareIgnoreCase(a, b) String.localeCompare(a.toLowerCase(), b.toLowerCase()), + + /** + * Returns an object representing a Node's computed CSS style. + * + * @param {Node} node + * @returns {Object} + */ + computedStyle: function computedStyle(node) { + while (node instanceof Ci.nsIDOMText && node.parentNode) + node = node.parentNode; + return node.ownerDocument.defaultView.getComputedStyle(node, null); + }, + + /** + * Copies a string to the system clipboard. If verbose is specified + * the copied string is also echoed to the command line. + * + * @param {string} str + * @param {boolean} verbose + */ + copyToClipboard: function copyToClipboard(str, verbose) { + const clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); + clipboardHelper.copyString(str); + + if (verbose) + util.dactyl.echomsg("Yanked " + str); + }, + + /** + * Converts any arbitrary string into an URI object. + * + * @param {string} str + * @returns {Object} + */ + // FIXME: newURI needed too? + createURI: function createURI(str) { + const fixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup); + return fixup.createFixupURI(str, fixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP); + }, + + /** + * Expands brace globbing patterns in a string. + * + * Example: + * "a{b,c}d" => ["abd", "acd"] + * + * @param {string} pattern The pattern to deglob. + * @returns [string] The resulting strings. + */ + debrace: function debrace(pattern) { + if (pattern.indexOf("{") == -1) + return [pattern]; + + function split(pattern, re, fn, dequote) { + let end = 0, match, res = []; + while (match = re.exec(pattern)) { + end = match.index + match[0].length; + res.push(match[1]); + if (fn) + fn(match); + } + res.push(pattern.substr(end)); + return res.map(function (s) util.dequote(s, dequote)); + } + let patterns = [], res = []; + let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy, + function (match) { + patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy, + null, ",{}")); + }, "{}"); + function rec(acc) { + if (acc.length == patterns.length) + res.push(array(substrings).zip(acc).flatten().join("")); + else + for (let [, pattern] in Iterator(patterns[acc.length])) + rec(acc.concat(pattern)); + } + rec([]); + return res; + }, + + /** + * Removes certain backslash-quoted characters while leaving other + * backslash-quoting sequences untouched. + * + * @param {string} pattern The string to unquote. + * @param {string} chars The characters to unquote. + * @returns {string} + */ + dequote: function dequote(pattern, chars) + pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0), + + /** + * Converts HTML special characters in str to the equivalent HTML + * entities. + * + * @param {string} str + * @returns {string} + */ + escapeHTML: function escapeHTML(str) { + return str.replace(/&/g, "&").replace(/str. + * + * @param {string} str + * @returns {string} + */ + escapeRegex: function escapeRegex(str) { + return str.replace(/([\\{}()[\].?*+])/g, "\\$1"); + }, + + /** + * Escapes quotes, newline and tab characters in str. The returned + * string is delimited by delimiter or " if delimiter is not + * specified. {@see String#quote}. + * + * @param {string} str + * @param {string} delimiter + * @returns {string} + */ + escapeString: function escapeString(str, delimiter) { + if (delimiter == undefined) + delimiter = '"'; + return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter; + }, + + /** + * Evaluates an XPath expression in the current or provided + * document. It provides the xhtml, xhtml2 and dactyl XML + * namespaces. The result may be used as an iterator. + * + * @param {string} expression The XPath expression to evaluate. + * @param {Document} doc The document to evaluate the expression in. + * @default The current document. + * @param {Node} elem The context element. + * @default doc + * @param {boolean} asIterator Whether to return the results as an + * XPath iterator. + */ + evaluateXPath: function (expression, doc, elem, asIterator) { + if (!doc) + doc = util.activeWindow.content.document; + if (!elem) + elem = doc; + if (isarray(expression)) + expression = util.makeXPath(expression); + + let result = doc.evaluate(expression, elem, + function lookupNamespaceURI(prefix) { + return { + xul: XUL.uri, + xhtml: XHTML.uri, + xhtml2: "http://www.w3.org/2002/06/xhtml2", + dactyl: NS.uri + }[prefix] || null; + }, + asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE, + null + ); + + return { + __proto__: result, + __iterator__: asIterator + ? function () { let elem; while ((elem = this.iterateNext())) yield elem; } + : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); } + } + }, + + extend: function extend(dest) { + Array.slice(arguments, 1).filter(util.identity).forEach(function (src) { + for (let [k, v] in Iterator(src)) { + let get = src.__lookupGetter__(k), + set = src.__lookupSetter__(k); + if (!get && !set) + dest[k] = v; + if (get) + dest.__defineGetter__(k, get); + if (set) + dest.__defineSetter__(k, set); + } + }); + return dest; + }, + + /** + * Converts bytes to a pretty printed data size string. + * + * @param {number} bytes The number of bytes. + * @param {string} decimalPlaces The number of decimal places to use if + * humanReadable is true. + * @param {boolean} humanReadable Use byte multiples. + * @returns {string} + */ + formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable) { + const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; + let unitIndex = 0; + let tmpNum = parseInt(bytes, 10) || 0; + let strNum = [tmpNum + ""]; + + if (humanReadable) { + while (tmpNum >= 1024) { + tmpNum /= 1024; + if (++unitIndex > (unitVal.length - 1)) + break; + } + + let decPower = Math.pow(10, decimalPlaces); + strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2); + + if (!strNum[1]) + strNum[1] = ""; + + while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces) + strNum[1] += "0"; + } + + for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000 + strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u); + + if (unitIndex) // decimalPlaces only when > Bytes + strNum[0] += "." + strNum[1]; + + return strNum[0] + " " + unitVal[unitIndex]; + }, + + /** + * Sends a synchronous or asynchronous HTTP request to url and + * returns the XMLHttpRequest object. If callback is specified the + * request is asynchronous and the callback is invoked with the + * object as its argument. + * + * @param {string} url + * @param {function(XMLHttpRequest)} callback + * @returns {XMLHttpRequest} + */ + httpGet: function httpGet(url, callback) { + try { + let xmlhttp = services.create("xmlhttp"); + xmlhttp.mozBackgroundRequest = true; + if (callback) { + xmlhttp.onreadystatechange = function () { + if (xmlhttp.readyState == 4) + callback(xmlhttp); + }; + } + xmlhttp.open("GET", url, !!callback); + xmlhttp.send(null); + return xmlhttp; + } + catch (e) { + util.dactyl.log("Error opening " + String.quote(url) + ": " + e, 1); + return null; + } + }, + + /** + * The identity function. + * + * @param {Object} k + * @returns {Object} + */ + identity: function identity(k) k, + + /** + * Returns the intersection of two rectangles. + * + * @param {Object} r1 + * @param {Object} r2 + * @returns {Object} + */ + intersection: function (r1, r2) ({ + get width() this.right - this.left, + get height() this.bottom - this.top, + left: Math.max(r1.left, r2.left), + right: Math.min(r1.right, r2.right), + top: Math.max(r1.top, r2.top), + bottom: Math.min(r1.bottom, r2.bottom) + }), + + /** + * Returns an XPath union expression constructed from the specified node + * tests. An expression is built with node tests for both the null and + * XHTML namespaces. See {@link Buffer#evaluateXPath}. + * + * @param nodes {Array(string)} + * @returns {string} + */ + makeXPath: function makeXPath(nodes) { + return util.Array(nodes).map(util.debrace).flatten() + .map(function (node) [node, "xhtml:" + node]).flatten() + .map(function (node) "//" + node).join(" | "); + }, + + /** + * Returns the array that results from applying func to each + * property of obj. + * + * @param {Object} obj + * @param {function} func + * @returns {Array} + */ + map: function map(obj, func) { + let ary = []; + for (let i in Iterator(obj)) + ary.push(func(i)); + return ary; + }, + + /** + * Memoize the lookup of a property in an object. + * + * @param {object} obj The object to alter. + * @param {string} key The name of the property to memoize. + * @param {function} getter A function of zero to two arguments which + * will return the property's value. obj is + * passed as the first argument, key as the + * second. + */ + memoize: memoize, + + newThread: function () services.get("threadManager").newThread(0), + + /** + * Converts a URI string into a URI object. + * + * @param {string} uri + * @returns {nsIURI} + */ + // FIXME: createURI needed too? + newURI: function (uri) { + return services.get("io").newURI(uri, null, null); + }, + + /** + * Pretty print a JavaScript object. Use HTML markup to color certain items + * if color is true. + * + * @param {Object} object The object to pretty print. + * @param {boolean} color Whether the output should be colored. + * @returns {string} + */ + objectToString: function objectToString(object, color) { + // Use E4X literals so html is automatically quoted + // only when it's asked for. No one wants to see < + // on their console or :map :foo in their buffer + // when they expect :map :foo. + XML.prettyPrinting = false; + XML.ignoreWhitespace = false; + + if (object === null) + return "null\n"; + + if (typeof object != "object") + return false; + + const NAMESPACES = util.Array.toObject([ + [NS, 'dactyl'], + [XHTML, 'html'], + [XUL, 'xul'] + ]); + if (object instanceof Ci.nsIDOMElement) { + let elem = object; + if (elem.nodeType == elem.TEXT_NODE) + return elem.data; + function namespaced(node) { + var ns = NAMESPACES[node.namespaceURI]; + if (ns) + return ns + ":" + node.localName; + return node.localName.toLowerCase(); + } + try { + let tag = "<" + [namespaced(elem)].concat( + [namespaced(a) + "=" + template.highlight(a.value, true) + for ([i, a] in util.Array.iteritems(elem.attributes))]).join(" "); + + if (!elem.firstChild || /^\s*$/.test(elem.firstChild) && !elem.firstChild.nextSibling) + tag += '/>'; + else + tag += '>...'; + return tag; + } + catch (e) { + return {}.toString.call(elem); + } + } + + try { // for window.JSON + var obj = String(object); + } + catch (e) { + obj = "[Object]"; + } + obj = template.highlightFilter(util.clip(obj, 150), "\n", !color ? function () "^J" : function () ^J); + let string = <>{obj}::
    ; + + let keys = []; + try { // window.content often does not want to be queried with "var i in object" + let hasValue = !("__iterator__" in object || isinstance(object, ["Generator", "Iterator"])); + if (object.dactyl && object.modules && object.modules.modules == object.modules) { + object = Iterator(object); + hasValue = false; + } + for (let i in object) { + let value = ]]>; + try { + value = object[i]; + } + catch (e) {} + if (!hasValue) { + if (isarray(i) && i.length == 2) + [i, value] = i; + else + var noVal = true; + } + + value = template.highlight(value, true, 150); + let key = {i}; + if (!isNaN(i)) + i = parseInt(i); + else if (/^[A-Z_]+$/.test(i)) + i = ""; + keys.push([i, <>{key}{noVal ? "" : <>: {value}}
    ]); + } + } + catch (e) {} + + function compare(a, b) { + if (!isNaN(a[0]) && !isNaN(b[0])) + return a[0] - b[0]; + return String.localeCompare(a[0], b[0]); + } + string += template.map(keys.sort(compare), function (f) f[1]); + return color ? string : [s for each (s in string)].join(""); + }, + + /** + * A generator that returns the values between start and end, + * in step increments. + * + * @param {number} start The interval's start value. + * @param {number} end The interval's end value. + * @param {boolean} step The value to step the range by. May be + * negative. @default 1 + * @returns {Iterator(Object)} + */ + range: function range(start, end, step) { + if (!step) + step = 1; + if (step > 0) { + for (; start < end; start += step) + yield start; + } + else { + while (start > end) + yield start += step; + } + }, + + /** + * An interruptible generator that returns all values between start + * and end. The thread yields every time milliseconds. + * + * @param {number} start The interval's start value. + * @param {number} end The interval's end value. + * @param {number} time The time in milliseconds between thread yields. + * @returns {Iterator(Object)} + */ + interruptibleRange: function interruptibleRange(start, end, time) { + let endTime = Date.now() + time; + while (start < end) { + if (Date.now() > endTime) { + util.threadYield(true, true); + endTime = Date.now() + time; + } + yield start++; + } + }, + + /** + * Reads a string from the system clipboard. + * + * This is same as Firefox's readFromClipboard function, but is needed for + * apps like Thunderbird which do not provide it. + * + * @returns {string} + */ + readFromClipboard: function readFromClipboard() { + let str; + + try { + const clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard); + const transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + + transferable.addDataFlavor("text/unicode"); + + if (clipboard.supportsSelectionClipboard()) + clipboard.getData(transferable, clipboard.kSelectionClipboard); + else + clipboard.getData(transferable, clipboard.kGlobalClipboard); + + let data = {}; + let dataLen = {}; + + transferable.getTransferData("text/unicode", data, dataLen); + + if (data) { + data = data.value.QueryInterface(Ci.nsISupportsString); + str = data.data.substring(0, dataLen.value / 2); + } + } + catch (e) {} + + return str; + }, + + /** + * Scrolls an element into view if and only if it's not already + * fully visible. + * + * @param {Node} elem The element to make visible. + */ + scrollIntoView: function scrollIntoView(elem) { + let win = elem.ownerDocument.defaultView; + let rect = elem.getBoundingClientRect(); + if (!(rect && rect.top < win.innerHeight && rect.bottom >= 0 && rect.left < win.innerWidth && rect.right >= 0)) + elem.scrollIntoView(); + }, + + /** + * Returns the selection controller for the given window. + * + * @param {Window} window + * @returns {nsISelectionController} + */ + selectionController: function (win) + win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController), + + /** + * Suspend execution for at least 'delay' milliseconds. Functions by + * yielding execution to the next item in the main event queue, and + * so may lead to unexpected call graphs, and long delays if another + * handler yields execution while waiting. + * + * @param {number} delay The time period for which to sleep in milliseconds. + */ + sleep: function (delay) { + let mainThread = services.get("threadManager").mainThread; + + let end = Date.now() + delay; + while (Date.now() < end) + mainThread.processNextEvent(true); + return true; + }, + + highlightFilter: function highlightFilter(str, filter, highlight) { + return this.highlightSubstrings(str, (function () { + if (filter.length == 0) + return; + let lcstr = String.toLowerCase(str); + let lcfilter = filter.toLowerCase(); + let start = 0; + while ((start = lcstr.indexOf(lcfilter, start)) > -1) { + yield [start, filter.length]; + start += filter.length; + } + })(), highlight || template.filter); + }, + + /** + * Behaves like String.split, except that when 'limit' is reached, + * the trailing element contains the entire trailing portion of the + * string. + * + * util.split("a, b, c, d, e", /, /, 3) -> ["a", "b", "c, d, e"] + * @param {string} str The string to split. + * @param {RegExp|string} re The regular expression on which to split the string. + * @param {number} limit The maximum number of elements to return. + * @returns {[string]} + */ + split: function (str, re, limit) { + if (!re.global) + re = RegExp(re.source || re, "g"); + let match, start = 0, res = []; + while ((match = re.exec(str)) && --limit && match[0].length) { + res.push(str.substring(start, match.index)); + start = match.index + match[0].length; + } + if (limit) + res.push(str.substring(start)); + return res; + }, + + /** + * Split a string on literal occurrences of a marker. + * + * Specifically this ignores occurrences preceded by a backslash, or + * contained within 'single' or "double" quotes. + * + * It assumes backslash escaping on strings, and will thus not count quotes + * that are preceded by a backslash or within other quotes as starting or + * ending quoted sections of the string. + * + * @param {string} str + * @param {RegExp} marker + */ + splitLiteral: function splitLiteral(str, marker) { + let results = []; + let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source); + let cont = true; + + while (cont) { + cont = false; + str = str.replace(resep, function (match, before) { + results.push(before); + cont = true; + return ""; + }); + } + + results.push(str); + return results; + }, + + threadYield: function (flush, interruptable) { + let mainThread = services.get("threadManager").mainThread; + /* FIXME */ + util.interrupted = false; + do { + mainThread.processNextEvent(!flush); + if (util.interrupted) + throw new Error("Interrupted"); + } + while (flush === true && mainThread.hasPendingEvents()); + }, + + /** + * Converts an E4X XML literal to a DOM node. + * + * @param {Node} node + * @param {Document} doc + * @param {Object} nodes If present, nodes with the "key" attribute are + * stored here, keyed to the value thereof. + * @returns {Node} + */ + xmlToDom: function xmlToDom(node, doc, nodes) { + XML.prettyPrinting = false; + if (node.length() != 1) { + let domnode = doc.createDocumentFragment(); + for each (let child in node) + domnode.appendChild(xmlToDom(child, doc, nodes)); + return domnode; + } + switch (node.nodeKind()) { + case "text": + return doc.createTextNode(String(node)); + case "element": + let domnode = doc.createElementNS(node.namespace(), node.localName()); + for each (let attr in node.@*) + domnode.setAttributeNS(attr.name() == "highlight" ? NS.uri : attr.namespace(), attr.name(), String(attr)); + for each (let child in node.*) + domnode.appendChild(xmlToDom(child, doc, nodes)); + if (nodes && node.@key) + nodes[node.@key] = domnode; + return domnode; + default: + return null; + } + } +}, { + Array: array +}); + +/** + * Math utility methods. + * @singleton + */ +const GlobalMath = Math; +var Math = { + __proto__: GlobalMath, + + /** + * Returns the specified value constrained to the range min - + * max. + * + * @param {number} value The value to constrain. + * @param {number} min The minimum constraint. + * @param {number} max The maximum constraint. + * @returns {number} + */ + constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max) +}; + +// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n");} + +endmodule(); + +// vim: set fdm=marker sw=4 ts=4 et ft=javascript: -- cgit v1.2.3