]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/util.jsm
finalize changelog for 7904
[dactyl.git] / common / modules / util.jsm
index 7941ebf587249f94d4a90979432023c773cbe67a..a0d0f090f2b06df38c674cce543b1170e1e46901 100644 (file)
@@ -1,6 +1,6 @@
 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
-// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
+// Copyright (c) 2008-2014 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.
@@ -8,19 +8,24 @@
 
 try {
 
-Components.utils.import("resource://dactyl/bootstrap.jsm");
-    let frag=1;
 defineModule("util", {
-    exports: ["frag", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
-    require: ["services"],
-    use: ["commands", "config", "highlight", "messages", "storage", "template"]
-}, this);
+    exports: ["DOM", "$", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
+    require: ["dom", "promises", "services"]
+});
+
+lazyRequire("overlay", ["overlay"]);
+lazyRequire("storage", ["File", "storage"]);
+lazyRequire("template", ["template"]);
+
+var Magic = Class("Magic", {
+    init: function init(str) {
+        this.str = str;
+    },
+
+    get message() this.str,
 
-var XBL = Namespace("xbl", "http://www.mozilla.org/xbl");
-var XHTML = Namespace("html", "http://www.w3.org/1999/xhtml");
-var XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
-var NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator");
-default xml namespace = XHTML;
+    toString: function () this.str
+});
 
 var FailedAssertion = Class("FailedAssertion", ErrorBase, {
     init: function init(message, level, noTrace) {
@@ -34,63 +39,73 @@ var FailedAssertion = Class("FailedAssertion", ErrorBase, {
     noTrace: true
 });
 
-var Point = Struct("x", "y");
+var Point = Struct("Point", "x", "y");
 
-var wrapCallback = function wrapCallback(fn) {
-    fn.wrapper = function wrappedCallback () {
-        try {
-            return fn.apply(this, arguments);
-        }
-        catch (e) {
-            util.reportError(e);
-            return undefined;
-        }
-    };
+var wrapCallback = function wrapCallback(fn, isEvent) {
+    if (!fn.wrapper)
+        fn.wrapper = function wrappedCallback() {
+            try {
+                let res = fn.apply(this, arguments);
+                if (isEvent && res === false) {
+                    arguments[0].preventDefault();
+                    arguments[0].stopPropagation();
+                }
+                return res;
+            }
+            catch (e) {
+                util.reportError(e);
+                return undefined;
+            }
+        };
     fn.wrapper.wrapped = fn;
     return fn.wrapper;
 }
 
-var getAttr = function getAttr(elem, ns, name)
-    elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null;
-var setAttr = function setAttr(elem, ns, name, val) {
-    if (val == null)
-        elem.removeAttributeNS(ns, name);
-    else
-        elem.setAttributeNS(ns, name, val);
-}
-
 var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
-    init: function () {
+    Magic: Magic,
+
+    init: function init() {
         this.Array = array;
 
         this.addObserver(this);
-        this.overlays = {};
+        this.windows = [];
     },
 
-    cleanup: function cleanup() {
-        for (let { document: doc } in iter(services.windowMediator.getEnumerator(null))) {
-            for (let elem in values(doc.dactylOverlayElements || []))
-                if (elem.parentNode)
-                    elem.parentNode.removeChild(elem);
+    activeWindow: deprecated("overlay.activeWindow", { get: function activeWindow() overlay.activeWindow }),
+    overlayObject: deprecated("overlay.overlayObject", { get: function overlayObject() overlay.bound.overlayObject }),
+    overlayWindow: deprecated("overlay.overlayWindow", { get: function overlayWindow() overlay.bound.overlayWindow }),
+
+    compileMatcher: deprecated("DOM.compileMatcher", { get: function compileMatcher() DOM.compileMatcher }),
+    computedStyle: deprecated("DOM#style", function computedStyle(elem) DOM(elem).style),
+    domToString: deprecated("DOM.stringify", { get: function domToString() DOM.stringify }),
+    editableInputs: deprecated("DOM.editableInputs", { get: function editableInputs(elem) DOM.editableInputs }),
+    escapeHTML: deprecated("DOM.escapeHTML", { get: function escapeHTML(elem) DOM.escapeHTML }),
+    evaluateXPath: deprecated("DOM.XPath",
+        function evaluateXPath(path, elem, asIterator) DOM.XPath(path, elem || util.activeWindow.content.document, asIterator)),
+    isVisible: deprecated("DOM#isVisible", function isVisible(elem) DOM(elem).isVisible),
+    makeXPath: deprecated("DOM.makeXPath", { get: function makeXPath(elem) DOM.makeXPath }),
+    namespaces: deprecated("DOM.namespaces", { get: function namespaces(elem) DOM.namespaces }),
+    namespaceNames: deprecated("DOM.namespaceNames", { get: function namespaceNames(elem) DOM.namespaceNames }),
+    parseForm: deprecated("DOM#formData", function parseForm(elem) values(DOM(elem).formData).toArray()),
+    scrollIntoView: deprecated("DOM#scrollIntoView", function scrollIntoView(elem, alignWithTop) DOM(elem).scrollIntoView(alignWithTop)),
+    validateMatcher: deprecated("DOM.validateMatcher", { get: function validateMatcher() DOM.validateMatcher }),
 
-            for (let [elem, ns, name, orig, value] in values(doc.dactylOverlayAttributes || []))
-                if (getAttr(elem, ns, name) === value)
-                    setAttr(elem, ns, name, orig);
+    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)),
 
-            delete doc.dactylOverlayElements;
-            delete doc.dactylOverlayAttributes;
-            delete doc.dactylOverlays;
-        }
-    },
+    chromePackages: deprecated("config.chromePackages", { get: function chromePackages() config.chromePackages }),
+    haveGecko: deprecated("config.haveGecko", { get: function haveGecko() config.bound.haveGecko }),
+    OS: deprecated("config.OS", { get: function OS() config.OS }),
 
-    // FIXME: Only works for Pentadactyl
-    get activeWindow() services.windowMediator.getMostRecentWindow("navigator:browser"),
     dactyl: update(function dactyl(obj) {
         if (obj)
             var global = Class.objectGlobal(obj);
+
         return {
-            __noSuchMethod__: function (meth, args) {
-                let win = util.activeWindow;
+            __noSuchMethod__: function __noSuchMethod__(meth, args) {
+                let win = overlay.activeWindow;
+
                 var dactyl = global && global.dactyl || win && win.dactyl;
                 if (!dactyl)
                     return null;
@@ -102,7 +117,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             }
         };
     }, {
-        __noSuchMethod__: function () this().__noSuchMethod__.apply(null, arguments)
+        __noSuchMethod__: function __noSuchMethod__() this().__noSuchMethod__.apply(null, arguments)
     }),
 
     /**
@@ -119,8 +134,10 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         if (!obj.observers)
             obj.observers = obj.observe;
 
+        let cleanup = ["dactyl-cleanup-modules", "quit-application"];
+
         function register(meth) {
-            for (let target in Set(["dactyl-cleanup-modules", "quit-application"].concat(Object.keys(obj.observers))))
+            for (let target of RealSet(cleanup.concat(Object.keys(obj.observers))))
                 try {
                     services.observer[meth](obj, target, true);
                 }
@@ -130,7 +147,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         Class.replaceProperty(obj, "observe",
             function (subject, target, data) {
                 try {
-                    if (target == "quit-application" || target == "dactyl-cleanup-modules")
+                    if (~cleanup.indexOf(target))
                         register("removeObserver");
                     if (obj.observers[target])
                         obj.observers[target].call(obj, subject, data);
@@ -143,7 +160,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 }
             });
 
-        obj.observe.unregister = function () register("removeObserver");
+        obj.observe.unregister = () => register("removeObserver");
         register("addObserver");
     }, { dump: dump, Error: Error }),
 
@@ -155,12 +172,21 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {string} message The message to present to the
      *     user on failure.
      */
-    assert: function (condition, message, quiet) {
+    assert: function assert(condition, message, quiet) {
         if (!condition)
             throw FailedAssertion(message, 1, quiet === undefined ? true : quiet);
         return condition;
     },
 
+    /**
+     * CamelCases a -non-camel-cased identifier name.
+     *
+     * @param {string} name The name to mangle.
+     * @returns {string} The mangled name.
+     */
+    camelCase: function camelCase(name) String.replace(name, /-(.)/g,
+                                                       (m, m1) => m1.toUpperCase()),
+
     /**
      * Capitalizes the first character of the given string.
      * @param {string} str The string to capitalize
@@ -192,55 +218,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return RegExp("[" + util.regexp.escape(list) + "]");
     },
 
-    get chromePackages() {
-        // Horrible hack.
-        let res = {};
-        function process(manifest) {
-            for each (let line in manifest.split(/\n+/)) {
-                let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line);
-                if (match)
-                    res[match[2]] = true;
-            }
-        }
-        function processJar(file) {
-            let jar = services.ZipReader(file);
-            if (jar) {
-                if (jar.hasEntry("chrome.manifest"))
-                    process(File.readStream(jar.getInputStream("chrome.manifest")));
-                jar.close();
-            }
-        }
-
-        for each (let dir in ["UChrm", "AChrom"]) {
-            dir = File(services.directory.get(dir, Ci.nsIFile));
-            if (dir.exists() && dir.isDirectory())
-                for (let file in dir.iterDirectory())
-                    if (/\.manifest$/.test(file.leafName))
-                        process(file.read());
-
-            dir = File(dir.parent);
-            if (dir.exists() && dir.isDirectory())
-                for (let file in dir.iterDirectory())
-                    if (/\.jar$/.test(file.leafName))
-                        processJar(file);
-
-            dir = dir.child("extensions");
-            if (dir.exists() && dir.isDirectory())
-                for (let ext in dir.iterDirectory()) {
-                    if (/\.xpi$/.test(ext.leafName))
-                        processJar(ext);
-                    else {
-                        if (ext.isFile())
-                            ext = File(ext.read().replace(/\n*$/, ""));
-                        let mf = ext.child("chrome.manifest");
-                        if (mf.exists())
-                            process(mf.read());
-                    }
-                }
-        }
-        return Object.keys(res).sort();
-    },
-
     /**
      * Returns a shallow copy of *obj*.
      *
@@ -284,12 +261,14 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
 
         function frame() update(
             function _frame(obj)
-                _frame === stack.top || _frame.valid(obj) ?
-                    _frame.elements.map(function (e) callable(e) ? e(obj) : e).join("") : "",
+                _frame === stack.top || _frame.valid(obj)
+                    ? _frame.elements.map(e => callable(e) ? e(obj) : e)
+                                     .join("")
+                    : "",
             {
                 elements: [],
                 seen: {},
-                valid: function (obj) this.elements.every(function (e) !e.test || e.test(obj))
+                valid: function valid(obj) this.elements.every(e => !e.test || e.test(obj))
             });
 
         let end = 0;
@@ -318,8 +297,9 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 char = char.toLowerCase();
 
                 stack.top.elements.push(update(
-                    function (obj) obj[char] != null ? quote(obj, char) : "",
-                    { test: function (obj) obj[char] != null }));
+                    function (obj) obj[char] != null ? quote(obj, char)
+                                                     : "",
+                    { test: function test(obj) obj[char] != null }));
 
                 for (let elem in array.iterValues(stack))
                     elem.seen[char] = true;
@@ -363,28 +343,30 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
 
         let unknown = util.identity;
         if (!keepUnknown)
-            unknown = function () "";
+            unknown = () => "";
 
         function frame() update(
             function _frame(obj)
-                _frame === stack.top || _frame.valid(obj) ?
-                    _frame.elements.map(function (e) callable(e) ? e(obj) : e).join("") : "",
+                _frame === stack.top || _frame.valid(obj)
+                    ? _frame.elements.map(e => callable(e) ? e(obj) : e)
+                            .join("")
+                    : "",
             {
                 elements: [],
-                seen: {},
-                valid: function (obj) this.elements.every(function (e) !e.test || e.test(obj))
+                seen: RealSet(),
+                valid: function valid(obj) this.elements.every(e => (!e.test || e.test(obj)))
             });
 
         let defaults = { lt: "<", gt: ">" };
 
-        let re = util.regexp(<![CDATA[
+        let re = util.regexp(literal(/*
             ([^]*?) // 1
             (?:
                 (<\{) | // 2
                 (< ((?:[a-z]-)?[a-z-]+?) (?:\[([0-9]+)\])? >) | // 3 4 5
                 (\}>) // 6
             )
-        ]]>, "gixy");
+        */), "gixy");
         macro = String(macro);
         let end = 0;
         for (let match in re.iterate(macro)) {
@@ -400,35 +382,46 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
             }
             else if (close) {
                 stack.pop();
-                util.assert(stack.length, /*L*/"Unmatched %] in macro");
+                util.assert(stack.length, /*L*/"Unmatched }> in macro");
             }
             else {
                 let [, flags, name] = /^((?:[a-z]-)*)(.*)/.exec(macro);
-                flags = Set(flags);
+                flags = RealSet(flags);
 
                 let quote = util.identity;
-                if (flags.q)
+                if (flags.has("q"))
                     quote = function quote(obj) typeof obj === "number" ? obj : String.quote(obj);
-                if (flags.e)
+                if (flags.has("e"))
                     quote = function quote(obj) "";
 
-                if (Set.has(defaults, name))
+                if (hasOwnProperty(defaults, name))
                     stack.top.elements.push(quote(defaults[name]));
                 else {
+                    let index = idx;
                     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] != "") }));
+                            obj => obj[name] != null && idx in obj[name] ? quote(obj[name][idx])
+                                                                         : hasOwnProperty(obj, name) ? "" : unknown(full),
+                            {
+                                test: function test(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] != "") }));
+                            obj => obj[name] != null ? quote(obj[name])
+                                                     : hasOwnProperty(obj, name) ? "" : unknown(full),
+                            {
+                                test: function test(obj) obj[name] != null
+                                                      && obj[name] !== false
+                                                      && (!flags.e || obj[name] != "")
+                            }));
                     }
 
                     for (let elem in array.iterValues(stack))
-                        elem.seen[name] = true;
+                        elem.seen.add(name);
                 }
             }
         }
@@ -439,80 +432,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         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))
-            if (/^xpath:/.test(elem))
-                xpath.push(elem.substr(6));
-            else
-                css.push(elem);
-
-        return update(
-            function matcher(node) {
-                if (matcher.xpath)
-                    for (let elem in util.evaluateXPath(matcher.xpath, node))
-                        yield elem;
-
-                if (matcher.css)
-                    for (let [, elem] in iter(node.querySelectorAll(matcher.css)))
-                        yield elem;
-            }, {
-                css: css.join(", "),
-                xpath: xpath.join(" | ")
-            });
-    },
-
-    /**
-     * 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 = services.XMLDocument();
-        return this.testValues(list, function (value) {
-            if (/^xpath:/.test(value))
-                evaluator.createExpression(value.substr(6), util.evaluateXPath.resolver);
-            else
-                node.querySelector(value);
-            return true;
-        });
-    },
-
-    /**
-     * Returns an object representing a Node's computed CSS style.
-     *
-     * @param {Node} node
-     * @returns {Object}
-     */
-    computedStyle: function computedStyle(node) {
-        while (!(node instanceof Ci.nsIDOMElement) && node.parentNode)
-            node = node.parentNode;
-        try {
-            var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
-        }
-        catch (e) {}
-        if (res == null) {
-            util.dumpStack(_("error.nullComputedStyle", node));
-            Cu.reportError(Error(_("error.nullComputedStyle", node)));
-            return {};
-        }
-        return res;
-    },
-
     /**
      * Converts any arbitrary string into an URI object. Returns null on
      * failure.
@@ -522,7 +441,9 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      */
     createURI: function createURI(str) {
         try {
-            return services.urifixup.createFixupURI(str, services.urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
+            let uri = services.urifixup.createFixupURI(str, services.urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
+            uri instanceof Ci.nsIURL;
+            return uri;
         }
         catch (e) {
             return null;
@@ -539,55 +460,79 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @returns [string] The resulting strings.
      */
     debrace: function debrace(pattern) {
-        if (isArray(pattern)) {
+        try {
+            if (isArray(pattern)) {
+                // JƤgermonkey hates us.
+                let obj = ({
+                    res: [],
+                    rec: function rec(acc) {
+                        let vals;
+
+                        while (isString(vals = pattern[acc.length]))
+                            acc.push(vals);
+
+                        if (acc.length == pattern.length)
+                            this.res.push(acc.join(""));
+                        else
+                            for (let val in values(vals))
+                                this.rec(acc.concat(val));
+                    }
+                });
+                obj.rec([]);
+                return obj.res;
+            }
+
+            if (!pattern.contains("{"))
+                return [pattern];
+
             let res = [];
-            let rec = function rec(acc) {
-                let vals;
 
-                while (isString(vals = pattern[acc.length]))
-                    acc.push(vals);
+            let split = 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(s => util.dequote(s, dequote));
+            };
+
+            let patterns = [];
+            let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy,
+                function (match) {
+                    patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy,
+                        null, ",{}"));
+                }, "{}");
 
-                if (acc.length == pattern.length)
-                    res.push(acc.join(""))
+            let rec = function rec(acc) {
+                if (acc.length == patterns.length)
+                    res.push(array(substrings).zip(acc).flatten().join(""));
                 else
-                    for (let val in values(vals))
-                        rec(acc.concat(val));
-            }
+                    for (let [, pattern] in Iterator(patterns[acc.length]))
+                        rec(acc.concat(pattern));
+            };
             rec([]);
             return res;
         }
-
-        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));
+        catch (e if e.message && e.message.contains("res is undefined")) {
+            // prefs.safeSet() would be reset on :rehash
+            prefs.set("javascript.options.methodjit.chrome", false);
+            util.dactyl.warn(_(UTF8("error.damnYouJƤgermonkey")));
+            return [];
         }
-        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(""));
-            else
-                for (let [, pattern] in Iterator(patterns[acc.length]))
-                    rec(acc.concat(pattern));
-        }
-        rec([]);
-        return res;
+    /**
+     * Briefly delay the execution of the passed function.
+     *
+     * @param {function} callback The function to delay.
+     */
+    delay: function delay(callback) {
+        let { mainThread } = services.threading;
+        mainThread.dispatch(callback,
+                            mainThread.DISPATCH_NORMAL);
     },
 
     /**
@@ -599,45 +544,18 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @returns {string}
      */
     dequote: function dequote(pattern, chars)
-        pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0),
+        pattern.replace(/\\(.)/, (m0, m1) => chars.contains(m1) ? 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.
+     * Returns the nsIDocShell for the given window.
+     *
+     * @param {Window} win The window for which to get the docShell.
+     * @returns {nsIDocShell}
      */
-    domToString: function (node, html) {
-        if (node instanceof Ci.nsISelection && node.isCollapsed)
-            return "";
 
-        if (node instanceof Ci.nsIDOMNode) {
-            let range = node.ownerDocument.createRange();
-            range.selectNode(node);
-            node = range;
-        }
-        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);
-        if (node instanceof Ci.nsISelection)
-            encoder.setSelection(node);
-        else if (node instanceof Ci.nsIDOMRange)
-            encoder.setRange(node);
-
-        let str = services.String(encoder.encodeToString());
-        if (html)
-            return str.data;
-
-        let [result, length] = [{}, {}];
-        services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
-        return result.value.QueryInterface(Ci.nsISupportsString).data;
-    },
+     docShell: function docShell(win)
+            win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+               .QueryInterface(Ci.nsIDocShell),
 
     /**
      * Prints a message to the console. If *msg* is an object it is pretty
@@ -654,7 +572,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {string} stack The stack trace from an Error.
      * @returns {[string]} The stack frames.
      */
-    stackLines: function (stack) {
+    stackLines: function stackLines(stack) {
         let lines = [];
         let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g;
         while (match = re.exec(stack))
@@ -669,29 +587,10 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {string} msg The trace message.
      * @param {number} frames The number of frames to print.
      */
-    dumpStack: function dumpStack(msg, frames) {
+    dumpStack: function dumpStack(msg="Stack", frames=null) {
         let stack = util.stackLines(Error().stack);
         stack = stack.slice(1, 1 + (frames || stack.length)).join("\n").replace(/^/gm, "    ");
-        util.dump((arguments.length == 0 ? "Stack" : msg) + "\n" + stack + "\n");
-    },
-
-    /**
-     * The set of input element type attribute values that mark the element as
-     * an editable field.
-     */
-    editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
-                         "month", "number", "password", "range", "search",
-                         "tel", "text", "time", "url", "week"]),
-
-    /**
-     * Converts HTML special characters in *str* to the equivalent HTML
-     * entities.
-     *
-     * @param {string} str
-     * @returns {string}
-     */
-    escapeHTML: function escapeHTML(str) {
-        return str.replace(/&/g, "&amp;").replace(/</g, "&lt;");
+        util.dump(msg + "\n" + stack + "\n");
     },
 
     /**
@@ -709,69 +608,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         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 {Node} elem The context element.
-     * @default The current document.
-     * @param {boolean} asIterator Whether to return the results as an
-     *     XPath iterator.
-     * @returns {Object} Iterable result of the evaluation.
-     */
-    evaluateXPath: update(
-        function evaluateXPath(expression, elem, asIterator) {
-            try {
-                if (!elem)
-                    elem = util.activeWindow.content.document;
-                let doc = elem.ownerDocument || elem;
-                if (isArray(expression))
-                    expression = util.makeXPath(expression);
-
-                let result = doc.evaluate(expression, elem,
-                    evaluateXPath.resolver,
-                    asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
-                    null
-                );
-
-                return Object.create(result, {
-                    __iterator__: {
-                        value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
-                                          : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
-                    }
-                });
-            }
-            catch (e) {
-                throw e.stack ? e : Error(e);
-            }
-        },
-        {
-            resolver: function lookupNamespaceURI(prefix) ({
-                    xul: XUL.uri,
-                    xhtml: XHTML.uri,
-                    xhtml2: "http://www.w3.org/2002/06/xhtml2",
-                    dactyl: NS.uri
-                }[prefix] || null)
-        }),
-
-    extend: function extend(dest) {
-        Array.slice(arguments, 1).filter(util.identity).forEach(function (src) {
-            for (let [k, v] in Iterator(src)) {
-                let get = src.__lookupGetter__(k),
-                    set = src.__lookupSetter__(k);
-                if (!get && !set)
-                    dest[k] = v;
-                if (get)
-                    dest.__defineGetter__(k, get);
-                if (set)
-                    dest.__defineSetter__(k, set);
-            }
-        });
-        return dest;
-    },
-
     /**
      * Converts *bytes* to a pretty printed data size string.
      *
@@ -821,14 +657,14 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      */
     formatSeconds: function formatSeconds(seconds) {
         function pad(n, val) ("0000000" + val).substr(-Math.max(n, String(val).length));
-        function div(num, denom) [Math.round(num / denom), Math.round(num % denom)];
+        function div(num, denom) [Math.floor(num / denom), Math.round(num % denom)];
         let days, hours, minutes;
 
-        [minutes, seconds] = div(seconds, 60);
+        [minutes, seconds] = div(Math.round(seconds), 60);
         [hours, minutes]   = div(minutes, 60);
         [days, hours]      = div(hours,   24);
         if (days)
-            return /*L*/days + " days " + hours + " hours"
+            return /*L*/days + " days " + hours + " hours";
         if (hours)
             return /*L*/hours + "h " + minutes + "m";
         if (minutes)
@@ -845,13 +681,16 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     getFile: function getFile(uri) {
         try {
             if (isString(uri))
-                uri = util.newURI(util.fixURI(uri));
+                uri = util.newURI(uri);
 
             if (uri instanceof Ci.nsIFileURL)
                 return File(uri.file);
 
+            if (uri instanceof Ci.nsIFile)
+                return File(uri);
+
             let channel = services.io.newChannelFromURI(uri);
-            channel.cancel(Cr.NS_BINDING_ABORTED);
+            try { channel.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
             if (channel instanceof Ci.nsIFileChannel)
                 return File(channel.file);
         }
@@ -865,7 +704,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {string} url
      * @returns {string|null}
      */
-    getHost: function (url) {
+    getHost: function getHost(url) {
         try {
             return util.createURI(url).host;
         }
@@ -873,15 +712,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return null;
     },
 
-    /**
-     * Returns true if the current Gecko runtime is of the given version
-     * or greater.
-     *
-     * @param {string} ver The required version.
-     * @returns {boolean}
-     */
-    haveGecko: function (ver) services.versionCompare.compare(services.runtime.platformVersion, ver) >= 0,
-
     /**
      * Sends a synchronous or asynchronous HTTP request to *url* and returns
      * the XMLHttpRequest object. If *callback* is specified the request is
@@ -889,38 +719,112 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * argument.
      *
      * @param {string} url
-     * @param {function(XMLHttpRequest)} callback
+     * @param {object} params Optional parameters for this request:
+     *    method: {string} The request method. @default "GET"
+     *
+     *    params: {object} Parameters to append to *url*'s query string.
+     *    data: {*} POST data to send to the server. Ordinary objects
+     *              are converted to FormData objects, with one datum
+     *              for each property/value pair.
+     *
+     *    onload:   {function(XMLHttpRequest, Event)} The request's load event handler.
+     *    onerror:  {function(XMLHttpRequest, Event)} The request's error event handler.
+     *    callback: {function(XMLHttpRequest, Event)} An event handler
+     *              called for either error or load events.
+     *
+     *    background: {boolean} Whether to perform the request in the
+     *                background. @default true
+     *
+     *    mimeType: {string} Override the response mime type with the
+     *              given value.
+     *    responseType: {string} Override the type of the "response"
+     *                  property.
+     *
+     *    headers: {objects} Extra request headers.
+     *
+     *    user: {string} The user name to send via HTTP Authentication.
+     *    pass: {string} The password to send via HTTP Authentication.
+     *
+     *    quiet: {boolean} If true, don't report errors.
+     *
      * @returns {XMLHttpRequest}
      */
-    httpGet: function httpGet(url, callback, self) {
-        let params = callback;
-        if (!isObject(params))
-            params = { callback: params && function () callback.apply(self, arguments) };
+    httpGet: function httpGet(url, params={}, self) {
+        if (callable(params))
+            // Deprecated.
+            params = { callback: params.bind(self) };
 
         try {
             let xmlhttp = services.Xmlhttp();
-            xmlhttp.mozBackgroundRequest = true;
+            xmlhttp.mozBackgroundRequest = hasOwnProperty(params, "background") ? params.background : true;
 
             let async = params.callback || params.onload || params.onerror;
             if (async) {
-                xmlhttp.onload = function handler(event) { util.trapErrors(params.onload || params.callback, params, xmlhttp, event) };
-                xmlhttp.onerror = function handler(event) { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event) };
+                xmlhttp.addEventListener("load",  event => { util.trapErrors(params.onload  || params.callback, params, xmlhttp, event); }, false);
+                xmlhttp.addEventListener("error", event => { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event); }, false);
             }
+
+            if (isObject(params.params)) {
+                let data = [encodeURIComponent(k) + "=" + encodeURIComponent(v)
+                            for ([k, v] in iter(params.params))];
+                let uri = util.newURI(url);
+                uri.query += (uri.query ? "&" : "") + data.join("&");
+
+                url = uri.spec;
+            }
+
+            if (isObject(params.data) && !(params.data instanceof Ci.nsISupports)) {
+                let data = services.FormData();
+                for (let [k, v] in iter(params.data))
+                    data.append(k, v);
+                params.data = data;
+            }
+
             if (params.mimeType)
                 xmlhttp.overrideMimeType(params.mimeType);
 
-            xmlhttp.open(params.method || "GET", url, async,
-                         params.user, params.pass);
+            let args = [params.method || "GET", url, async];
+            if (params.user != null || params.pass != null)
+                args.push(params.user);
+            if (params.pass != null)
+                args.push(prams.pass);
+            xmlhttp.open.apply(xmlhttp, args);
+
+            for (let [header, val] in Iterator(params.headers || {}))
+                xmlhttp.setRequestHeader(header, val);
 
-            xmlhttp.send(null);
+            if (params.responseType)
+                xmlhttp.responseType = params.responseType;
+
+            if (params.notificationCallbacks)
+                xmlhttp.channel.notificationCallbacks = params.notificationCallbacks;
+
+            xmlhttp.send(params.data);
             return xmlhttp;
         }
         catch (e) {
-            util.dactyl.log(_("error.cantOpen", String.quote(url), e), 1);
+            if (!params.quiet)
+                util.reportError(e);
             return null;
         }
     },
 
+    /**
+     * Like #httpGet, but returns a promise rather than accepting
+     * callbacks.
+     *
+     * @param {string} url The URL to fetch.
+     * @param {object} params Parameter object, as in #httpGet.
+     */
+    fetchUrl: promises.withCallbacks(function fetchUrl([accept, reject, deferred], url, params) {
+        params = update({}, params);
+        params.onload = accept;
+        params.onerror = reject;
+
+        let req = this.httpGet(url, params);
+        promises.oncancel(deferred, req.cancel);
+    }),
+
     /**
      * The identity function.
      *
@@ -936,7 +840,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {Object} r2
      * @returns {Object}
      */
-    intersection: function (r1, r2) ({
+    intersection: function intersection(r1, r2) ({
         get width()  this.right - this.left,
         get height() this.bottom - this.top,
         left: Math.max(r1.left, r2.left),
@@ -951,7 +855,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {nsIStackFrame} frame
      * @returns {boolean}
      */
-    isDactyl: Class.memoize(function () {
+    isDactyl: Class.Memoize(function () {
         let base = util.regexp.escape(Components.stack.filename.replace(/[^\/]+$/, ""));
         let re = RegExp("^(?:.* -> )?(?:resource://dactyl(?!-content/eval.js)|" + base + ")\\S+$");
         return function isDactyl(frame) re.test(frame.filename);
@@ -966,24 +870,6 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      */
     isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain),
 
-    /** Dactyl's notion of the current operating system platform. */
-    OS: memoize({
-        _arch: services.runtime.OS,
-        /**
-         * @property {string} The normalised name of the OS. This is one of
-         *     "Windows", "Mac OS X" or "Unix".
-         */
-        get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix",
-        /** @property {boolean} True if the OS is Windows. */
-        get isWindows() this._arch == "WINNT",
-        /** @property {boolean} True if the OS is Mac OS X. */
-        get isMacOSX() this._arch == "Darwin",
-        /** @property {boolean} True if the OS is some other *nix variant. */
-        get isUnix() !this.isWindows && !this.isMacOSX,
-        /** @property {RegExp} A RegExp which matches illegal characters in path components. */
-        get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /\//g
-    }),
-
     /**
      * Returns true if *host* is a subdomain of *domain*.
      *
@@ -998,39 +884,31 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx - 1] == ".");
     },
 
-    /**
-     * Returns true if the given DOM node is currently visible.
-     *
-     * @param {Node} node
-     * @returns {boolean}
-     */
-    isVisible: function (node) {
-        let style = util.computedStyle(node);
-        return style.visibility == "visible" && style.display != "none";
-    },
-
     /**
      * Iterates over all currently open documents, including all
      * top-level window and sub-frames thereof.
      */
-    iterDocuments: function iterDocuments() {
+    iterDocuments: function iterDocuments(types) {
+        types = types ? types.map(s => "type" + util.capitalize(s))
+                      : ["typeChrome", "typeContent"];
+
         let windows = services.windowMediator.getXULWindowEnumerator(null);
         while (windows.hasMoreElements()) {
             let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
-            for each (let type in ["typeChrome", "typeContent"]) {
+            for (let type of types) {
                 let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type],
                                                                       Ci.nsIDocShell.ENUMERATE_FORWARDS);
                 while (docShells.hasMoreElements())
                     let (viewer = docShells.getNext().QueryInterface(Ci.nsIDocShell).contentViewer) {
                         if (viewer)
                             yield viewer.DOMDocument;
-                    }
+                    };
             }
         }
     },
 
     // ripped from Firefox; modified
-    unsafeURI: Class.memoize(function () util.regexp(String.replace(<![CDATA[
+    unsafeURI: Class.Memoize(() => util.regexp(String.replace(literal(/*
             [
                 \s
                 // Invisible characters (bug 452979)
@@ -1044,7 +922,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 // Bidi formatting characters. (RFC 3987 sections 3.2 and 4.1 paragraph 6)
                 U200E U200F U202A U202B U202C U202D U202E
             ]
-        ]]>, /U/g, "\\u"),
+        */), /U/g, "\\u"),
         "gx")),
     losslessDecodeURI: function losslessDecodeURI(url) {
         return url.split("%25").map(function (url) {
@@ -1055,21 +933,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 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
-     * XHTML namespaces. See {@link Buffer#evaluateXPath}.
-     *
-     * @param nodes {Array(string)}
-     * @returns {string}
-     */
-    makeXPath: function makeXPath(nodes) {
-        return array(nodes).map(util.debrace).flatten()
-                           .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
-                           .map(function (node) "//" + node).join(" | ");
+            }, this).join("%25").replace(/[\s.,>)]$/, encodeURIComponent);
     },
 
     /**
@@ -1082,16 +946,21 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      *      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"),
+        function makeDTD(obj) {
+            function escape(val) {
+               let isDOM = DOM.isJSONXML(val);
+               return String.replace(val == null ? "null" :
+                                     isDOM       ? DOM.toXML(val)
+                                                 : val,
+                                     isDOM ? /['%]/g
+                                           : /['"%&<>]/g,
+                                     m => map[m]);
+            }
 
-    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)),
+            return iter(obj).map(([k, v]) =>
+                                 ["<!ENTITY ", k, " '", escape(v), "'>"].join(""))
+                            .join("\n");
+        },
 
     /**
      * Converts a URI string into a URI object.
@@ -1099,8 +968,19 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {string} uri
      * @returns {nsIURI}
      */
-    // FIXME: createURI needed too?
-    newURI: function newURI(uri, charset, base) this.withProperErrors("newURI", services.io, uri, charset, base),
+    newURI: function newURI(uri, charset, base) {
+        if (uri instanceof Ci.nsIURI)
+            var res = uri.clone();
+        else {
+            let idx = uri.lastIndexOf(" -> ");
+            if (~idx)
+                uri = uri.slice(idx + 4);
+
+            res = this.withProperErrors("newURI", services.io, uri, charset, base);
+        }
+        res instanceof Ci.nsIURL;
+        return res;
+    },
 
     /**
      * Removes leading garbage prepended to URIs by the subscript
@@ -1117,61 +997,18 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @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 object + "\n";
 
         if (!isObject(object))
             return String(object);
 
-        function namespaced(node) {
-            var ns = NAMESPACES[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
-            if (!ns)
-                return node.localName;
-            if (color)
-                return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</>
-            return ns + ":" + node.localName;
-        }
-
         if (object instanceof Ci.nsIDOMElement) {
-            const NAMESPACES = array.toObject([
-                [NS, "dactyl"],
-                [XHTML, "html"],
-                [XUL, "xul"]
-            ]);
             let elem = object;
             if (elem.nodeType == elem.TEXT_NODE)
                 return elem.data;
 
-            try {
-                let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
-                if (color)
-                    return <span highlight="HelpXMLBlock"><span highlight="HelpXMLTagStart">&lt;{
-                            namespaced(elem)} {
-                                template.map(array.iterValues(elem.attributes),
-                                    function (attr)
-                                        <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> +
-                                        <span highlight="HelpXMLString">{attr.value}</span>,
-                                    <> </>)
-                            }{ !hasChildren ? "/>" : ">"
-                        }</span>{ !hasChildren ? "" : <>...</> +
-                            <span highlight="HtmlTagEnd">&lt;{namespaced(elem)}></span>
-                    }</span>;
-
-                let tag = "<" + [namespaced(elem)].concat(
-                    [namespaced(a) + "=" + template.highlight(a.value, true)
-                     for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
-                return tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">");
-            }
-            catch (e) {
-                return {}.toString.call(elem);
-            }
+            return DOM(elem).repr(color);
         }
 
         try { // for window.JSON
@@ -1180,326 +1017,163 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         catch (e) {
             obj = Object.prototype.toString.call(obj);
         }
-        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>::&#x0a;</>;
+
+        if (color) {
+            obj = template.highlightFilter(util.clip(obj, 150), "\n",
+                                           () => ["span", { highlight: "NonText" },
+                                                      "^J"]);
+
+            var head = ["span", { highlight: "Title Object" }, obj, "::\n"];
+        }
+        else
+            head = util.clip(obj, 150).replace(/\n/g, "^J") + "::\n";
 
         let keys = [];
 
         // window.content often does not want to be queried with "var i in object"
         try {
             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>]]>;
+
+            let keyIter = object;
+            if (iter.iteratorProp in object) {
+                keyIter = (k for (k of object));
+                hasValue = false;
+            }
+            else if ("__iterator__" in object && !callable(object.__iterator__))
+                keyIter = keys(object);
+
+            for (let i in keyIter) {
+                let value = Magic("<no value>");
                 try {
                     value = object[i];
                 }
                 catch (e) {}
+
                 if (!hasValue) {
                     if (isArray(i) && i.length == 2)
                         [i, value] = i;
-                    else
+                    else {
                         var noVal = true;
+                        value = i;
+                    }
                 }
 
-                value = template.highlight(value, true, 150);
-                let key = <span highlight="Key">{i}</span>;
+                let key = i;
                 if (!isNaN(i))
                     i = parseInt(i);
                 else if (/^[A-Z_]+$/.test(i))
                     i = "";
-                keys.push([i, <>{key}{noVal ? "" : <>: {value}</>}&#x0a;</>]);
+
+                if (color)
+                    value = template.highlight(value, true, 150, !color);
+                else if (value instanceof Magic)
+                    value = String(value);
+                else
+                    value = util.clip(String(value).replace(/\n/g, "^J"), 150);
+
+                if (noVal)
+                    var val = value;
+                else if (color)
+                    val = [["span", { highlight: "Key" }, key], ": ", value];
+                else
+                    val = key + ": " + value;
+
+                keys.push([i, val]);
             }
         }
-        catch (e) {}
+        catch (e) {
+            util.reportError(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 ? <div style="white-space: pre-wrap;">{string}</div> : [s for each (s in string)].join("");
-    },
 
-    observers: {
-        "dactyl-cleanup-modules": function (subject, reason) {
-            defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules " + reason);
+        let vals = template.map(keys.sort(compare), f => f[1],
+                                "\n");
 
-            for (let module in values(defineModule.modules))
-                if (module.cleanup) {
-                    util.dump("cleanup: " + module.constructor.className);
-                    util.trapErrors(module.cleanup, module, reason);
-                }
-
-            JSMLoader.cleanup();
-
-            if (!this.rehashing)
-                services.observer.addObserver(this, "dactyl-rehash", true);
-        },
-        "dactyl-rehash": function () {
-            services.observer.removeObserver(this, "dactyl-rehash");
-
-            defineModule.loadLog.push("dactyl: util: observe: dactyl-rehash");
-            if (!this.rehashing)
-                for (let module in values(defineModule.modules)) {
-                    defineModule.loadLog.push("dactyl: util: init(" + module + ")");
-                    if (module.reinit)
-                        module.reinit();
-                    else
-                        module.init();
-                }
-        },
-        "dactyl-purge": function () {
-            this.rehashing = 1;
-        },
-
-        "toplevel-window-ready": function (window, data) {
-            window.addEventListener("DOMContentLoaded", wrapCallback(function listener(event) {
-                if (event.originalTarget === window.document) {
-                    window.removeEventListener("DOMContentLoaded", listener.wrapper, true);
-                    util._loadOverlays(window);
-                }
-            }), true);
-        },
-        "chrome-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
-        "content-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); }
-    },
-
-    _loadOverlays: function _loadOverlays(window) {
-        if (!window.dactylOverlays)
-            window.dactylOverlays = [];
-
-        for each (let obj in util.overlays[window.document.documentURI] || []) {
-            if (window.dactylOverlays.indexOf(obj) >= 0)
-                continue;
-            window.dactylOverlays.push(obj);
-            this._loadOverlay(window, obj(window));
+        if (color) {
+            return ["div", { style: "white-space: pre-wrap" }, head, vals];
         }
+        return head + vals.join("");
     },
 
-    _loadOverlay: function _loadOverlay(window, obj) {
-        let doc = window.document;
-        if (!doc.dactylOverlayElements) {
-            doc.dactylOverlayElements = [];
-            doc.dactylOverlayAttributes = [];
-        }
-
-        function overlay(key, fn) {
-            if (obj[key]) {
-                let iterator = Iterator(obj[key]);
-                if (!isObject(obj[key]))
-                    iterator = ([elem.@id, elem.elements(), elem.@*::*.(function::name() != "id")] for each (elem in obj[key]));
-
-                for (let [elem, xml, attr] in iterator) {
-                    if (elem = doc.getElementById(elem)) {
-                        let node = util.xmlToDom(xml, doc, obj.objects);
-                        if (!(node instanceof Ci.nsIDOMDocumentFragment))
-                            doc.dactylOverlayElements.push(node);
-                        else
-                            for (let n in array.iterValues(node.childNodes))
-                                doc.dactylOverlayElements.push(n);
-
-                        fn(elem, node);
-                        for each (let attr in attr || []) {
-                            let ns = attr.namespace(), name = attr.localName();
-                            doc.dactylOverlayAttributes.push([elem, ns, name, getAttr(elem, ns, name), String(attr)]);
-                            if (attr.name() != "highlight")
-                                elem.setAttributeNS(ns, name, String(attr));
-                            else
-                                highlight.highlightNode(elem, String(attr));
-                        }
-                    }
-                }
-            }
-        }
-
-        overlay("before", function (elem, dom) elem.parentNode.insertBefore(dom, elem));
-        overlay("after", function (elem, dom) elem.parentNode.insertBefore(dom, elem.nextSibling));
-        overlay("append", function (elem, dom) elem.appendChild(dom));
-        overlay("prepend", function (elem, dom) elem.insertBefore(dom, elem.firstChild));
-        if (obj.init)
-            obj.init(window);
-
-        if (obj.load)
-            if (doc.readyState === "complete")
-                obj.load(window);
-            else
-                doc.addEventListener("load", wrapCallback(function load(event) {
-                    if (event.originalTarget === event.target) {
-                        doc.removeEventListener("load", load.wrapper, true);
-                        obj.load(window, event);
-                    }
-                }), true);
-    },
+    prettifyJSON: function prettifyJSON(data, indent, invalidOK) {
+        const INDENT = indent || "    ";
 
-    /**
-     * 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);
-
-        Object.getOwnPropertyNames(overrides).forEach(function (k) {
-            let orig, desc = Object.getOwnPropertyDescriptor(overrides, k);
-            if (desc.value instanceof Class.Property)
-                desc = desc.value.init(k) || desc.value;
-
-            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);
+        function rec(data, level, seen) {
+            if (isObject(data)) {
+                seen = RealSet(seen);
+                if (seen.add(data))
+                    throw Error("Recursive object passed");
             }
 
-            // Guard against horrible add-ons that use eval-based monkey
-            // patching.
-            let value = desc.value;
-            if (callable(desc.value)) {
-
-                delete desc.value;
-                delete desc.writable;
-                desc.get = function get() value;
-                desc.set = function set(val) {
-                    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;
-                        util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
-                        util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
-                    }
-                };
-            }
+            let prefix = level + INDENT;
 
-            try {
-                Object.defineProperty(object, k, desc);
+            if (data === undefined)
+                data = null;
 
-                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;
+            if (~["boolean", "number"].indexOf(typeof data) || data === null)
+                res.push(String(data));
+            else if (isinstance(data, ["String", _]))
+                res.push(JSON.stringify(String(data)));
+            else if (isArray(data)) {
+                if (data.length == 0)
+                    res.push("[]");
+                else {
+                    res.push("[\n");
+                    for (let [i, val] in Iterator(data)) {
+                        if (i)
+                            res.push(",\n");
+                        res.push(prefix);
+                        rec(val, prefix, seen);
                     }
+                    res.push("\n", level, "]");
                 }
-                catch (f) {}
-                util.reportError(e);
             }
-        }, this);
-
-        return function unwrap() {
-            for each (let k in Object.getOwnPropertyNames(original))
-                if (Object.getOwnPropertyDescriptor(object, k).configurable)
-                    Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
-                else {
-                    try {
-                        object[k] = original[k];
-                    }
-                    catch (e) {}
+            else if (isObject(data)) {
+                res.push("{\n");
+
+                let i = 0;
+                for (let [key, val] in Iterator(data)) {
+                    if (i++)
+                        res.push(",\n");
+                    res.push(prefix, JSON.stringify(key), ": ");
+                    rec(val, prefix, seen);
                 }
-        };
-    },
-
-    overlayWindow: function (url, fn) {
-        if (url instanceof Ci.nsIDOMWindow)
-            util._loadOverlay(url, fn);
-        else {
-            Array.concat(url).forEach(function (url) {
-                if (!this.overlays[url])
-                    this.overlays[url] = [];
-                this.overlays[url].push(fn);
-            }, this);
-
-            for (let doc in util.iterDocuments())
-                if (["interactive", "complete"].indexOf(doc.readyState) >= 0)
-                    this._loadOverlays(doc.defaultView);
+                if (i > 0)
+                    res.push("\n", level, "}");
                 else
-                    this.observe(doc.defaultView, "toplevel-window-ready");
-        }
-    },
-
-    /**
-     * Parses the fields of a form and returns a URL/POST-data pair
-     * that is the equivalent of submitting the form.
-     *
-     * @param {nsINode} field One of the fields of the given form.
-     * @returns {array}
-     */
-    // Nuances gleaned from browser.jar/content/browser/browser.js
-    parseForm: function parseForm(field) {
-        function encode(name, value, param) {
-            param = param ? "%s" : "";
-            if (post)
-                return name + "=" + encodeComponent(value + param);
-            return encodeComponent(name) + "=" + encodeComponent(value) + param;
+                    res[res.length - 1] = "{}";
+            }
+            else if (invalidOK)
+                res.push({}.toString.call(data));
+            else
+                throw Error("Invalid JSON object");
         }
 
-        let form = field.form;
-        let doc = form.ownerDocument;
+        let res = [];
+        rec(data, "", RealSet());
+        return res.join("");
+    },
 
-        let charset = doc.characterSet;
-        let converter = services.CharsetConv(charset);
-        for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
-            let c = services.CharsetConv(cs);
-            if (c) {
-                converter = services.CharsetConv(cs);
-                charset = cs;
-            }
-        }
+    observers: {
+        "dactyl-cleanup-modules": function cleanupModules(subject, reason) {
+            defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules " + reason);
 
-        let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
-        let url = util.newURI(form.action, charset, uri).spec;
-
-        let post = form.method.toUpperCase() == "POST";
-
-        let encodeComponent = encodeURIComponent;
-        if (charset !== "UTF-8")
-            encodeComponent = function encodeComponent(str)
-                escape(converter.ConvertFromUnicode(str) + converter.Finish());
-
-        let elems = [];
-        if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
-            elems.push(encode(field.name, field.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));
+            for (let module in values(defineModule.modules))
+                if (module.cleanup) {
+                    util.dump("cleanup: " + module.constructor.className);
+                    util.trapErrors(module.cleanup, module, reason);
                 }
-            }
-
-        if (post)
-            return [url, elems.join('&'), charset, elems];
-        return [url + "?" + elems.join('&'), null, charset, elems];
+        }
     },
 
     /**
@@ -1557,7 +1231,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      *
      * This is similar to Perl's extended regular expression format.
      *
-     * @param {string|XML} expr The expression to compile into a RegExp.
+     * @param {string} expr The expression to compile into a RegExp.
      * @param {string} flags Flags to apply to the new RegExp.
      * @param {object} tokens The tokens to substitute. @optional
      * @returns {RegExp} A custom regexp object.
@@ -1572,7 +1246,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         expr = String.replace(expr, /\\(.)/, function (m, m1) {
             if (m1 === "c")
                 flags = flags.replace(/i/g, "") + "i";
-            else if (m === "C")
+            else if (m1 === "C")
                 flags = flags.replace(/i/g, "");
             else
                 return m;
@@ -1581,11 +1255,16 @@ 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,
+                                  (m, n1, n2) => !n1 && hasOwnProperty(tokens, n2) ?    tokens[n2].dactylSource
+                                                                                     || tokens[n2].source
+                                                                                     || tokens[n2]
+                                                                                   : m);
 
         // Strip comments and white space.
         if (/x/.test(flags))
-            expr = String.replace(expr, /(\\.)|\/\/[^\n]*|\/\*[^]*?\*\/|\s+/gm, function (m, m1) m1 || "");
+            expr = String.replace(expr, /(\\.)|\/\/[^\n]*|\/\*[^]*?\*\/|\s+/gm,
+                                  (m, m1) => m1 || "");
 
         // Replace (?P<named> parameters)
         if (/\(\?P</.test(expr)) {
@@ -1601,9 +1280,10 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         }
 
         let res = update(RegExp(expr, flags.replace("x", "")), {
-            closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "closure")),
+            bound: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "bound")),
+            closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "bound")),
             dactylPropertyNames: ["exec", "match", "test", "toSource", "toString", "global", "ignoreCase", "lastIndex", "multiLine", "source", "sticky"],
-            iterate: function (str, idx) util.regexp.iterate(this, str, idx)
+            iterate: function iterate(str, idx) util.regexp.iterate(this, str, idx)
         });
 
         // Return a struct with properties for named parameters if we
@@ -1629,7 +1309,9 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
          * @param {RegExp} re The regexp showable source of which is to be returned.
          * @returns {string}
          */
-        getSource: function regexp_getSource(re) re.source.replace(/\\(.)/g, function (m0, m1) m1 === "/" ? "/" : m0),
+        getSource: function regexp_getSource(re) re.source.replace(/\\(.)/g,
+                                                                   (m0, m1) => m1 === "/" ? m1
+                                                                                          : m0),
 
         /**
          * Iterates over all matches of the given regexp in the given
@@ -1652,15 +1334,25 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         }())
     }),
 
+    /**
+     * Flushes the startup or jar cache.
+     */
+    flushCache: function flushCache(file) {
+        if (file)
+            services.observer.notifyObservers(file, "flush-cache-entry", "");
+        else
+            services.observer.notifyObservers(null, "startupcache-invalidate", "");
+    },
+
     /**
      * Reloads dactyl in entirety by disabling the add-on and
      * re-enabling it.
      */
-    rehash: function (args) {
-        storage.session.commandlineArgs = args;
+    rehash: function rehash(args) {
+        storage.storeForSession("commandlineArgs", args);
         this.timeout(function () {
-            services.observer.notifyObservers(null, "startupcache-invalidate", "");
-            this.rehashing = true;
+            this.flushCache();
+            cache.flush(bind("test", /^literal:/));
             let addon = config.addon;
             addon.userDisabled = true;
             addon.userDisabled = false;
@@ -1668,31 +1360,32 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
     },
 
     errorCount: 0,
-    errors: Class.memoize(function () []),
+    errors: Class.Memoize(() => []),
     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) {
+    reportError: function reportError(error) {
         if (error.noTrace)
             return;
 
         if (isString(error))
             error = Error(error);
 
-        if (Cu.reportError)
-            Cu.reportError(error);
+        Cu.reportError(error);
 
         try {
             this.errorCount++;
 
             let obj = update({}, error, {
                 toString: function () String(error),
-                stack: <>{util.stackLines(String(error.stack || Error().stack)).join("\n").replace(/^/mg, "\t")}</>
+                stack: Magic(util.stackLines(String(error.stack || Error().stack)).join("\n").replace(/^/mg, "\t"))
             });
 
+            services.console.logStringMessage(obj.stack);
+
             this.errors.push([new Date, obj + "\n" + obj.stack]);
             this.errors = this.errors.slice(-this.maxErrors);
             this.errors.toString = function () [k + "\n" + v for ([k, v] in array.iterValues(this))].join("\n\n");
@@ -1733,20 +1426,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
 
         let ary = host.split(".");
         ary = [ary.slice(i).join(".") for (i in util.range(ary.length, 0, -1))];
-        return ary.filter(function (h) h.length >= base.length);
-    },
-
-    /**
-     * 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, alignWithTop) {
-        let win = elem.ownerDocument.defaultView;
-        let rect = elem.getBoundingClientRect();
-        if (!(rect && rect.bottom <= win.innerHeight && rect.top >= 0 && rect.left < win.innerWidth && rect.right > 0))
-            elem.scrollIntoView(arguments.length > 1 ? alignWithTop : Math.abs(rect.top) < Math.abs(win.innerHeight - rect.bottom));
+        return ary.filter(h => h.length >= base.length);
     },
 
     /**
@@ -1755,11 +1435,17 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {Window} window
      * @returns {nsISelectionController}
      */
-    selectionController: function (win)
+    selectionController: function selectionController(win)
         win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
            .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
            .QueryInterface(Ci.nsISelectionController),
 
+    /**
+     * Escapes a string against shell meta-characters and argument
+     * separators.
+     */
+    shellEscape: function shellEscape(str) '"' + String.replace(str, /[\\"$`]/g, "\\$&") + '"',
+
     /**
      * Suspend execution for at least *delay* milliseconds. Functions by
      * yielding execution to the next item in the main event queue, and
@@ -1768,7 +1454,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      *
      * @param {number} delay The time period for which to sleep in milliseconds.
      */
-    sleep: function (delay) {
+    sleep: function sleep(delay) {
         let mainThread = services.threading.mainThread;
 
         let end = Date.now() + delay;
@@ -1789,7 +1475,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {number} limit The maximum number of elements to return.
      * @returns {[string]}
      */
-    split: function (str, re, limit) {
+    split: function split(str, re, limit) {
         re.lastIndex = 0;
         if (!re.global)
             re = RegExp(re.source || re, "g");
@@ -1849,7 +1535,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      *      interrupted by pressing <C-c>, in which case,
      *      Error("Interrupted") will be thrown.
      */
-    threadYield: function (flush, interruptable) {
+    threadYield: function threadYield(flush, interruptable) {
         this.yielders++;
         try {
             let mainThread = services.threading.mainThread;
@@ -1871,7 +1557,7 @@ 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 {function|Promise} 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.
@@ -1881,6 +1567,15 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      *      thrown.
      */
     waitFor: function waitFor(test, self, timeout, interruptable) {
+        if (!callable(test)) {
+            let done = false;
+            var promise = test,
+                retVal;
+            promise.then((arg) => { retVal = arg; done = true; },
+                         (arg) => { retVal = arg; done = true; });
+            test = () => done;
+        }
+
         let end = timeout && Date.now() + timeout, result;
 
         let timer = services.Timer(function () {}, 10, services.Timer.TYPE_REPEATING_SLACK);
@@ -1891,7 +1586,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
         finally {
             timer.cancel();
         }
-        return result;
+        return promise ? retVal: result;
     },
 
     /**
@@ -1911,7 +1606,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @returns {function} A new function which may not execute
      *      synchronously.
      */
-    yieldable: function yieldable(func)
+    yieldable: deprecated("Task.spawn", function yieldable(func)
         function magic() {
             let gen = func.apply(this, arguments);
             (function next() {
@@ -1920,7 +1615,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
                 }
                 catch (e if e instanceof StopIteration) {};
             })();
-        },
+        }),
 
     /**
      * Wraps a callback function such that its errors are not lost. This
@@ -1951,14 +1646,14 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {function} func The function to call
      * @param {object} self The 'this' object for the function.
      */
-    trapErrors: function trapErrors(func, self) {
+    trapErrors: function trapErrors(func, self, ...args) {
         try {
             if (!callable(func))
                 func = self[func];
-            return func.apply(self || this, Array.slice(arguments, 2));
+            return func.apply(self || this, args);
         }
         catch (e) {
-            util.reportError(e);
+            this.reportError(e);
             return undefined;
         }
     },
@@ -1988,8 +1683,9 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {nsIDOMWindow} win The window for which to find domains.
      * @returns {[string]} The visible domains.
      */
-    visibleHosts: function (win) {
-        let res = [], seen = {};
+    visibleHosts: function visibleHosts(win) {
+        let res = [],
+            seen = RealSet();
         (function rec(frame) {
             try {
                 if (frame.location.hostname)
@@ -1998,7 +1694,7 @@ 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));
+        return res.filter(h => !seen.add(h));
     },
 
     /**
@@ -2008,8 +1704,9 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {nsIDOMWindow} win The window for which to find URIs.
      * @returns {[nsIURI]} The visible URIs.
      */
-    visibleURIs: function (win) {
-        let res = [], seen = {};
+    visibleURIs: function visibleURIs(win) {
+        let res = [],
+            seen = RealSet();
         (function rec(frame) {
             try {
                 res = res.concat(util.newURI(frame.location.href));
@@ -2017,7 +1714,16 @@ 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(h => !seen.add(h.spec));
+    },
+
+    /**
+     * Like Cu.getWeakReference, but won't crash if you pass null.
+     */
+    weakReference: function weakReference(jsval) {
+        if (jsval == null)
+            return { get: function get() null };
+        return Cu.getWeakReference(jsval);
     },
 
     /**
@@ -2028,59 +1734,13 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
      * @param {object} self The 'this' object of the method.
      * @param ... Arguments to pass to *meth*.
      */
-    withProperErrors: function withProperErrors(meth, self) {
+    withProperErrors: function withProperErrors(meth, self, ...args) {
         try {
-            return (callable(meth) ? meth : self[meth]).apply(self, Array.slice(arguments, withProperErrors.length));
+            return (callable(meth) ? meth : self[meth]).apply(self, args);
         }
         catch (e) {
             throw e.stack ? e : Error(e);
         }
-    },
-
-    /**
-     * Converts an E4X XML literal to a DOM node. Any attribute named
-     * highlight is present, it is transformed into dactyl:highlight,
-     * and the named highlight groups are guaranteed to be loaded.
-     *
-     * @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 (typeof node === "string") // Sandboxes can't currently pass us XML objects.
-            node = XML(node);
-
-        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.@*::*)
-                if (attr.name() != "highlight")
-                    domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr));
-
-            for each (let child in node.*::*)
-                domnode.appendChild(xmlToDom(child, doc, nodes));
-            if (nodes && node.@key)
-                nodes[node.@key] = domnode;
-
-            if ("@highlight" in node)
-                highlight.highlightNode(domnode, String(node.@highlight), nodes || true);
-            return domnode;
-        default:
-            return null;
-        }
     }
 }, {
     Array: array
@@ -2091,7 +1751,7 @@ var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
  * @singleton
  */
 var GlobalMath = Math;
-var Math = update(Object.create(GlobalMath), {
+this.Math = update(Object.create(GlobalMath), {
     /**
      * Returns the specified *value* constrained to the range *min* - *max*.
      *
@@ -2107,4 +1767,4 @@ endModule();
 
 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
 
-// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
+// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: