diff options
author | Kris Maglione <kris@vimperator.org> | 2010-08-31 21:09:13 -0400 |
---|---|---|
committer | Kris Maglione <kris@vimperator.org> | 2010-08-31 21:09:13 -0400 |
commit | 8b0d9586b23eb166fafb064e75c4956021d73ca1 (patch) | |
tree | cc3a8bdda4e19dc18eadd5ed0edc4aa2131b43e2 /common/modules | |
parent | 5632e14721897b9e7e23d493f95358bb7df73314 (diff) | |
download | pentadactyl-8b0d9586b23eb166fafb064e75c4956021d73ca1.tar.gz |
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
Diffstat (limited to 'common/modules')
-rw-r--r-- | common/modules/base.jsm | 798 | ||||
-rw-r--r-- | common/modules/bookmarkcache.jsm | 162 | ||||
-rw-r--r-- | common/modules/highlight.jsm | 241 | ||||
-rw-r--r-- | common/modules/services.jsm | 133 | ||||
-rw-r--r-- | common/modules/storage.jsm | 535 | ||||
-rw-r--r-- | common/modules/styles.jsm | 373 | ||||
-rw-r--r-- | common/modules/template.jsm | 311 | ||||
-rw-r--r-- | common/modules/util.jsm | 815 |
8 files changed, 3201 insertions, 167 deletions
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 <maglione.k@gmail.com> +// +// 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 + * <b>unsorted</b> 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 <maglione.k at Gmail> +// +// 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 <maglione.k at Gmail> +// +// 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 <maglione.k@gmail.com> + */ +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 = <![CDATA[ + ; + display: inline-block !important; + position: static !important; + margin: 0px !important; padding: 0px !important; + width: 3em !important; min-width: 3em !important; max-width: 3em !important; + height: 1em !important; min-height: 1em !important; max-height: 1em !important; + overflow: hidden !important; + ]]>; + 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, + <span style={"text-align: center; line-height: 1em;" + h.value + style}>XXX</span>, + 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 <maglione.k at Gmail> +// +// 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 <b>buf</b> 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 <maglione.k at Gmail> +// +// 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 <maglione.k@gmail.com> + */ +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 <b>filter</b> 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 <maglione.k at Gmail> +// +// 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) + <> + <a href={item.item.url} highlight="URL">{text}</a>  + { + !(item.extra && item.extra.length) ? "" : + <span class="extra-info"> + ({ + template.map(item.extra, function (e) + <>{e[0]}: <span highlight={e[2]}>{e[1]}</span></>, + <> </>/* Non-breaking space */) + }) + </span> + } + </>, + + filter: function (str) <span highlight="Filter">{str}</span>, + + 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); + } + + // <e4x> + return <div highlight={highlightGroup || "CompItem"} style="white-space: nowrap"> + <!-- The non-breaking spaces prevent empty elements + - from pushing the baseline down and enlarging + - the row. + --> + <li highlight="CompResult">{text} </li> + <li highlight="CompDesc">{desc} </li> + </div>; + // </e4x> + }, + + genericTable: function genericTable(items, format) { + completion.listCompleter(function (context) { + context.filterFunc = null; + if (format) + context.format = format; + context.completions = items; + }); + }, + + gradient: function (left, right) + <div highlight="Gradient"> + <div style="height: 0px"> + <div highlight={right + " Gradient"} + style="border: 0 !important; margin: 0 !important; padding: 0 !important;"/> + </div> + <table width="100%" style="height: 100%"> + <tr> + { template.map(util.range(0, 100), function (i) + <td highlight={left} style={"opacity: " + (1 - i / 100)} + />) } + </tr> + </table> + </div>, + + // 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 <span highlight="Number">{str}</span>; + case "string": + if (processStrings) + str = str.quote(); + return <span highlight="String">{str}</span>; + case "boolean": + return <span highlight="Boolean">{str}</span>; + case "function": + // Vim generally doesn't like /foo*/, because */ looks like a comment terminator. + // Using /foo*(:?)/ instead. + if (processStrings) + return <span highlight="Function">{str.replace(/\{(.|\n)*(?:)/g, "{ ... }")}</span>; + <>}</>; /* Vim */ + return <>{arg}</>; + case "undefined": + return <span highlight="Null">{arg}</span>; + 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 () <span highlight="NonText">^J</span>); + return <span highlight="Object">{str}</span>; + case "xml": + return arg; + default: + return <![CDATA[<unknown type>]]>; + } + } + catch (e) { + return <![CDATA[<unknown>]]>; + } + }, + + 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 <a highlight="URL" href={str}>{str}</a>; + else + return str; + }, + + icon: function (item, text) <> + <span highlight="CompIcon">{item.icon ? <img src={item.icon}/> : <></>}</span><span class="td-strut"/>{text} + </>, + + jumps: function jumps(index, elems) { + // <e4x> + return <table> + <tr style="text-align: left;" highlight="Title"> + <th colspan="2">jump</th><th>title</th><th>URI</th> + </tr> + { + this.map(Iterator(elems), function ([idx, val]) + <tr> + <td class="indicator">{idx == index ? ">" : ""}</td> + <td>{Math.abs(idx - index)}</td> + <td style="width: 250px; max-width: 500px; overflow: hidden;">{val.title}</td> + <td><a href={val.URI.spec} highlight="URL jump-list">{val.URI.spec}</a></td> + </tr>) + } + </table>; + // </e4x> + }, + + options: function options(title, opts) { + // <e4x> + return <table> + <tr highlight="Title" align="left"> + <th>--- {title} ---</th> + </tr> + { + this.map(opts, function (opt) + <tr> + <td> + <span style={opt.isDefault ? "" : "font-weight: bold"}>{opt.pre}{opt.name}</span><span>{opt.value}</span> + {opt.isDefault || opt.default == null ? "" : <span class="extra-info"> (default: {opt.default})</span>} + </td> + </tr>) + } + </table>; + // </e4x> + }, + + table: function table(title, data, indent) { + let table = + // <e4x> + <table> + <tr highlight="Title" align="left"> + <th colspan="2">{title}</th> + </tr> + { + this.map(data, function (datum) + <tr> + <td style={"font-weight: bold; min-width: 150px; padding-left: " + (indent || "2ex")}>{datum[0]}</td> + <td>{template.maybeXML(datum[1])}</td> + </tr>) + } + </table>; + // </e4x> + 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. + // <e4x> + return <table> + <tr highlight="Title" align="left"> + { + this.map(headings, function (h) + <th>{h}</th>) + } + </tr> + { + this.map(iter, function (row) + <tr> + { + template.map(Iterator(row), function ([i, d]) + <td style={style[i] || ""}>{d}</td>) + } + </tr>) + } + </table>; + // </e4x> + }, + + usage: function usage(iter) { + // <e4x> + return <table> + { + this.map(iter, function (item) + <tr> + <td highlight="Title" style="padding-right: 20px">{item.name || item.names[0]}</td> + <td>{item.description}</td> + </tr>) + } + </table>; + // </e4x> + } +}); + +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 <stubenschrott@vimperator.org> +// Copyright (c) 2007-2009 by Doug Kearns <dougkearns@gmail.com> +// Copyright (c) 2008-2009 by Kris Maglione <maglione.k@gmail.com> +// +// 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 <b>obj</b>. + * + * @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 <b>length</b>, 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 <b>verbose</b> 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 <b>str</b> to the equivalent HTML + * entities. + * + * @param {string} str + * @returns {string} + */ + escapeHTML: function escapeHTML(str) { + return str.replace(/&/g, "&").replace(/</g, "<"); + }, + + /** + * Escapes Regular Expression special characters in <b>str</b>. + * + * @param {string} str + * @returns {string} + */ + escapeRegex: function escapeRegex(str) { + return str.replace(/([\\{}()[\].?*+])/g, "\\$1"); + }, + + /** + * Escapes quotes, newline and tab characters in <b>str</b>. The returned + * string is delimited by <b>delimiter</b> or " if <b>delimiter</b> 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 <b>doc</b> + * @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 <b>bytes</b> 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 + * <b>humanReadable</b> 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 <b>url</b> and + * returns the XMLHttpRequest object. If <b>callback</b> is specified the + * request is asynchronous and the <b>callback</b> 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 <b>func</b> to each + * property of <b>obj</b>. + * + * @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. <b>obj</b> is + * passed as the first argument, <b>key</b> 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 <b>color</b> 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 <C-f> :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 += '>...</' + namespaced(elem) + '>'; + 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 () <span highlight="NonText">^J</span>); + let string = <><span highlight="Title Object">{obj}</span>::<br/>
</>; + + 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 = <![CDATA[<no 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 = <span highlight="Key">{i}</span>; + if (!isNaN(i)) + i = parseInt(i); + else if (/^[A-Z_]+$/.test(i)) + i = ""; + keys.push([i, <>{key}{noVal ? "" : <>: {value}</>}<br/>
</>]); + } + } + 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 <b>start</b> and <b>end</b>, + * in <b>step</b> 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 <b>start</b> + * and <b>end</b>. The thread yields every <b>time</b> 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 <b>value</b> constrained to the range <b>min</b> - + * <b>max</b>. + * + * @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: |