]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/dom.jsm
Import 1.0rc1 supporting Firefox up to 11.*
[dactyl.git] / common / modules / dom.jsm
1 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
2 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
3 //
4 // This work is licensed for reuse under an MIT license. Details are
5 // given in the LICENSE.txt file included with this file.
6 /* use strict */
7
8 Components.utils.import("resource://dactyl/bootstrap.jsm");
9 defineModule("dom", {
10     exports: ["$", "DOM", "NS", "XBL", "XHTML", "XUL"]
11 }, this);
12
13 var XBL = Namespace("xbl", "http://www.mozilla.org/xbl");
14 var XHTML = Namespace("html", "http://www.w3.org/1999/xhtml");
15 var XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
16 var NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator");
17 default xml namespace = XHTML;
18
19 function BooleanAttribute(attr) ({
20     get: function (elem) elem.getAttribute(attr) == "true",
21     set: function (elem, val) {
22         if (val === "false" || !val)
23             elem.removeAttribute(attr);
24         else
25             elem.setAttribute(attr, true);
26     }
27 });
28
29 /**
30  * @class
31  *
32  * A jQuery-inspired DOM utility framework.
33  *
34  * Please note that while this currently implements an Array-like
35  * interface, this is *not a defined interface* and is very likely to
36  * change in the near future.
37  */
38 var DOM = Class("DOM", {
39     init: function init(val, context, nodes) {
40         let self;
41         let length = 0;
42
43         if (nodes)
44             this.nodes = nodes;
45
46         if (context instanceof Ci.nsIDOMDocument)
47             this.document = context;
48
49         if (typeof val == "string")
50             val = context.querySelectorAll(val);
51
52         if (val == null)
53             ;
54         else if (typeof val == "xml" && context instanceof Ci.nsIDOMDocument)
55             this[length++] = DOM.fromXML(val, context, this.nodes);
56         else if (val instanceof Ci.nsIDOMNode || val instanceof Ci.nsIDOMWindow)
57             this[length++] = val;
58         else if ("length" in val)
59             for (let i = 0; i < val.length; i++)
60                 this[length++] = val[i];
61         else if ("__iterator__" in val || isinstance(val, ["Iterator", "Generator"]))
62             for (let elem in val)
63                 this[length++] = elem;
64         else
65             this[length++] = val;
66
67         this.length = length;
68         return self || this;
69     },
70
71     __iterator__: function __iterator__() {
72         for (let i = 0; i < this.length; i++)
73             yield this[i];
74     },
75
76     Empty: function Empty() this.constructor(null, this.document),
77
78     nodes: Class.Memoize(function () ({})),
79
80     get items() {
81         for (let i = 0; i < this.length; i++)
82             yield this.eq(i);
83     },
84
85     get document() this._document || this[0] && (this[0].ownerDocument || this[0].document || this[0]),
86     set document(val) this._document = val,
87
88     attrHooks: array.toObject([
89         ["", {
90             href: { get: function (elem) elem.href || elem.getAttribute("href") },
91             src:  { get: function (elem) elem.src || elem.getAttribute("src") },
92             checked: { get: function (elem) elem.hasAttribute("checked") ? elem.getAttribute("checked") == "true" : elem.checked,
93                        set: function (elem, val) { elem.setAttribute("checked", !!val); elem.checked = val } },
94             collapsed: BooleanAttribute("collapsed"),
95             disabled: BooleanAttribute("disabled"),
96             hidden: BooleanAttribute("hidden"),
97             readonly: BooleanAttribute("readonly")
98         }]
99     ]),
100
101     matcher: function matcher(sel) function (elem) elem.mozMatchesSelector && elem.mozMatchesSelector(sel),
102
103     each: function each(fn, self) {
104         let obj = self || this.Empty();
105         for (let i = 0; i < this.length; i++)
106             fn.call(self || update(obj, [this[i]]), this[i], i);
107         return this;
108     },
109
110     eachDOM: function eachDOM(val, fn, self) {
111         XML.prettyPrinting = XML.ignoreWhitespace = false;
112         if (isString(val))
113             val = XML(val);
114
115         if (typeof val == "xml")
116             return this.each(function (elem, i) {
117                 fn.call(this, DOM.fromXML(val, elem.ownerDocument), elem, i);
118             }, self || this);
119
120         let dom = this;
121         function munge(val, container, idx) {
122             if (val instanceof Ci.nsIDOMRange)
123                 return val.extractContents();
124             if (val instanceof Ci.nsIDOMNode)
125                 return val;
126
127             if (typeof val == "xml") {
128                 val = dom.constructor(val, dom.document);
129                 if (container)
130                     container[idx] = val[0];
131             }
132
133             if (isObject(val) && "length" in val) {
134                 let frag = dom.document.createDocumentFragment();
135                 for (let i = 0; i < val.length; i++)
136                     frag.appendChild(munge(val[i], val, i));
137                 return frag;
138             }
139             return val;
140         }
141
142         if (callable(val))
143             return this.each(function (elem, i) {
144                 util.withProperErrors(fn, this, munge(val.call(this, elem, i)), elem, i);
145             }, self || this);
146
147         if (this.length)
148             util.withProperErrors(fn, self || this, munge(val), this[0], 0);
149         return this;
150     },
151
152     eq: function eq(idx) {
153         return this.constructor(this[idx >= 0 ? idx : this.length + idx]);
154     },
155
156     find: function find(val) {
157         return this.map(function (elem) elem.querySelectorAll(val));
158     },
159
160     findAnon: function findAnon(attr, val) {
161         return this.map(function (elem) elem.ownerDocument.getAnonymousElementByAttribute(elem, attr, val));
162     },
163
164     filter: function filter(val, self) {
165         let res = this.Empty();
166
167         if (!callable(val))
168             val = this.matcher(val);
169
170         this.constructor(Array.filter(this, val, self || this));
171         let obj = self || this.Empty();
172         for (let i = 0; i < this.length; i++)
173             if (val.call(self || update(obj, [this[i]]), this[i], i))
174                 res[res.length++] = this[i];
175
176         return res;
177     },
178
179     is: function is(val) {
180         return this.some(this.matcher(val));
181     },
182
183     reverse: function reverse() {
184         Array.reverse(this);
185         return this;
186     },
187
188     all: function all(fn, self) {
189         let res = this.Empty();
190
191         this.each(function (elem) {
192             while(true) {
193                 elem = fn.call(this, elem)
194                 if (elem instanceof Ci.nsIDOMElement)
195                     res[res.length++] = elem;
196                 else if (elem && "length" in elem)
197                     for (let i = 0; i < tmp.length; i++)
198                         res[res.length++] = tmp[j];
199                 else
200                     break;
201             }
202         }, self || this);
203         return res;
204     },
205
206     map: function map(fn, self) {
207         let res = this.Empty();
208         let obj = self || this.Empty();
209
210         for (let i = 0; i < this.length; i++) {
211             let tmp = fn.call(self || update(obj, [this[i]]), this[i], i);
212             if (isObject(tmp) && "length" in tmp)
213                 for (let j = 0; j < tmp.length; j++)
214                     res[res.length++] = tmp[j];
215             else if (tmp != null)
216                 res[res.length++] = tmp;
217         }
218
219         return res;
220     },
221
222     slice: function eq(start, end) {
223         return this.constructor(Array.slice(this, start, end));
224     },
225
226     some: function some(fn, self) {
227         for (let i = 0; i < this.length; i++)
228             if (fn.call(self || this, this[i], i))
229                 return true;
230         return false;
231     },
232
233     get parent() this.map(function (elem) elem.parentNode, this),
234
235     get offsetParent() this.map(function (elem) {
236         do {
237             var parent = elem.offsetParent;
238             if (parent instanceof Ci.nsIDOMElement && DOM(parent).position != "static")
239                 return parent;
240         }
241         while (parent);
242     }, this),
243
244     get ancestors() this.all(function (elem) elem.parentNode),
245
246     get children() this.map(function (elem) Array.filter(elem.childNodes,
247                                                          function (e) e instanceof Ci.nsIDOMElement),
248                             this),
249
250     get contents() this.map(function (elem) elem.childNodes, this),
251
252     get siblings() this.map(function (elem) Array.filter(elem.parentNode.childNodes,
253                                                          function (e) e != elem && e instanceof Ci.nsIDOMElement),
254                             this),
255
256     get siblingsBefore() this.all(function (elem) elem.previousElementSibling),
257     get siblingsAfter() this.all(function (elem) elem.nextElementSibling),
258
259     get class() let (self = this) ({
260         toString: function () self[0].className,
261
262         get list() Array.slice(self[0].classList),
263         set list(val) self.attr("class", val.join(" ")),
264
265         each: function each(meth, arg) {
266             return self.each(function (elem) {
267                 elem.classList[meth](arg);
268             })
269         },
270
271         add: function add(cls) this.each("add", cls),
272         remove: function remove(cls) this.each("remove", cls),
273         toggle: function toggle(cls, val, thisObj) {
274             if (callable(val))
275                 return self.each(function (elem, i) {
276                     this.class.toggle(cls, val.call(thisObj || this, elem, i));
277                 });
278             return this.each(val == null ? "toggle" : val ? "add" : "remove", cls);
279         },
280
281         has: function has(cls) this[0].classList.has(cls)
282     }),
283
284     get highlight() let (self = this) ({
285         toString: function () self.attrNS(NS, "highlight") || "",
286
287         get list() let (s = this.toString().trim()) s ? s.split(/\s+/) : [],
288         set list(val) {
289             let str = array.uniq(val).join(" ").trim();
290             self.attrNS(NS, "highlight", str || null);
291         },
292
293         has: function has(hl) ~this.list.indexOf(hl),
294
295         add: function add(hl) self.each(function () {
296             highlight.loaded[hl] = true;
297             this.highlight.list = this.highlight.list.concat(hl);
298         }),
299
300         remove: function remove(hl) self.each(function () {
301             this.highlight.list = this.highlight.list.filter(function (h) h != hl);
302         }),
303
304         toggle: function toggle(hl, val, thisObj) self.each(function (elem, i) {
305             let { highlight } = this;
306             let v = callable(val) ? val.call(thisObj || this, elem, i) : val;
307
308             highlight[(v == null ? highlight.has(hl) : !v) ? "remove" : "add"](hl)
309         }),
310     }),
311
312     get rect() this[0] instanceof Ci.nsIDOMWindow ? { width: this[0].scrollMaxX + this[0].innerWidth,
313                                                       height: this[0].scrollMaxY + this[0].innerHeight,
314                                                       get right() this.width + this.left,
315                                                       get bottom() this.height + this.top,
316                                                       top: -this[0].scrollY,
317                                                       left: -this[0].scrollX } :
318                this[0]                            ? this[0].getBoundingClientRect() : {},
319
320     get viewport() {
321         if (this[0] instanceof Ci.nsIDOMWindow)
322             return {
323                 get width() this.right - this.left,
324                 get height() this.bottom - this.top,
325                 bottom: this[0].innerHeight,
326                 right: this[0].innerWidth,
327                 top: 0, left: 0
328             };
329
330         let r = this.rect;
331         return {
332             width: this[0].clientWidth,
333             height: this[0].clientHeight,
334             top: r.top + this[0].clientTop,
335             get bottom() this.top + this.height,
336             left: r.left + this[0].clientLeft,
337             get right() this.left + this.width
338         }
339     },
340
341     scrollPos: function scrollPos(left, top) {
342         if (arguments.length == 0) {
343             if (this[0] instanceof Ci.nsIDOMElement)
344                 return { top: this[0].scrollTop, left: this[0].scrollLeft,
345                          height: this[0].scrollHeight, width: this[0].scrollWidth,
346                          innerHeight: this[0].clientHeight, innerWidth: this[0].innerWidth };
347
348             if (this[0] instanceof Ci.nsIDOMWindow)
349                 return { top: this[0].scrollY, left: this[0].scrollX,
350                          height: this[0].scrollMaxY + this[0].innerHeight,
351                          width: this[0].scrollMaxX + this[0].innerWidth,
352                          innerHeight: this[0].innerHeight, innerWidth: this[0].innerWidth };
353
354             return null;
355         }
356         let func = callable(left) && left;
357
358         return this.each(function (elem, i) {
359             if (func)
360                 ({ left, top }) = func.call(this, elem, i);
361
362             if (elem instanceof Ci.nsIDOMWindow)
363                 elem.scrollTo(left == null ? elem.scrollX : left,
364                               top  == null ? elem.scrollY : top);
365             else {
366                 if (left != null)
367                     elem.scrollLeft = left;
368                 if (top != null)
369                     elem.scrollTop = top;
370             }
371         });
372     },
373
374     /**
375      * Returns true if the given DOM node is currently visible.
376      * @returns {boolean}
377      */
378     get isVisible() {
379         let style = this[0] && this.style;
380         return style && style.visibility == "visible" && style.display != "none";
381     },
382
383     get editor() {
384         if (!this.length)
385             return;
386
387         this[0] instanceof Ci.nsIDOMNSEditableElement;
388         try {
389             if (this[0].editor instanceof Ci.nsIEditor)
390                 var editor = this[0].editor;
391         }
392         catch (e) {
393             util.reportError(e);
394         }
395
396         try {
397             if (!editor)
398                 editor = this[0].QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
399                                 .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession)
400                                 .getEditorForWindow(this[0]);
401         }
402         catch (e) {}
403
404         editor instanceof Ci.nsIPlaintextEditor;
405         editor instanceof Ci.nsIHTMLEditor;
406         return editor;
407     },
408
409     get isEditable() !!this.editor,
410
411     get isInput() isinstance(this[0], [Ci.nsIDOMHTMLInputElement,
412                                        Ci.nsIDOMHTMLTextAreaElement,
413                                        Ci.nsIDOMXULTextBoxElement])
414                     && this.isEditable,
415
416     /**
417      * Returns an object representing a Node's computed CSS style.
418      * @returns {Object}
419      */
420     get style() {
421         let node = this[0];
422         if (node instanceof Ci.nsIDOMWindow)
423             node = node.document;
424         if (node instanceof Ci.nsIDOMDocument)
425             node = node.documentElement;
426         while (node && !(node instanceof Ci.nsIDOMElement) && node.parentNode)
427             node = node.parentNode;
428
429         try {
430             var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
431         }
432         catch (e) {}
433
434         if (res == null) {
435             util.dumpStack(_("error.nullComputedStyle", node));
436             Cu.reportError(Error(_("error.nullComputedStyle", node)));
437             return {};
438         }
439         return res;
440     },
441
442     /**
443      * Parses the fields of a form and returns a URL/POST-data pair
444      * that is the equivalent of submitting the form.
445      *
446      * @returns {object} An object with the following elements:
447      *      url: The URL the form points to.
448      *      postData: A string containing URL-encoded post data, if this
449      *                form is to be POSTed
450      *      charset: The character set of the GET or POST data.
451      *      elements: The key=value pairs used to generate query information.
452      */
453     // Nuances gleaned from browser.jar/content/browser/browser.js
454     get formData() {
455         function encode(name, value, param) {
456             param = param ? "%s" : "";
457             if (post)
458                 return name + "=" + encodeComponent(value + param);
459             return encodeComponent(name) + "=" + encodeComponent(value) + param;
460         }
461
462         let field = this[0];
463         let form = field.form;
464         let doc = form.ownerDocument;
465
466         let charset = doc.characterSet;
467         let converter = services.CharsetConv(charset);
468         for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
469             let c = services.CharsetConv(cs);
470             if (c) {
471                 converter = services.CharsetConv(cs);
472                 charset = cs;
473             }
474         }
475
476         let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
477         let url = util.newURI(form.action, charset, uri).spec;
478
479         let post = form.method.toUpperCase() == "POST";
480
481         let encodeComponent = encodeURIComponent;
482         if (charset !== "UTF-8")
483             encodeComponent = function encodeComponent(str)
484                 escape(converter.ConvertFromUnicode(str) + converter.Finish());
485
486         let elems = [];
487         if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
488             elems.push(encode(field.name, field.value));
489
490         for (let [, elem] in iter(form.elements))
491             if (elem.name && !elem.disabled) {
492                 if (DOM(elem).isInput
493                         || /^(?:hidden|textarea)$/.test(elem.type)
494                         || elem.type == "submit" && elem == field
495                         || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
496                     elems.push(encode(elem.name, elem.value, elem === field));
497                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
498                     for (let [, opt] in Iterator(elem.options))
499                         if (opt.selected)
500                             elems.push(encode(elem.name, opt.value));
501                 }
502             }
503
504         if (post)
505             return { url: url, postData: elems.join('&'), charset: charset, elements: elems };
506         return { url: url + "?" + elems.join('&'), postData: null, charset: charset, elements: elems };
507     },
508
509     /**
510      * Generates an XPath expression for the given element.
511      *
512      * @returns {string}
513      */
514     get xpath() {
515         function quote(val) "'" + val.replace(/[\\']/g, "\\$&") + "'";
516         if (!(this[0] instanceof Ci.nsIDOMElement))
517             return null;
518
519         let res = [];
520         let doc = this.document;
521         for (let elem = this[0];; elem = elem.parentNode) {
522             if (!(elem instanceof Ci.nsIDOMElement))
523                 res.push("");
524             else if (elem.id)
525                 res.push("id(" + quote(elem.id) + ")");
526             else {
527                 let name = elem.localName;
528                 if (elem.namespaceURI && (elem.namespaceURI != XHTML || doc.xmlVersion))
529                     if (elem.namespaceURI in DOM.namespaceNames)
530                         name = DOM.namespaceNames[elem.namespaceURI] + ":" + name;
531                     else
532                         name = "*[local-name()=" + quote(name) + " and namespace-uri()=" + quote(elem.namespaceURI) + "]";
533
534                 res.push(name + "[" + (1 + iter(DOM.XPath("./" + name, elem.parentNode)).indexOf(elem)) + "]");
535                 continue;
536             }
537             break;
538         }
539
540         return res.reverse().join("/");
541     },
542
543     /**
544      * Returns a string or XML representation of this node.
545      *
546      * @param {boolean} color If true, return a colored, XML
547      *  representation of this node.
548      */
549     repr: function repr(color) {
550         XML.ignoreWhitespace = XML.prettyPrinting = false;
551
552         function namespaced(node) {
553             var ns = DOM.namespaceNames[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
554             if (!ns)
555                 return node.localName;
556             if (color)
557                 return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</>
558             return ns + ":" + node.localName;
559         }
560
561         let res = [];
562         this.each(function (elem) {
563             try {
564                 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
565                 if (color)
566                     res.push(<span highlight="HelpXML"><span highlight="HelpXMLTagStart">&lt;{
567                             namespaced(elem)} {
568                                 template.map(array.iterValues(elem.attributes),
569                                     function (attr)
570                                         <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> +
571                                         <span highlight="HelpXMLString">{attr.value}</span>,
572                                     <> </>)
573                             }{ !hasChildren ? "/>" : ">"
574                         }</span>{ !hasChildren ? "" : <>...</> +
575                             <span highlight="HtmlTagEnd">&lt;{namespaced(elem)}></span>
576                     }</span>);
577                 else {
578                     let tag = "<" + [namespaced(elem)].concat(
579                         [namespaced(a) + "=" + template.highlight(a.value, true)
580                          for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
581
582                     res.push(tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">"));
583                 }
584             }
585             catch (e) {
586                 res.push({}.toString.call(elem));
587             }
588         }, this);
589         return template.map(res, util.identity, <>,</>);
590     },
591
592     attr: function attr(key, val) {
593         return this.attrNS("", key, val);
594     },
595
596     attrNS: function attrNS(ns, key, val) {
597         if (val !== undefined)
598             key = array.toObject([[key, val]]);
599
600         let hooks = this.attrHooks[ns] || {};
601
602         if (isObject(key))
603             return this.each(function (elem, i) {
604                 for (let [k, v] in Iterator(key)) {
605                     if (callable(v))
606                         v = v.call(this, elem, i);
607
608                     if (Set.has(hooks, k) && hooks[k].set)
609                         hooks[k].set.call(this, elem, v, k);
610                     else if (v == null)
611                         elem.removeAttributeNS(ns, k);
612                     else
613                         elem.setAttributeNS(ns, k, v);
614                 }
615             });
616
617         if (!this.length)
618             return null;
619
620         if (Set.has(hooks, key) && hooks[key].get)
621             return hooks[key].get.call(this, this[0], key);
622
623         if (!this[0].hasAttributeNS(ns, key))
624             return null;
625
626         return this[0].getAttributeNS(ns, key);
627     },
628
629     css: update(function css(key, val) {
630         if (val !== undefined)
631             key = array.toObject([[key, val]]);
632
633         if (isObject(key))
634             return this.each(function (elem) {
635                 for (let [k, v] in Iterator(key))
636                     elem.style[css.property(k)] = v;
637             });
638
639         return this[0].style[css.property(key)];
640     }, {
641         name: function (property) property.replace(/[A-Z]/g, function (m0) "-" + m0.toLowerCase()),
642
643         property: function (name) name.replace(/-(.)/g, function (m0, m1) m1.toUpperCase())
644     }),
645
646     append: function append(val) {
647         return this.eachDOM(val, function (elem, target) {
648             target.appendChild(elem);
649         });
650     },
651
652     prepend: function prepend(val) {
653         return this.eachDOM(val, function (elem, target) {
654             target.insertBefore(elem, target.firstChild);
655         });
656     },
657
658     before: function before(val) {
659         return this.eachDOM(val, function (elem, target) {
660             target.parentNode.insertBefore(elem, target);
661         });
662     },
663
664     after: function after(val) {
665         return this.eachDOM(val, function (elem, target) {
666             target.parentNode.insertBefore(elem, target.nextSibling);
667         });
668     },
669
670     appendTo: function appendTo(elem) {
671         if (!(elem instanceof this.constructor))
672             elem = this.constructor(elem, this.document);
673         elem.append(this);
674         return this;
675     },
676
677     prependTo: function prependTo(elem) {
678         if (!(elem instanceof this.constructor))
679             elem = this.constructor(elem, this.document);
680         elem.prepend(this);
681         return this;
682     },
683
684     insertBefore: function insertBefore(elem) {
685         if (!(elem instanceof this.constructor))
686             elem = this.constructor(elem, this.document);
687         elem.before(this);
688         return this;
689     },
690
691     insertAfter: function insertAfter(elem) {
692         if (!(elem instanceof this.constructor))
693             elem = this.constructor(elem, this.document);
694         elem.after(this);
695         return this;
696     },
697
698     remove: function remove() {
699         return this.each(function (elem) {
700             if (elem.parentNode)
701                 elem.parentNode.removeChild(elem);
702         }, this);
703     },
704
705     empty: function empty() {
706         return this.each(function (elem) {
707             while (elem.firstChild)
708                 elem.removeChild(elem.firstChild);
709         }, this);
710     },
711
712     toggle: function toggle(val, self) {
713         if (callable(val))
714             return this.each(function (elem, i) {
715                 this[val.call(self || this, elem, i) ? "show" : "hide"]();
716             });
717
718         if (arguments.length)
719             return this[val ? "show" : "hide"]();
720
721         let hidden = this.map(function (elem) elem.style.display == "none");
722         return this.each(function (elem, i) {
723             this[hidden[i] ? "show" : "hide"]();
724         });
725     },
726     hide: function hide() {
727         return this.each(function (elem) { elem.style.display = "none"; }, this);
728     },
729     show: function show() {
730         for (let i = 0; i < this.length; i++)
731             if (!this[i].dactylDefaultDisplay && this[i].style.display)
732                 this[i].style.display = "";
733
734         this.each(function (elem) {
735             if (!elem.dactylDefaultDisplay)
736                 elem.dactylDefaultDisplay = this.style.display;
737         });
738
739         return this.each(function (elem) {
740             elem.style.display = elem.dactylDefaultDisplay == "none" ? "block" : "";
741         }, this);
742     },
743
744     createContents: function createContents()
745         this.each(DOM.createContents, this),
746
747     isScrollable: function isScrollable(direction)
748         this.length && DOM.isScrollable(this[0], direction),
749
750     getSet: function getSet(args, get, set) {
751         if (!args.length)
752             return this[0] && get.call(this, this[0]);
753
754         let [fn, self] = args;
755         if (!callable(fn))
756             fn = function () args[0];
757
758         return this.each(function (elem, i) {
759             set.call(this, elem, fn.call(self || this, elem, i));
760         }, this);
761     },
762
763     html: function html(txt, self) {
764         return this.getSet(arguments,
765                            function (elem) elem.innerHTML,
766                            function (elem, val) { elem.innerHTML = val });
767     },
768
769     text: function text(txt, self) {
770         return this.getSet(arguments,
771                            function (elem) elem.textContent,
772                            function (elem, val) { elem.textContent = val });
773     },
774
775     val: function val(txt) {
776         return this.getSet(arguments,
777                            function (elem) elem.value,
778                            function (elem, val) { elem.value = val == null ? "" : val });
779     },
780
781     listen: function listen(event, listener, capture) {
782         if (isObject(event))
783             capture = listener;
784         else
785             event = array.toObject([[event, listener]]);
786
787         for (let [k, v] in Iterator(event))
788             event[k] = util.wrapCallback(v, true);
789
790         return this.each(function (elem) {
791             for (let [k, v] in Iterator(event))
792                 elem.addEventListener(k, v, capture);
793         });
794     },
795     unlisten: function unlisten(event, listener, capture) {
796         if (isObject(event))
797             capture = listener;
798         else
799             event = array.toObject([[key, val]]);
800
801         return this.each(function (elem) {
802             for (let [k, v] in Iterator(event))
803                 elem.removeEventListener(k, v.wrapper || v, capture);
804         });
805     },
806
807     dispatch: function dispatch(event, params, extraProps) {
808         this.canceled = false;
809         return this.each(function (elem) {
810             let evt = DOM.Event(this.document, event, params, elem);
811             if (!DOM.Event.dispatch(elem, evt, extraProps))
812                 this.canceled = true;
813         }, this);
814     },
815
816     focus: function focus(arg, extra) {
817         if (callable(arg))
818             return this.listen("focus", arg, extra);
819
820         let elem = this[0];
821         let flags = arg || services.focus.FLAG_BYMOUSE;
822         try {
823             if (elem instanceof Ci.nsIDOMDocument)
824                 elem = elem.defaultView;
825             if (elem instanceof Ci.nsIDOMElement)
826                 services.focus.setFocus(elem, flags);
827             else if (elem instanceof Ci.nsIDOMWindow) {
828                 services.focus.focusedWindow = elem;
829                 if (services.focus.focusedWindow != elem)
830                     services.focus.clearFocus(elem);
831             }
832         }
833         catch (e) {
834             util.dump(elem);
835             util.reportError(e);
836         }
837         return this;
838     },
839     blur: function blur(arg, extra) {
840         if (callable(arg))
841             return this.listen("blur", arg, extra);
842         return this.each(function (elem) { elem.blur(); }, this);
843     },
844
845     /**
846      * Scrolls an element into view if and only if it's not already
847      * fully visible.
848      */
849     scrollIntoView: function scrollIntoView(alignWithTop) {
850         return this.each(function (elem) {
851             function getAlignment(viewport) {
852                 if (alignWithTop !== undefined)
853                     return alignWithTop;
854                 if (rect.bottom < viewport.top)
855                     return true;
856                 if (rect.top > viewport.bottom)
857                     return false;
858                 return Math.abs(rect.top) < Math.abs(viewport.bottom - rect.bottom)
859             }
860
861             let rect;
862             function fix(parent) {
863                 if (!(parent[0] instanceof Ci.nsIDOMWindow)
864                         && parent.style.overflow == "visible")
865                     return;
866
867                 ({ rect }) = DOM(elem);
868                 let { viewport } = parent;
869                 let isect = util.intersection(rect, viewport);
870
871                 if (isect.height < Math.min(viewport.height, rect.height)) {
872                     let { top } = parent.scrollPos();
873                     if (getAlignment(viewport))
874                         parent.scrollPos(null, top - (viewport.top - rect.top));
875                     else
876                         parent.scrollPos(null, top - (viewport.bottom - rect.bottom));
877
878                 }
879             }
880
881             for (let parent in this.ancestors.items)
882                 fix(parent);
883
884             fix(DOM(this.document.defaultView));
885         });
886     },
887 }, {
888     /**
889      * Creates an actual event from a pseudo-event object.
890      *
891      * The pseudo-event object (such as may be retrieved from
892      * DOM.Event.parse) should have any properties you want the event to
893      * have.
894      *
895      * @param {Document} doc The DOM document to associate this event with
896      * @param {Type} type The type of event (keypress, click, etc.)
897      * @param {Object} opts The pseudo-event. @optional
898      */
899     Event: Class("Event", {
900         init: function Event(doc, type, opts, target) {
901             const DEFAULTS = {
902                 HTML: {
903                     type: type, bubbles: true, cancelable: false
904                 },
905                 Key: {
906                     type: type,
907                     bubbles: true, cancelable: true,
908                     view: doc.defaultView,
909                     ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
910                     keyCode: 0, charCode: 0
911                 },
912                 Mouse: {
913                     type: type,
914                     bubbles: true, cancelable: true,
915                     view: doc.defaultView,
916                     detail: 1,
917                     get screenX() this.view.mozInnerScreenX
918                                 + Math.max(0, this.clientX + (DOM(target || opts.target).rect.left || 0)),
919                     get screenY() this.view.mozInnerScreenY
920                                 + Math.max(0, this.clientY + (DOM(target || opts.target).rect.top || 0)),
921                     clientX: 0,
922                     clientY: 0,
923                     ctrlKey: false, altKey: false, shiftKey: false, metaKey: false,
924                     button: 0,
925                     relatedTarget: null
926                 }
927             };
928
929             opts = opts || {};
930             var t = this.constructor.types[type] || "";
931             var evt = doc.createEvent(t + "Events");
932
933             let params = DEFAULTS[t || "HTML"];
934             let args = Object.keys(params);
935             update(params, this.constructor.defaults[type],
936                    iter.toObject([k, opts[k]] for (k in opts) if (k in params)));
937
938             evt["init" + t + "Event"].apply(evt, args.map(function (k) params[k]));
939             return evt;
940         }
941     }, {
942         init: function init() {
943             // NOTE: the order of ["Esc", "Escape"] or ["Escape", "Esc"]
944             //       matters, so use that string as the first item, that you
945             //       want to refer to within dactyl's source code for
946             //       comparisons like if (key == "<Esc>") { ... }
947             this.keyTable = {
948                 add: ["Plus", "Add"],
949                 back_space: ["BS"],
950                 count: ["count"],
951                 delete: ["Del"],
952                 escape: ["Esc", "Escape"],
953                 insert: ["Insert", "Ins"],
954                 leader: ["Leader"],
955                 left_shift: ["LT", "<"],
956                 nop: ["Nop"],
957                 pass: ["Pass"],
958                 return: ["Return", "CR", "Enter"],
959                 right_shift: [">"],
960                 slash: ["/"],
961                 space: ["Space", " "],
962                 subtract: ["Minus", "Subtract"]
963             };
964
965             this.key_key = {};
966             this.code_key = {};
967             this.key_code = {};
968             this.code_nativeKey = {};
969
970             for (let list in values(this.keyTable))
971                 for (let v in values(list)) {
972                     if (v.length == 1)
973                         v = v.toLowerCase();
974                     this.key_key[v.toLowerCase()] = v;
975                 }
976
977             for (let [k, v] in Iterator(Ci.nsIDOMKeyEvent)) {
978                 this.code_nativeKey[v] = k.substr(4);
979
980                 k = k.substr(7).toLowerCase();
981                 let names = [k.replace(/(^|_)(.)/g, function (m, n1, n2) n2.toUpperCase())
982                               .replace(/^NUMPAD/, "k")];
983
984                 if (names[0].length == 1)
985                     names[0] = names[0].toLowerCase();
986
987                 if (k in this.keyTable)
988                     names = this.keyTable[k];
989
990                 this.code_key[v] = names[0];
991                 for (let [, name] in Iterator(names)) {
992                     this.key_key[name.toLowerCase()] = name;
993                     this.key_code[name.toLowerCase()] = v;
994                 }
995             }
996
997             // HACK: as Gecko does not include an event for <, we must add this in manually.
998             if (!("<" in this.key_code)) {
999                 this.key_code["<"] = 60;
1000                 this.key_code["lt"] = 60;
1001                 this.code_key[60] = "lt";
1002             }
1003
1004             return this;
1005         },
1006
1007
1008         code_key:       Class.Memoize(function (prop) this.init()[prop]),
1009         code_nativeKey: Class.Memoize(function (prop) this.init()[prop]),
1010         keyTable:       Class.Memoize(function (prop) this.init()[prop]),
1011         key_code:       Class.Memoize(function (prop) this.init()[prop]),
1012         key_key:        Class.Memoize(function (prop) this.init()[prop]),
1013         pseudoKeys:     Set(["count", "leader", "nop", "pass"]),
1014
1015         /**
1016          * Converts a user-input string of keys into a canonical
1017          * representation.
1018          *
1019          * <C-A> maps to <C-a>, <C-S-a> maps to <C-S-A>
1020          * <C- > maps to <C-Space>, <S-a> maps to A
1021          * << maps to <lt><lt>
1022          *
1023          * <S-@> is preserved, as in Vim, to allow untypeable key-combinations
1024          * in macros.
1025          *
1026          * canonicalKeys(canonicalKeys(x)) == canonicalKeys(x) for all values
1027          * of x.
1028          *
1029          * @param {string} keys Messy form.
1030          * @param {boolean} unknownOk Whether unknown keys are passed
1031          *     through rather than being converted to <lt>keyname>.
1032          *     @default false
1033          * @returns {string} Canonical form.
1034          */
1035         canonicalKeys: function canonicalKeys(keys, unknownOk) {
1036             if (arguments.length === 1)
1037                 unknownOk = true;
1038             return this.parse(keys, unknownOk).map(this.closure.stringify).join("");
1039         },
1040
1041         iterKeys: function iterKeys(keys) iter(function () {
1042             let match, re = /<.*?>?>|[^<]/g;
1043             while (match = re.exec(keys))
1044                 yield match[0];
1045         }()),
1046
1047         /**
1048          * Converts an event string into an array of pseudo-event objects.
1049          *
1050          * These objects can be used as arguments to {@link #stringify} or
1051          * {@link DOM.Event}, though they are unlikely to be much use for other
1052          * purposes. They have many of the properties you'd expect to find on a
1053          * real event, but none of the methods.
1054          *
1055          * Also may contain two "special" parameters, .dactylString and
1056          * .dactylShift these are set for characters that can never by
1057          * typed, but may appear in mappings, for example <Nop> is passed as
1058          * dactylString, and dactylShift is set when a user specifies
1059          * <S-@> where @ is a non-case-changeable, non-space character.
1060          *
1061          * @param {string} keys The string to parse.
1062          * @param {boolean} unknownOk Whether unknown keys are passed
1063          *     through rather than being converted to <lt>keyname>.
1064          *     @default false
1065          * @returns {Array[Object]}
1066          */
1067         parse: function parse(input, unknownOk) {
1068             if (isArray(input))
1069                 return array.flatten(input.map(function (k) this.parse(k, unknownOk), this));
1070
1071             if (arguments.length === 1)
1072                 unknownOk = true;
1073
1074             let out = [];
1075             for (let match in util.regexp.iterate(/<.*?>?>|[^<]|<(?!.*>)/g, input)) {
1076                 let evt_str = match[0];
1077
1078                 let evt_obj = { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false,
1079                                 keyCode: 0, charCode: 0, type: "keypress" };
1080
1081                 if (evt_str.length == 1) {
1082                     evt_obj.charCode = evt_str.charCodeAt(0);
1083                     evt_obj._keyCode = this.key_code[evt_str[0].toLowerCase()];
1084                     evt_obj.shiftKey = evt_str !== evt_str.toLowerCase();
1085                 }
1086                 else {
1087                     let [match, modifier, keyname] = evt_str.match(/^<((?:[*12CASM⌘]-)*)(.+?)>$/i) || [false, '', ''];
1088                     modifier = Set(modifier.toUpperCase());
1089                     keyname = keyname.toLowerCase();
1090                     evt_obj.dactylKeyname = keyname;
1091                     if (/^u[0-9a-f]+$/.test(keyname))
1092                         keyname = String.fromCharCode(parseInt(keyname.substr(1), 16));
1093
1094                     if (keyname && (unknownOk || keyname.length == 1 || /mouse$/.test(keyname) ||
1095                                     this.key_code[keyname] || Set.has(this.pseudoKeys, keyname))) {
1096                         evt_obj.globKey  ="*" in modifier;
1097                         evt_obj.ctrlKey  ="C" in modifier;
1098                         evt_obj.altKey   ="A" in modifier;
1099                         evt_obj.shiftKey ="S" in modifier;
1100                         evt_obj.metaKey  ="M" in modifier || "⌘" in modifier;
1101                         evt_obj.dactylShift = evt_obj.shiftKey;
1102
1103                         if (keyname.length == 1) { // normal characters
1104                             if (evt_obj.shiftKey)
1105                                 keyname = keyname.toUpperCase();
1106
1107                             evt_obj.dactylShift = evt_obj.shiftKey && keyname.toUpperCase() == keyname.toLowerCase();
1108                             evt_obj.charCode = keyname.charCodeAt(0);
1109                             evt_obj.keyCode = this.key_code[keyname.toLowerCase()];
1110                         }
1111                         else if (Set.has(this.pseudoKeys, keyname)) {
1112                             evt_obj.dactylString = "<" + this.key_key[keyname] + ">";
1113                         }
1114                         else if (/mouse$/.test(keyname)) { // mouse events
1115                             evt_obj.type = (/2-/.test(modifier) ? "dblclick" : "click");
1116                             evt_obj.button = ["leftmouse", "middlemouse", "rightmouse"].indexOf(keyname);
1117                             delete evt_obj.keyCode;
1118                             delete evt_obj.charCode;
1119                         }
1120                         else { // spaces, control characters, and <
1121                             evt_obj.keyCode = this.key_code[keyname];
1122                             evt_obj.charCode = 0;
1123                         }
1124                     }
1125                     else { // an invalid sequence starting with <, treat as a literal
1126                         out = out.concat(this.parse("<lt>" + evt_str.substr(1)));
1127                         continue;
1128                     }
1129                 }
1130
1131                 // TODO: make a list of characters that need keyCode and charCode somewhere
1132                 if (evt_obj.keyCode == 32 || evt_obj.charCode == 32)
1133                     evt_obj.charCode = evt_obj.keyCode = 32; // <Space>
1134                 if (evt_obj.keyCode == 60 || evt_obj.charCode == 60)
1135                     evt_obj.charCode = evt_obj.keyCode = 60; // <lt>
1136
1137                 evt_obj.modifiers = (evt_obj.ctrlKey  && Ci.nsIDOMNSEvent.CONTROL_MASK)
1138                                   | (evt_obj.altKey   && Ci.nsIDOMNSEvent.ALT_MASK)
1139                                   | (evt_obj.shiftKey && Ci.nsIDOMNSEvent.SHIFT_MASK)
1140                                   | (evt_obj.metaKey  && Ci.nsIDOMNSEvent.META_MASK);
1141
1142                 out.push(evt_obj);
1143             }
1144             return out;
1145         },
1146
1147         /**
1148          * Converts the specified event to a string in dactyl key-code
1149          * notation. Returns null for an unknown event.
1150          *
1151          * @param {Event} event
1152          * @returns {string}
1153          */
1154         stringify: function stringify(event) {
1155             if (isArray(event))
1156                 return event.map(function (e) this.stringify(e), this).join("");
1157
1158             if (event.dactylString)
1159                 return event.dactylString;
1160
1161             let key = null;
1162             let modifier = "";
1163
1164             if (event.globKey)
1165                 modifier += "*-";
1166             if (event.ctrlKey)
1167                 modifier += "C-";
1168             if (event.altKey)
1169                 modifier += "A-";
1170             if (event.metaKey)
1171                 modifier += "M-";
1172
1173             if (/^key/.test(event.type)) {
1174                 let charCode = event.type == "keyup" ? 0 : event.charCode; // Why? --Kris
1175                 if (charCode == 0) {
1176                     if (event.keyCode in this.code_key) {
1177                         key = this.code_key[event.keyCode];
1178
1179                         if (event.shiftKey && (key.length > 1 || event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1180                             modifier += "S-";
1181                         else if (!modifier && key.length === 1)
1182                             if (event.shiftKey)
1183                                 key = key.toUpperCase();
1184                             else
1185                                 key = key.toLowerCase();
1186
1187                         if (!modifier && key.length == 1)
1188                             return key;
1189                     }
1190                 }
1191                 // [Ctrl-Bug] special handling of mysterious <C-[>, <C-\\>, <C-]>, <C-^>, <C-_> bugs (OS/X)
1192                 //            (i.e., cntrl codes 27--31)
1193                 // ---
1194                 // For more information, see:
1195                 //     [*] Referenced mailing list msg: http://www.mozdev.org/pipermail/pentadactyl/2008-May/001548.html
1196                 //     [*] Mozilla bug 416227: event.charCode in keypress handler has unexpected values on Mac for Ctrl with chars in "[ ] _ \"
1197                 //         https://bugzilla.mozilla.org/show_bug.cgi?id=416227
1198                 //     [*] Mozilla bug 432951: Ctrl+'foo' doesn't seem same charCode as Meta+'foo' on Cocoa
1199                 //         https://bugzilla.mozilla.org/show_bug.cgi?id=432951
1200                 // ---
1201                 //
1202                 // The following fixes are only activated if config.OS.isMacOSX.
1203                 // Technically, they prevent mappings from <C-Esc> (and
1204                 // <C-C-]> if your fancy keyboard permits such things<?>), but
1205                 // these <C-control> mappings are probably pathological (<C-Esc>
1206                 // certainly is on Windows), and so it is probably
1207                 // harmless to remove the config.OS.isMacOSX if desired.
1208                 //
1209                 else if (config.OS.isMacOSX && event.ctrlKey && charCode >= 27 && charCode <= 31) {
1210                     if (charCode == 27) { // [Ctrl-Bug 1/5] the <C-[> bug
1211                         key = "Esc";
1212                         modifier = modifier.replace("C-", "");
1213                     }
1214                     else // [Ctrl-Bug 2,3,4,5/5] the <C-\\>, <C-]>, <C-^>, <C-_> bugs
1215                         key = String.fromCharCode(charCode + 64);
1216                 }
1217                 // a normal key like a, b, c, 0, etc.
1218                 else if (charCode) {
1219                     key = String.fromCharCode(charCode);
1220
1221                     if (!/^[^<\s]$/i.test(key) && key in this.key_code) {
1222                         // a named charCode key (<Space> and <lt>) space can be shifted, <lt> must be forced
1223                         if ((key.match(/^\s$/) && event.shiftKey) || event.dactylShift)
1224                             modifier += "S-";
1225
1226                         key = this.code_key[this.key_code[key]];
1227                     }
1228                     else {
1229                         // a shift modifier is only allowed if the key is alphabetical and used in a C-A-M- mapping in the uppercase,
1230                         // or if the shift has been forced for a non-alphabetical character by the user while :map-ping
1231                         if (key !== key.toLowerCase() && (event.ctrlKey || event.altKey || event.metaKey) || event.dactylShift)
1232                             modifier += "S-";
1233                         if (/^\s$/.test(key))
1234                             key = let (s = charCode.toString(16)) "U" + "0000".substr(4 - s.length) + s;
1235                         else if (modifier.length == 0)
1236                             return key;
1237                     }
1238                 }
1239                 if (key == null) {
1240                     if (event.shiftKey)
1241                         modifier += "S-";
1242                     key = this.key_key[event.dactylKeyname] || event.dactylKeyname;
1243                 }
1244                 if (key == null)
1245                     return null;
1246             }
1247             else if (event.type == "click" || event.type == "dblclick") {
1248                 if (event.shiftKey)
1249                     modifier += "S-";
1250                 if (event.type == "dblclick")
1251                     modifier += "2-";
1252                 // TODO: triple and quadruple click
1253
1254                 switch (event.button) {
1255                 case 0:
1256                     key = "LeftMouse";
1257                     break;
1258                 case 1:
1259                     key = "MiddleMouse";
1260                     break;
1261                 case 2:
1262                     key = "RightMouse";
1263                     break;
1264                 }
1265             }
1266
1267             if (key == null)
1268                 return null;
1269
1270             return "<" + modifier + key + ">";
1271         },
1272
1273
1274         defaults: {
1275             load:   { bubbles: false },
1276             submit: { cancelable: true }
1277         },
1278
1279         types: Class.Memoize(function () iter(
1280             {
1281                 Mouse: "click mousedown mouseout mouseover mouseup dblclick " +
1282                        "hover " +
1283                        "popupshowing popupshown popuphiding popuphidden " +
1284                        "contextmenu",
1285                 Key:   "keydown keypress keyup",
1286                 "":    "change command dactyl-input input submit " +
1287                        "load unload pageshow pagehide DOMContentLoaded " +
1288                        "resize scroll"
1289             }
1290         ).map(function ([k, v]) v.split(" ").map(function (v) [v, k]))
1291          .flatten()
1292          .toObject()),
1293
1294         /**
1295          * Dispatches an event to an element as if it were a native event.
1296          *
1297          * @param {Node} target The DOM node to which to dispatch the event.
1298          * @param {Event} event The event to dispatch.
1299          */
1300         dispatch: Class.Memoize(function ()
1301             config.haveGecko("2b")
1302                 ? function dispatch(target, event, extra) {
1303                     try {
1304                         this.feedingEvent = extra;
1305
1306                         if (target instanceof Ci.nsIDOMElement)
1307                             // This causes a crash on Gecko<2.0, it seems.
1308                             return (target.ownerDocument || target.document || target).defaultView
1309                                    .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
1310                                    .dispatchDOMEventViaPresShell(target, event, true);
1311                         else {
1312                             target.dispatchEvent(event);
1313                             return !event.getPreventDefault();
1314                         }
1315                     }
1316                     catch (e) {
1317                         util.reportError(e);
1318                     }
1319                     finally {
1320                         this.feedingEvent = null;
1321                     }
1322                 }
1323                 : function dispatch(target, event, extra) {
1324                     try {
1325                         this.feedingEvent = extra;
1326                         target.dispatchEvent(update(event, extra));
1327                     }
1328                     finally {
1329                         this.feedingEvent = null;
1330                     }
1331                 })
1332     }),
1333
1334     createContents: Class.Memoize(function () services.has("dactyl") && services.dactyl.createContents
1335         || function (elem) {}),
1336
1337     isScrollable: Class.Memoize(function () services.has("dactyl") && services.dactyl.getScrollable
1338         ? function (elem, dir) services.dactyl.getScrollable(elem) & (dir ? services.dactyl["DIRECTION_" + dir.toUpperCase()] : ~0)
1339         : function (elem, dir) true),
1340
1341     /**
1342      * The set of input element type attribute values that mark the element as
1343      * an editable field.
1344      */
1345     editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
1346                          "month", "number", "password", "range", "search",
1347                          "tel", "text", "time", "url", "week"]),
1348
1349     /**
1350      * Converts a given DOM Node, Range, or Selection to a string. If
1351      * *html* is true, the output is HTML, otherwise it is presentation
1352      * text.
1353      *
1354      * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
1355      *      stringify.
1356      * @param {boolean} html Whether the output should be HTML rather
1357      *      than presentation text.
1358      */
1359     stringify: function stringify(node, html) {
1360         if (node instanceof Ci.nsISelection && node.isCollapsed)
1361             return "";
1362
1363         if (node instanceof Ci.nsIDOMNode) {
1364             let range = node.ownerDocument.createRange();
1365             range.selectNode(node);
1366             node = range;
1367         }
1368         let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
1369         doc = doc.ownerDocument || doc;
1370
1371         let encoder = services.HtmlEncoder();
1372         encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
1373         if (node instanceof Ci.nsISelection)
1374             encoder.setSelection(node);
1375         else if (node instanceof Ci.nsIDOMRange)
1376             encoder.setRange(node);
1377
1378         let str = services.String(encoder.encodeToString());
1379         if (html)
1380             return str.data;
1381
1382         let [result, length] = [{}, {}];
1383         services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
1384         return result.value.QueryInterface(Ci.nsISupportsString).data;
1385     },
1386
1387     /**
1388      * Compiles a CSS spec and XPath pattern matcher based on the given
1389      * list. List elements prefixed with "xpath:" are parsed as XPath
1390      * patterns, while other elements are parsed as CSS specs. The
1391      * returned function will, given a node, return an iterator of all
1392      * descendants of that node which match the given specs.
1393      *
1394      * @param {[string]} list The list of patterns to match.
1395      * @returns {function(Node)}
1396      */
1397     compileMatcher: function compileMatcher(list) {
1398         let xpath = [], css = [];
1399         for (let elem in values(list))
1400             if (/^xpath:/.test(elem))
1401                 xpath.push(elem.substr(6));
1402             else
1403                 css.push(elem);
1404
1405         return update(
1406             function matcher(node) {
1407                 if (matcher.xpath)
1408                     for (let elem in DOM.XPath(matcher.xpath, node))
1409                         yield elem;
1410
1411                 if (matcher.css)
1412                     for (let [, elem] in iter(util.withProperErrors("querySelectorAll", node, matcher.css)))
1413                         yield elem;
1414             }, {
1415                 css: css.join(", "),
1416                 xpath: xpath.join(" | ")
1417             });
1418     },
1419
1420     /**
1421      * Validates a list as input for {@link #compileMatcher}. Returns
1422      * true if and only if every element of the list is a valid XPath or
1423      * CSS selector.
1424      *
1425      * @param {[string]} list The list of patterns to test
1426      * @returns {boolean} True when the patterns are all valid.
1427      */
1428     validateMatcher: function validateMatcher(list) {
1429         return this.testValues(list, DOM.closure.testMatcher);
1430     },
1431
1432     testMatcher: function testMatcher(value) {
1433         let evaluator = services.XPathEvaluator();
1434         let node = services.XMLDocument();
1435         if (/^xpath:/.test(value))
1436             util.withProperErrors("createExpression", evaluator, value.substr(6), DOM.XPath.resolver);
1437         else
1438             util.withProperErrors("querySelector", node, value);
1439         return true;
1440     },
1441
1442     /**
1443      * Converts HTML special characters in *str* to the equivalent HTML
1444      * entities.
1445      *
1446      * @param {string} str
1447      * @returns {string}
1448      */
1449     escapeHTML: function escapeHTML(str) {
1450         let map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" };
1451         return str.replace(/['"&<>]/g, function (m) map[m]);
1452     },
1453
1454     /**
1455      * Converts an E4X XML literal to a DOM node. Any attribute named
1456      * highlight is present, it is transformed into dactyl:highlight,
1457      * and the named highlight groups are guaranteed to be loaded.
1458      *
1459      * @param {Node} node
1460      * @param {Document} doc
1461      * @param {Object} nodes If present, nodes with the "key" attribute are
1462      *     stored here, keyed to the value thereof.
1463      * @returns {Node}
1464      */
1465     fromXML: function fromXML(node, doc, nodes) {
1466         XML.ignoreWhitespace = XML.prettyPrinting = false;
1467         if (typeof node === "string") // Sandboxes can't currently pass us XML objects.
1468             node = XML(node);
1469
1470         if (node.length() != 1) {
1471             let domnode = doc.createDocumentFragment();
1472             for each (let child in node)
1473                 domnode.appendChild(fromXML(child, doc, nodes));
1474             return domnode;
1475         }
1476
1477         switch (node.nodeKind()) {
1478         case "text":
1479             return doc.createTextNode(String(node));
1480         case "element":
1481             let domnode = doc.createElementNS(node.namespace(), node.localName());
1482
1483             for each (let attr in node.@*::*)
1484                 if (attr.name() != "highlight")
1485                     domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr));
1486
1487             for each (let child in node.*::*)
1488                 domnode.appendChild(fromXML(child, doc, nodes));
1489             if (nodes && node.@key)
1490                 nodes[node.@key] = domnode;
1491
1492             if ("@highlight" in node)
1493                 highlight.highlightNode(domnode, String(node.@highlight), nodes || true);
1494             return domnode;
1495         default:
1496             return null;
1497         }
1498     },
1499
1500     /**
1501      * Evaluates an XPath expression in the current or provided
1502      * document. It provides the xhtml, xhtml2 and dactyl XML
1503      * namespaces. The result may be used as an iterator.
1504      *
1505      * @param {string} expression The XPath expression to evaluate.
1506      * @param {Node} elem The context element.
1507      * @param {boolean} asIterator Whether to return the results as an
1508      *     XPath iterator.
1509      * @param {object} namespaces Additional namespaces to recognize.
1510      *     @optional
1511      * @returns {Object} Iterable result of the evaluation.
1512      */
1513     XPath: update(
1514         function XPath(expression, elem, asIterator, namespaces) {
1515             try {
1516                 let doc = elem.ownerDocument || elem;
1517
1518                 if (isArray(expression))
1519                     expression = DOM.makeXPath(expression);
1520
1521                 let resolver = XPath.resolver;
1522                 if (namespaces) {
1523                     namespaces = update({}, DOM.namespaces, namespaces);
1524                     resolver = function (prefix) namespaces[prefix] || null;
1525                 }
1526
1527                 let result = doc.evaluate(expression, elem,
1528                     resolver,
1529                     asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
1530                     null
1531                 );
1532
1533                 return Object.create(result, {
1534                     __iterator__: {
1535                         value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
1536                                           : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
1537                     }
1538                 });
1539             }
1540             catch (e) {
1541                 throw e.stack ? e : Error(e);
1542             }
1543         },
1544         {
1545             resolver: function lookupNamespaceURI(prefix) (DOM.namespaces[prefix] || null)
1546         }),
1547
1548     /**
1549      * Returns an XPath union expression constructed from the specified node
1550      * tests. An expression is built with node tests for both the null and
1551      * XHTML namespaces. See {@link DOM.XPath}.
1552      *
1553      * @param nodes {Array(string)}
1554      * @returns {string}
1555      */
1556     makeXPath: function makeXPath(nodes) {
1557         return array(nodes).map(util.debrace).flatten()
1558                            .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
1559                            .map(function (node) "//" + node).join(" | ");
1560     },
1561
1562     namespaces: {
1563         xul: XUL.uri,
1564         xhtml: XHTML.uri,
1565         html: XHTML.uri,
1566         xhtml2: "http://www.w3.org/2002/06/xhtml2",
1567         dactyl: NS.uri
1568     },
1569
1570     namespaceNames: Class.Memoize(function ()
1571         iter(this.namespaces).map(function ([k, v]) [v, k]).toObject()),
1572 });
1573
1574 Object.keys(DOM.Event.types).forEach(function (event) {
1575     let name = event.replace(/-(.)/g, function (m, m1) m1.toUpperCase());
1576     if (!Set.has(DOM.prototype, name))
1577         DOM.prototype[name] =
1578             function _event(arg, extra) {
1579                 return this[callable(arg) ? "listen" : "dispatch"](event, arg, extra);
1580             };
1581 });
1582
1583 var $ = DOM;
1584
1585 endModule();
1586
1587 // catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1588
1589 // vim: set sw=4 ts=4 et ft=javascript: