summaryrefslogtreecommitdiff
path: root/common/modules
diff options
context:
space:
mode:
authorKris Maglione <kris@vimperator.org>2010-08-31 21:09:13 -0400
committerKris Maglione <kris@vimperator.org>2010-08-31 21:09:13 -0400
commit8b0d9586b23eb166fafb064e75c4956021d73ca1 (patch)
treecc3a8bdda4e19dc18eadd5ed0edc4aa2131b43e2 /common/modules
parent5632e14721897b9e7e23d493f95358bb7df73314 (diff)
downloadpentadactyl-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.jsm798
-rw-r--r--common/modules/bookmarkcache.jsm162
-rw-r--r--common/modules/highlight.jsm241
-rw-r--r--common/modules/services.jsm133
-rw-r--r--common/modules/storage.jsm535
-rw-r--r--common/modules/styles.jsm373
-rw-r--r--common/modules/template.jsm311
-rw-r--r--common/modules/util.jsm815
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>&#160;
+ {
+ !(item.extra && item.extra.length) ? "" :
+ <span class="extra-info">
+ ({
+ template.map(item.extra, function (e)
+ <>{e[0]}: <span highlight={e[2]}>{e[1]}</span></>,
+ <>&#xa0;</>/* 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}&#160;</li>
+ <li highlight="CompDesc">{desc}&#160;</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, "&amp;").replace(/</g, "&lt;");
+ },
+
+ /**
+ * 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 &lt;
+ // 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/>&#xa;</>;
+
+ 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/>&#xa;</>]);
+ }
+ }
+ 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: