]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/dom.jsm
Imported Upstream version 1.1+hg7904
[dactyl.git] / common / modules / dom.jsm
index 981ec9c8a26d1cb3d332f928e3f88b1cc1e5db14..72d80135751b7b1779436cfdf877c59baf7f985a 100644 (file)
@@ -1,20 +1,24 @@
 // 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.
-/* use strict */
+"use strict";
 
-Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("dom", {
     exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
-}, this);
+});
+
+lazyRequire("highlight", ["highlight"]);
+lazyRequire("messages", ["_"]);
+lazyRequire("overlay", ["overlay"]);
+lazyRequire("prefs", ["prefs"]);
+lazyRequire("template", ["template"]);
 
-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;
+var XBL = "http://www.mozilla.org/xbl";
+var XHTML = "http://www.w3.org/1999/xhtml";
+var XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+var NS = "http://vimperator.org/namespaces/liberator";
 
 function BooleanAttribute(attr) ({
     get: function (elem) elem.getAttribute(attr) == "true",
@@ -51,16 +55,20 @@ var DOM = Class("DOM", {
 
         if (val == null)
             ;
-        else if (typeof val == "xml" && context instanceof Ci.nsIDOMDocument)
-            this[length++] = DOM.fromXML(val, context, this.nodes);
+        else if (DOM.isJSONXML(val)) {
+            if (context instanceof Ci.nsIDOMDocument)
+                this[length++] = DOM.fromJSON(val, context, this.nodes);
+            else
+                this[length++] = val;
+        }
         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 if ("length" in val)
+            for (let i = 0; i < val.length; i++)
+                this[length++] = val[i];
         else
             this[length++] = val;
 
@@ -90,7 +98,7 @@ var DOM = Class("DOM", {
             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 } },
+                       set: function (elem, val) { elem.setAttribute("checked", !!val); elem.checked = val; } },
             collapsed: BooleanAttribute("collapsed"),
             disabled: BooleanAttribute("disabled"),
             hidden: BooleanAttribute("hidden"),
@@ -98,7 +106,7 @@ var DOM = Class("DOM", {
         }]
     ]),
 
-    matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
+    matcher: function matcher(sel) elem => (elem.mozMatchesSelector && elem.mozMatchesSelector(sel)),
 
     each: function each(fn, self) {
         let obj = self || this.Empty();
@@ -108,15 +116,6 @@ var DOM = Class("DOM", {
     },
 
     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)
@@ -124,7 +123,7 @@ var DOM = Class("DOM", {
             if (val instanceof Ci.nsIDOMNode)
                 return val;
 
-            if (typeof val == "xml") {
+            if (DOM.isJSONXML(val)) {
                 val = dom.constructor(val, dom.document);
                 if (container)
                     container[idx] = val[0];
@@ -139,6 +138,9 @@ var DOM = Class("DOM", {
             return val;
         }
 
+        if (DOM.isJSONXML(val))
+            val = (function () this).bind(val);
+
         if (callable(val))
             return this.each(function (elem, i) {
                 util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
@@ -154,11 +156,11 @@ var DOM = Class("DOM", {
     },
 
     find: function find(val) {
-        return this.map(function (elem) elem.querySelectorAll(val));
+        return this.map(elem => elem.querySelectorAll(val));
     },
 
     findAnon: function findAnon(attr, val) {
-        return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
+        return this.map(elem => elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
     },
 
     filter: function filter(val, self) {
@@ -189,13 +191,13 @@ var DOM = Class("DOM", {
         let res = this.Empty();
 
         this.each(function (elem) {
-            while(true) {
-                elem = fn.call(this, elem)
-                if (elem instanceof Ci.nsIDOMElement)
+            while (true) {
+                elem = fn.call(this, elem);
+                if (elem instanceof Ci.nsIDOMNode)
                     res[res.length++] = elem;
                 else if (elem && "length" in elem)
-                    for (let i = 0; i < tmp.length; i++)
-                        res[res.length++] = tmp[j];
+                    for (let i = 0; i < elem.length; i++)
+                        res[res.length++] = elem[j];
                 else
                     break;
             }
@@ -209,7 +211,7 @@ var DOM = Class("DOM", {
 
         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)
+            if (isObject(tmp) && !(tmp instanceof Ci.nsIDOMNode) && "length" in tmp)
                 for (let j = 0; j < tmp.length; j++)
                     res[res.length++] = tmp[j];
             else if (tmp != null)
@@ -230,7 +232,7 @@ var DOM = Class("DOM", {
         return false;
     },
 
-    get parent() this.map(function (elem) elem.parentNode, this),
+    get parent() this.map(elem => elem.parentNode, this),
 
     get offsetParent() this.map(function (elem) {
         do {
@@ -241,20 +243,23 @@ var DOM = Class("DOM", {
         while (parent);
     }, this),
 
-    get ancestors() this.all(function (elem) elem.parentNode),
+    get ancestors() this.all(elem => elem.parentNode),
 
-    get children() this.map(function (elem) Array.filter(elem.childNodes,
-                                                         function (e) e instanceof Ci.nsIDOMElement),
+    get children() this.map(elem => Array.filter(elem.childNodes,
+                                                 e => e instanceof Ci.nsIDOMElement),
                             this),
 
-    get contents() this.map(function (elem) elem.childNodes, this),
+    get contents() this.map(elem => elem.childNodes, this),
 
-    get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
-                                                         function (e) e != elem && e instanceof Ci.nsIDOMElement),
+    get siblings() this.map(elem => Array.filter(elem.parentNode.childNodes,
+                                                 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 siblingsBefore() this.all(elem => elem.previousElementSibling),
+    get siblingsAfter() this.all(elem => elem.nextElementSibling),
+
+    get allSiblingsBefore() this.all(elem => elem.previousSibling),
+    get allSiblingsAfter() this.all(elem => elem.nextSibling),
 
     get class() let (self = this) ({
         toString: function () self[0].className,
@@ -265,7 +270,7 @@ var DOM = Class("DOM", {
         each: function each(meth, arg) {
             return self.each(function (elem) {
                 elem.classList[meth](arg);
-            })
+            });
         },
 
         add: function add(cls) this.each("add", cls),
@@ -298,14 +303,14 @@ var DOM = Class("DOM", {
         }),
 
         remove: function remove(hl) self.each(function () {
-            this.highlight.list = this.highlight.list.filter(function (h) h != hl);
+            this.highlight.list = this.highlight.list.filter(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)
+            highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl);
         }),
     }),
 
@@ -318,24 +323,28 @@ var DOM = Class("DOM", {
                this[0]                            ? this[0].getBoundingClientRect() : {},
 
     get viewport() {
-        if (this[0] instanceof Ci.nsIDOMWindow)
+        let node = this[0];
+        if (node instanceof Ci.nsIDOMDocument)
+            node = node.defaultView;
+
+        if (node instanceof Ci.nsIDOMWindow)
             return {
                 get width() this.right - this.left,
                 get height() this.bottom - this.top,
-                bottom: this[0].innerHeight,
-                right: this[0].innerWidth,
+                bottom: node.innerHeight,
+                right: node.innerWidth,
                 top: 0, left: 0
             };
 
         let r = this.rect;
         return {
-            width: this[0].clientWidth,
-            height: this[0].clientHeight,
-            top: r.top + this[0].clientTop,
+            width: node.clientWidth,
+            height: node.clientHeight,
+            top: r.top + node.clientTop,
             get bottom() this.top + this.height,
-            left: r.left + this[0].clientLeft,
+            left: r.left + node.clientLeft,
             get right() this.left + this.width
-        }
+        };
     },
 
     scrollPos: function scrollPos(left, top) {
@@ -406,7 +415,7 @@ var DOM = Class("DOM", {
         return editor;
     },
 
-    get isEditable() !!this.editor,
+    get isEditable() !!this.editor || this[0] instanceof Ci.nsIDOMElement && this.style.MozUserModify == "read-write",
 
     get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
                                        Ci.nsIDOMHTMLTextAreaElement,
@@ -465,7 +474,7 @@ var DOM = Class("DOM", {
 
         let charset = doc.characterSet;
         let converter = services.CharsetConv(charset);
-        for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
+        for (let cs of form.acceptCharset.split(/\s*,\s*|\s+/)) {
             let c = services.CharsetConv(cs);
             if (c) {
                 converter = services.CharsetConv(cs);
@@ -492,8 +501,15 @@ var DOM = Class("DOM", {
                 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));
+                        || elem.checked && /^(?:checkbox|radio)$/.test(elem.type)) {
+
+                    if (elem !== field)
+                        elems.push(encode(elem.name, elem.value));
+                    else if (overlay.getData(elem, "had-focus"))
+                        elems.push(encode(elem.name, elem.value, true));
+                    else
+                        elems.push(encode(elem.name, "", true));
+                }
                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
                     for (let [, opt] in Iterator(elem.options))
                         if (opt.selected)
@@ -547,36 +563,39 @@ var DOM = Class("DOM", {
      *  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];
+            var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[1];
             if (!ns)
                 return node.localName;
             if (color)
-                return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</>
+                return [["span", { highlight: "HelpXMLNamespace" }, ns],
+                        node.localName];
             return ns + ":" + node.localName;
         }
 
         let res = [];
         this.each(function (elem) {
             try {
-                let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
+                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>);
+                    res.push(["span", { highlight: "HelpXML" },
+                        ["span", { highlight: "HelpXMLTagStart" },
+                            "<", namespaced(elem), " ",
+                            template.map(array.iterValues(elem.attributes),
+                                attr => [
+                                    ["span", { highlight: "HelpXMLAttribute" }, namespaced(attr)],
+                                    ["span", { highlight: "HelpXMLString" }, attr.value]
+                                ],
+                                " "),
+                            !hasChildren ? "/>" : ">",
+                        ],
+                        !hasChildren ? "" :
+                            ["", "...",
+                             ["span", { highlight: "HtmlTagEnd" }, "<", namespaced(elem), ">"]]
+                    ]);
                 else {
                     let tag = "<" + [namespaced(elem)].concat(
-                        [namespaced(a) + "=" + template.highlight(a.value, true)
+                        [namespaced(a) + '="' + String.replace(a.value, /["<]/, DOM.escapeHTML) + '"'
                          for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
 
                     res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
@@ -586,7 +605,8 @@ var DOM = Class("DOM", {
                 res.push({}.toString.call(elem));
             }
         }, this);
-        return template.map(res, util.identity, <>,</>);
+        res = template.map(res, util.identity, ",");
+        return color ? res : res.join("");
     },
 
     attr: function attr(key, val) {
@@ -605,7 +625,7 @@ var DOM = Class("DOM", {
                     if (callable(v))
                         v = v.call(this, elem, i);
 
-                    if (Set.has(hooks, k) && hooks[k].set)
+                    if (hasOwnProperty(hooks, k) && hooks[k].set)
                         hooks[k].set.call(this, elem, v, k);
                     else if (v == null)
                         elem.removeAttributeNS(ns, k);
@@ -617,7 +637,7 @@ var DOM = Class("DOM", {
         if (!this.length)
             return null;
 
-        if (Set.has(hooks, key) && hooks[key].get)
+        if (hasOwnProperty(hooks, key) && hooks[key].get)
             return hooks[key].get.call(this, this[0], key);
 
         if (!this[0].hasAttributeNS(ns, key))
@@ -638,9 +658,9 @@ var DOM = Class("DOM", {
 
         return this[0].style[css.property(key)];
     }, {
-        name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
+        name: function (property) property.replace(/[A-Z]/g, m0 => "-" + m0.toLowerCase()),
 
-        property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
+        property: function (name) name.replace(/-(.)/g, (m0, m1) => m1.toUpperCase())
     }),
 
     append: function append(val) {
@@ -709,6 +729,15 @@ var DOM = Class("DOM", {
         }, this);
     },
 
+    fragment: function fragment() {
+        let frag = this.document.createDocumentFragment();
+        this.appendTo(frag);
+        return this;
+    },
+
+    clone: function clone(deep)
+        this.map(elem => elem.cloneNode(deep)),
+
     toggle: function toggle(val, self) {
         if (callable(val))
             return this.each(function (elem, i) {
@@ -718,7 +747,7 @@ var DOM = Class("DOM", {
         if (arguments.length)
             return this[val ? "show" : "hide"]();
 
-        let hidden = this.map(function (elem) elem.style.display == "none");
+        let hidden = this.map(elem => elem.style.display == "none");
         return this.each(function (elem, i) {
             this[hidden[i] ? "show" : "hide"]();
         });
@@ -753,7 +782,7 @@ var DOM = Class("DOM", {
 
         let [fn, self] = args;
         if (!callable(fn))
-            fn = function () args[0];
+            fn = () => args[0];
 
         return this.each(function (elem, i) {
             set.call(this, elem, fn.call(self || this, elem, i));
@@ -762,20 +791,20 @@ var DOM = Class("DOM", {
 
     html: function html(txt, self) {
         return this.getSet(arguments,
-                           function (elem) elem.innerHTML,
-                           function (elem, val) { elem.innerHTML = val });
+                           elem => elem.innerHTML,
+                           util.wrapCallback((elem, val) => { elem.innerHTML = val; }));
     },
 
     text: function text(txt, self) {
         return this.getSet(arguments,
-                           function (elem) elem.textContent,
-                           function (elem, val) { elem.textContent = val });
+                           elem => elem.textContent,
+                           (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 });
+                           elem => elem.value,
+                           (elem, val) => { elem.value = val == null ? "" : val; });
     },
 
     listen: function listen(event, listener, capture) {
@@ -784,25 +813,44 @@ var DOM = Class("DOM", {
         else
             event = array.toObject([[event, listener]]);
 
-        for (let [k, v] in Iterator(event))
-            event[k] = util.wrapCallback(v, true);
+        for (let [evt, callback] in Iterator(event))
+            event[evt] = util.wrapCallback(callback, true);
 
         return this.each(function (elem) {
-            for (let [k, v] in Iterator(event))
-                elem.addEventListener(k, v, capture);
+            for (let [evt, callback] in Iterator(event))
+                elem.addEventListener(evt, callback, capture);
         });
     },
     unlisten: function unlisten(event, listener, capture) {
         if (isObject(event))
             capture = listener;
         else
-            event = array.toObject([[key, val]]);
+            event = array.toObject([[event, listener]]);
 
         return this.each(function (elem) {
             for (let [k, v] in Iterator(event))
                 elem.removeEventListener(k, v.wrapper || v, capture);
         });
     },
+    once: function once(event, listener, capture) {
+        if (isObject(event))
+            capture = listener;
+        else
+            event = array.toObject([[event, listener]]);
+
+        for (let pair in Iterator(event)) {
+            let [evt, callback] = pair;
+            event[evt] = util.wrapCallback(function wrapper(event) {
+                this.removeEventListener(evt, wrapper.wrapper, capture);
+                return callback.apply(this, arguments);
+            }, true);
+        }
+
+        return this.each(function (elem) {
+            for (let [k, v] in Iterator(event))
+                elem.addEventListener(k, v, capture);
+        });
+    },
 
     dispatch: function dispatch(event, params, extraProps) {
         this.canceled = false;
@@ -855,7 +903,7 @@ var DOM = Class("DOM", {
                     return true;
                 if (rect.top > viewport.bottom)
                     return false;
-                return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom)
+                return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom);
             }
 
             let rect;
@@ -935,7 +983,7 @@ var DOM = Class("DOM", {
             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]));
+            evt["init" + t + "Event"].apply(evt, args.map(k => params[k]));
             return evt;
         }
     }, {
@@ -945,21 +993,30 @@ var DOM = Class("DOM", {
             //       want to refer to within dactyl's source code for
             //       comparisons like if (key == "<Esc>") { ... }
             this.keyTable = {
-                add: ["Plus", "Add"],
+                add: ["+", "Plus", "Add"],
+                back_quote: ["`"],
+                back_slash: ["\\"],
                 back_space: ["BS"],
+                comma: [","],
                 count: ["count"],
+                close_bracket: ["]"],
                 delete: ["Del"],
+                equals: ["="],
                 escape: ["Esc", "Escape"],
                 insert: ["Insert", "Ins"],
                 leader: ["Leader"],
                 left_shift: ["LT", "<"],
                 nop: ["Nop"],
+                open_bracket: ["["],
                 pass: ["Pass"],
+                period: ["."],
+                quote: ["'"],
                 return: ["Return", "CR", "Enter"],
                 right_shift: [">"],
+                semicolon: [";"],
                 slash: ["/"],
                 space: ["Space", " "],
-                subtract: ["Minus", "Subtract"]
+                subtract: ["-", "Minus", "Subtract"]
             };
 
             this.key_key = {};
@@ -975,10 +1032,13 @@ var DOM = Class("DOM", {
                 }
 
             for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
+                if (!/^DOM_VK_/.test(k))
+                    continue;
+
                 this.code_nativeKey[v] = k.substr(4);
 
                 k = k.substr(7).toLowerCase();
-                let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
+                let names = [k.replace(/(^|_)(.)/g, (m, n1, n2) => n2.toUpperCase())
                               .replace(/^NUMPAD/, "k")];
 
                 if (names[0].length == 1)
@@ -1004,13 +1064,12 @@ var DOM = Class("DOM", {
             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"]),
+        pseudoKeys:     RealSet(["count", "leader", "nop", "pass"]),
 
         /**
          * Converts a user-input string of keys into a canonical
@@ -1029,13 +1088,11 @@ var DOM = Class("DOM", {
          * @param {string} keys Messy form.
          * @param {boolean} unknownOk Whether unknown keys are passed
          *     through rather than being converted to <lt>keyname>.
-         *     @default false
+         *     @default true
          * @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("");
+        canonicalKeys: function canonicalKeys(keys, unknownOk=true) {
+            return this.parse(keys, unknownOk).map(this.bound.stringify).join("");
         },
 
         iterKeys: function iterKeys(keys) iter(function () {
@@ -1061,15 +1118,12 @@ var DOM = Class("DOM", {
          * @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
+         *     @default true
          * @returns {Array[Object]}
          */
-        parse: function parse(input, unknownOk) {
+        parse: function parse(input, unknownOk=true) {
             if (isArray(input))
-                return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
-
-            if (arguments.length === 1)
-                unknownOk = true;
+                return array.flatten(input.map(k => this.parse(k, unknownOk)));
 
             let out = [];
             for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
@@ -1085,19 +1139,19 @@ var DOM = Class("DOM", {
                 }
                 else {
                     let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
-                    modifier = Set(modifier.toUpperCase());
+                    modifier = RealSet(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;
+                                    this.key_code[keyname] || this.pseudoKeys.has(keyname))) {
+                        evt_obj.globKey  = modifier.has("*");
+                        evt_obj.ctrlKey  = modifier.has("C");
+                        evt_obj.altKey   = modifier.has("A");
+                        evt_obj.shiftKey = modifier.has("S");
+                        evt_obj.metaKey  = modifier.has("M") || modifier.has("⌘");
                         evt_obj.dactylShift = evt_obj.shiftKey;
 
                         if (keyname.length == 1) { // normal characters
@@ -1108,11 +1162,11 @@ var DOM = Class("DOM", {
                             evt_obj.charCode = keyname.charCodeAt(0);
                             evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
                         }
-                        else if (Set.has(this.pseudoKeys, keyname)) {
+                        else if (this.pseudoKeys.has(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.type = (modifier.has("2") ? "dblclick" : "click");
                             evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
                             delete evt_obj.keyCode;
                             delete evt_obj.charCode;
@@ -1153,7 +1207,7 @@ var DOM = Class("DOM", {
          */
         stringify: function stringify(event) {
             if (isArray(event))
-                return event.map(function (e) this.stringify(e), this).join("");
+                return event.map(e => this.stringify(e)).join("");
 
             if (event.dactylString)
                 return event.dactylString;
@@ -1176,7 +1230,9 @@ var DOM = Class("DOM", {
                     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)
+                        if (event.shiftKey && (key.length > 1 || key.toUpperCase() == key.toLowerCase()
+                                               || event.ctrlKey || event.altKey || event.metaKey)
+                                || event.dactylShift)
                             modifier += "S-";
                         else if (!modifier && key.length === 1)
                             if (event.shiftKey)
@@ -1270,13 +1326,12 @@ var DOM = Class("DOM", {
             return "<" + modifier + key + ">";
         },
 
-
         defaults: {
             load:   { bubbles: false },
             submit: { cancelable: true }
         },
 
-        types: Class.Memoize(function () iter(
+        types: Class.Memoize(() => iter(
             {
                 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
                        "hover " +
@@ -1287,7 +1342,7 @@ var DOM = Class("DOM", {
                        "load unload pageshow pagehide DOMContentLoaded " +
                        "resize scroll"
             }
-        ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
+        ).map(([k, v]) => v.split(" ").map(v => [v, k]))
          .flatten()
          .toObject()),
 
@@ -1297,54 +1352,53 @@ var DOM = Class("DOM", {
          * @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;
-                    }
+        dispatch: function dispatch(target, event, extra) {
+            try {
+                this.feedingEvent = extra;
+
+                if (target instanceof Ci.nsIDOMElement)
+                    return (target.ownerDocument || target.document || target).defaultView
+                           .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
+                           .dispatchDOMEventViaPresShell(target, event, true);
+                else {
+                    target.dispatchEvent(event);
+                    return !event.defaultPrevented;
                 }
-                : function dispatch(target, event, extra) {
-                    try {
-                        this.feedingEvent = extra;
-                        target.dispatchEvent(update(event, extra));
-                    }
-                    finally {
-                        this.feedingEvent = null;
-                    }
-                })
+            }
+            catch (e) {
+                util.reportError(e);
+            }
+            finally {
+                this.feedingEvent = null;
+            }
+        }
     }),
 
-    createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
-        || function (elem) {}),
+    createContents: Class.Memoize(() => services.has("dactyl") && services.dactyl.createContents
+        || (elem => {})),
+
+    isScrollable: Class.Memoize(() => services.has("dactyl") && services.dactyl.getScrollable
+        ? (elem, dir) => services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
+        : (elem, dir) => true),
+
+    isJSONXML: function isJSONXML(val) isArray(val) && isinstance(val[0], ["String", "Array", "XML", DOM.DOMString])
+                                    || isObject(val) && "toDOM" in val,
 
-    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),
+    DOMString: function DOMString(val) ({
+        __proto__: DOMString.prototype,
+
+        toDOM: function toDOM(doc) doc.createTextNode(val),
+
+        toString: function () val
+    }),
 
     /**
      * 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"]),
+    editableInputs: RealSet(["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
@@ -1426,7 +1480,7 @@ var DOM = Class("DOM", {
      * @returns {boolean} True when the patterns are all valid.
      */
     validateMatcher: function validateMatcher(list) {
-        return this.testValues(list, DOM.closure.testMatcher);
+        return this.testValues(list, DOM.bound.testMatcher);
     },
 
     testMatcher: function testMatcher(value) {
@@ -1444,57 +1498,281 @@ var DOM = Class("DOM", {
      * entities.
      *
      * @param {string} str
+     * @param {boolean} simple If true, only escape for the simple case
+     *     of text nodes.
      * @returns {string}
      */
-    escapeHTML: function escapeHTML(str) {
+    escapeHTML: function escapeHTML(str, simple) {
         let map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" };
-        return str.replace(/['"&<>]/g, function (m) map[m]);
+        let regexp = simple ? /[<>]/g : /['"&<>]/g;
+        return str.replace(regexp, 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;
+    fromJSON: update(function fromJSON(xml, doc, nodes, namespaces) {
+        if (!doc)
+            doc = document;
+
+        function tag(args, namespaces) {
+            let _namespaces = namespaces;
+
+            // Deal with common error case
+            if (args == null) {
+                util.reportError(Error("Unexpected null when processing XML."));
+                args = ["html:i", {}, "[NULL]"];
+            }
+
+            if (isinstance(args, ["String", "Number", "Boolean", _]))
+                return doc.createTextNode(args);
+            if (isObject(args) && "toDOM" in args)
+                return args.toDOM(doc, namespaces, nodes);
+            if (args instanceof Ci.nsIDOMNode)
+                return args;
+            if (args instanceof DOM)
+                return args.fragment();
+            if ("toJSONXML" in args)
+                args = args.toJSONXML();
+
+            let [name, attr] = args;
+
+            if (!isString(name) || args.length == 0 || name === "") {
+                var frag = doc.createDocumentFragment();
+                Array.forEach(args, function (arg) {
+                    if (!isArray(arg[0]))
+                        arg = [arg];
+                    arg.forEach(function (arg) {
+                        frag.appendChild(tag(arg, namespaces));
+                    });
+                });
+                return frag;
+            }
+
+            attr = attr || {};
+
+            function parseNamespace(name) DOM.parseNamespace(name, namespaces);
+
+            // FIXME: Surely we can do better.
+            for (var key in attr) {
+                if (/^xmlns(?:$|:)/.test(key)) {
+                    if (_namespaces === namespaces)
+                        namespaces = Object.create(namespaces);
+
+                    namespaces[key.substr(6)] = namespaces[attr[key]] || attr[key];
+                }}
+
+            var args = Array.slice(args, 2);
+            var vals = parseNamespace(name);
+            var elem = doc.createElementNS(vals[0] || namespaces[""],
+                                           name);
+
+            for (var key in attr)
+                if (!/^xmlns(?:$|:)/.test(key)) {
+                    var val = attr[key];
+                    if (nodes && key == "key")
+                        nodes[val] = elem;
+
+                    vals = parseNamespace(key);
+                    if (key == "highlight")
+                        ;
+                    else if (typeof val == "function")
+                        elem.addEventListener(key.replace(/^on/, ""), val, false);
+                    else
+                        elem.setAttributeNS(vals[0] || "", key, val);
+                }
+            args.forEach(function (e) {
+                elem.appendChild(tag(e, namespaces));
+            });
+
+            if ("highlight" in attr)
+                highlight.highlightNode(elem, attr.highlight, nodes || true);
+            return elem;
         }
 
-        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;
+        if (namespaces)
+            namespaces = update({}, fromJSON.namespaces, namespaces);
+        else
+            namespaces = fromJSON.namespaces;
+
+        return tag(xml, namespaces);
+    }, {
+        namespaces: {
+            "": "http://www.w3.org/1999/xhtml",
+            dactyl: String(NS),
+            html: "http://www.w3.org/1999/xhtml",
+            xmlns: "http://www.w3.org/2000/xmlns/",
+            xul: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         }
+    }),
+
+    toXML: function toXML(xml) {
+        // Meh. For now.
+        let doc = services.XMLDocument();
+        let node = this.fromJSON(xml, doc);
+        return services.XMLSerializer()
+                       .serializeToString(node);
+    },
+
+    toPrettyXML: function toPrettyXML(xml, asXML, indent, namespaces) {
+        const INDENT = indent || "    ";
+
+        const EMPTY = RealSet("area base basefont br col frame hr img input isindex link meta param"
+                            .split(" "));
+
+        function namespaced(namespaces, namespace, localName) {
+            for (let [k, v] in Iterator(namespaces))
+                if (v == namespace)
+                    return (k ? k + ":" + localName : localName);
+
+            throw Error("No such namespace");
+        }
+
+        function isFragment(args) !isString(args[0]) || args.length == 0 || args[0] === "";
+
+        function hasString(args) {
+            return args.some(a => (isString(a) || isFragment(a) && hasString(a)));
+        }
+
+        function isStrings(args) {
+            if (!isArray(args))
+                return util.dump("ARGS: " + {}.toString.call(args) + " " + args), false;
+            return args.every(a => (isinstance(a, ["String", DOM.DOMString]) || isFragment(a) && isStrings(a)));
+        }
+
+        function tag(args, namespaces, indent) {
+            let _namespaces = namespaces;
+
+            if (args == "")
+                return "";
+
+            if (isinstance(args, ["String", "Number", "Boolean", _, DOM.DOMString]))
+                return indent +
+                       DOM.escapeHTML(String(args), true);
+
+            if (isObject(args) && "toDOM" in args)
+                return indent +
+                       services.XMLSerializer()
+                               .serializeToString(args.toDOM(services.XMLDocument()))
+                               .replace(/^/m, indent);
+
+            if (args instanceof Ci.nsIDOMNode)
+                return indent +
+                       services.XMLSerializer()
+                               .serializeToString(args)
+                               .replace(/^/m, indent);
+
+            if ("toJSONXML" in args)
+                args = args.toJSONXML();
+
+            // Deal with common error case
+            if (args == null) {
+                util.reportError(Error("Unexpected null when processing XML."));
+                return "[NULL]";
+            }
+
+            let [name, attr] = args;
+
+            if (isFragment(args)) {
+                let res = [];
+                let join = isArray(args) && isStrings(args) ? "" : "\n";
+                Array.forEach(args, function (arg) {
+                    if (!isArray(arg[0]))
+                        arg = [arg];
+
+                    let contents = [];
+                    arg.forEach(function (arg) {
+                        let string = tag(arg, namespaces, indent);
+                        if (string)
+                            contents.push(string);
+                    });
+                    if (contents.length)
+                        res.push(contents.join("\n"), join);
+                });
+                if (res[res.length - 1] == join)
+                    res.pop();
+                return res.join("");
+            }
+
+            attr = attr || {};
+
+            function parseNamespace(name) {
+                var m = /^(?:(.*):)?(.*)$/.exec(name);
+                return [namespaces[m[1]], m[2]];
+            }
+
+            // FIXME: Surely we can do better.
+            let skipAttr = {};
+            for (var key in attr) {
+                if (/^xmlns(?:$|:)/.test(key)) {
+                    if (_namespaces === namespaces)
+                        namespaces = update({}, namespaces);
+
+                    let ns = namespaces[attr[key]] || attr[key];
+                    if (ns == namespaces[key.substr(6)])
+                        skipAttr[key] = true;
+
+                    attr[key] = namespaces[key.substr(6)] = ns;
+                }}
+
+            var args = Array.slice(args, 2);
+            var vals = parseNamespace(name);
+
+            let res = [indent, "<", name];
+
+            for (let [key, val] in Iterator(attr)) {
+                if (hasOwnProperty(skipAttr, key))
+                    continue;
+
+                let vals = parseNamespace(key);
+                if (typeof val == "function") {
+                    key = key.replace(/^(?:on)?/, "on");
+                    val = val.toSource() + "(event)";
+                }
+
+                if (key != "highlight" || vals[0] == String(NS))
+                    res.push(" ", key, '="', DOM.escapeHTML(val), '"');
+                else
+                    res.push(" ", namespaced(namespaces, String(NS), "highlight"),
+                             '="', DOM.escapeHTML(val), '"');
+            }
+
+            if ((vals[0] || namespaces[""]) == String(XHTML) && EMPTY.has(vals[1])
+                    || asXML && !args.length)
+                res.push("/>");
+            else {
+                res.push(">");
+
+                if (isStrings(args))
+                    res.push(args.map(e => tag(e, namespaces, "")).join(""),
+                             "</", name, ">");
+                else {
+                    let contents = [];
+                    args.forEach(function (e) {
+                        let string = tag(e, namespaces, indent + INDENT);
+                        if (string)
+                            contents.push(string);
+                    });
+
+                    res.push("\n", contents.join("\n"), "\n", indent, "</", name, ">");
+                }
+            }
+
+            return res.join("");
+        }
+
+        if (namespaces)
+            namespaces = update({}, DOM.fromJSON.namespaces, namespaces);
+        else
+            namespaces = DOM.fromJSON.namespaces;
+
+        return tag(xml, namespaces, "");
+    },
+
+    parseNamespace: function parseNamespace(name, namespaces) {
+        if (name == "xmlns")
+            return [DOM.fromJSON.namespaces.xmlns, "xmlns"];
+
+        var m = /^(?:(.*):)?(.*)$/.exec(name);
+        return [(namespaces || DOM.fromJSON.namespaces)[m[1]],
+                m[2]];
     },
 
     /**
@@ -1521,7 +1799,7 @@ var DOM = Class("DOM", {
                 let resolver = XPath.resolver;
                 if (namespaces) {
                     namespaces = update({}, DOM.namespaces, namespaces);
-                    resolver = function (prefix) namespaces[prefix] || null;
+                    resolver = prefix => namespaces[prefix] || null;
                 }
 
                 let result = doc.evaluate(expression, elem,
@@ -1530,12 +1808,16 @@ var DOM = Class("DOM", {
                     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); }
-                    }
-                });
+                let res = {
+                    iterateNext: function () result.iterateNext(),
+                    get resultType() result.resultType,
+                    get snapshotLength() result.snapshotLength,
+                    snapshotItem: function (i) result.snapshotItem(i),
+                    __iterator__:
+                        asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
+                                   : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
+                };
+                return res;
             }
             catch (e) {
                 throw e.stack ? e : Error(e);
@@ -1555,25 +1837,27 @@ var DOM = Class("DOM", {
      */
     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(" | ");
+                           .map(node => /^[a-z]+:/.test(node) ? node
+                                                              : [node, "xhtml:" + node])
+                           .flatten()
+                           .map(node => "//" + node).join(" | ");
     },
 
     namespaces: {
-        xul: XUL.uri,
-        xhtml: XHTML.uri,
-        html: XHTML.uri,
+        xul: XUL,
+        xhtml: XHTML,
+        html: XHTML,
         xhtml2: "http://www.w3.org/2002/06/xhtml2",
-        dactyl: NS.uri
+        dactyl: NS
     },
 
     namespaceNames: Class.Memoize(function ()
-        iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
+        iter(this.namespaces).map(([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))
+    let name = event.replace(/-(.)/g, (m, m1) => m1.toUpperCase());
+    if (!hasOwnProperty(DOM.prototype, name))
         DOM.prototype[name] =
             function _event(arg, extra) {
                 return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
@@ -1586,4 +1870,4 @@ 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:
+// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: