]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/dom.jsm
Import 1.0rc1 supporting Firefox up to 11.*
[dactyl.git] / common / modules / dom.jsm
diff --git a/common/modules/dom.jsm b/common/modules/dom.jsm
new file mode 100644 (file)
index 0000000..981ec9c
--- /dev/null
@@ -0,0 +1,1589 @@
+// Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
+// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
+//
+// This work is licensed for reuse under an MIT license. Details are
+// given in the LICENSE.txt file included with this file.
+/* use strict */
+
+Components.utils.import("resource://dactyl/bootstrap.jsm");
+defineModule("dom", {
+    exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
+}, this);
+
+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;
+
+function BooleanAttribute(attr) ({
+    get: function (elem) elem.getAttribute(attr) == "true",
+    set: function (elem, val) {
+        if (val === "false" || !val)
+            elem.removeAttribute(attr);
+        else
+            elem.setAttribute(attr, true);
+    }
+});
+
+/**
+ * @class
+ *
+ * A jQuery-inspired DOM utility framework.
+ *
+ * Please note that while this currently implements an Array-like
+ * interface, this is *not a defined interface* and is very likely to
+ * change in the near future.
+ */
+var DOM = Class("DOM", {
+    init: function init(val, context, nodes) {
+        let self;
+        let length = 0;
+
+        if (nodes)
+            this.nodes = nodes;
+
+        if (context instanceof Ci.nsIDOMDocument)
+            this.document = context;
+
+        if (typeof val == "string")
+            val = context.querySelectorAll(val);
+
+        if (val == null)
+            ;
+        else if (typeof val == "xml" && context instanceof Ci.nsIDOMDocument)
+            this[length++] = DOM.fromXML(val, context, this.nodes);
+        else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
+            this[length++] = val;
+        else if ("length" in val)
+            for (let i = 0; i < val.length; i++)
+                this[length++] = val[i];
+        else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
+            for (let elem in val)
+                this[length++] = elem;
+        else
+            this[length++] = val;
+
+        this.length = length;
+        return self || this;
+    },
+
+    __iterator__: function __iterator__() {
+        for (let i = 0; i < this.length; i++)
+            yield this[i];
+    },
+
+    Empty: function Empty() this.constructor(null, this.document),
+
+    nodes: Class.Memoize(function () ({})),
+
+    get items() {
+        for (let i = 0; i < this.length; i++)
+            yield this.eq(i);
+    },
+
+    get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
+    set document(val) this._document = val,
+
+    attrHooks: array.toObject([
+        ["", {
+            href: { get: function (elem) elem.href || elem.getAttribute("href") },
+            src:  { get: function (elem) elem.src || elem.getAttribute("src") },
+            checked: { get: function (elem) elem.hasAttribute("checked") ? elem.getAttribute("checked") == "true" : elem.checked,
+                       set: function (elem, val) { elem.setAttribute("checked", !!val); elem.checked = val } },
+            collapsed: BooleanAttribute("collapsed"),
+            disabled: BooleanAttribute("disabled"),
+            hidden: BooleanAttribute("hidden"),
+            readonly: BooleanAttribute("readonly")
+        }]
+    ]),
+
+    matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
+
+    each: function each(fn, self) {
+        let obj = self || this.Empty();
+        for (let i = 0; i < this.length; i++)
+            fn.call(self || update(obj, [this[i]]), this[i], i);
+        return this;
+    },
+
+    eachDOM: function eachDOM(val, fn, self) {
+        XML.prettyPrinting = XML.ignoreWhitespace = false;
+        if (isString(val))
+            val = XML(val);
+
+        if (typeof val == "xml")
+            return this.each(function (elem, i) {
+                fn.call(this, DOM.fromXML(val, elem.ownerDocument), elem, i);
+            }, self || this);
+
+        let dom = this;
+        function munge(val, container, idx) {
+            if (val instanceof Ci.nsIDOMRange)
+                return val.extractContents();
+            if (val instanceof Ci.nsIDOMNode)
+                return val;
+
+            if (typeof val == "xml") {
+                val = dom.constructor(val, dom.document);
+                if (container)
+                    container[idx] = val[0];
+            }
+
+            if (isObject(val) && "length" in val) {
+                let frag = dom.document.createDocumentFragment();
+                for (let i = 0; i < val.length; i++)
+                    frag.appendChild(munge(val[i], val, i));
+                return frag;
+            }
+            return val;
+        }
+
+        if (callable(val))
+            return this.each(function (elem, i) {
+                util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
+            }, self || this);
+
+        if (this.length)
+            util.withProperErrors(fn, self || this, munge(val), this[0], 0);
+        return this;
+    },
+
+    eq: function eq(idx) {
+        return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
+    },
+
+    find: function find(val) {
+        return this.map(function (elem) elem.querySelectorAll(val));
+    },
+
+    findAnon: function findAnon(attr, val) {
+        return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
+    },
+
+    filter: function filter(val, self) {
+        let res = this.Empty();
+
+        if (!callable(val))
+            val = this.matcher(val);
+
+        this.constructor(Array.filter(this, val, self || this));
+        let obj = self || this.Empty();
+        for (let i = 0; i < this.length; i++)
+            if (val.call(self || update(obj, [this[i]]), this[i], i))
+                res[res.length++] = this[i];
+
+        return res;
+    },
+
+    is: function is(val) {
+        return this.some(this.matcher(val));
+    },
+
+    reverse: function reverse() {
+        Array.reverse(this);
+        return this;
+    },
+
+    all: function all(fn, self) {
+        let res = this.Empty();
+
+        this.each(function (elem) {
+            while(true) {
+                elem = fn.call(this, elem)
+                if (elem instanceof Ci.nsIDOMElement)
+                    res[res.length++] = elem;
+                else if (elem && "length" in elem)
+                    for (let i = 0; i < tmp.length; i++)
+                        res[res.length++] = tmp[j];
+                else
+                    break;
+            }
+        }, self || this);
+        return res;
+    },
+
+    map: function map(fn, self) {
+        let res = this.Empty();
+        let obj = self || this.Empty();
+
+        for (let i = 0; i < this.length; i++) {
+            let tmp = fn.call(self || update(obj, [this[i]]), this[i], i);
+            if (isObject(tmp) && "length" in tmp)
+                for (let j = 0; j < tmp.length; j++)
+                    res[res.length++] = tmp[j];
+            else if (tmp != null)
+                res[res.length++] = tmp;
+        }
+
+        return res;
+    },
+
+    slice: function eq(start, end) {
+        return this.constructor(Array.slice(this, start, end));
+    },
+
+    some: function some(fn, self) {
+        for (let i = 0; i < this.length; i++)
+            if (fn.call(self || this, this[i], i))
+                return true;
+        return false;
+    },
+
+    get parent() this.map(function (elem) elem.parentNode, this),
+
+    get offsetParent() this.map(function (elem) {
+        do {
+            var parent = elem.offsetParent;
+            if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
+                return parent;
+        }
+        while (parent);
+    }, this),
+
+    get ancestors() this.all(function (elem) elem.parentNode),
+
+    get children() this.map(function (elem) Array.filter(elem.childNodes,
+                                                         function (e) e instanceof Ci.nsIDOMElement),
+                            this),
+
+    get contents() this.map(function (elem) elem.childNodes, this),
+
+    get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
+                                                         function (e) e != elem && e instanceof Ci.nsIDOMElement),
+                            this),
+
+    get siblingsBefore() this.all(function (elem) elem.previousElementSibling),
+    get siblingsAfter() this.all(function (elem) elem.nextElementSibling),
+
+    get class() let (self = this) ({
+        toString: function () self[0].className,
+
+        get list() Array.slice(self[0].classList),
+        set list(val) self.attr("class", val.join(" ")),
+
+        each: function each(meth, arg) {
+            return self.each(function (elem) {
+                elem.classList[meth](arg);
+            })
+        },
+
+        add: function add(cls) this.each("add", cls),
+        remove: function remove(cls) this.each("remove", cls),
+        toggle: function toggle(cls, val, thisObj) {
+            if (callable(val))
+                return self.each(function (elem, i) {
+                    this.class.toggle(cls, val.call(thisObj || this, elem, i));
+                });
+            return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
+        },
+
+        has: function has(cls) this[0].classList.has(cls)
+    }),
+
+    get highlight() let (self = this) ({
+        toString: function () self.attrNS(NS, "highlight") || "",
+
+        get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
+        set list(val) {
+            let str = array.uniq(val).join(" ").trim();
+            self.attrNS(NS, "highlight", str || null);
+        },
+
+        has: function has(hl) ~this.list.indexOf(hl),
+
+        add: function add(hl) self.each(function () {
+            highlight.loaded[hl] = true;
+            this.highlight.list = this.highlight.list.concat(hl);
+        }),
+
+        remove: function remove(hl) self.each(function () {
+            this.highlight.list = this.highlight.list.filter(function (h) h != hl);
+        }),
+
+        toggle: function toggle(hl, val, thisObj) self.each(function (elem, i) {
+            let { highlight } = this;
+            let v = callable(val) ? val.call(thisObj || this, elem, i) : val;
+
+            highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl)
+        }),
+    }),
+
+    get rect() this[0] instanceof Ci.nsIDOMWindow ? { width: this[0].scrollMaxX + this[0].innerWidth,
+                                                      height: this[0].scrollMaxY + this[0].innerHeight,
+                                                      get right() this.width + this.left,
+                                                      get bottom() this.height + this.top,
+                                                      top: -this[0].scrollY,
+                                                      left: -this[0].scrollX } :
+               this[0]                            ? this[0].getBoundingClientRect() : {},
+
+    get viewport() {
+        if (this[0] instanceof Ci.nsIDOMWindow)
+            return {
+                get width() this.right - this.left,
+                get height() this.bottom - this.top,
+                bottom: this[0].innerHeight,
+                right: this[0].innerWidth,
+                top: 0, left: 0
+            };
+
+        let r = this.rect;
+        return {
+            width: this[0].clientWidth,
+            height: this[0].clientHeight,
+            top: r.top + this[0].clientTop,
+            get bottom() this.top + this.height,
+            left: r.left + this[0].clientLeft,
+            get right() this.left + this.width
+        }
+    },
+
+    scrollPos: function scrollPos(left, top) {
+        if (arguments.length == 0) {
+            if (this[0] instanceof Ci.nsIDOMElement)
+                return { top: this[0].scrollTop, left: this[0].scrollLeft,
+                         height: this[0].scrollHeight, width: this[0].scrollWidth,
+                         innerHeight: this[0].clientHeight, innerWidth: this[0].innerWidth };
+
+            if (this[0] instanceof Ci.nsIDOMWindow)
+                return { top: this[0].scrollY, left: this[0].scrollX,
+                         height: this[0].scrollMaxY + this[0].innerHeight,
+                         width: this[0].scrollMaxX + this[0].innerWidth,
+                         innerHeight: this[0].innerHeight, innerWidth: this[0].innerWidth };
+
+            return null;
+        }
+        let func = callable(left) && left;
+
+        return this.each(function (elem, i) {
+            if (func)
+                ({ left, top }) = func.call(this, elem, i);
+
+            if (elem instanceof Ci.nsIDOMWindow)
+                elem.scrollTo(left == null ? elem.scrollX : left,
+                              top  == null ? elem.scrollY : top);
+            else {
+                if (left != null)
+                    elem.scrollLeft = left;
+                if (top != null)
+                    elem.scrollTop = top;
+            }
+        });
+    },
+
+    /**
+     * Returns true if the given DOM node is currently visible.
+     * @returns {boolean}
+     */
+    get isVisible() {
+        let style = this[0] && this.style;
+        return style && style.visibility == "visible" && style.display != "none";
+    },
+
+    get editor() {
+        if (!this.length)
+            return;
+
+        this[0] instanceof Ci.nsIDOMNSEditableElement;
+        try {
+            if (this[0].editor instanceof Ci.nsIEditor)
+                var editor = this[0].editor;
+        }
+        catch (e) {
+            util.reportError(e);
+        }
+
+        try {
+            if (!editor)
+                editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
+                                .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
+                                .getEditorForWindow(this[0]);
+        }
+        catch (e) {}
+
+        editor instanceof Ci.nsIPlaintextEditor;
+        editor instanceof Ci.nsIHTMLEditor;
+        return editor;
+    },
+
+    get isEditable() !!this.editor,
+
+    get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
+                                       Ci.nsIDOMHTMLTextAreaElement,
+                                       Ci.nsIDOMXULTextBoxElement])
+                    && this.isEditable,
+
+    /**
+     * Returns an object representing a Node's computed CSS style.
+     * @returns {Object}
+     */
+    get style() {
+        let node = this[0];
+        if (node instanceof Ci.nsIDOMWindow)
+            node = node.document;
+        if (node instanceof Ci.nsIDOMDocument)
+            node = node.documentElement;
+        while (node && !(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;
+    },
+
+    /**
+     * Parses the fields of a form and returns a URL/POST-data pair
+     * that is the equivalent of submitting the form.
+     *
+     * @returns {object} An object with the following elements:
+     *      url: The URL the form points to.
+     *      postData: A string containing URL-encoded post data, if this
+     *                form is to be POSTed
+     *      charset: The character set of the GET or POST data.
+     *      elements: The key=value pairs used to generate query information.
+     */
+    // Nuances gleaned from browser.jar/content/browser/browser.js
+    get formData() {
+        function encode(name, value, param) {
+            param = param ? "%s" : "";
+            if (post)
+                return name + "=" + encodeComponent(value + param);
+            return encodeComponent(name) + "=" + encodeComponent(value) + param;
+        }
+
+        let field = this[0];
+        let form = field.form;
+        let doc = form.ownerDocument;
+
+        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;
+            }
+        }
+
+        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 (DOM(elem).isInput
+                        || /^(?: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: url, postData: elems.join('&'), charset: charset, elements: elems };
+        return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
+    },
+
+    /**
+     * Generates an XPath expression for the given element.
+     *
+     * @returns {string}
+     */
+    get xpath() {
+        function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
+        if (!(this[0] instanceof Ci.nsIDOMElement))
+            return null;
+
+        let res = [];
+        let doc = this.document;
+        for (let elem = this[0];; elem = elem.parentNode) {
+            if (!(elem instanceof Ci.nsIDOMElement))
+                res.push("");
+            else if (elem.id)
+                res.push("id(" + quote(elem.id) + ")");
+            else {
+                let name = elem.localName;
+                if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion))
+                    if (elem.namespaceURI in DOM.namespaceNames)
+                        name = DOM.namespaceNames[elem.namespaceURI] + ":" + name;
+                    else
+                        name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
+
+                res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
+                continue;
+            }
+            break;
+        }
+
+        return res.reverse().join("/");
+    },
+
+    /**
+     * Returns a string or XML representation of this node.
+     *
+     * @param {boolean} color If true, return a colored, XML
+     *  representation of this node.
+     */
+    repr: function repr(color) {
+        XML.ignoreWhitespace = XML.prettyPrinting = false;
+
+        function namespaced(node) {
+            var ns = DOM.namespaceNames[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;
+        }
+
+        let res = [];
+        this.each(function (elem) {
+            try {
+                let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
+                if (color)
+                    res.push(<span highlight="HelpXML"><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>);
+                else {
+                    let tag = "<" + [namespaced(elem)].concat(
+                        [namespaced(a) + "=" + template.highlight(a.value, true)
+                         for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
+
+                    res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
+                }
+            }
+            catch (e) {
+                res.push({}.toString.call(elem));
+            }
+        }, this);
+        return template.map(res, util.identity, <>,</>);
+    },
+
+    attr: function attr(key, val) {
+        return this.attrNS("", key, val);
+    },
+
+    attrNS: function attrNS(ns, key, val) {
+        if (val !== undefined)
+            key = array.toObject([[key, val]]);
+
+        let hooks = this.attrHooks[ns] || {};
+
+        if (isObject(key))
+            return this.each(function (elem, i) {
+                for (let [k, v] in Iterator(key)) {
+                    if (callable(v))
+                        v = v.call(this, elem, i);
+
+                    if (Set.has(hooks, k) && hooks[k].set)
+                        hooks[k].set.call(this, elem, v, k);
+                    else if (v == null)
+                        elem.removeAttributeNS(ns, k);
+                    else
+                        elem.setAttributeNS(ns, k, v);
+                }
+            });
+
+        if (!this.length)
+            return null;
+
+        if (Set.has(hooks, key) && hooks[key].get)
+            return hooks[key].get.call(this, this[0], key);
+
+        if (!this[0].hasAttributeNS(ns, key))
+            return null;
+
+        return this[0].getAttributeNS(ns, key);
+    },
+
+    css: update(function css(key, val) {
+        if (val !== undefined)
+            key = array.toObject([[key, val]]);
+
+        if (isObject(key))
+            return this.each(function (elem) {
+                for (let [k, v] in Iterator(key))
+                    elem.style[css.property(k)] = v;
+            });
+
+        return this[0].style[css.property(key)];
+    }, {
+        name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
+
+        property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
+    }),
+
+    append: function append(val) {
+        return this.eachDOM(val, function (elem, target) {
+            target.appendChild(elem);
+        });
+    },
+
+    prepend: function prepend(val) {
+        return this.eachDOM(val, function (elem, target) {
+            target.insertBefore(elem, target.firstChild);
+        });
+    },
+
+    before: function before(val) {
+        return this.eachDOM(val, function (elem, target) {
+            target.parentNode.insertBefore(elem, target);
+        });
+    },
+
+    after: function after(val) {
+        return this.eachDOM(val, function (elem, target) {
+            target.parentNode.insertBefore(elem, target.nextSibling);
+        });
+    },
+
+    appendTo: function appendTo(elem) {
+        if (!(elem instanceof this.constructor))
+            elem = this.constructor(elem, this.document);
+        elem.append(this);
+        return this;
+    },
+
+    prependTo: function prependTo(elem) {
+        if (!(elem instanceof this.constructor))
+            elem = this.constructor(elem, this.document);
+        elem.prepend(this);
+        return this;
+    },
+
+    insertBefore: function insertBefore(elem) {
+        if (!(elem instanceof this.constructor))
+            elem = this.constructor(elem, this.document);
+        elem.before(this);
+        return this;
+    },
+
+    insertAfter: function insertAfter(elem) {
+        if (!(elem instanceof this.constructor))
+            elem = this.constructor(elem, this.document);
+        elem.after(this);
+        return this;
+    },
+
+    remove: function remove() {
+        return this.each(function (elem) {
+            if (elem.parentNode)
+                elem.parentNode.removeChild(elem);
+        }, this);
+    },
+
+    empty: function empty() {
+        return this.each(function (elem) {
+            while (elem.firstChild)
+                elem.removeChild(elem.firstChild);
+        }, this);
+    },
+
+    toggle: function toggle(val, self) {
+        if (callable(val))
+            return this.each(function (elem, i) {
+                this[val.call(self || this, elem, i) ? "show" : "hide"]();
+            });
+
+        if (arguments.length)
+            return this[val ? "show" : "hide"]();
+
+        let hidden = this.map(function (elem) elem.style.display == "none");
+        return this.each(function (elem, i) {
+            this[hidden[i] ? "show" : "hide"]();
+        });
+    },
+    hide: function hide() {
+        return this.each(function (elem) { elem.style.display = "none"; }, this);
+    },
+    show: function show() {
+        for (let i = 0; i < this.length; i++)
+            if (!this[i].dactylDefaultDisplay && this[i].style.display)
+                this[i].style.display = "";
+
+        this.each(function (elem) {
+            if (!elem.dactylDefaultDisplay)
+                elem.dactylDefaultDisplay = this.style.display;
+        });
+
+        return this.each(function (elem) {
+            elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
+        }, this);
+    },
+
+    createContents: function createContents()
+        this.each(DOM.createContents, this),
+
+    isScrollable: function isScrollable(direction)
+        this.length && DOM.isScrollable(this[0], direction),
+
+    getSet: function getSet(args, get, set) {
+        if (!args.length)
+            return this[0] && get.call(this, this[0]);
+
+        let [fn, self] = args;
+        if (!callable(fn))
+            fn = function () args[0];
+
+        return this.each(function (elem, i) {
+            set.call(this, elem, fn.call(self || this, elem, i));
+        }, this);
+    },
+
+    html: function html(txt, self) {
+        return this.getSet(arguments,
+                           function (elem) elem.innerHTML,
+                           function (elem, val) { elem.innerHTML = val });
+    },
+
+    text: function text(txt, self) {
+        return this.getSet(arguments,
+                           function (elem) elem.textContent,
+                           function (elem, val) { elem.textContent = val });
+    },
+
+    val: function val(txt) {
+        return this.getSet(arguments,
+                           function (elem) elem.value,
+                           function (elem, val) { elem.value = val == null ? "" : val });
+    },
+
+    listen: function listen(event, listener, capture) {
+        if (isObject(event))
+            capture = listener;
+        else
+            event = array.toObject([[event, listener]]);
+
+        for (let [k, v] in Iterator(event))
+            event[k] = util.wrapCallback(v, true);
+
+        return this.each(function (elem) {
+            for (let [k, v] in Iterator(event))
+                elem.addEventListener(k, v, capture);
+        });
+    },
+    unlisten: function unlisten(event, listener, capture) {
+        if (isObject(event))
+            capture = listener;
+        else
+            event = array.toObject([[key, val]]);
+
+        return this.each(function (elem) {
+            for (let [k, v] in Iterator(event))
+                elem.removeEventListener(k, v.wrapper || v, capture);
+        });
+    },
+
+    dispatch: function dispatch(event, params, extraProps) {
+        this.canceled = false;
+        return this.each(function (elem) {
+            let evt = DOM.Event(this.document, event, params, elem);
+            if (!DOM.Event.dispatch(elem, evt, extraProps))
+                this.canceled = true;
+        }, this);
+    },
+
+    focus: function focus(arg, extra) {
+        if (callable(arg))
+            return this.listen("focus", arg, extra);
+
+        let elem = this[0];
+        let flags = arg || services.focus.FLAG_BYMOUSE;
+        try {
+            if (elem instanceof Ci.nsIDOMDocument)
+                elem = elem.defaultView;
+            if (elem instanceof Ci.nsIDOMElement)
+                services.focus.setFocus(elem, flags);
+            else if (elem instanceof Ci.nsIDOMWindow) {
+                services.focus.focusedWindow = elem;
+                if (services.focus.focusedWindow != elem)
+                    services.focus.clearFocus(elem);
+            }
+        }
+        catch (e) {
+            util.dump(elem);
+            util.reportError(e);
+        }
+        return this;
+    },
+    blur: function blur(arg, extra) {
+        if (callable(arg))
+            return this.listen("blur", arg, extra);
+        return this.each(function (elem) { elem.blur(); }, this);
+    },
+
+    /**
+     * Scrolls an element into view if and only if it's not already
+     * fully visible.
+     */
+    scrollIntoView: function scrollIntoView(alignWithTop) {
+        return this.each(function (elem) {
+            function getAlignment(viewport) {
+                if (alignWithTop !== undefined)
+                    return alignWithTop;
+                if (rect.bottom < viewport.top)
+                    return true;
+                if (rect.top > viewport.bottom)
+                    return false;
+                return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom)
+            }
+
+            let rect;
+            function fix(parent) {
+                if (!(parent[0] instanceof Ci.nsIDOMWindow)
+                        && parent.style.overflow == "visible")
+                    return;
+
+                ({ rect }) = DOM(elem);
+                let { viewport } = parent;
+                let isect = util.intersection(rect, viewport);
+
+                if (isect.height < Math.min(viewport.height, rect.height)) {
+                    let { top } = parent.scrollPos();
+                    if (getAlignment(viewport))
+                        parent.scrollPos(null, top - (viewport.top - rect.top));
+                    else
+                        parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
+
+                }
+            }
+
+            for (let parent in this.ancestors.items)
+                fix(parent);
+
+            fix(DOM(this.document.defaultView));
+        });
+    },
+}, {
+    /**
+     * Creates an actual event from a pseudo-event object.
+     *
+     * The pseudo-event object (such as may be retrieved from
+     * DOM.Event.parse) should have any properties you want the event to
+     * have.
+     *
+     * @param {Document} doc The DOM document to associate this event with
+     * @param {Type} type The type of event (keypress, click, etc.)
+     * @param {Object} opts The pseudo-event. @optional
+     */
+    Event: Class("Event", {
+        init: function Event(doc, type, opts, target) {
+            const DEFAULTS = {
+                HTML: {
+                    type: type, bubbles: true, cancelable: false
+                },
+                Key: {
+                    type: type,
+                    bubbles: true, cancelable: true,
+                    view: doc.defaultView,
+                    ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
+                    keyCode: 0, charCode: 0
+                },
+                Mouse: {
+                    type: type,
+                    bubbles: true, cancelable: true,
+                    view: doc.defaultView,
+                    detail: 1,
+                    get screenX() this.view.mozInnerScreenX
+                                + Math.max(0, this.clientX + (DOM(target || opts.target).rect.left || 0)),
+                    get screenY() this.view.mozInnerScreenY
+                                + Math.max(0, this.clientY + (DOM(target || opts.target).rect.top || 0)),
+                    clientX: 0,
+                    clientY: 0,
+                    ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
+                    button: 0,
+                    relatedTarget: null
+                }
+            };
+
+            opts = opts || {};
+            var t = this.constructor.types[type] || "";
+            var evt = doc.createEvent(t + "Events");
+
+            let params = DEFAULTS[t || "HTML"];
+            let args = Object.keys(params);
+            update(params, this.constructor.defaults[type],
+                   iter.toObject([k, opts[k]] for (k in opts) if (k in params)));
+
+            evt["init" + t + "Event"].apply(evt, args.map(function (k) params[k]));
+            return evt;
+        }
+    }, {
+        init: function init() {
+            // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
+            //       matters, so use that string as the first item, that you
+            //       want to refer to within dactyl's source code for
+            //       comparisons like if (key == "<Esc>") { ... }
+            this.keyTable = {
+                add: ["Plus", "Add"],
+                back_space: ["BS"],
+                count: ["count"],
+                delete: ["Del"],
+                escape: ["Esc", "Escape"],
+                insert: ["Insert", "Ins"],
+                leader: ["Leader"],
+                left_shift: ["LT", "<"],
+                nop: ["Nop"],
+                pass: ["Pass"],
+                return: ["Return", "CR", "Enter"],
+                right_shift: [">"],
+                slash: ["/"],
+                space: ["Space", " "],
+                subtract: ["Minus", "Subtract"]
+            };
+
+            this.key_key = {};
+            this.code_key = {};
+            this.key_code = {};
+            this.code_nativeKey = {};
+
+            for (let list in values(this.keyTable))
+                for (let v in values(list)) {
+                    if (v.length == 1)
+                        v = v.toLowerCase();
+                    this.key_key[v.toLowerCase()] = v;
+                }
+
+            for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
+                this.code_nativeKey[v] = k.substr(4);
+
+                k = k.substr(7).toLowerCase();
+                let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
+                              .replace(/^NUMPAD/, "k")];
+
+                if (names[0].length == 1)
+                    names[0] = names[0].toLowerCase();
+
+                if (k in this.keyTable)
+                    names = this.keyTable[k];
+
+                this.code_key[v] = names[0];
+                for (let [, name] in Iterator(names)) {
+                    this.key_key[name.toLowerCase()] = name;
+                    this.key_code[name.toLowerCase()] = v;
+                }
+            }
+
+            // HACK: as Gecko does not include an event for <, we must add this in manually.
+            if (!("<" in this.key_code)) {
+                this.key_code["<"] = 60;
+                this.key_code["lt"] = 60;
+                this.code_key[60] = "lt";
+            }
+
+            return this;
+        },
+
+
+        code_key:       Class.Memoize(function (prop) this.init()[prop]),
+        code_nativeKey: Class.Memoize(function (prop) this.init()[prop]),
+        keyTable:       Class.Memoize(function (prop) this.init()[prop]),
+        key_code:       Class.Memoize(function (prop) this.init()[prop]),
+        key_key:        Class.Memoize(function (prop) this.init()[prop]),
+        pseudoKeys:     Set(["count", "leader", "nop", "pass"]),
+
+        /**
+         * Converts a user-input string of keys into a canonical
+         * representation.
+         *
+         * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
+         * <C- > maps to <C-Space>, <S-a> maps to A
+         * << maps to <lt><lt>
+         *
+         * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
+         * in macros.
+         *
+         * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
+         * of x.
+         *
+         * @param {string} keys Messy form.
+         * @param {boolean} unknownOk Whether unknown keys are passed
+         *     through rather than being converted to <lt>keyname>.
+         *     @default false
+         * @returns {string} Canonical form.
+         */
+        canonicalKeys: function canonicalKeys(keys, unknownOk) {
+            if (arguments.length === 1)
+                unknownOk = true;
+            return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
+        },
+
+        iterKeys: function iterKeys(keys) iter(function () {
+            let match, re = /<.*?>?>|[^<]/g;
+            while (match = re.exec(keys))
+                yield match[0];
+        }()),
+
+        /**
+         * Converts an event string into an array of pseudo-event objects.
+         *
+         * These objects can be used as arguments to {@link #stringify} or
+         * {@link DOM.Event}, though they are unlikely to be much use for other
+         * purposes. They have many of the properties you'd expect to find on a
+         * real event, but none of the methods.
+         *
+         * Also may contain two "special" parameters, .dactylString and
+         * .dactylShift these are set for characters that can never by
+         * typed, but may appear in mappings, for example <Nop> is passed as
+         * dactylString, and dactylShift is set when a user specifies
+         * <S-@> where @ is a non-case-changeable, non-space character.
+         *
+         * @param {string} keys The string to parse.
+         * @param {boolean} unknownOk Whether unknown keys are passed
+         *     through rather than being converted to <lt>keyname>.
+         *     @default false
+         * @returns {Array[Object]}
+         */
+        parse: function parse(input, unknownOk) {
+            if (isArray(input))
+                return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
+
+            if (arguments.length === 1)
+                unknownOk = true;
+
+            let out = [];
+            for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
+                let evt_str = match[0];
+
+                let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
+                                keyCode: 0, charCode: 0, type: "keypress" };
+
+                if (evt_str.length == 1) {
+                    evt_obj.charCode = evt_str.charCodeAt(0);
+                    evt_obj._keyCode = this.key_code[evt_str[0].toLowerCase()];
+                    evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
+                }
+                else {
+                    let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
+                    modifier = Set(modifier.toUpperCase());
+                    keyname = keyname.toLowerCase();
+                    evt_obj.dactylKeyname = keyname;
+                    if (/^u[0-9a-f]+$/.test(keyname))
+                        keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
+
+                    if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
+                                    this.key_code[keyname] || Set.has(this.pseudoKeys, keyname))) {
+                        evt_obj.globKey  ="*" in modifier;
+                        evt_obj.ctrlKey  ="C" in modifier;
+                        evt_obj.altKey   ="A" in modifier;
+                        evt_obj.shiftKey ="S" in modifier;
+                        evt_obj.metaKey  ="M" in modifier || "⌘" in modifier;
+                        evt_obj.dactylShift = evt_obj.shiftKey;
+
+                        if (keyname.length == 1) { // normal characters
+                            if (evt_obj.shiftKey)
+                                keyname = keyname.toUpperCase();
+
+                            evt_obj.dactylShift = evt_obj.shiftKey && keyname.toUpperCase() == keyname.toLowerCase();
+                            evt_obj.charCode = keyname.charCodeAt(0);
+                            evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
+                        }
+                        else if (Set.has(this.pseudoKeys, keyname)) {
+                            evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
+                        }
+                        else if (/mouse$/.test(keyname)) { // mouse events
+                            evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
+                            evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
+                            delete evt_obj.keyCode;
+                            delete evt_obj.charCode;
+                        }
+                        else { // spaces, control characters, and <
+                            evt_obj.keyCode = this.key_code[keyname];
+                            evt_obj.charCode = 0;
+                        }
+                    }
+                    else { // an invalid sequence starting with <, treat as a literal
+                        out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
+                        continue;
+                    }
+                }
+
+                // TODO: make a list of characters that need keyCode and charCode somewhere
+                if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
+                    evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
+                if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
+                    evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
+
+                evt_obj.modifiers = (evt_obj.ctrlKey  && Ci.nsIDOMNSEvent.CONTROL_MASK)
+                                  | (evt_obj.altKey   && Ci.nsIDOMNSEvent.ALT_MASK)
+                                  | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
+                                  | (evt_obj.metaKey  && Ci.nsIDOMNSEvent.META_MASK);
+
+                out.push(evt_obj);
+            }
+            return out;
+        },
+
+        /**
+         * Converts the specified event to a string in dactyl key-code
+         * notation. Returns null for an unknown event.
+         *
+         * @param {Event} event
+         * @returns {string}
+         */
+        stringify: function stringify(event) {
+            if (isArray(event))
+                return event.map(function (e) this.stringify(e), this).join("");
+
+            if (event.dactylString)
+                return event.dactylString;
+
+            let key = null;
+            let modifier = "";
+
+            if (event.globKey)
+                modifier += "*-";
+            if (event.ctrlKey)
+                modifier += "C-";
+            if (event.altKey)
+                modifier += "A-";
+            if (event.metaKey)
+                modifier += "M-";
+
+            if (/^key/.test(event.type)) {
+                let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
+                if (charCode == 0) {
+                    if (event.keyCode in this.code_key) {
+                        key = this.code_key[event.keyCode];
+
+                        if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
+                            modifier += "S-";
+                        else if (!modifier && key.length === 1)
+                            if (event.shiftKey)
+                                key = key.toUpperCase();
+                            else
+                                key = key.toLowerCase();
+
+                        if (!modifier && key.length == 1)
+                            return key;
+                    }
+                }
+                // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
+                //            (i.e., cntrl codes 27--31)
+                // ---
+                // For more information, see:
+                //     [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
+                //     [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
+                //         https://bugzilla.mozilla.org/show_bug.cgi?id=416227
+                //     [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
+                //         https://bugzilla.mozilla.org/show_bug.cgi?id=432951
+                // ---
+                //
+                // The following fixes are only activated if config.OS.isMacOSX.
+                // Technically, they prevent mappings from <C-Esc> (and
+                // <C-C-]> if your fancy keyboard permits such things<?>), but
+                // these <C-control> mappings are probably pathological (<C-Esc>
+                // certainly is on Windows), and so it is probably
+                // harmless to remove the config.OS.isMacOSX if desired.
+                //
+                else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
+                    if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
+                        key = "Esc";
+                        modifier = modifier.replace("C-", "");
+                    }
+                    else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
+                        key = String.fromCharCode(charCode + 64);
+                }
+                // a normal key like a, b, c, 0, etc.
+                else if (charCode) {
+                    key = String.fromCharCode(charCode);
+
+                    if (!/^[^<\s]$/i.test(key) && key in this.key_code) {
+                        // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
+                        if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
+                            modifier += "S-";
+
+                        key = this.code_key[this.key_code[key]];
+                    }
+                    else {
+                        // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
+                        // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
+                        if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
+                            modifier += "S-";
+                        if (/^\s$/.test(key))
+                            key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
+                        else if (modifier.length == 0)
+                            return key;
+                    }
+                }
+                if (key == null) {
+                    if (event.shiftKey)
+                        modifier += "S-";
+                    key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
+                }
+                if (key == null)
+                    return null;
+            }
+            else if (event.type == "click" || event.type == "dblclick") {
+                if (event.shiftKey)
+                    modifier += "S-";
+                if (event.type == "dblclick")
+                    modifier += "2-";
+                // TODO: triple and quadruple click
+
+                switch (event.button) {
+                case 0:
+                    key = "LeftMouse";
+                    break;
+                case 1:
+                    key = "MiddleMouse";
+                    break;
+                case 2:
+                    key = "RightMouse";
+                    break;
+                }
+            }
+
+            if (key == null)
+                return null;
+
+            return "<" + modifier + key + ">";
+        },
+
+
+        defaults: {
+            load:   { bubbles: false },
+            submit: { cancelable: true }
+        },
+
+        types: Class.Memoize(function () iter(
+            {
+                Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
+                       "hover " +
+                       "popupshowing popupshown popuphiding popuphidden " +
+                       "contextmenu",
+                Key:   "keydown keypress keyup",
+                "":    "change command dactyl-input input submit " +
+                       "load unload pageshow pagehide DOMContentLoaded " +
+                       "resize scroll"
+            }
+        ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
+         .flatten()
+         .toObject()),
+
+        /**
+         * Dispatches an event to an element as if it were a native event.
+         *
+         * @param {Node} target The DOM node to which to dispatch the event.
+         * @param {Event} event The event to dispatch.
+         */
+        dispatch: Class.Memoize(function ()
+            config.haveGecko("2b")
+                ? function dispatch(target, event, extra) {
+                    try {
+                        this.feedingEvent = extra;
+
+                        if (target instanceof Ci.nsIDOMElement)
+                            // This causes a crash on Gecko<2.0, it seems.
+                            return (target.ownerDocument || target.document || target).defaultView
+                                   .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
+                                   .dispatchDOMEventViaPresShell(target, event, true);
+                        else {
+                            target.dispatchEvent(event);
+                            return !event.getPreventDefault();
+                        }
+                    }
+                    catch (e) {
+                        util.reportError(e);
+                    }
+                    finally {
+                        this.feedingEvent = null;
+                    }
+                }
+                : function dispatch(target, event, extra) {
+                    try {
+                        this.feedingEvent = extra;
+                        target.dispatchEvent(update(event, extra));
+                    }
+                    finally {
+                        this.feedingEvent = null;
+                    }
+                })
+    }),
+
+    createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
+        || function (elem) {}),
+
+    isScrollable: Class.Memoize(function () services.has("dactyl") && services.dactyl.getScrollable
+        ? function (elem, dir) services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
+        : function (elem, dir) true),
+
+    /**
+     * 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 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.
+     */
+    stringify: function stringify(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;
+    },
+
+    /**
+     * 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 DOM.XPath(matcher.xpath, node))
+                        yield elem;
+
+                if (matcher.css)
+                    for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, 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) {
+        return this.testValues(list, DOM.closure.testMatcher);
+    },
+
+    testMatcher: function testMatcher(value) {
+        let evaluator = services.XPathEvaluator();
+        let node = services.XMLDocument();
+        if (/^xpath:/.test(value))
+            util.withProperErrors("createExpression", evaluator, value.substr(6), DOM.XPath.resolver);
+        else
+            util.withProperErrors("querySelector", node, value);
+        return true;
+    },
+
+    /**
+     * Converts HTML special characters in *str* to the equivalent HTML
+     * entities.
+     *
+     * @param {string} str
+     * @returns {string}
+     */
+    escapeHTML: function escapeHTML(str) {
+        let map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" };
+        return str.replace(/['"&<>]/g, function (m) map[m]);
+    },
+
+    /**
+     * 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}
+     */
+    fromXML: function fromXML(node, doc, nodes) {
+        XML.ignoreWhitespace = 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(fromXML(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(fromXML(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;
+        }
+    },
+
+    /**
+     * 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.
+     * @param {boolean} asIterator Whether to return the results as an
+     *     XPath iterator.
+     * @param {object} namespaces Additional namespaces to recognize.
+     *     @optional
+     * @returns {Object} Iterable result of the evaluation.
+     */
+    XPath: update(
+        function XPath(expression, elem, asIterator, namespaces) {
+            try {
+                let doc = elem.ownerDocument || elem;
+
+                if (isArray(expression))
+                    expression = DOM.makeXPath(expression);
+
+                let resolver = XPath.resolver;
+                if (namespaces) {
+                    namespaces = update({}, DOM.namespaces, namespaces);
+                    resolver = function (prefix) namespaces[prefix] || null;
+                }
+
+                let result = doc.evaluate(expression, elem,
+                    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) (DOM.namespaces[prefix] || null)
+        }),
+
+    /**
+     * 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 DOM.XPath}.
+     *
+     * @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(" | ");
+    },
+
+    namespaces: {
+        xul: XUL.uri,
+        xhtml: XHTML.uri,
+        html: XHTML.uri,
+        xhtml2: "http://www.w3.org/2002/06/xhtml2",
+        dactyl: NS.uri
+    },
+
+    namespaceNames: Class.Memoize(function ()
+        iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
+});
+
+Object.keys(DOM.Event.types).forEach(function (event) {
+    let name = event.replace(/-(.)/g, function (m, m1) m1.toUpperCase());
+    if (!Set.has(DOM.prototype, name))
+        DOM.prototype[name] =
+            function _event(arg, extra) {
+                return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
+            };
+});
+
+var $ = DOM;
+
+endModule();
+
+// catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+
+// vim: set sw=4 ts=4 et ft=javascript: