]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/dom.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[dactyl.git] / common / modules / dom.jsm
index 9f7f7e5f9a38bd831e456506876c0c688559688b..7a0c5556fca4791a9c87083e0862435e1e79bc99 100644 (file)
@@ -1,23 +1,23 @@
 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
-// Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
+// Copyright (c) 2008-2012 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);
+});
 
-this.lazyRequire("highlight", ["highlight"]);
-this.lazyRequire("template", ["template"]);
+lazyRequire("highlight", ["highlight"]);
+lazyRequire("messages", ["_"]);
+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",
@@ -56,6 +56,12 @@ var DOM = Class("DOM", {
             ;
         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 ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
@@ -111,15 +117,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)
@@ -127,7 +124,7 @@ var DOM = Class("DOM", {
             if (val instanceof Ci.nsIDOMNode)
                 return val;
 
-            if (typeof val == "xml") {
+            if (typeof val == "xml" || DOM.isJSONXML(val)) {
                 val = dom.constructor(val, dom.document);
                 if (container)
                     container[idx] = val[0];
@@ -502,8 +499,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)
@@ -557,14 +561,13 @@ 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;
         }
 
@@ -573,20 +576,24 @@ var DOM = Class("DOM", {
             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>);
+                    res.push(["span", { highlight: "HelpXML" },
+                        ["span", { highlight: "HelpXMLTagStart" },
+                            "<", namespaced(elem), " ",
+                            template.map(array.iterValues(elem.attributes),
+                                function (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) + ">"));
@@ -596,7 +603,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) {
@@ -719,6 +727,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(function (elem) elem.cloneNode(deep)),
+
     toggle: function toggle(val, self) {
         if (callable(val))
             return this.each(function (elem, i) {
@@ -773,7 +790,7 @@ var DOM = Class("DOM", {
     html: function html(txt, self) {
         return this.getSet(arguments,
                            function (elem) elem.innerHTML,
-                           function (elem, val) { elem.innerHTML = val });
+                           util.wrapCallback(function (elem, val) { elem.innerHTML = val }));
     },
 
     text: function text(txt, self) {
@@ -1013,6 +1030,9 @@ 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();
@@ -1378,6 +1398,17 @@ var DOM = Class("DOM", {
         ? function (elem, dir) services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
         : function (elem, dir) true),
 
+    isJSONXML: function isJSONXML(val) isArray(val) && isinstance(val[0], ["String", "Array", "XML", DOM.DOMString])
+                                    || isObject(val) && "toDOM" in val,
+
+    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.
@@ -1484,11 +1515,14 @@ 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, function (m) map[m]);
     },
 
     /**
@@ -1502,39 +1536,282 @@ var DOM = Class("DOM", {
      *     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;
+    fromXML: deprecated("DOM.fromJSON", { get: function fromXML()
+               prefs.get("javascript.options.xml.chrome") !== false
+            && require("dom-e4x").fromXML }),
+
+    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 (isXML(args))
+                return DOM.fromXML(args, doc, nodes);
+            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 = Set("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(function (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(function (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 (isXML(args))
+                return indent +
+                       args.toXMLString()
+                           .replace(/^/m, indent);
+
+            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 (Set.has(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) && Set.has(EMPTY, vals[1])
+                    || asXML && !args.length)
+                res.push("/>");
+            else {
+                res.push(">");
+
+                if (isStrings(args))
+                    res.push(args.map(function (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]];
     },
 
     /**
@@ -1604,11 +1881,11 @@ var DOM = Class("DOM", {
     },
 
     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 ()