]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/util.jsm
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / modules / util.jsm
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 try {
10
11 Components.utils.import("resource://dactyl/bootstrap.jsm");
12     let frag=1;
13 defineModule("util", {
14     exports: ["frag", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
15     require: ["services"],
16     use: ["commands", "config", "highlight", "messages", "storage", "template"]
17 }, this);
18
19 var XBL = Namespace("xbl", "http://www.mozilla.org/xbl");
20 var XHTML = Namespace("html", "http://www.w3.org/1999/xhtml");
21 var XUL = Namespace("xul", "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
22 var NS = Namespace("dactyl", "http://vimperator.org/namespaces/liberator");
23 default xml namespace = XHTML;
24
25 var FailedAssertion = Class("FailedAssertion", ErrorBase, {
26     init: function init(message, level, noTrace) {
27         if (noTrace !== undefined)
28             this.noTrace = noTrace;
29         init.supercall(this, message, level);
30     },
31
32     level: 3,
33
34     noTrace: true
35 });
36
37 var Point = Struct("x", "y");
38
39 var wrapCallback = function wrapCallback(fn) {
40     fn.wrapper = function wrappedCallback () {
41         try {
42             return fn.apply(this, arguments);
43         }
44         catch (e) {
45             util.reportError(e);
46             return undefined;
47         }
48     };
49     fn.wrapper.wrapped = fn;
50     return fn.wrapper;
51 }
52
53 var getAttr = function getAttr(elem, ns, name)
54     elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null;
55 var setAttr = function setAttr(elem, ns, name, val) {
56     if (val == null)
57         elem.removeAttributeNS(ns, name);
58     else
59         elem.setAttributeNS(ns, name, val);
60 }
61
62 var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
63     init: function () {
64         this.Array = array;
65
66         this.addObserver(this);
67         this.overlays = {};
68     },
69
70     cleanup: function cleanup() {
71         for (let { document: doc } in iter(services.windowMediator.getEnumerator(null))) {
72             for (let elem in values(doc.dactylOverlayElements || []))
73                 if (elem.parentNode)
74                     elem.parentNode.removeChild(elem);
75
76             for (let [elem, ns, name, orig, value] in values(doc.dactylOverlayAttributes || []))
77                 if (getAttr(elem, ns, name) === value)
78                     setAttr(elem, ns, name, orig);
79
80             delete doc.dactylOverlayElements;
81             delete doc.dactylOverlayAttributes;
82             delete doc.dactylOverlays;
83         }
84     },
85
86     // FIXME: Only works for Pentadactyl
87     get activeWindow() services.windowMediator.getMostRecentWindow("navigator:browser"),
88     dactyl: update(function dactyl(obj) {
89         if (obj)
90             var global = Class.objectGlobal(obj);
91         return {
92             __noSuchMethod__: function (meth, args) {
93                 let win = util.activeWindow;
94                 var dactyl = global && global.dactyl || win && win.dactyl;
95                 if (!dactyl)
96                     return null;
97
98                 let prop = dactyl[meth];
99                 if (callable(prop))
100                     return prop.apply(dactyl, args);
101                 return prop;
102             }
103         };
104     }, {
105         __noSuchMethod__: function () this().__noSuchMethod__.apply(null, arguments)
106     }),
107
108     /**
109      * Registers a obj as a new observer with the observer service. obj.observe
110      * must be an object where each key is the name of a target to observe and
111      * each value is a function(subject, data) to be called when the given
112      * target is broadcast. obj.observe will be replaced with a new opaque
113      * function. The observer is automatically unregistered on application
114      * shutdown.
115      *
116      * @param {object} obj
117      */
118     addObserver: update(function addObserver(obj) {
119         if (!obj.observers)
120             obj.observers = obj.observe;
121
122         function register(meth) {
123             for (let target in Set(["dactyl-cleanup-modules", "quit-application"].concat(Object.keys(obj.observers))))
124                 try {
125                     services.observer[meth](obj, target, true);
126                 }
127                 catch (e) {}
128         }
129
130         Class.replaceProperty(obj, "observe",
131             function (subject, target, data) {
132                 try {
133                     if (target == "quit-application" || target == "dactyl-cleanup-modules")
134                         register("removeObserver");
135                     if (obj.observers[target])
136                         obj.observers[target].call(obj, subject, data);
137                 }
138                 catch (e) {
139                     if (typeof util === "undefined")
140                         addObserver.dump("dactyl: error: " + e + "\n" + (e.stack || addObserver.Error().stack).replace(/^/gm, "dactyl:    "));
141                     else
142                         util.reportError(e);
143                 }
144             });
145
146         obj.observe.unregister = function () register("removeObserver");
147         register("addObserver");
148     }, { dump: dump, Error: Error }),
149
150     /*
151      * Tests a condition and throws a FailedAssertion error on
152      * failure.
153      *
154      * @param {boolean} condition The condition to test.
155      * @param {string} message The message to present to the
156      *     user on failure.
157      */
158     assert: function (condition, message, quiet) {
159         if (!condition)
160             throw FailedAssertion(message, 1, quiet === undefined ? true : quiet);
161         return condition;
162     },
163
164     /**
165      * Capitalizes the first character of the given string.
166      * @param {string} str The string to capitalize
167      * @returns {string}
168      */
169     capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1).toLowerCase(),
170
171     /**
172      * Returns a RegExp object that matches characters specified in the range
173      * expression *list*, or signals an appropriate error if *list* is invalid.
174      *
175      * @param {string} list Character list, e.g., "a b d-xA-Z" produces /[abd-xA-Z]/.
176      * @param {string} accepted Character range(s) to accept, e.g. "a-zA-Z" for
177      *     ASCII letters. Used to validate *list*.
178      * @returns {RegExp}
179      */
180     charListToRegexp: function charListToRegexp(list, accepted) {
181         list = list.replace(/\s+/g, "");
182
183         // check for chars not in the accepted range
184         this.assert(RegExp("^[" + accepted + "-]+$").test(list),
185                     _("error.charactersOutsideRange", accepted.quote()));
186
187         // check for illegal ranges
188         for (let [match] in this.regexp.iterate(/.-./g, list))
189             this.assert(match.charCodeAt(0) <= match.charCodeAt(2),
190                         _("error.invalidCharacterRange", list.slice(list.indexOf(match))));
191
192         return RegExp("[" + util.regexp.escape(list) + "]");
193     },
194
195     get chromePackages() {
196         // Horrible hack.
197         let res = {};
198         function process(manifest) {
199             for each (let line in manifest.split(/\n+/)) {
200                 let match = /^\s*(content|skin|locale|resource)\s+([^\s#]+)\s/.exec(line);
201                 if (match)
202                     res[match[2]] = true;
203             }
204         }
205         function processJar(file) {
206             let jar = services.ZipReader(file);
207             if (jar) {
208                 if (jar.hasEntry("chrome.manifest"))
209                     process(File.readStream(jar.getInputStream("chrome.manifest")));
210                 jar.close();
211             }
212         }
213
214         for each (let dir in ["UChrm", "AChrom"]) {
215             dir = File(services.directory.get(dir, Ci.nsIFile));
216             if (dir.exists() && dir.isDirectory())
217                 for (let file in dir.iterDirectory())
218                     if (/\.manifest$/.test(file.leafName))
219                         process(file.read());
220
221             dir = File(dir.parent);
222             if (dir.exists() && dir.isDirectory())
223                 for (let file in dir.iterDirectory())
224                     if (/\.jar$/.test(file.leafName))
225                         processJar(file);
226
227             dir = dir.child("extensions");
228             if (dir.exists() && dir.isDirectory())
229                 for (let ext in dir.iterDirectory()) {
230                     if (/\.xpi$/.test(ext.leafName))
231                         processJar(ext);
232                     else {
233                         if (ext.isFile())
234                             ext = File(ext.read().replace(/\n*$/, ""));
235                         let mf = ext.child("chrome.manifest");
236                         if (mf.exists())
237                             process(mf.read());
238                     }
239                 }
240         }
241         return Object.keys(res).sort();
242     },
243
244     /**
245      * Returns a shallow copy of *obj*.
246      *
247      * @param {Object} obj
248      * @returns {Object}
249      */
250     cloneObject: function cloneObject(obj) {
251         if (isArray(obj))
252             return obj.slice();
253         let newObj = {};
254         for (let [k, v] in Iterator(obj))
255             newObj[k] = v;
256         return newObj;
257     },
258
259     /**
260      * Clips a string to a given length. If the input string is longer
261      * than *length*, an ellipsis is appended.
262      *
263      * @param {string} str The string to truncate.
264      * @param {number} length The length of the returned string.
265      * @returns {string}
266      */
267     clip: function clip(str, length) {
268         return str.length <= length ? str : str.substr(0, length - 3) + "...";
269     },
270
271     /**
272      * Compares two strings, case insensitively. Return values are as
273      * in String#localeCompare.
274      *
275      * @param {string} a
276      * @param {string} b
277      * @returns {number}
278      */
279     compareIgnoreCase: function compareIgnoreCase(a, b) String.localeCompare(a.toLowerCase(), b.toLowerCase()),
280
281     compileFormat: function compileFormat(format) {
282         let stack = [frame()];
283         stack.__defineGetter__("top", function () this[this.length - 1]);
284
285         function frame() update(
286             function _frame(obj)
287                 _frame === stack.top || _frame.valid(obj) ?
288                     _frame.elements.map(function (e) callable(e) ? e(obj) : e).join("") : "",
289             {
290                 elements: [],
291                 seen: {},
292                 valid: function (obj) this.elements.every(function (e) !e.test || e.test(obj))
293             });
294
295         let end = 0;
296         for (let match in util.regexp.iterate(/(.*?)%(.)/gy, format)) {
297
298             let [, prefix, char] = match;
299             end += match[0].length;
300
301             if (prefix)
302                 stack.top.elements.push(prefix);
303             if (char === "%")
304                 stack.top.elements.push("%");
305             else if (char === "[") {
306                 let f = frame();
307                 stack.top.elements.push(f);
308                 stack.push(f);
309             }
310             else if (char === "]") {
311                 stack.pop();
312                 util.assert(stack.length, /*L*/"Unmatched %] in format");
313             }
314             else {
315                 let quote = function quote(obj, char) obj[char];
316                 if (char !== char.toLowerCase())
317                     quote = function quote(obj, char) Commands.quote(obj[char]);
318                 char = char.toLowerCase();
319
320                 stack.top.elements.push(update(
321                     function (obj) obj[char] != null ? quote(obj, char) : "",
322                     { test: function (obj) obj[char] != null }));
323
324                 for (let elem in array.iterValues(stack))
325                     elem.seen[char] = true;
326             }
327         }
328         if (end < format.length)
329             stack.top.elements.push(format.substr(end));
330
331         util.assert(stack.length === 1, /*L*/"Unmatched %[ in format");
332         return stack.top;
333     },
334
335     /**
336      * Compiles a macro string into a function which generates a string
337      * result based on the input *macro* and its parameters. The
338      * definitive documentation for macro strings resides in :help
339      * macro-string.
340      *
341      * Macro parameters may have any of the following flags:
342      *     e: The parameter is only tested for existence. Its
343      *        interpolation is always empty.
344      *     q: The result is quoted such that it is parsed as a single
345      *        argument by the Ex argument parser.
346      *
347      * The returned function has the following additional properties:
348      *
349      *     seen {set}: The set of parameters used in this macro.
350      *
351      *     valid {function(object)}: Returns true if every parameter of
352      *          this macro is provided by the passed object.
353      *
354      * @param {string} macro The macro string to compile.
355      * @param {boolean} keepUnknown If true, unknown macro parameters
356      *      are left untouched. Otherwise, they are replaced with the null
357      *      string.
358      * @returns {function}
359      */
360     compileMacro: function compileMacro(macro, keepUnknown) {
361         let stack = [frame()];
362         stack.__defineGetter__("top", function () this[this.length - 1]);
363
364         let unknown = util.identity;
365         if (!keepUnknown)
366             unknown = function () "";
367
368         function frame() update(
369             function _frame(obj)
370                 _frame === stack.top || _frame.valid(obj) ?
371                     _frame.elements.map(function (e) callable(e) ? e(obj) : e).join("") : "",
372             {
373                 elements: [],
374                 seen: {},
375                 valid: function (obj) this.elements.every(function (e) !e.test || e.test(obj))
376             });
377
378         let defaults = { lt: "<", gt: ">" };
379
380         let re = util.regexp(<![CDATA[
381             ([^]*?) // 1
382             (?:
383                 (<\{) | // 2
384                 (< ((?:[a-z]-)?[a-z-]+?) (?:\[([0-9]+)\])? >) | // 3 4 5
385                 (\}>) // 6
386             )
387         ]]>, "gixy");
388         macro = String(macro);
389         let end = 0;
390         for (let match in re.iterate(macro)) {
391             let [, prefix, open, full, macro, idx, close] = match;
392             end += match[0].length;
393
394             if (prefix)
395                 stack.top.elements.push(prefix);
396             if (open) {
397                 let f = frame();
398                 stack.top.elements.push(f);
399                 stack.push(f);
400             }
401             else if (close) {
402                 stack.pop();
403                 util.assert(stack.length, /*L*/"Unmatched %] in macro");
404             }
405             else {
406                 let [, flags, name] = /^((?:[a-z]-)*)(.*)/.exec(macro);
407                 flags = Set(flags);
408
409                 let quote = util.identity;
410                 if (flags.q)
411                     quote = function quote(obj) typeof obj === "number" ? obj : String.quote(obj);
412                 if (flags.e)
413                     quote = function quote(obj) "";
414
415                 if (Set.has(defaults, name))
416                     stack.top.elements.push(quote(defaults[name]));
417                 else {
418                     if (idx) {
419                         idx = Number(idx) - 1;
420                         stack.top.elements.push(update(
421                             function (obj) obj[name] != null && idx in obj[name] ? quote(obj[name][idx]) : Set.has(obj, name) ? "" : unknown(full),
422                             { test: function (obj) obj[name] != null && idx in obj[name] && obj[name][idx] !== false && (!flags.e || obj[name][idx] != "") }));
423                     }
424                     else {
425                         stack.top.elements.push(update(
426                             function (obj) obj[name] != null ? quote(obj[name]) : Set.has(obj, name) ? "" : unknown(full),
427                             { test: function (obj) obj[name] != null && obj[name] !== false && (!flags.e || obj[name] != "") }));
428                     }
429
430                     for (let elem in array.iterValues(stack))
431                         elem.seen[name] = true;
432                 }
433             }
434         }
435         if (end < macro.length)
436             stack.top.elements.push(macro.substr(end));
437
438         util.assert(stack.length === 1, /*L*/"Unmatched <{ in macro");
439         return stack.top;
440     },
441
442     /**
443      * Compiles a CSS spec and XPath pattern matcher based on the given
444      * list. List elements prefixed with "xpath:" are parsed as XPath
445      * patterns, while other elements are parsed as CSS specs. The
446      * returned function will, given a node, return an iterator of all
447      * descendants of that node which match the given specs.
448      *
449      * @param {[string]} list The list of patterns to match.
450      * @returns {function(Node)}
451      */
452     compileMatcher: function compileMatcher(list) {
453         let xpath = [], css = [];
454         for (let elem in values(list))
455             if (/^xpath:/.test(elem))
456                 xpath.push(elem.substr(6));
457             else
458                 css.push(elem);
459
460         return update(
461             function matcher(node) {
462                 if (matcher.xpath)
463                     for (let elem in util.evaluateXPath(matcher.xpath, node))
464                         yield elem;
465
466                 if (matcher.css)
467                     for (let [, elem] in iter(node.querySelectorAll(matcher.css)))
468                         yield elem;
469             }, {
470                 css: css.join(", "),
471                 xpath: xpath.join(" | ")
472             });
473     },
474
475     /**
476      * Validates a list as input for {@link #compileMatcher}. Returns
477      * true if and only if every element of the list is a valid XPath or
478      * CSS selector.
479      *
480      * @param {[string]} list The list of patterns to test
481      * @returns {boolean} True when the patterns are all valid.
482      */
483     validateMatcher: function validateMatcher(list) {
484         let evaluator = services.XPathEvaluator();
485         let node = services.XMLDocument();
486         return this.testValues(list, function (value) {
487             if (/^xpath:/.test(value))
488                 evaluator.createExpression(value.substr(6), util.evaluateXPath.resolver);
489             else
490                 node.querySelector(value);
491             return true;
492         });
493     },
494
495     /**
496      * Returns an object representing a Node's computed CSS style.
497      *
498      * @param {Node} node
499      * @returns {Object}
500      */
501     computedStyle: function computedStyle(node) {
502         while (!(node instanceof Ci.nsIDOMElement) && node.parentNode)
503             node = node.parentNode;
504         try {
505             var res = node.ownerDocument.defaultView.getComputedStyle(node, null);
506         }
507         catch (e) {}
508         if (res == null) {
509             util.dumpStack(_("error.nullComputedStyle", node));
510             Cu.reportError(Error(_("error.nullComputedStyle", node)));
511             return {};
512         }
513         return res;
514     },
515
516     /**
517      * Converts any arbitrary string into an URI object. Returns null on
518      * failure.
519      *
520      * @param {string} str
521      * @returns {nsIURI|null}
522      */
523     createURI: function createURI(str) {
524         try {
525             return services.urifixup.createFixupURI(str, services.urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
526         }
527         catch (e) {
528             return null;
529         }
530     },
531
532     /**
533      * Expands brace globbing patterns in a string.
534      *
535      * Example:
536      *     "a{b,c}d" => ["abd", "acd"]
537      *
538      * @param {string|[string|Array]} pattern The pattern to deglob.
539      * @returns [string] The resulting strings.
540      */
541     debrace: function debrace(pattern) {
542         if (isArray(pattern)) {
543             let res = [];
544             let rec = function rec(acc) {
545                 let vals;
546
547                 while (isString(vals = pattern[acc.length]))
548                     acc.push(vals);
549
550                 if (acc.length == pattern.length)
551                     res.push(acc.join(""))
552                 else
553                     for (let val in values(vals))
554                         rec(acc.concat(val));
555             }
556             rec([]);
557             return res;
558         }
559
560         if (pattern.indexOf("{") == -1)
561             return [pattern];
562
563         function split(pattern, re, fn, dequote) {
564             let end = 0, match, res = [];
565             while (match = re.exec(pattern)) {
566                 end = match.index + match[0].length;
567                 res.push(match[1]);
568                 if (fn)
569                     fn(match);
570             }
571             res.push(pattern.substr(end));
572             return res.map(function (s) util.dequote(s, dequote));
573         }
574         let patterns = [];
575         let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy,
576             function (match) {
577                 patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy,
578                     null, ",{}"));
579             }, "{}");
580
581         let res = [];
582         function rec(acc) {
583             if (acc.length == patterns.length)
584                 res.push(array(substrings).zip(acc).flatten().join(""));
585             else
586                 for (let [, pattern] in Iterator(patterns[acc.length]))
587                     rec(acc.concat(pattern));
588         }
589         rec([]);
590         return res;
591     },
592
593     /**
594      * Removes certain backslash-quoted characters while leaving other
595      * backslash-quoting sequences untouched.
596      *
597      * @param {string} pattern The string to unquote.
598      * @param {string} chars The characters to unquote.
599      * @returns {string}
600      */
601     dequote: function dequote(pattern, chars)
602         pattern.replace(/\\(.)/, function (m0, m1) chars.indexOf(m1) >= 0 ? m1 : m0),
603
604     /**
605      * Converts a given DOM Node, Range, or Selection to a string. If
606      * *html* is true, the output is HTML, otherwise it is presentation
607      * text.
608      *
609      * @param {nsIDOMNode | nsIDOMRange | nsISelection} node The node to
610      *      stringify.
611      * @param {boolean} html Whether the output should be HTML rather
612      *      than presentation text.
613      */
614     domToString: function (node, html) {
615         if (node instanceof Ci.nsISelection && node.isCollapsed)
616             return "";
617
618         if (node instanceof Ci.nsIDOMNode) {
619             let range = node.ownerDocument.createRange();
620             range.selectNode(node);
621             node = range;
622         }
623         let doc = (node.getRangeAt ? node.getRangeAt(0) : node).startContainer;
624         doc = doc.ownerDocument || doc;
625
626         let encoder = services.HtmlEncoder();
627         encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
628         if (node instanceof Ci.nsISelection)
629             encoder.setSelection(node);
630         else if (node instanceof Ci.nsIDOMRange)
631             encoder.setRange(node);
632
633         let str = services.String(encoder.encodeToString());
634         if (html)
635             return str.data;
636
637         let [result, length] = [{}, {}];
638         services.HtmlConverter().convert("text/html", str, str.data.length*2, "text/unicode", result, length);
639         return result.value.QueryInterface(Ci.nsISupportsString).data;
640     },
641
642     /**
643      * Prints a message to the console. If *msg* is an object it is pretty
644      * printed.
645      *
646      * @param {string|Object} msg The message to print.
647      */
648     dump: defineModule.dump,
649
650     /**
651      * Returns a list of reformatted stack frames from
652      * {@see Error#stack}.
653      *
654      * @param {string} stack The stack trace from an Error.
655      * @returns {[string]} The stack frames.
656      */
657     stackLines: function (stack) {
658         let lines = [];
659         let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g;
660         while (match = re.exec(stack))
661             lines.push(match[1].replace(/\n/g, "\\n").substr(0, 80) + "@" +
662                        util.fixURI(match[2]));
663         return lines;
664     },
665
666     /**
667      * Dumps a stack trace to the console.
668      *
669      * @param {string} msg The trace message.
670      * @param {number} frames The number of frames to print.
671      */
672     dumpStack: function dumpStack(msg, frames) {
673         let stack = util.stackLines(Error().stack);
674         stack = stack.slice(1, 1 + (frames || stack.length)).join("\n").replace(/^/gm, "    ");
675         util.dump((arguments.length == 0 ? "Stack" : msg) + "\n" + stack + "\n");
676     },
677
678     /**
679      * The set of input element type attribute values that mark the element as
680      * an editable field.
681      */
682     editableInputs: Set(["date", "datetime", "datetime-local", "email", "file",
683                          "month", "number", "password", "range", "search",
684                          "tel", "text", "time", "url", "week"]),
685
686     /**
687      * Converts HTML special characters in *str* to the equivalent HTML
688      * entities.
689      *
690      * @param {string} str
691      * @returns {string}
692      */
693     escapeHTML: function escapeHTML(str) {
694         return str.replace(/&/g, "&amp;").replace(/</g, "&lt;");
695     },
696
697     /**
698      * Escapes quotes, newline and tab characters in *str*. The returned string
699      * is delimited by *delimiter* or " if *delimiter* is not specified.
700      * {@see String#quote}.
701      *
702      * @param {string} str
703      * @param {string} delimiter
704      * @returns {string}
705      */
706     escapeString: function escapeString(str, delimiter) {
707         if (delimiter == undefined)
708             delimiter = '"';
709         return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter;
710     },
711
712     /**
713      * Evaluates an XPath expression in the current or provided
714      * document. It provides the xhtml, xhtml2 and dactyl XML
715      * namespaces. The result may be used as an iterator.
716      *
717      * @param {string} expression The XPath expression to evaluate.
718      * @param {Node} elem The context element.
719      * @default The current document.
720      * @param {boolean} asIterator Whether to return the results as an
721      *     XPath iterator.
722      * @returns {Object} Iterable result of the evaluation.
723      */
724     evaluateXPath: update(
725         function evaluateXPath(expression, elem, asIterator) {
726             try {
727                 if (!elem)
728                     elem = util.activeWindow.content.document;
729                 let doc = elem.ownerDocument || elem;
730                 if (isArray(expression))
731                     expression = util.makeXPath(expression);
732
733                 let result = doc.evaluate(expression, elem,
734                     evaluateXPath.resolver,
735                     asIterator ? Ci.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE : Ci.nsIDOMXPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
736                     null
737                 );
738
739                 return Object.create(result, {
740                     __iterator__: {
741                         value: asIterator ? function () { let elem; while ((elem = this.iterateNext())) yield elem; }
742                                           : function () { for (let i = 0; i < this.snapshotLength; i++) yield this.snapshotItem(i); }
743                     }
744                 });
745             }
746             catch (e) {
747                 throw e.stack ? e : Error(e);
748             }
749         },
750         {
751             resolver: function lookupNamespaceURI(prefix) ({
752                     xul: XUL.uri,
753                     xhtml: XHTML.uri,
754                     xhtml2: "http://www.w3.org/2002/06/xhtml2",
755                     dactyl: NS.uri
756                 }[prefix] || null)
757         }),
758
759     extend: function extend(dest) {
760         Array.slice(arguments, 1).filter(util.identity).forEach(function (src) {
761             for (let [k, v] in Iterator(src)) {
762                 let get = src.__lookupGetter__(k),
763                     set = src.__lookupSetter__(k);
764                 if (!get && !set)
765                     dest[k] = v;
766                 if (get)
767                     dest.__defineGetter__(k, get);
768                 if (set)
769                     dest.__defineSetter__(k, set);
770             }
771         });
772         return dest;
773     },
774
775     /**
776      * Converts *bytes* to a pretty printed data size string.
777      *
778      * @param {number} bytes The number of bytes.
779      * @param {string} decimalPlaces The number of decimal places to use if
780      *     *humanReadable* is true.
781      * @param {boolean} humanReadable Use byte multiples.
782      * @returns {string}
783      */
784     formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable) {
785         const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
786         let unitIndex = 0;
787         let tmpNum = parseInt(bytes, 10) || 0;
788         let strNum = [tmpNum + ""];
789
790         if (humanReadable) {
791             while (tmpNum >= 1024) {
792                 tmpNum /= 1024;
793                 if (++unitIndex > (unitVal.length - 1))
794                     break;
795             }
796
797             let decPower = Math.pow(10, decimalPlaces);
798             strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2);
799
800             if (!strNum[1])
801                 strNum[1] = "";
802
803             while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces)
804                 strNum[1] += "0";
805         }
806
807         for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000
808             strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u);
809
810         if (unitIndex) // decimalPlaces only when > Bytes
811             strNum[0] += "." + strNum[1];
812
813         return strNum[0] + " " + unitVal[unitIndex];
814     },
815
816     /**
817      * Converts *seconds* into a human readable time string.
818      *
819      * @param {number} seconds
820      * @returns {string}
821      */
822     formatSeconds: function formatSeconds(seconds) {
823         function pad(n, val) ("0000000" + val).substr(-Math.max(n, String(val).length));
824         function div(num, denom) [Math.round(num / denom), Math.round(num % denom)];
825         let days, hours, minutes;
826
827         [minutes, seconds] = div(seconds, 60);
828         [hours, minutes]   = div(minutes, 60);
829         [days, hours]      = div(hours,   24);
830         if (days)
831             return /*L*/days + " days " + hours + " hours"
832         if (hours)
833             return /*L*/hours + "h " + minutes + "m";
834         if (minutes)
835             return /*L*/minutes + ":" + pad(2, seconds);
836         return /*L*/seconds + "s";
837     },
838
839     /**
840      * Returns the file which backs a given URL, if available.
841      *
842      * @param {nsIURI} uri The URI for which to find a file.
843      * @returns {File|null}
844      */
845     getFile: function getFile(uri) {
846         try {
847             if (isString(uri))
848                 uri = util.newURI(util.fixURI(uri));
849
850             if (uri instanceof Ci.nsIFileURL)
851                 return File(uri.file);
852
853             let channel = services.io.newChannelFromURI(uri);
854             channel.cancel(Cr.NS_BINDING_ABORTED);
855             if (channel instanceof Ci.nsIFileChannel)
856                 return File(channel.file);
857         }
858         catch (e) {}
859         return null;
860     },
861
862     /**
863      * Returns the host for the given URL, or null if invalid.
864      *
865      * @param {string} url
866      * @returns {string|null}
867      */
868     getHost: function (url) {
869         try {
870             return util.createURI(url).host;
871         }
872         catch (e) {}
873         return null;
874     },
875
876     /**
877      * Returns true if the current Gecko runtime is of the given version
878      * or greater.
879      *
880      * @param {string} ver The required version.
881      * @returns {boolean}
882      */
883     haveGecko: function (ver) services.versionCompare.compare(services.runtime.platformVersion, ver) >= 0,
884
885     /**
886      * Sends a synchronous or asynchronous HTTP request to *url* and returns
887      * the XMLHttpRequest object. If *callback* is specified the request is
888      * asynchronous and the *callback* is invoked with the object as its
889      * argument.
890      *
891      * @param {string} url
892      * @param {function(XMLHttpRequest)} callback
893      * @returns {XMLHttpRequest}
894      */
895     httpGet: function httpGet(url, callback, self) {
896         let params = callback;
897         if (!isObject(params))
898             params = { callback: params && function () callback.apply(self, arguments) };
899
900         try {
901             let xmlhttp = services.Xmlhttp();
902             xmlhttp.mozBackgroundRequest = true;
903
904             let async = params.callback || params.onload || params.onerror;
905             if (async) {
906                 xmlhttp.onload = function handler(event) { util.trapErrors(params.onload || params.callback, params, xmlhttp, event) };
907                 xmlhttp.onerror = function handler(event) { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event) };
908             }
909             if (params.mimeType)
910                 xmlhttp.overrideMimeType(params.mimeType);
911
912             xmlhttp.open(params.method || "GET", url, async,
913                          params.user, params.pass);
914
915             xmlhttp.send(null);
916             return xmlhttp;
917         }
918         catch (e) {
919             util.dactyl.log(_("error.cantOpen", String.quote(url), e), 1);
920             return null;
921         }
922     },
923
924     /**
925      * The identity function.
926      *
927      * @param {Object} k
928      * @returns {Object}
929      */
930     identity: function identity(k) k,
931
932     /**
933      * Returns the intersection of two rectangles.
934      *
935      * @param {Object} r1
936      * @param {Object} r2
937      * @returns {Object}
938      */
939     intersection: function (r1, r2) ({
940         get width()  this.right - this.left,
941         get height() this.bottom - this.top,
942         left: Math.max(r1.left, r2.left),
943         right: Math.min(r1.right, r2.right),
944         top: Math.max(r1.top, r2.top),
945         bottom: Math.min(r1.bottom, r2.bottom)
946     }),
947
948     /**
949      * Returns true if the given stack frame resides in Dactyl code.
950      *
951      * @param {nsIStackFrame} frame
952      * @returns {boolean}
953      */
954     isDactyl: Class.memoize(function () {
955         let base = util.regexp.escape(Components.stack.filename.replace(/[^\/]+$/, ""));
956         let re = RegExp("^(?:.* -> )?(?:resource://dactyl(?!-content/eval.js)|" + base + ")\\S+$");
957         return function isDactyl(frame) re.test(frame.filename);
958     }),
959
960     /**
961      * Returns true if *url* is in the domain *domain*.
962      *
963      * @param {string} url
964      * @param {string} domain
965      * @returns {boolean}
966      */
967     isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain),
968
969     /** Dactyl's notion of the current operating system platform. */
970     OS: memoize({
971         _arch: services.runtime.OS,
972         /**
973          * @property {string} The normalised name of the OS. This is one of
974          *     "Windows", "Mac OS X" or "Unix".
975          */
976         get name() this.isWindows ? "Windows" : this.isMacOSX ? "Mac OS X" : "Unix",
977         /** @property {boolean} True if the OS is Windows. */
978         get isWindows() this._arch == "WINNT",
979         /** @property {boolean} True if the OS is Mac OS X. */
980         get isMacOSX() this._arch == "Darwin",
981         /** @property {boolean} True if the OS is some other *nix variant. */
982         get isUnix() !this.isWindows && !this.isMacOSX,
983         /** @property {RegExp} A RegExp which matches illegal characters in path components. */
984         get illegalCharacters() this.isWindows ? /[<>:"/\\|?*\x00-\x1f]/g : /\//g
985     }),
986
987     /**
988      * Returns true if *host* is a subdomain of *domain*.
989      *
990      * @param {string} host The host to check.
991      * @param {string} domain The base domain to check the host against.
992      * @returns {boolean}
993      */
994     isSubdomain: function isSubdomain(host, domain) {
995         if (host == null)
996             return false;
997         let idx = host.lastIndexOf(domain);
998         return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx - 1] == ".");
999     },
1000
1001     /**
1002      * Returns true if the given DOM node is currently visible.
1003      *
1004      * @param {Node} node
1005      * @returns {boolean}
1006      */
1007     isVisible: function (node) {
1008         let style = util.computedStyle(node);
1009         return style.visibility == "visible" && style.display != "none";
1010     },
1011
1012     /**
1013      * Iterates over all currently open documents, including all
1014      * top-level window and sub-frames thereof.
1015      */
1016     iterDocuments: function iterDocuments() {
1017         let windows = services.windowMediator.getXULWindowEnumerator(null);
1018         while (windows.hasMoreElements()) {
1019             let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
1020             for each (let type in ["typeChrome", "typeContent"]) {
1021                 let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type],
1022                                                                       Ci.nsIDocShell.ENUMERATE_FORWARDS);
1023                 while (docShells.hasMoreElements())
1024                     let (viewer = docShells.getNext().QueryInterface(Ci.nsIDocShell).contentViewer) {
1025                         if (viewer)
1026                             yield viewer.DOMDocument;
1027                     }
1028             }
1029         }
1030     },
1031
1032     // ripped from Firefox; modified
1033     unsafeURI: Class.memoize(function () util.regexp(String.replace(<![CDATA[
1034             [
1035                 \s
1036                 // Invisible characters (bug 452979)
1037                 U001C U001D U001E U001F // file/group/record/unit separator
1038                 U00AD // Soft hyphen
1039                 UFEFF // BOM
1040                 U2060 // Word joiner
1041                 U2062 U2063 // Invisible times/separator
1042                 U200B UFFFC // Zero-width space/no-break space
1043
1044                 // Bidi formatting characters. (RFC 3987 sections 3.2 and 4.1 paragraph 6)
1045                 U200E U200F U202A U202B U202C U202D U202E
1046             ]
1047         ]]>, /U/g, "\\u"),
1048         "gx")),
1049     losslessDecodeURI: function losslessDecodeURI(url) {
1050         return url.split("%25").map(function (url) {
1051                 // Non-UTF-8 compliant URLs cause "malformed URI sequence" errors.
1052                 try {
1053                     return decodeURI(url).replace(this.unsafeURI, encodeURIComponent);
1054                 }
1055                 catch (e) {
1056                     return url;
1057                 }
1058             }, this).join("%25");
1059     },
1060
1061     /**
1062      * Returns an XPath union expression constructed from the specified node
1063      * tests. An expression is built with node tests for both the null and
1064      * XHTML namespaces. See {@link Buffer#evaluateXPath}.
1065      *
1066      * @param nodes {Array(string)}
1067      * @returns {string}
1068      */
1069     makeXPath: function makeXPath(nodes) {
1070         return array(nodes).map(util.debrace).flatten()
1071                            .map(function (node) /^[a-z]+:/.test(node) ? node : [node, "xhtml:" + node]).flatten()
1072                            .map(function (node) "//" + node).join(" | ");
1073     },
1074
1075     /**
1076      * Creates a DTD fragment from the given object. Each property of
1077      * the object is converted to an ENTITY declaration. SGML special
1078      * characters other than ' and % are left intact.
1079      *
1080      * @param {object} obj The object to convert.
1081      * @returns {string} The DTD fragment containing entity declaration
1082      *      for *obj*.
1083      */
1084     makeDTD: let (map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" })
1085         function makeDTD(obj) iter(obj)
1086           .map(function ([k, v]) ["<!ENTITY ", k, " '", String.replace(v == null ? "null" : typeof v == "xml" ? v.toXMLString() : v,
1087                                                                        typeof v == "xml" ? /['%]/g : /['"%&<>]/g,
1088                                                                        function (m) map[m]),
1089                                   "'>"].join(""))
1090           .join("\n"),
1091
1092     map: deprecated("iter.map", function map(obj, fn, self) iter(obj).map(fn, self).toArray()),
1093     writeToClipboard: deprecated("dactyl.clipboardWrite", function writeToClipboard(str, verbose) util.dactyl.clipboardWrite(str, verbose)),
1094     readFromClipboard: deprecated("dactyl.clipboardRead", function readFromClipboard() util.dactyl.clipboardRead(false)),
1095
1096     /**
1097      * Converts a URI string into a URI object.
1098      *
1099      * @param {string} uri
1100      * @returns {nsIURI}
1101      */
1102     // FIXME: createURI needed too?
1103     newURI: function newURI(uri, charset, base) this.withProperErrors("newURI", services.io, uri, charset, base),
1104
1105     /**
1106      * Removes leading garbage prepended to URIs by the subscript
1107      * loader.
1108      */
1109     fixURI: function fixURI(url) String.replace(url, /.* -> /, ""),
1110
1111     /**
1112      * Pretty print a JavaScript object. Use HTML markup to color certain items
1113      * if *color* is true.
1114      *
1115      * @param {Object} object The object to pretty print.
1116      * @param {boolean} color Whether the output should be colored.
1117      * @returns {string}
1118      */
1119     objectToString: function objectToString(object, color) {
1120         // Use E4X literals so html is automatically quoted
1121         // only when it's asked for. No one wants to see &lt;
1122         // on their console or :map :foo in their buffer
1123         // when they expect :map <C-f> :foo.
1124         XML.prettyPrinting = false;
1125         XML.ignoreWhitespace = false;
1126
1127         if (object == null)
1128             return object + "\n";
1129
1130         if (!isObject(object))
1131             return String(object);
1132
1133         function namespaced(node) {
1134             var ns = NAMESPACES[node.namespaceURI] || /^(?:(.*?):)?/.exec(node.name)[0];
1135             if (!ns)
1136                 return node.localName;
1137             if (color)
1138                 return <><span highlight="HelpXMLNamespace">{ns}</span>{node.localName}</>
1139             return ns + ":" + node.localName;
1140         }
1141
1142         if (object instanceof Ci.nsIDOMElement) {
1143             const NAMESPACES = array.toObject([
1144                 [NS, "dactyl"],
1145                 [XHTML, "html"],
1146                 [XUL, "xul"]
1147             ]);
1148             let elem = object;
1149             if (elem.nodeType == elem.TEXT_NODE)
1150                 return elem.data;
1151
1152             try {
1153                 let hasChildren = elem.firstChild && (!/^\s*$/.test(elem.firstChild) || elem.firstChild.nextSibling)
1154                 if (color)
1155                     return <span highlight="HelpXMLBlock"><span highlight="HelpXMLTagStart">&lt;{
1156                             namespaced(elem)} {
1157                                 template.map(array.iterValues(elem.attributes),
1158                                     function (attr)
1159                                         <span highlight="HelpXMLAttribute">{namespaced(attr)}</span> +
1160                                         <span highlight="HelpXMLString">{attr.value}</span>,
1161                                     <> </>)
1162                             }{ !hasChildren ? "/>" : ">"
1163                         }</span>{ !hasChildren ? "" : <>...</> +
1164                             <span highlight="HtmlTagEnd">&lt;{namespaced(elem)}></span>
1165                     }</span>;
1166
1167                 let tag = "<" + [namespaced(elem)].concat(
1168                     [namespaced(a) + "=" + template.highlight(a.value, true)
1169                      for ([i, a] in array.iterItems(elem.attributes))]).join(" ");
1170                 return tag + (!hasChildren ? "/>" : ">...</" + namespaced(elem) + ">");
1171             }
1172             catch (e) {
1173                 return {}.toString.call(elem);
1174             }
1175         }
1176
1177         try { // for window.JSON
1178             var obj = String(object);
1179         }
1180         catch (e) {
1181             obj = Object.prototype.toString.call(obj);
1182         }
1183         obj = template.highlightFilter(util.clip(obj, 150), "\n", !color ? function () "^J" : function () <span highlight="NonText">^J</span>);
1184         let string = <><span highlight="Title Object">{obj}</span>::&#x0a;</>;
1185
1186         let keys = [];
1187
1188         // window.content often does not want to be queried with "var i in object"
1189         try {
1190             let hasValue = !("__iterator__" in object || isinstance(object, ["Generator", "Iterator"]));
1191             if (object.dactyl && object.modules && object.modules.modules == object.modules) {
1192                 object = Iterator(object);
1193                 hasValue = false;
1194             }
1195             for (let i in object) {
1196                 let value = <![CDATA[<no value>]]>;
1197                 try {
1198                     value = object[i];
1199                 }
1200                 catch (e) {}
1201                 if (!hasValue) {
1202                     if (isArray(i) && i.length == 2)
1203                         [i, value] = i;
1204                     else
1205                         var noVal = true;
1206                 }
1207
1208                 value = template.highlight(value, true, 150);
1209                 let key = <span highlight="Key">{i}</span>;
1210                 if (!isNaN(i))
1211                     i = parseInt(i);
1212                 else if (/^[A-Z_]+$/.test(i))
1213                     i = "";
1214                 keys.push([i, <>{key}{noVal ? "" : <>: {value}</>}&#x0a;</>]);
1215             }
1216         }
1217         catch (e) {}
1218
1219         function compare(a, b) {
1220             if (!isNaN(a[0]) && !isNaN(b[0]))
1221                 return a[0] - b[0];
1222             return String.localeCompare(a[0], b[0]);
1223         }
1224         string += template.map(keys.sort(compare), function (f) f[1]);
1225         return color ? <div style="white-space: pre-wrap;">{string}</div> : [s for each (s in string)].join("");
1226     },
1227
1228     observers: {
1229         "dactyl-cleanup-modules": function (subject, reason) {
1230             defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules " + reason);
1231
1232             for (let module in values(defineModule.modules))
1233                 if (module.cleanup) {
1234                     util.dump("cleanup: " + module.constructor.className);
1235                     util.trapErrors(module.cleanup, module, reason);
1236                 }
1237
1238             JSMLoader.cleanup();
1239
1240             if (!this.rehashing)
1241                 services.observer.addObserver(this, "dactyl-rehash", true);
1242         },
1243         "dactyl-rehash": function () {
1244             services.observer.removeObserver(this, "dactyl-rehash");
1245
1246             defineModule.loadLog.push("dactyl: util: observe: dactyl-rehash");
1247             if (!this.rehashing)
1248                 for (let module in values(defineModule.modules)) {
1249                     defineModule.loadLog.push("dactyl: util: init(" + module + ")");
1250                     if (module.reinit)
1251                         module.reinit();
1252                     else
1253                         module.init();
1254                 }
1255         },
1256         "dactyl-purge": function () {
1257             this.rehashing = 1;
1258         },
1259
1260         "toplevel-window-ready": function (window, data) {
1261             window.addEventListener("DOMContentLoaded", wrapCallback(function listener(event) {
1262                 if (event.originalTarget === window.document) {
1263                     window.removeEventListener("DOMContentLoaded", listener.wrapper, true);
1264                     util._loadOverlays(window);
1265                 }
1266             }), true);
1267         },
1268         "chrome-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); },
1269         "content-document-global-created": function (window, uri) { this.observe(window, "toplevel-window-ready", null); }
1270     },
1271
1272     _loadOverlays: function _loadOverlays(window) {
1273         if (!window.dactylOverlays)
1274             window.dactylOverlays = [];
1275
1276         for each (let obj in util.overlays[window.document.documentURI] || []) {
1277             if (window.dactylOverlays.indexOf(obj) >= 0)
1278                 continue;
1279             window.dactylOverlays.push(obj);
1280             this._loadOverlay(window, obj(window));
1281         }
1282     },
1283
1284     _loadOverlay: function _loadOverlay(window, obj) {
1285         let doc = window.document;
1286         if (!doc.dactylOverlayElements) {
1287             doc.dactylOverlayElements = [];
1288             doc.dactylOverlayAttributes = [];
1289         }
1290
1291         function overlay(key, fn) {
1292             if (obj[key]) {
1293                 let iterator = Iterator(obj[key]);
1294                 if (!isObject(obj[key]))
1295                     iterator = ([elem.@id, elem.elements(), elem.@*::*.(function::name() != "id")] for each (elem in obj[key]));
1296
1297                 for (let [elem, xml, attr] in iterator) {
1298                     if (elem = doc.getElementById(elem)) {
1299                         let node = util.xmlToDom(xml, doc, obj.objects);
1300                         if (!(node instanceof Ci.nsIDOMDocumentFragment))
1301                             doc.dactylOverlayElements.push(node);
1302                         else
1303                             for (let n in array.iterValues(node.childNodes))
1304                                 doc.dactylOverlayElements.push(n);
1305
1306                         fn(elem, node);
1307                         for each (let attr in attr || []) {
1308                             let ns = attr.namespace(), name = attr.localName();
1309                             doc.dactylOverlayAttributes.push([elem, ns, name, getAttr(elem, ns, name), String(attr)]);
1310                             if (attr.name() != "highlight")
1311                                 elem.setAttributeNS(ns, name, String(attr));
1312                             else
1313                                 highlight.highlightNode(elem, String(attr));
1314                         }
1315                     }
1316                 }
1317             }
1318         }
1319
1320         overlay("before", function (elem, dom) elem.parentNode.insertBefore(dom, elem));
1321         overlay("after", function (elem, dom) elem.parentNode.insertBefore(dom, elem.nextSibling));
1322         overlay("append", function (elem, dom) elem.appendChild(dom));
1323         overlay("prepend", function (elem, dom) elem.insertBefore(dom, elem.firstChild));
1324         if (obj.init)
1325             obj.init(window);
1326
1327         if (obj.load)
1328             if (doc.readyState === "complete")
1329                 obj.load(window);
1330             else
1331                 doc.addEventListener("load", wrapCallback(function load(event) {
1332                     if (event.originalTarget === event.target) {
1333                         doc.removeEventListener("load", load.wrapper, true);
1334                         obj.load(window, event);
1335                     }
1336                 }), true);
1337     },
1338
1339     /**
1340      * Overlays an object with the given property overrides. Each
1341      * property in *overrides* is added to *object*, replacing any
1342      * original value. Functions in *overrides* are augmented with the
1343      * new properties *super*, *supercall*, and *superapply*, in the
1344      * same manner as class methods, so that they man call their
1345      * overridden counterparts.
1346      *
1347      * @param {object} object The object to overlay.
1348      * @param {object} overrides An object containing properties to
1349      *      override.
1350      * @returns {function} A function which, when called, will remove
1351      *      the overlay.
1352      */
1353     overlayObject: function (object, overrides) {
1354         let original = Object.create(object);
1355         overrides = update(Object.create(original), overrides);
1356
1357         Object.getOwnPropertyNames(overrides).forEach(function (k) {
1358             let orig, desc = Object.getOwnPropertyDescriptor(overrides, k);
1359             if (desc.value instanceof Class.Property)
1360                 desc = desc.value.init(k) || desc.value;
1361
1362             if (k in object) {
1363                 for (let obj = object; obj && !orig; obj = Object.getPrototypeOf(obj))
1364                     if (orig = Object.getOwnPropertyDescriptor(obj, k))
1365                         Object.defineProperty(original, k, orig);
1366
1367                 if (!orig)
1368                     if (orig = Object.getPropertyDescriptor(object, k))
1369                         Object.defineProperty(original, k, orig);
1370             }
1371
1372             // Guard against horrible add-ons that use eval-based monkey
1373             // patching.
1374             let value = desc.value;
1375             if (callable(desc.value)) {
1376
1377                 delete desc.value;
1378                 delete desc.writable;
1379                 desc.get = function get() value;
1380                 desc.set = function set(val) {
1381                     if (!callable(val) || Function.prototype.toString(val).indexOf(sentinel) < 0)
1382                         Class.replaceProperty(this, k, val);
1383                     else {
1384                         let package_ = util.newURI(util.fixURI(Components.stack.caller.filename)).host;
1385                         util.reportError(Error(_("error.monkeyPatchOverlay", package_)));
1386                         util.dactyl.echoerr(_("error.monkeyPatchOverlay", package_));
1387                     }
1388                 };
1389             }
1390
1391             try {
1392                 Object.defineProperty(object, k, desc);
1393
1394                 if (callable(value)) {
1395                     let sentinel = "(function DactylOverlay() {}())"
1396                     value.toString = function toString() toString.toString.call(this).replace(/\}?$/, sentinel + "; $&");
1397                     value.toSource = function toSource() toSource.toSource.call(this).replace(/\}?$/, sentinel + "; $&");
1398                 }
1399             }
1400             catch (e) {
1401                 try {
1402                     if (value) {
1403                         object[k] = value;
1404                         return;
1405                     }
1406                 }
1407                 catch (f) {}
1408                 util.reportError(e);
1409             }
1410         }, this);
1411
1412         return function unwrap() {
1413             for each (let k in Object.getOwnPropertyNames(original))
1414                 if (Object.getOwnPropertyDescriptor(object, k).configurable)
1415                     Object.defineProperty(object, k, Object.getOwnPropertyDescriptor(original, k));
1416                 else {
1417                     try {
1418                         object[k] = original[k];
1419                     }
1420                     catch (e) {}
1421                 }
1422         };
1423     },
1424
1425     overlayWindow: function (url, fn) {
1426         if (url instanceof Ci.nsIDOMWindow)
1427             util._loadOverlay(url, fn);
1428         else {
1429             Array.concat(url).forEach(function (url) {
1430                 if (!this.overlays[url])
1431                     this.overlays[url] = [];
1432                 this.overlays[url].push(fn);
1433             }, this);
1434
1435             for (let doc in util.iterDocuments())
1436                 if (["interactive", "complete"].indexOf(doc.readyState) >= 0)
1437                     this._loadOverlays(doc.defaultView);
1438                 else
1439                     this.observe(doc.defaultView, "toplevel-window-ready");
1440         }
1441     },
1442
1443     /**
1444      * Parses the fields of a form and returns a URL/POST-data pair
1445      * that is the equivalent of submitting the form.
1446      *
1447      * @param {nsINode} field One of the fields of the given form.
1448      * @returns {array}
1449      */
1450     // Nuances gleaned from browser.jar/content/browser/browser.js
1451     parseForm: function parseForm(field) {
1452         function encode(name, value, param) {
1453             param = param ? "%s" : "";
1454             if (post)
1455                 return name + "=" + encodeComponent(value + param);
1456             return encodeComponent(name) + "=" + encodeComponent(value) + param;
1457         }
1458
1459         let form = field.form;
1460         let doc = form.ownerDocument;
1461
1462         let charset = doc.characterSet;
1463         let converter = services.CharsetConv(charset);
1464         for each (let cs in form.acceptCharset.split(/\s*,\s*|\s+/)) {
1465             let c = services.CharsetConv(cs);
1466             if (c) {
1467                 converter = services.CharsetConv(cs);
1468                 charset = cs;
1469             }
1470         }
1471
1472         let uri = util.newURI(doc.baseURI.replace(/\?.*/, ""), charset);
1473         let url = util.newURI(form.action, charset, uri).spec;
1474
1475         let post = form.method.toUpperCase() == "POST";
1476
1477         let encodeComponent = encodeURIComponent;
1478         if (charset !== "UTF-8")
1479             encodeComponent = function encodeComponent(str)
1480                 escape(converter.ConvertFromUnicode(str) + converter.Finish());
1481
1482         let elems = [];
1483         if (field instanceof Ci.nsIDOMHTMLInputElement && field.type == "submit")
1484             elems.push(encode(field.name, field.value));
1485
1486         for (let [, elem] in iter(form.elements))
1487             if (elem.name && !elem.disabled) {
1488                 if (Set.has(util.editableInputs, elem.type)
1489                         || /^(?:hidden|textarea)$/.test(elem.type)
1490                         || elem.type == "submit" && elem == field
1491                         || elem.checked && /^(?:checkbox|radio)$/.test(elem.type))
1492                     elems.push(encode(elem.name, elem.value, elem === field));
1493                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
1494                     for (let [, opt] in Iterator(elem.options))
1495                         if (opt.selected)
1496                             elems.push(encode(elem.name, opt.value));
1497                 }
1498             }
1499
1500         if (post)
1501             return [url, elems.join('&'), charset, elems];
1502         return [url + "?" + elems.join('&'), null, charset, elems];
1503     },
1504
1505     /**
1506      * A generator that returns the values between *start* and *end*, in *step*
1507      * increments.
1508      *
1509      * @param {number} start The interval's start value.
1510      * @param {number} end The interval's end value.
1511      * @param {boolean} step The value to step the range by. May be
1512      *     negative. @default 1
1513      * @returns {Iterator(Object)}
1514      */
1515     range: function range(start, end, step) {
1516         if (!step)
1517             step = 1;
1518         if (step > 0) {
1519             for (; start < end; start += step)
1520                 yield start;
1521         }
1522         else {
1523             while (start > end)
1524                 yield start += step;
1525         }
1526     },
1527
1528     /**
1529      * An interruptible generator that returns all values between *start* and
1530      * *end*. The thread yields every *time* milliseconds.
1531      *
1532      * @param {number} start The interval's start value.
1533      * @param {number} end The interval's end value.
1534      * @param {number} time The time in milliseconds between thread yields.
1535      * @returns {Iterator(Object)}
1536      */
1537     interruptibleRange: function interruptibleRange(start, end, time) {
1538         let endTime = Date.now() + time;
1539         while (start < end) {
1540             if (Date.now() > endTime) {
1541                 util.threadYield(true, true);
1542                 endTime = Date.now() + time;
1543             }
1544             yield start++;
1545         }
1546     },
1547
1548     /**
1549      * Creates a new RegExp object based on the value of expr stripped
1550      * of all white space and interpolated with the values from tokens.
1551      * If tokens, any string in the form of <key> in expr is replaced
1552      * with the value of the property, 'key', from tokens, if that
1553      * property exists. If the property value is itself a RegExp, its
1554      * source is substituted rather than its string value.
1555      *
1556      * Additionally, expr is stripped of all JavaScript comments.
1557      *
1558      * This is similar to Perl's extended regular expression format.
1559      *
1560      * @param {string|XML} expr The expression to compile into a RegExp.
1561      * @param {string} flags Flags to apply to the new RegExp.
1562      * @param {object} tokens The tokens to substitute. @optional
1563      * @returns {RegExp} A custom regexp object.
1564      */
1565     regexp: update(function (expr, flags, tokens) {
1566         flags = flags || [k for ([k, v] in Iterator({ g: "global", i: "ignorecase", m: "multiline", y: "sticky" }))
1567                           if (expr[v])].join("");
1568
1569         if (isinstance(expr, ["RegExp"]))
1570             expr = expr.source;
1571
1572         expr = String.replace(expr, /\\(.)/, function (m, m1) {
1573             if (m1 === "c")
1574                 flags = flags.replace(/i/g, "") + "i";
1575             else if (m === "C")
1576                 flags = flags.replace(/i/g, "");
1577             else
1578                 return m;
1579             return "";
1580         });
1581
1582         // Replace replacement <tokens>.
1583         if (tokens)
1584             expr = String.replace(expr, /(\(?P)?<(\w+)>/g, function (m, n1, n2) !n1 && Set.has(tokens, n2) ? tokens[n2].dactylSource || tokens[n2].source || tokens[n2] : m);
1585
1586         // Strip comments and white space.
1587         if (/x/.test(flags))
1588             expr = String.replace(expr, /(\\.)|\/\/[^\n]*|\/\*[^]*?\*\/|\s+/gm, function (m, m1) m1 || "");
1589
1590         // Replace (?P<named> parameters)
1591         if (/\(\?P</.test(expr)) {
1592             var source = expr;
1593             let groups = ["wholeMatch"];
1594             expr = expr.replace(/((?:[^[(\\]|\\.|\[(?:[^\]]|\\.)*\])*)\((?:\?P<([^>]+)>|(\?))?/gy,
1595                 function (m0, m1, m2, m3) {
1596                     if (!m3)
1597                         groups.push(m2 || "-group-" + groups.length);
1598                     return m1 + "(" + (m3 || "");
1599                 });
1600             var struct = Struct.apply(null, groups);
1601         }
1602
1603         let res = update(RegExp(expr, flags.replace("x", "")), {
1604             closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "closure")),
1605             dactylPropertyNames: ["exec", "match", "test", "toSource", "toString", "global", "ignoreCase", "lastIndex", "multiLine", "source", "sticky"],
1606             iterate: function (str, idx) util.regexp.iterate(this, str, idx)
1607         });
1608
1609         // Return a struct with properties for named parameters if we
1610         // have them.
1611         if (struct)
1612             update(res, {
1613                 exec: function exec() let (match = exec.superapply(this, arguments)) match && struct.fromArray(match),
1614                 dactylSource: source, struct: struct
1615             });
1616         return res;
1617     }, {
1618         /**
1619          * Escapes Regular Expression special characters in *str*.
1620          *
1621          * @param {string} str
1622          * @returns {string}
1623          */
1624         escape: function regexp_escape(str) str.replace(/([\\{}()[\]^$.?*+|])/g, "\\$1"),
1625
1626         /**
1627          * Given a RegExp, returns its source in the form showable to the user.
1628          *
1629          * @param {RegExp} re The regexp showable source of which is to be returned.
1630          * @returns {string}
1631          */
1632         getSource: function regexp_getSource(re) re.source.replace(/\\(.)/g, function (m0, m1) m1 === "/" ? "/" : m0),
1633
1634         /**
1635          * Iterates over all matches of the given regexp in the given
1636          * string.
1637          *
1638          * @param {RegExp} regexp The regular expression to execute.
1639          * @param {string} string The string to search.
1640          * @param {number} lastIndex The index at which to begin searching. @optional
1641          */
1642         iterate: function iterate(regexp, string, lastIndex) iter(function () {
1643             regexp.lastIndex = lastIndex = lastIndex || 0;
1644             let match;
1645             while (match = regexp.exec(string)) {
1646                 lastIndex = regexp.lastIndex;
1647                 yield match;
1648                 regexp.lastIndex = lastIndex;
1649                 if (match[0].length == 0 || !regexp.global)
1650                     break;
1651             }
1652         }())
1653     }),
1654
1655     /**
1656      * Reloads dactyl in entirety by disabling the add-on and
1657      * re-enabling it.
1658      */
1659     rehash: function (args) {
1660         storage.session.commandlineArgs = args;
1661         this.timeout(function () {
1662             services.observer.notifyObservers(null, "startupcache-invalidate", "");
1663             this.rehashing = true;
1664             let addon = config.addon;
1665             addon.userDisabled = true;
1666             addon.userDisabled = false;
1667         });
1668     },
1669
1670     errorCount: 0,
1671     errors: Class.memoize(function () []),
1672     maxErrors: 15,
1673     /**
1674      * Reports an error to the Error Console and the standard output,
1675      * along with a stack trace and other relevant information. The
1676      * error is appended to {@see #errors}.
1677      */
1678     reportError: function (error) {
1679         if (error.noTrace)
1680             return;
1681
1682         if (isString(error))
1683             error = Error(error);
1684
1685         if (Cu.reportError)
1686             Cu.reportError(error);
1687
1688         try {
1689             this.errorCount++;
1690
1691             let obj = update({}, error, {
1692                 toString: function () String(error),
1693                 stack: <>{util.stackLines(String(error.stack || Error().stack)).join("\n").replace(/^/mg, "\t")}</>
1694             });
1695
1696             this.errors.push([new Date, obj + "\n" + obj.stack]);
1697             this.errors = this.errors.slice(-this.maxErrors);
1698             this.errors.toString = function () [k + "\n" + v for ([k, v] in array.iterValues(this))].join("\n\n");
1699
1700             this.dump(String(error));
1701             this.dump(obj);
1702             this.dump("");
1703         }
1704         catch (e) {
1705             try {
1706                 this.dump(String(error));
1707                 this.dump(util.stackLines(error.stack).join("\n"));
1708             }
1709             catch (e) { dump(e + "\n"); }
1710         }
1711
1712         // ctypes.open("libc.so.6").declare("kill", ctypes.default_abi, ctypes.void_t, ctypes.int, ctypes.int)(
1713         //     ctypes.open("libc.so.6").declare("getpid", ctypes.default_abi, ctypes.int)(), 2)
1714     },
1715
1716     /**
1717      * Given a domain, returns an array of all non-toplevel subdomains
1718      * of that domain.
1719      *
1720      * @param {string} host The host for which to find subdomains.
1721      * @returns {[string]}
1722      */
1723     subdomains: function subdomains(host) {
1724         if (/(^|\.)\d+$|:.*:/.test(host))
1725             // IP address or similar
1726             return [host];
1727
1728         let base = host.replace(/.*\.(.+?\..+?)$/, "$1");
1729         try {
1730             base = services.tld.getBaseDomainFromHost(host);
1731         }
1732         catch (e) {}
1733
1734         let ary = host.split(".");
1735         ary = [ary.slice(i).join(".") for (i in util.range(ary.length, 0, -1))];
1736         return ary.filter(function (h) h.length >= base.length);
1737     },
1738
1739     /**
1740      * Scrolls an element into view if and only if it's not already
1741      * fully visible.
1742      *
1743      * @param {Node} elem The element to make visible.
1744      */
1745     scrollIntoView: function scrollIntoView(elem, alignWithTop) {
1746         let win = elem.ownerDocument.defaultView;
1747         let rect = elem.getBoundingClientRect();
1748         if (!(rect && rect.bottom <= win.innerHeight && rect.top >= 0 && rect.left < win.innerWidth && rect.right > 0))
1749             elem.scrollIntoView(arguments.length > 1 ? alignWithTop : Math.abs(rect.top) < Math.abs(win.innerHeight - rect.bottom));
1750     },
1751
1752     /**
1753      * Returns the selection controller for the given window.
1754      *
1755      * @param {Window} window
1756      * @returns {nsISelectionController}
1757      */
1758     selectionController: function (win)
1759         win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1760            .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
1761            .QueryInterface(Ci.nsISelectionController),
1762
1763     /**
1764      * Suspend execution for at least *delay* milliseconds. Functions by
1765      * yielding execution to the next item in the main event queue, and
1766      * so may lead to unexpected call graphs, and long delays if another
1767      * handler yields execution while waiting.
1768      *
1769      * @param {number} delay The time period for which to sleep in milliseconds.
1770      */
1771     sleep: function (delay) {
1772         let mainThread = services.threading.mainThread;
1773
1774         let end = Date.now() + delay;
1775         while (Date.now() < end)
1776             mainThread.processNextEvent(true);
1777         return true;
1778     },
1779
1780     /**
1781      * Behaves like String.split, except that when *limit* is reached,
1782      * the trailing element contains the entire trailing portion of the
1783      * string.
1784      *
1785      *     util.split("a, b, c, d, e", /, /, 3) -> ["a", "b", "c, d, e"]
1786      *
1787      * @param {string} str The string to split.
1788      * @param {RegExp|string} re The regular expression on which to split the string.
1789      * @param {number} limit The maximum number of elements to return.
1790      * @returns {[string]}
1791      */
1792     split: function (str, re, limit) {
1793         re.lastIndex = 0;
1794         if (!re.global)
1795             re = RegExp(re.source || re, "g");
1796         let match, start = 0, res = [];
1797         while (--limit && (match = re.exec(str)) && match[0].length) {
1798             res.push(str.substring(start, match.index));
1799             start = match.index + match[0].length;
1800         }
1801         res.push(str.substring(start));
1802         return res;
1803     },
1804
1805     /**
1806      * Split a string on literal occurrences of a marker.
1807      *
1808      * Specifically this ignores occurrences preceded by a backslash, or
1809      * contained within 'single' or "double" quotes.
1810      *
1811      * It assumes backslash escaping on strings, and will thus not count quotes
1812      * that are preceded by a backslash or within other quotes as starting or
1813      * ending quoted sections of the string.
1814      *
1815      * @param {string} str
1816      * @param {RegExp} marker
1817      * @returns {[string]}
1818      */
1819     splitLiteral: function splitLiteral(str, marker) {
1820         let results = [];
1821         let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source);
1822         let cont = true;
1823
1824         while (cont) {
1825             cont = false;
1826             str = str.replace(resep, function (match, before) {
1827                 results.push(before);
1828                 cont = match !== "";
1829                 return "";
1830             });
1831         }
1832
1833         results.push(str);
1834         return results;
1835     },
1836
1837     yielders: 0,
1838     /**
1839      * Yields execution to the next event in the current thread's event
1840      * queue. This is a potentially dangerous operation, since any
1841      * yielders higher in the event stack will prevent execution from
1842      * returning to the caller until they have finished their wait. The
1843      * potential for deadlock is high.
1844      *
1845      * @param {boolean} flush If true, flush all events in the event
1846      *      queue before returning. Otherwise, wait for an event to
1847      *      process before proceeding.
1848      * @param {boolean} interruptable If true, this yield may be
1849      *      interrupted by pressing <C-c>, in which case,
1850      *      Error("Interrupted") will be thrown.
1851      */
1852     threadYield: function (flush, interruptable) {
1853         this.yielders++;
1854         try {
1855             let mainThread = services.threading.mainThread;
1856             /* FIXME */
1857             util.interrupted = false;
1858             do {
1859                 mainThread.processNextEvent(!flush);
1860                 if (util.interrupted)
1861                     throw Error("Interrupted");
1862             }
1863             while (flush === true && mainThread.hasPendingEvents());
1864         }
1865         finally {
1866             this.yielders--;
1867         }
1868     },
1869
1870     /**
1871      * Waits for the function *test* to return true, or *timeout*
1872      * milliseconds to expire.
1873      *
1874      * @param {function} test The predicate on which to wait.
1875      * @param {object} self The 'this' object for *test*.
1876      * @param {Number} timeout The maximum number of milliseconds to
1877      *      wait.
1878      *      @optional
1879      * @param {boolean} interruptable If true, may be interrupted by
1880      *      pressing <C-c>, in which case, Error("Interrupted") will be
1881      *      thrown.
1882      */
1883     waitFor: function waitFor(test, self, timeout, interruptable) {
1884         let end = timeout && Date.now() + timeout, result;
1885
1886         let timer = services.Timer(function () {}, 10, services.Timer.TYPE_REPEATING_SLACK);
1887         try {
1888             while (!(result = test.call(self)) && (!end || Date.now() < end))
1889                 this.threadYield(false, interruptable);
1890         }
1891         finally {
1892             timer.cancel();
1893         }
1894         return result;
1895     },
1896
1897     /**
1898      * Makes the passed function yieldable. Each time the function calls
1899      * yield, execution is suspended for the yielded number of
1900      * milliseconds.
1901      *
1902      * Example:
1903      *      let func = yieldable(function () {
1904      *          util.dump(Date.now()); // 0
1905      *          yield 1500;
1906      *          util.dump(Date.now()); // 1500
1907      *      });
1908      *      func();
1909      *
1910      * @param {function} func The function to mangle.
1911      * @returns {function} A new function which may not execute
1912      *      synchronously.
1913      */
1914     yieldable: function yieldable(func)
1915         function magic() {
1916             let gen = func.apply(this, arguments);
1917             (function next() {
1918                 try {
1919                     util.timeout(next, gen.next());
1920                 }
1921                 catch (e if e instanceof StopIteration) {};
1922             })();
1923         },
1924
1925     /**
1926      * Wraps a callback function such that its errors are not lost. This
1927      * is useful for DOM event listeners, which ordinarily eat errors.
1928      * The passed function has the property *wrapper* set to the new
1929      * wrapper function, while the wrapper has the property *wrapped*
1930      * set to the original callback.
1931      *
1932      * @param {function} callback The callback to wrap.
1933      * @returns {function}
1934      */
1935     wrapCallback: wrapCallback,
1936
1937     /**
1938      * Returns the top-level chrome window for the given window.
1939      *
1940      * @param {Window} win The child window.
1941      * @returns {Window} The top-level parent window.
1942      */
1943     topWindow: function topWindow(win)
1944             win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1945                .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
1946                .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow),
1947
1948     /**
1949      * Traps errors in the called function, possibly reporting them.
1950      *
1951      * @param {function} func The function to call
1952      * @param {object} self The 'this' object for the function.
1953      */
1954     trapErrors: function trapErrors(func, self) {
1955         try {
1956             if (!callable(func))
1957                 func = self[func];
1958             return func.apply(self || this, Array.slice(arguments, 2));
1959         }
1960         catch (e) {
1961             util.reportError(e);
1962             return undefined;
1963         }
1964     },
1965
1966     /**
1967      * Returns the file path of a given *url*, for debugging purposes.
1968      * If *url* points to a file (even if indirectly), the native
1969      * filesystem path is returned. Otherwise, the URL itself is
1970      * returned.
1971      *
1972      * @param {string} url The URL to mangle.
1973      * @returns {string} The path to the file.
1974      */
1975     urlPath: function urlPath(url) {
1976         try {
1977             return util.getFile(url).path;
1978         }
1979         catch (e) {
1980             return url;
1981         }
1982     },
1983
1984     /**
1985      * Returns a list of all domains and subdomains of documents in the
1986      * given window and all of its descendant frames.
1987      *
1988      * @param {nsIDOMWindow} win The window for which to find domains.
1989      * @returns {[string]} The visible domains.
1990      */
1991     visibleHosts: function (win) {
1992         let res = [], seen = {};
1993         (function rec(frame) {
1994             try {
1995                 if (frame.location.hostname)
1996                     res = res.concat(util.subdomains(frame.location.hostname));
1997             }
1998             catch (e) {}
1999             Array.forEach(frame.frames, rec);
2000         })(win);
2001         return res.filter(function (h) !Set.add(seen, h));
2002     },
2003
2004     /**
2005      * Returns a list of URIs of documents in the given window and all
2006      * of its descendant frames.
2007      *
2008      * @param {nsIDOMWindow} win The window for which to find URIs.
2009      * @returns {[nsIURI]} The visible URIs.
2010      */
2011     visibleURIs: function (win) {
2012         let res = [], seen = {};
2013         (function rec(frame) {
2014             try {
2015                 res = res.concat(util.newURI(frame.location.href));
2016             }
2017             catch (e) {}
2018             Array.forEach(frame.frames, rec);
2019         })(win);
2020         return res.filter(function (h) !Set.add(seen, h.spec));
2021     },
2022
2023     /**
2024      * Wraps native exceptions thrown by the called function so that a
2025      * proper stack trace may be retrieved from them.
2026      *
2027      * @param {function|string} meth The method to call.
2028      * @param {object} self The 'this' object of the method.
2029      * @param ... Arguments to pass to *meth*.
2030      */
2031     withProperErrors: function withProperErrors(meth, self) {
2032         try {
2033             return (callable(meth) ? meth : self[meth]).apply(self, Array.slice(arguments, withProperErrors.length));
2034         }
2035         catch (e) {
2036             throw e.stack ? e : Error(e);
2037         }
2038     },
2039
2040     /**
2041      * Converts an E4X XML literal to a DOM node. Any attribute named
2042      * highlight is present, it is transformed into dactyl:highlight,
2043      * and the named highlight groups are guaranteed to be loaded.
2044      *
2045      * @param {Node} node
2046      * @param {Document} doc
2047      * @param {Object} nodes If present, nodes with the "key" attribute are
2048      *     stored here, keyed to the value thereof.
2049      * @returns {Node}
2050      */
2051     xmlToDom: function xmlToDom(node, doc, nodes) {
2052         XML.prettyPrinting = false;
2053         if (typeof node === "string") // Sandboxes can't currently pass us XML objects.
2054             node = XML(node);
2055
2056         if (node.length() != 1) {
2057             let domnode = doc.createDocumentFragment();
2058             for each (let child in node)
2059                 domnode.appendChild(xmlToDom(child, doc, nodes));
2060             return domnode;
2061         }
2062
2063         switch (node.nodeKind()) {
2064         case "text":
2065             return doc.createTextNode(String(node));
2066         case "element":
2067             let domnode = doc.createElementNS(node.namespace(), node.localName());
2068
2069             for each (let attr in node.@*::*)
2070                 if (attr.name() != "highlight")
2071                     domnode.setAttributeNS(attr.namespace(), attr.localName(), String(attr));
2072
2073             for each (let child in node.*::*)
2074                 domnode.appendChild(xmlToDom(child, doc, nodes));
2075             if (nodes && node.@key)
2076                 nodes[node.@key] = domnode;
2077
2078             if ("@highlight" in node)
2079                 highlight.highlightNode(domnode, String(node.@highlight), nodes || true);
2080             return domnode;
2081         default:
2082             return null;
2083         }
2084     }
2085 }, {
2086     Array: array
2087 });
2088
2089 /**
2090  * Math utility methods.
2091  * @singleton
2092  */
2093 var GlobalMath = Math;
2094 var Math = update(Object.create(GlobalMath), {
2095     /**
2096      * Returns the specified *value* constrained to the range *min* - *max*.
2097      *
2098      * @param {number} value The value to constrain.
2099      * @param {number} min The minimum constraint.
2100      * @param {number} max The maximum constraint.
2101      * @returns {number}
2102      */
2103     constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max)
2104 });
2105
2106 endModule();
2107
2108 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
2109
2110 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: