]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/util.jsm
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / modules / util.jsm
index 673366a099188f42f964583bc1a7f44a78d6f0b5..7941ebf587249f94d4a90979432023c773cbe67a 100644 (file)
@@ -13,7 +13,7 @@ Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("util", {
     exports: ["frag", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
     require: ["services"],
-    use: ["commands", "config", "highlight", "storage", "template"]
+    use: ["commands", "config", "highlight", "messages", "storage", "template"]
 }, this);
 
 var XBL = Namespace("xbl", "http://www.mozilla.org/xbl");
@@ -115,12 +115,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      *
      * @param {object} obj
      */
-    addObserver: function (obj) {
+    addObserver: update(function addObserver(obj) {
         if (!obj.observers)
             obj.observers = obj.observe;
 
         function register(meth) {
-            for (let target in set(["dactyl-cleanup-modules", "quit-application"].concat(Object.keys(obj.observers))))
+            for (let target in Set(["dactyl-cleanup-modules", "quit-application"].concat(Object.keys(obj.observers))))
                 try {
                     services.observer[meth](obj, target, true);
                 }
@@ -137,7 +137,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 }
                 catch (e) {
                     if (typeof util === "undefined")
-                        dump("dactyl: error: " + e + "\n" + (e.stack || Error().stack).replace(/^/gm, "dactyl:    "));
+                        addObserver.dump("dactyl: error: " + e + "\n" + (e.stack || addObserver.Error().stack).replace(/^/gm, "dactyl:    "));
                     else
                         util.reportError(e);
                 }
@@ -145,7 +145,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
 
         obj.observe.unregister = function () register("removeObserver");
         register("addObserver");
-    },
+    }, { dump: dump, Error: Error }),
 
     /*
      * Tests a condition and throws a FailedAssertion error on
@@ -158,6 +158,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     assert: function (condition, message, quiet) {
         if (!condition)
             throw FailedAssertion(message, 1, quiet === undefined ? true : quiet);
+        return condition;
     },
 
     /**
@@ -165,7 +166,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {string} str The string to capitalize
      * @returns {string}
      */
-    capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1),
+    capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1).toLowerCase(),
 
     /**
      * Returns a RegExp object that matches characters specified in the range
@@ -181,12 +182,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
 
         // check for chars not in the accepted range
         this.assert(RegExp("^[" + accepted + "-]+$").test(list),
-                    "Character list outside the range " + accepted.quote());
+                    _("error.charactersOutsideRange", accepted.quote()));
 
         // check for illegal ranges
         for (let [match] in this.regexp.iterate(/.-./g, list))
             this.assert(match.charCodeAt(0) <= match.charCodeAt(2),
-                        "Invalid character range: " + list.slice(list.indexOf(match)))
+                        _("error.invalidCharacterRange", list.slice(list.indexOf(match))));
 
         return RegExp("[" + util.regexp.escape(list) + "]");
     },
@@ -308,7 +309,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             }
             else if (char === "]") {
                 stack.pop();
-                util.assert(stack.length, "Unmatched %] in format");
+                util.assert(stack.length, /*L*/"Unmatched %] in format");
             }
             else {
                 let quote = function quote(obj, char) obj[char];
@@ -327,10 +328,35 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         if (end < format.length)
             stack.top.elements.push(format.substr(end));
 
-        util.assert(stack.length === 1, "Unmatched %[ in format");
+        util.assert(stack.length === 1, /*L*/"Unmatched %[ in format");
         return stack.top;
     },
 
+    /**
+     * Compiles a macro string into a function which generates a string
+     * result based on the input *macro* and its parameters. The
+     * definitive documentation for macro strings resides in :help
+     * macro-string.
+     *
+     * Macro parameters may have any of the following flags:
+     *     e: The parameter is only tested for existence. Its
+     *        interpolation is always empty.
+     *     q: The result is quoted such that it is parsed as a single
+     *        argument by the Ex argument parser.
+     *
+     * The returned function has the following additional properties:
+     *
+     *     seen {set}: The set of parameters used in this macro.
+     *
+     *     valid {function(object)}: Returns true if every parameter of
+     *          this macro is provided by the passed object.
+     *
+     * @param {string} macro The macro string to compile.
+     * @param {boolean} keepUnknown If true, unknown macro parameters
+     *      are left untouched. Otherwise, they are replaced with the null
+     *      string.
+     * @returns {function}
+     */
     compileMacro: function compileMacro(macro, keepUnknown) {
         let stack = [frame()];
         stack.__defineGetter__("top", function () this[this.length - 1]);
@@ -355,14 +381,14 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             ([^]*?) // 1
             (?:
                 (<\{) | // 2
-                (< ((?:[a-z]-)?[a-z-]+?) >) | // 3 4
-                (\}>) // 5
+                (< ((?:[a-z]-)?[a-z-]+?) (?:\[([0-9]+)\])? >) | // 3 4 5
+                (\}>) // 6
             )
         ]]>, "gixy");
         macro = String(macro);
         let end = 0;
         for (let match in re.iterate(macro)) {
-            let [, prefix, open, full, macro, close] = match;
+            let [, prefix, open, full, macro, idx, close] = match;
             end += match[0].length;
 
             if (prefix)
@@ -374,11 +400,11 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             }
             else if (close) {
                 stack.pop();
-                util.assert(stack.length, "Unmatched %] in macro");
+                util.assert(stack.length, /*L*/"Unmatched %] in macro");
             }
             else {
                 let [, flags, name] = /^((?:[a-z]-)*)(.*)/.exec(macro);
-                flags = set(flags);
+                flags = Set(flags);
 
                 let quote = util.identity;
                 if (flags.q)
@@ -386,12 +412,20 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 if (flags.e)
                     quote = function quote(obj) "";
 
-                if (set.has(defaults, name))
+                if (Set.has(defaults, name))
                     stack.top.elements.push(quote(defaults[name]));
                 else {
-                    stack.top.elements.push(update(
-                        function (obj) obj[name] != null ? quote(obj[name]) : set.has(obj, name) ? "" : unknown(full),
-                        { test: function (obj) obj[name] != null && obj[name] !== false && (!flags.e || obj[name] != "") }));
+                    if (idx) {
+                        idx = Number(idx) - 1;
+                        stack.top.elements.push(update(
+                            function (obj) obj[name] != null && idx in obj[name] ? quote(obj[name][idx]) : Set.has(obj, name) ? "" : unknown(full),
+                            { test: function (obj) obj[name] != null && idx in obj[name] && obj[name][idx] !== false && (!flags.e || obj[name][idx] != "") }));
+                    }
+                    else {
+                        stack.top.elements.push(update(
+                            function (obj) obj[name] != null ? quote(obj[name]) : Set.has(obj, name) ? "" : unknown(full),
+                            { test: function (obj) obj[name] != null && obj[name] !== false && (!flags.e || obj[name] != "") }));
+                    }
 
                     for (let elem in array.iterValues(stack))
                         elem.seen[name] = true;
@@ -401,10 +435,20 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         if (end < macro.length)
             stack.top.elements.push(macro.substr(end));
 
-        util.assert(stack.length === 1, "Unmatched <{ in macro");
+        util.assert(stack.length === 1, /*L*/"Unmatched <{ in macro");
         return stack.top;
     },
 
+    /**
+     * Compiles a CSS spec and XPath pattern matcher based on the given
+     * list. List elements prefixed with "xpath:" are parsed as XPath
+     * patterns, while other elements are parsed as CSS specs. The
+     * returned function will, given a node, return an iterator of all
+     * descendants of that node which match the given specs.
+     *
+     * @param {[string]} list The list of patterns to match.
+     * @returns {function(Node)}
+     */
     compileMatcher: function compileMatcher(list) {
         let xpath = [], css = [];
         for (let elem in values(list))
@@ -428,10 +472,18 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             });
     },
 
-    validateMatcher: function validateMatcher(values) {
+    /**
+     * Validates a list as input for {@link #compileMatcher}. Returns
+     * true if and only if every element of the list is a valid XPath or
+     * CSS selector.
+     *
+     * @param {[string]} list The list of patterns to test
+     * @returns {boolean} True when the patterns are all valid.
+     */
+    validateMatcher: function validateMatcher(list) {
         let evaluator = services.XPathEvaluator();
-        let node = util.xmlToDom(<div/>, document);
-        return this.testValues(values, function (value) {
+        let node = services.XMLDocument();
+        return this.testValues(list, function (value) {
             if (/^xpath:/.test(value))
                 evaluator.createExpression(value.substr(6), util.evaluateXPath.resolver);
             else
@@ -483,10 +535,28 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * Example:
      *     "a{b,c}d" => ["abd", "acd"]
      *
-     * @param {string} pattern The pattern to deglob.
+     * @param {string|[string|Array]} pattern The pattern to deglob.
      * @returns [string] The resulting strings.
      */
     debrace: function debrace(pattern) {
+        if (isArray(pattern)) {
+            let res = [];
+            let rec = function rec(acc) {
+                let vals;
+
+                while (isString(vals = pattern[acc.length]))
+                    acc.push(vals);
+
+                if (acc.length == pattern.length)
+                    res.push(acc.join(""))
+                else
+                    for (let val in values(vals))
+                        rec(acc.concat(val));
+            }
+            rec([]);
+            return res;
+        }
+
         if (pattern.indexOf("{") == -1)
             return [pattern];
 
@@ -501,12 +571,14 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             res.push(pattern.substr(end));
             return res.map(function (s) util.dequote(s, dequote));
         }
-        let patterns = [], res = [];
+        let patterns = [];
         let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy,
             function (match) {
                 patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy,
                     null, ",{}"));
             }, "{}");
+
+        let res = [];
         function rec(acc) {
             if (acc.length == patterns.length)
                 res.push(array(substrings).zip(acc).flatten().join(""));
@@ -529,6 +601,16 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     dequote: function dequote(pattern, chars)
         pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0),
 
+    /**
+     * Converts a given DOM Node, Range, or Selection to a string. If
+     * *html* is true, the output is HTML, otherwise it is presentation
+     * text.
+     *
+     * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
+     *      stringify.
+     * @param {boolean} html Whether the output should be HTML rather
+     *      than presentation text.
+     */
     domToString: function (node, html) {
         if (node instanceof Ci.nsISelection && node.isCollapsed)
             return "";
@@ -538,7 +620,8 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             range.selectNode(node);
             node = range;
         }
-        let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer.ownerDocument;
+        let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
+        doc = doc.ownerDocument || doc;
 
         let encoder = services.HtmlEncoder();
         encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
@@ -564,6 +647,13 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      */
     dump: defineModule.dump,
 
+    /**
+     * Returns a list of reformatted stack frames from
+     * {@seeĀ Error#stack}.
+     *
+     * @param {string} stack The stack trace from an Error.
+     * @returns {[string]} The stack frames.
+     */
     stackLines: function (stack) {
         let lines = [];
         let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g;
@@ -589,7 +679,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * The set of input element type attribute values that mark the element as
      * an editable field.
      */
-    editableInputs: set(["date", "datetime", "datetime-local", "email", "file",
+    editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
                          "month", "number", "password", "range", "search",
                          "tel", "text", "time", "url", "week"]),
 
@@ -738,12 +828,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         [hours, minutes]   = div(minutes, 60);
         [days, hours]      = div(hours,   24);
         if (days)
-            return days + " days " + hours + " hours"
+            return /*L*/days + " days " + hours + " hours"
         if (hours)
-            return hours + "h " + minutes + "m";
+            return /*L*/hours + "h " + minutes + "m";
         if (minutes)
-            return minutes + ":" + pad(2, seconds);
-        return seconds + "s";
+            return /*L*/minutes + ":" + pad(2, seconds);
+        return /*L*/seconds + "s";
     },
 
     /**
@@ -758,12 +848,12 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 uri = util.newURI(util.fixURI(uri));
 
             if (uri instanceof Ci.nsIFileURL)
-                return File(uri.QueryInterface(Ci.nsIFileURL).file);
+                return File(uri.file);
 
             let channel = services.io.newChannelFromURI(uri);
             channel.cancel(Cr.NS_BINDING_ABORTED);
             if (channel instanceof Ci.nsIFileChannel)
-                return File(channel.QueryInterface(Ci.nsIFileChannel).file);
+                return File(channel.file);
         }
         catch (e) {}
         return null;
@@ -826,7 +916,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             return xmlhttp;
         }
         catch (e) {
-            util.dactyl.log("Error opening " + String.quote(url) + ": " + e, 1);
+            util.dactyl.log(_("error.cantOpen", String.quote(url), e), 1);
             return null;
         }
     },
@@ -939,6 +1029,35 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         }
     },
 
+    // ripped from Firefox; modified
+    unsafeURI: Class.memoize(function () util.regexp(String.replace(<![CDATA[
+            [
+                \s
+                // Invisible characters (bug 452979)
+                U001C U001D U001E U001F // file/group/record/unit separator
+                U00AD // Soft hyphen
+                UFEFF // BOM
+                U2060 // Word joiner
+                U2062 U2063 // Invisible times/separator
+                U200B UFFFC // Zero-width space/no-break space
+
+                // Bidi formatting characters. (RFC 3987 sections 3.2 and 4.1 paragraph 6)
+                U200E U200F U202A U202B U202C U202D U202E
+            ]
+        ]]>, /U/g, "\\u"),
+        "gx")),
+    losslessDecodeURI: function losslessDecodeURI(url) {
+        return url.split("%25").map(function (url) {
+                // Non-UTF-8 compliant URLs cause "malformed URI sequence" errors.
+                try {
+                    return decodeURI(url).replace(this.unsafeURI, encodeURIComponent);
+                }
+                catch (e) {
+                    return url;
+                }
+            }, this).join("%25");
+    },
+
     /**
      * Returns an XPath union expression constructed from the specified node
      * tests. An expression is built with node tests for both the null and
@@ -949,10 +1068,27 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      */
     makeXPath: function makeXPath(nodes) {
         return array(nodes).map(util.debrace).flatten()
-                           .map(function (node) [node, "xhtml:" + node]).flatten()
+                           .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
                            .map(function (node) "//" + node).join(" | ");
     },
 
+    /**
+     * Creates a DTD fragment from the given object. Each property of
+     * the object is converted to an ENTITY declaration. SGML special
+     * characters other than ' and % are left intact.
+     *
+     * @param {object} obj The object to convert.
+     * @returns {string} The DTD fragment containing entity declaration
+     *      for *obj*.
+     */
+    makeDTD: let (map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" })
+        function makeDTD(obj) iter(obj)
+          .map(function ([k, v]) ["<!ENTITY ", k, " '", String.replace(v == null ? "null" : typeof v == "xml" ? v.toXMLString() : v,
+                                                                       typeof v == "xml" ? /['%]/g : /['"%&<>]/g,
+                                                                       function (m) map[m]),
+                                  "'>"].join(""))
+          .join("\n"),
+
     map: deprecated("iter.map", function map(obj, fn, self) iter(obj).map(fn, self).toArray()),
     writeToClipboard: deprecated("dactyl.clipboardWrite", function writeToClipboard(str, verbose) util.dactyl.clipboardWrite(str, verbose)),
     readFromClipboard: deprecated("dactyl.clipboardRead", function readFromClipboard() util.dactyl.clipboardRead(false)),
@@ -964,7 +1100,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @returns {nsIURI}
      */
     // FIXME: createURI needed too?
-    newURI: function (uri, charset, base) services.io.newURI(uri, charset, base),
+    newURI: function newURI(uri, charset, base) this.withProperErrors("newURI", services.io, uri, charset, base),
 
     /**
      * Removes leading garbage prepended to URIs by the subscript
@@ -1029,7 +1165,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                     }</span>;
 
                 let tag = "<" + [namespaced(elem)].concat(
-                    [namespaced(a) + "=" +  template.highlight(a.value, true)
+                    [namespaced(a) + "=" + template.highlight(a.value, true)
                      for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
                 return tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">");
             }
@@ -1090,13 +1226,13 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     },
 
     observers: {
-        "dactyl-cleanup-modules": function () {
-            defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules");
+        "dactyl-cleanup-modules": function (subject, reason) {
+            defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules " + reason);
 
             for (let module in values(defineModule.modules))
                 if (module.cleanup) {
                     util.dump("cleanup: " + module.constructor.className);
-                    util.trapErrors(module.cleanup, module);
+                    util.trapErrors(module.cleanup, module, reason);
                 }
 
             JSMLoader.cleanup();
@@ -1120,6 +1256,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         "dactyl-purge": function () {
             this.rehashing = 1;
         },
+
         "toplevel-window-ready": function (window, data) {
             window.addEventListener("DOMContentLoaded", wrapCallback(function listener(event) {
                 if (event.originalTarget === window.document) {
@@ -1199,6 +1336,20 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 }), true);
     },
 
+    /**
+     * Overlays an object with the given property overrides. Each
+     * property in *overrides* is added to *object*, replacing any
+     * original value. Functions in *overrides* are augmented with the
+     * new properties *super*, *supercall*, and *superapply*, in the
+     * same manner as class methods, so that they man call their
+     * overridden counterparts.
+     *
+     * @param {object} object The object to overlay.
+     * @param {object} overrides An object containing properties to
+     *      override.
+     * @returns {function} A function which, when called, will remove
+     *      the overlay.
+     */
     overlayObject: function (object, overrides) {
         let original = Object.create(object);
         overrides = update(Object.create(original), overrides);
@@ -1208,24 +1359,26 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             if (desc.value instanceof Class.Property)
                 desc = desc.value.init(k) || desc.value;
 
-            for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
-                if (orig = Object.getOwnPropertyDescriptor(obj, k))
-                    Object.defineProperty(original, k, orig);
+            if (k in object) {
+                for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
+                    if (orig = Object.getOwnPropertyDescriptor(obj, k))
+                        Object.defineProperty(original, k, orig);
+
+                if (!orig)
+                    if (orig = Object.getPropertyDescriptor(object, k))
+                        Object.defineProperty(original, k, orig);
+            }
 
             // Guard against horrible add-ons that use eval-based monkey
             // patching.
+            let value = desc.value;
             if (callable(desc.value)) {
-                let value = desc.value;
-
-                let sentinel = "(function DactylOverlay() {}())"
-                value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
-                value.toSource = function toSource() toString.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
 
                 delete desc.value;
                 delete desc.writable;
                 desc.get = function get() value;
                 desc.set = function set(val) {
-                    if (String(val).indexOf(sentinel) < 0)
+                    if (!callable(val) || Function.prototype.toString(val).indexOf(sentinel) < 0)
                         Class.replaceProperty(this, k, val);
                     else {
                         let package_ = util.newURI(util.fixURI(Components.stack.caller.filename)).host;
@@ -1235,12 +1388,37 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 };
             }
 
-            Object.defineProperty(object, k, desc);
+            try {
+                Object.defineProperty(object, k, desc);
+
+                if (callable(value)) {
+                    let sentinel = "(function DactylOverlay() {}())"
+                    value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
+                    value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
+                }
+            }
+            catch (e) {
+                try {
+                    if (value) {
+                        object[k] = value;
+                        return;
+                    }
+                }
+                catch (f) {}
+                util.reportError(e);
+            }
         }, this);
 
         return function unwrap() {
             for each (let k in Object.getOwnPropertyNames(original))
-                Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
+                if (Object.getOwnPropertyDescriptor(object, k).configurable)
+                    Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
+                else {
+                    try {
+                        object[k] = original[k];
+                    }
+                    catch (e) {}
+                }
         };
     },
 
@@ -1305,17 +1483,20 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
             elems.push(encode(field.name, field.value));
 
-        for (let [, elem] in iter(form.elements)) {
-            if (set.has(util.editableInputs, elem.type)
-                    || /^(?:hidden|textarea)$/.test(elem.type)
-                    || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
-                elems.push(encode(elem.name, elem.value, elem === field));
-            else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
-                for (let [, opt] in Iterator(elem.options))
-                    if (opt.selected)
-                        elems.push(encode(elem.name, opt.value));
+        for (let [, elem] in iter(form.elements))
+            if (elem.name && !elem.disabled) {
+                if (Set.has(util.editableInputs, elem.type)
+                        || /^(?:hidden|textarea)$/.test(elem.type)
+                        || elem.type == "submit" && elem == field
+                        || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
+                    elems.push(encode(elem.name, elem.value, elem === field));
+                else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
+                    for (let [, opt] in Iterator(elem.options))
+                        if (opt.selected)
+                            elems.push(encode(elem.name, opt.value));
+                }
             }
-        }
+
         if (post)
             return [url, elems.join('&'), charset, elems];
         return [url + "?" + elems.join('&'), null, charset, elems];
@@ -1400,7 +1581,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
 
         // Replace replacement <tokens>.
         if (tokens)
-            expr = String.replace(expr, /(\(?P)?<(\w+)>/g, function (m, n1, n2) !n1 && set.has(tokens, n2) ? tokens[n2].dactylSource || tokens[n2].source || tokens[n2] : m);
+            expr = String.replace(expr, /(\(?P)?<(\w+)>/g, function (m, n1, n2) !n1 && Set.has(tokens, n2) ? tokens[n2].dactylSource || tokens[n2].source || tokens[n2] : m);
 
         // Strip comments and white space.
         if (/x/.test(flags))
@@ -1471,9 +1652,14 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         }())
     }),
 
+    /**
+     * Reloads dactyl in entirety by disabling the add-on and
+     * re-enabling it.
+     */
     rehash: function (args) {
-        JSMLoader.commandlineArgs = args;
+        storage.session.commandlineArgs = args;
         this.timeout(function () {
+            services.observer.notifyObservers(null, "startupcache-invalidate", "");
             this.rehashing = true;
             let addon = config.addon;
             addon.userDisabled = true;
@@ -1484,6 +1670,11 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     errorCount: 0,
     errors: Class.memoize(function () []),
     maxErrors: 15,
+    /**
+     * Reports an error to the Error Console and the standard output,
+     * along with a stack trace and other relevant information. The
+     * error is appended to {@seeĀ #errors}.
+     */
     reportError: function (error) {
         if (error.noTrace)
             return;
@@ -1541,7 +1732,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         catch (e) {}
 
         let ary = host.split(".");
-        ary = [ary.slice(i).join(".") for (i in util.range(ary.length - 1, 0, -1))];
+        ary = [ary.slice(i).join(".") for (i in util.range(ary.length, 0, -1))];
         return ary.filter(function (h) h.length >= base.length);
     },
 
@@ -1586,20 +1777,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         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
@@ -1658,6 +1835,20 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     },
 
     yielders: 0,
+    /**
+     * Yields execution to the next event in the current thread's event
+     * queue. This is a potentially dangerous operation, since any
+     * yielders higher in the event stack will prevent execution from
+     * returning to the caller until they have finished their wait. The
+     * potential for deadlock is high.
+     *
+     * @param {boolean} flush If true, flush all events in the event
+     *      queue before returning. Otherwise, wait for an event to
+     *      process before proceeding.
+     * @param {boolean} interruptable If true, this yield may be
+     *      interrupted by pressing <C-c>, in which case,
+     *      Error("Interrupted") will be thrown.
+     */
     threadYield: function (flush, interruptable) {
         this.yielders++;
         try {
@@ -1667,7 +1858,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             do {
                 mainThread.processNextEvent(!flush);
                 if (util.interrupted)
-                    throw new Error("Interrupted");
+                    throw Error("Interrupted");
             }
             while (flush === true && mainThread.hasPendingEvents());
         }
@@ -1676,6 +1867,19 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         }
     },
 
+    /**
+     * Waits for the function *test* to return true, or *timeout*
+     * milliseconds to expire.
+     *
+     * @param {function} test The predicate on which to wait.
+     * @param {object} self The 'this' object for *test*.
+     * @param {Number} timeout The maximum number of milliseconds to
+     *      wait.
+     *      @optional
+     * @param {boolean} interruptable If true, may be interrupted by
+     *      pressing <C-c>, in which case, Error("Interrupted") will be
+     *      thrown.
+     */
     waitFor: function waitFor(test, self, timeout, interruptable) {
         let end = timeout && Date.now() + timeout, result;
 
@@ -1690,6 +1894,23 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return result;
     },
 
+    /**
+     * Makes the passed function yieldable. Each time the function calls
+     * yield, execution is suspended for the yielded number of
+     * milliseconds.
+     *
+     * Example:
+     *      let func = yieldable(function () {
+     *          util.dump(Date.now()); // 0
+     *          yield 1500;
+     *          util.dump(Date.now()); // 1500
+     *      });
+     *      func();
+     *
+     * @param {function} func The function to mangle.
+     * @returns {function} A new function which may not execute
+     *      synchronously.
+     */
     yieldable: function yieldable(func)
         function magic() {
             let gen = func.apply(this, arguments);
@@ -1701,6 +1922,16 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             })();
         },
 
+    /**
+     * Wraps a callback function such that its errors are not lost. This
+     * is useful for DOM event listeners, which ordinarily eat errors.
+     * The passed function has the property *wrapper* set to the new
+     * wrapper function, while the wrapper has the property *wrapped*
+     * set to the original callback.
+     *
+     * @param {function} callback The callback to wrap.
+     * @returns {function}
+     */
     wrapCallback: wrapCallback,
 
     /**
@@ -1722,7 +1953,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      */
     trapErrors: function trapErrors(func, self) {
         try {
-            if (isString(func))
+            if (!callable(func))
                 func = self[func];
             return func.apply(self || this, Array.slice(arguments, 2));
         }
@@ -1732,6 +1963,15 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         }
     },
 
+    /**
+     * Returns the file path of a given *url*, for debugging purposes.
+     * If *url* points to a file (even if indirectly), the native
+     * filesystem path is returned. Otherwise, the URL itself is
+     * returned.
+     *
+     * @param {string} url The URL to mangle.
+     * @returns {string} The path to the file.
+     */
     urlPath: function urlPath(url) {
         try {
             return util.getFile(url).path;
@@ -1741,18 +1981,33 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         }
     },
 
+    /**
+     * Returns a list of all domains and subdomains of documents in the
+     * given window and all of its descendant frames.
+     *
+     * @param {nsIDOMWindow} win The window for which to find domains.
+     * @returns {[string]} The visible domains.
+     */
     visibleHosts: function (win) {
         let res = [], seen = {};
         (function rec(frame) {
             try {
-                res = res.concat(util.subdomains(frame.location.host));
+                if (frame.location.hostname)
+                    res = res.concat(util.subdomains(frame.location.hostname));
             }
             catch (e) {}
             Array.forEach(frame.frames, rec);
         })(win);
-        return res.filter(function (h) !set.add(seen, h));
+        return res.filter(function (h) !Set.add(seen, h));
     },
 
+    /**
+     * Returns a list of URIs of documents in the given window and all
+     * of its descendant frames.
+     *
+     * @param {nsIDOMWindow} win The window for which to find URIs.
+     * @returns {[nsIURI]} The visible URIs.
+     */
     visibleURIs: function (win) {
         let res = [], seen = {};
         (function rec(frame) {
@@ -1762,7 +2017,24 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             catch (e) {}
             Array.forEach(frame.frames, rec);
         })(win);
-        return res.filter(function (h) !set.add(seen, h.spec));
+        return res.filter(function (h) !Set.add(seen, h.spec));
+    },
+
+    /**
+     * Wraps native exceptions thrown by the called function so that a
+     * proper stack trace may be retrieved from them.
+     *
+     * @param {function|string} meth The method to call.
+     * @param {object} self The 'this' object of the method.
+     * @param ... Arguments to pass to *meth*.
+     */
+    withProperErrors: function withProperErrors(meth, self) {
+        try {
+            return (callable(meth) ? meth : self[meth]).apply(self, Array.slice(arguments, withProperErrors.length));
+        }
+        catch (e) {
+            throw e.stack ? e : Error(e);
+        }
     },
 
     /**