]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/util.jsm
d6eaf80ceaa1d0430d6886b9c8d6b860bd750bb8
[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-2013 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 defineModule("util", {
12     exports: ["DOM", "$", "FailedAssertion", "Math", "NS", "Point", "Util", "XBL", "XHTML", "XUL", "util"],
13     require: ["dom", "services"]
14 });
15
16 lazyRequire("overlay", ["overlay"]);
17 lazyRequire("storage", ["File", "storage"]);
18 lazyRequire("template", ["template"]);
19
20 var Magic = Class("Magic", {
21     init: function init(str) {
22         this.str = str;
23     },
24
25     get message() this.str,
26
27     toString: function () this.str
28 });
29
30 var FailedAssertion = Class("FailedAssertion", ErrorBase, {
31     init: function init(message, level, noTrace) {
32         if (noTrace !== undefined)
33             this.noTrace = noTrace;
34         init.supercall(this, message, level);
35     },
36
37     level: 3,
38
39     noTrace: true
40 });
41
42 var Point = Struct("Point", "x", "y");
43
44 var wrapCallback = function wrapCallback(fn, isEvent) {
45     if (!fn.wrapper)
46         fn.wrapper = function wrappedCallback() {
47             try {
48                 let res = fn.apply(this, arguments);
49                 if (isEvent && res === false) {
50                     arguments[0].preventDefault();
51                     arguments[0].stopPropagation();
52                 }
53                 return res;
54             }
55             catch (e) {
56                 util.reportError(e);
57                 return undefined;
58             }
59         };
60     fn.wrapper.wrapped = fn;
61     return fn.wrapper;
62 }
63
64 var Util = Module("Util", XPCOM([Ci.nsIObserver, Ci.nsISupportsWeakReference]), {
65     Magic: Magic,
66
67     init: function init() {
68         this.Array = array;
69
70         this.addObserver(this);
71         this.windows = [];
72     },
73
74     activeWindow: deprecated("overlay.activeWindow", { get: function activeWindow() overlay.activeWindow }),
75     overlayObject: deprecated("overlay.overlayObject", { get: function overlayObject() overlay.closure.overlayObject }),
76     overlayWindow: deprecated("overlay.overlayWindow", { get: function overlayWindow() overlay.closure.overlayWindow }),
77
78     compileMatcher: deprecated("DOM.compileMatcher", { get: function compileMatcher() DOM.compileMatcher }),
79     computedStyle: deprecated("DOM#style", function computedStyle(elem) DOM(elem).style),
80     domToString: deprecated("DOM.stringify", { get: function domToString() DOM.stringify }),
81     editableInputs: deprecated("DOM.editableInputs", { get: function editableInputs(elem) DOM.editableInputs }),
82     escapeHTML: deprecated("DOM.escapeHTML", { get: function escapeHTML(elem) DOM.escapeHTML }),
83     evaluateXPath: deprecated("DOM.XPath",
84         function evaluateXPath(path, elem, asIterator) DOM.XPath(path, elem || util.activeWindow.content.document, asIterator)),
85     isVisible: deprecated("DOM#isVisible", function isVisible(elem) DOM(elem).isVisible),
86     makeXPath: deprecated("DOM.makeXPath", { get: function makeXPath(elem) DOM.makeXPath }),
87     namespaces: deprecated("DOM.namespaces", { get: function namespaces(elem) DOM.namespaces }),
88     namespaceNames: deprecated("DOM.namespaceNames", { get: function namespaceNames(elem) DOM.namespaceNames }),
89     parseForm: deprecated("DOM#formData", function parseForm(elem) values(DOM(elem).formData).toArray()),
90     scrollIntoView: deprecated("DOM#scrollIntoView", function scrollIntoView(elem, alignWithTop) DOM(elem).scrollIntoView(alignWithTop)),
91     validateMatcher: deprecated("DOM.validateMatcher", { get: function validateMatcher() DOM.validateMatcher }),
92     xmlToDom: deprecated("DOM.fromJSON", function xmlToDom() DOM.fromXML.apply(DOM, arguments)),
93
94     map: deprecated("iter.map", function map(obj, fn, self) iter(obj).map(fn, self).toArray()),
95     writeToClipboard: deprecated("dactyl.clipboardWrite", function writeToClipboard(str, verbose) util.dactyl.clipboardWrite(str, verbose)),
96     readFromClipboard: deprecated("dactyl.clipboardRead", function readFromClipboard() util.dactyl.clipboardRead(false)),
97
98     chromePackages: deprecated("config.chromePackages", { get: function chromePackages() config.chromePackages }),
99     haveGecko: deprecated("config.haveGecko", { get: function haveGecko() config.closure.haveGecko }),
100     OS: deprecated("config.OS", { get: function OS() config.OS }),
101
102     dactyl: update(function dactyl(obj) {
103         if (obj)
104             var global = Class.objectGlobal(obj);
105
106         return {
107             __noSuchMethod__: function __noSuchMethod__(meth, args) {
108                 let win = overlay.activeWindow;
109
110                 var dactyl = global && global.dactyl || win && win.dactyl;
111                 if (!dactyl)
112                     return null;
113
114                 let prop = dactyl[meth];
115                 if (callable(prop))
116                     return prop.apply(dactyl, args);
117                 return prop;
118             }
119         };
120     }, {
121         __noSuchMethod__: function __noSuchMethod__() this().__noSuchMethod__.apply(null, arguments)
122     }),
123
124     /**
125      * Registers a obj as a new observer with the observer service. obj.observe
126      * must be an object where each key is the name of a target to observe and
127      * each value is a function(subject, data) to be called when the given
128      * target is broadcast. obj.observe will be replaced with a new opaque
129      * function. The observer is automatically unregistered on application
130      * shutdown.
131      *
132      * @param {object} obj
133      */
134     addObserver: update(function addObserver(obj) {
135         if (!obj.observers)
136             obj.observers = obj.observe;
137
138         let cleanup = ["dactyl-cleanup-modules", "quit-application"];
139
140         function register(meth) {
141             for (let target in Set(cleanup.concat(Object.keys(obj.observers))))
142                 try {
143                     services.observer[meth](obj, target, true);
144                 }
145                 catch (e) {}
146         }
147
148         Class.replaceProperty(obj, "observe",
149             function (subject, target, data) {
150                 try {
151                     if (~cleanup.indexOf(target))
152                         register("removeObserver");
153                     if (obj.observers[target])
154                         obj.observers[target].call(obj, subject, data);
155                 }
156                 catch (e) {
157                     if (typeof util === "undefined")
158                         addObserver.dump("dactyl: error: " + e + "\n" + (e.stack || addObserver.Error().stack).replace(/^/gm, "dactyl:    "));
159                     else
160                         util.reportError(e);
161                 }
162             });
163
164         obj.observe.unregister = () => register("removeObserver");
165         register("addObserver");
166     }, { dump: dump, Error: Error }),
167
168     /*
169      * Tests a condition and throws a FailedAssertion error on
170      * failure.
171      *
172      * @param {boolean} condition The condition to test.
173      * @param {string} message The message to present to the
174      *     user on failure.
175      */
176     assert: function assert(condition, message, quiet) {
177         if (!condition)
178             throw FailedAssertion(message, 1, quiet === undefined ? true : quiet);
179         return condition;
180     },
181
182     /**
183      * CamelCases a -non-camel-cased identifier name.
184      *
185      * @param {string} name The name to mangle.
186      * @returns {string} The mangled name.
187      */
188     camelCase: function camelCase(name) String.replace(name, /-(.)/g,
189                                                        (m, m1) => m1.toUpperCase()),
190
191     /**
192      * Capitalizes the first character of the given string.
193      * @param {string} str The string to capitalize
194      * @returns {string}
195      */
196     capitalize: function capitalize(str) str && str[0].toUpperCase() + str.slice(1).toLowerCase(),
197
198     /**
199      * Returns a RegExp object that matches characters specified in the range
200      * expression *list*, or signals an appropriate error if *list* is invalid.
201      *
202      * @param {string} list Character list, e.g., "a b d-xA-Z" produces /[abd-xA-Z]/.
203      * @param {string} accepted Character range(s) to accept, e.g. "a-zA-Z" for
204      *     ASCII letters. Used to validate *list*.
205      * @returns {RegExp}
206      */
207     charListToRegexp: function charListToRegexp(list, accepted) {
208         list = list.replace(/\s+/g, "");
209
210         // check for chars not in the accepted range
211         this.assert(RegExp("^[" + accepted + "-]+$").test(list),
212                     _("error.charactersOutsideRange", accepted.quote()));
213
214         // check for illegal ranges
215         for (let [match] in this.regexp.iterate(/.-./g, list))
216             this.assert(match.charCodeAt(0) <= match.charCodeAt(2),
217                         _("error.invalidCharacterRange", list.slice(list.indexOf(match))));
218
219         return RegExp("[" + util.regexp.escape(list) + "]");
220     },
221
222     /**
223      * Returns a shallow copy of *obj*.
224      *
225      * @param {Object} obj
226      * @returns {Object}
227      */
228     cloneObject: function cloneObject(obj) {
229         if (isArray(obj))
230             return obj.slice();
231         let newObj = {};
232         for (let [k, v] in Iterator(obj))
233             newObj[k] = v;
234         return newObj;
235     },
236
237     /**
238      * Clips a string to a given length. If the input string is longer
239      * than *length*, an ellipsis is appended.
240      *
241      * @param {string} str The string to truncate.
242      * @param {number} length The length of the returned string.
243      * @returns {string}
244      */
245     clip: function clip(str, length) {
246         return str.length <= length ? str : str.substr(0, length - 3) + "...";
247     },
248
249     /**
250      * Compares two strings, case insensitively. Return values are as
251      * in String#localeCompare.
252      *
253      * @param {string} a
254      * @param {string} b
255      * @returns {number}
256      */
257     compareIgnoreCase: function compareIgnoreCase(a, b) String.localeCompare(a.toLowerCase(), b.toLowerCase()),
258
259     compileFormat: function compileFormat(format) {
260         let stack = [frame()];
261         stack.__defineGetter__("top", function () this[this.length - 1]);
262
263         function frame() update(
264             function _frame(obj)
265                 _frame === stack.top || _frame.valid(obj)
266                     ? _frame.elements.map(e => callable(e) ? e(obj) : e)
267                                      .join("")
268                     : "",
269             {
270                 elements: [],
271                 seen: {},
272                 valid: function valid(obj) this.elements.every(e => !e.test || e.test(obj))
273             });
274
275         let end = 0;
276         for (let match in util.regexp.iterate(/(.*?)%(.)/gy, format)) {
277
278             let [, prefix, char] = match;
279             end += match[0].length;
280
281             if (prefix)
282                 stack.top.elements.push(prefix);
283             if (char === "%")
284                 stack.top.elements.push("%");
285             else if (char === "[") {
286                 let f = frame();
287                 stack.top.elements.push(f);
288                 stack.push(f);
289             }
290             else if (char === "]") {
291                 stack.pop();
292                 util.assert(stack.length, /*L*/"Unmatched %] in format");
293             }
294             else {
295                 let quote = function quote(obj, char) obj[char];
296                 if (char !== char.toLowerCase())
297                     quote = function quote(obj, char) Commands.quote(obj[char]);
298                 char = char.toLowerCase();
299
300                 stack.top.elements.push(update(
301                     function (obj) obj[char] != null ? quote(obj, char)
302                                                      : "",
303                     { test: function test(obj) obj[char] != null }));
304
305                 for (let elem in array.iterValues(stack))
306                     elem.seen[char] = true;
307             }
308         }
309         if (end < format.length)
310             stack.top.elements.push(format.substr(end));
311
312         util.assert(stack.length === 1, /*L*/"Unmatched %[ in format");
313         return stack.top;
314     },
315
316     /**
317      * Compiles a macro string into a function which generates a string
318      * result based on the input *macro* and its parameters. The
319      * definitive documentation for macro strings resides in :help
320      * macro-string.
321      *
322      * Macro parameters may have any of the following flags:
323      *     e: The parameter is only tested for existence. Its
324      *        interpolation is always empty.
325      *     q: The result is quoted such that it is parsed as a single
326      *        argument by the Ex argument parser.
327      *
328      * The returned function has the following additional properties:
329      *
330      *     seen {set}: The set of parameters used in this macro.
331      *
332      *     valid {function(object)}: Returns true if every parameter of
333      *          this macro is provided by the passed object.
334      *
335      * @param {string} macro The macro string to compile.
336      * @param {boolean} keepUnknown If true, unknown macro parameters
337      *      are left untouched. Otherwise, they are replaced with the null
338      *      string.
339      * @returns {function}
340      */
341     compileMacro: function compileMacro(macro, keepUnknown) {
342         let stack = [frame()];
343         stack.__defineGetter__("top", function () this[this.length - 1]);
344
345         let unknown = util.identity;
346         if (!keepUnknown)
347             unknown = () => "";
348
349         function frame() update(
350             function _frame(obj)
351                 _frame === stack.top || _frame.valid(obj)
352                     ? _frame.elements.map(e => callable(e) ? e(obj) : e)
353                             .join("")
354                     : "",
355             {
356                 elements: [],
357                 seen: {},
358                 valid: function valid(obj) this.elements.every(e => (!e.test || e.test(obj)))
359             });
360
361         let defaults = { lt: "<", gt: ">" };
362
363         let re = util.regexp(literal(/*
364             ([^]*?) // 1
365             (?:
366                 (<\{) | // 2
367                 (< ((?:[a-z]-)?[a-z-]+?) (?:\[([0-9]+)\])? >) | // 3 4 5
368                 (\}>) // 6
369             )
370         */), "gixy");
371         macro = String(macro);
372         let end = 0;
373         for (let match in re.iterate(macro)) {
374             let [, prefix, open, full, macro, idx, close] = match;
375             end += match[0].length;
376
377             if (prefix)
378                 stack.top.elements.push(prefix);
379             if (open) {
380                 let f = frame();
381                 stack.top.elements.push(f);
382                 stack.push(f);
383             }
384             else if (close) {
385                 stack.pop();
386                 util.assert(stack.length, /*L*/"Unmatched }> in macro");
387             }
388             else {
389                 let [, flags, name] = /^((?:[a-z]-)*)(.*)/.exec(macro);
390                 flags = Set(flags);
391
392                 let quote = util.identity;
393                 if (flags.q)
394                     quote = function quote(obj) typeof obj === "number" ? obj : String.quote(obj);
395                 if (flags.e)
396                     quote = function quote(obj) "";
397
398                 if (Set.has(defaults, name))
399                     stack.top.elements.push(quote(defaults[name]));
400                 else {
401                     let index = idx;
402                     if (idx) {
403                         idx = Number(idx) - 1;
404                         stack.top.elements.push(update(
405                             obj => obj[name] != null && idx in obj[name] ? quote(obj[name][idx])
406                                                                                  : Set.has(obj, name) ? "" : unknown(full),
407                             {
408                                 test: function test(obj) obj[name] != null && idx in obj[name]
409                                                       && obj[name][idx] !== false
410                                                       && (!flags.e || obj[name][idx] != "")
411                             }));
412                     }
413                     else {
414                         stack.top.elements.push(update(
415                             obj => obj[name] != null ? quote(obj[name])
416                                                              : Set.has(obj, name) ? "" : unknown(full),
417                             {
418                                 test: function test(obj) obj[name] != null
419                                                       && obj[name] !== false
420                                                       && (!flags.e || obj[name] != "")
421                             }));
422                     }
423
424                     for (let elem in array.iterValues(stack))
425                         elem.seen[name] = true;
426                 }
427             }
428         }
429         if (end < macro.length)
430             stack.top.elements.push(macro.substr(end));
431
432         util.assert(stack.length === 1, /*L*/"Unmatched <{ in macro");
433         return stack.top;
434     },
435
436     /**
437      * Converts any arbitrary string into an URI object. Returns null on
438      * failure.
439      *
440      * @param {string} str
441      * @returns {nsIURI|null}
442      */
443     createURI: function createURI(str) {
444         try {
445             let uri = services.urifixup.createFixupURI(str, services.urifixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP);
446             uri instanceof Ci.nsIURL;
447             return uri;
448         }
449         catch (e) {
450             return null;
451         }
452     },
453
454     /**
455      * Expands brace globbing patterns in a string.
456      *
457      * Example:
458      *     "a{b,c}d" => ["abd", "acd"]
459      *
460      * @param {string|[string|Array]} pattern The pattern to deglob.
461      * @returns [string] The resulting strings.
462      */
463     debrace: function debrace(pattern) {
464         try {
465             if (isArray(pattern)) {
466                 // Jägermonkey hates us.
467                 let obj = ({
468                     res: [],
469                     rec: function rec(acc) {
470                         let vals;
471
472                         while (isString(vals = pattern[acc.length]))
473                             acc.push(vals);
474
475                         if (acc.length == pattern.length)
476                             this.res.push(acc.join(""));
477                         else
478                             for (let val in values(vals))
479                                 this.rec(acc.concat(val));
480                     }
481                 });
482                 obj.rec([]);
483                 return obj.res;
484             }
485
486             if (pattern.indexOf("{") == -1)
487                 return [pattern];
488
489             let res = [];
490
491             let split = function split(pattern, re, fn, dequote) {
492                 let end = 0, match, res = [];
493                 while (match = re.exec(pattern)) {
494                     end = match.index + match[0].length;
495                     res.push(match[1]);
496                     if (fn)
497                         fn(match);
498                 }
499                 res.push(pattern.substr(end));
500                 return res.map(s => util.dequote(s, dequote));
501             };
502
503             let patterns = [];
504             let substrings = split(pattern, /((?:[^\\{]|\\.)*)\{((?:[^\\}]|\\.)*)\}/gy,
505                 function (match) {
506                     patterns.push(split(match[2], /((?:[^\\,]|\\.)*),/gy,
507                         null, ",{}"));
508                 }, "{}");
509
510             let rec = function rec(acc) {
511                 if (acc.length == patterns.length)
512                     res.push(array(substrings).zip(acc).flatten().join(""));
513                 else
514                     for (let [, pattern] in Iterator(patterns[acc.length]))
515                         rec(acc.concat(pattern));
516             };
517             rec([]);
518             return res;
519         }
520         catch (e if e.message && ~e.message.indexOf("res is undefined")) {
521             // prefs.safeSet() would be reset on :rehash
522             prefs.set("javascript.options.methodjit.chrome", false);
523             util.dactyl.warn(_(UTF8("error.damnYouJägermonkey")));
524             return [];
525         }
526     },
527
528     /**
529      * Briefly delay the execution of the passed function.
530      *
531      * @param {function} callback The function to delay.
532      */
533     delay: function delay(callback) {
534         let { mainThread } = services.threading;
535         mainThread.dispatch(callback,
536                             mainThread.DISPATCH_NORMAL);
537     },
538
539     /**
540      * Removes certain backslash-quoted characters while leaving other
541      * backslash-quoting sequences untouched.
542      *
543      * @param {string} pattern The string to unquote.
544      * @param {string} chars The characters to unquote.
545      * @returns {string}
546      */
547     dequote: function dequote(pattern, chars)
548         pattern.replace(/\\(.)/, (m0, m1) => chars.indexOf(m1) >= 0 ? m1 : m0),
549
550     /**
551      * Returns the nsIDocShell for the given window.
552      *
553      * @param {Window} win The window for which to get the docShell.
554      * @returns {nsIDocShell}
555      */
556
557      docShell: function docShell(win)
558             win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
559                .QueryInterface(Ci.nsIDocShell),
560
561     /**
562      * Prints a message to the console. If *msg* is an object it is pretty
563      * printed.
564      *
565      * @param {string|Object} msg The message to print.
566      */
567     dump: defineModule.dump,
568
569     /**
570      * Returns a list of reformatted stack frames from
571      * {@see Error#stack}.
572      *
573      * @param {string} stack The stack trace from an Error.
574      * @returns {[string]} The stack frames.
575      */
576     stackLines: function stackLines(stack) {
577         let lines = [];
578         let match, re = /([^]*?)@([^@\n]*)(?:\n|$)/g;
579         while (match = re.exec(stack))
580             lines.push(match[1].replace(/\n/g, "\\n").substr(0, 80) + "@" +
581                        util.fixURI(match[2]));
582         return lines;
583     },
584
585     /**
586      * Dumps a stack trace to the console.
587      *
588      * @param {string} msg The trace message.
589      * @param {number} frames The number of frames to print.
590      */
591     dumpStack: function dumpStack(msg="Stack", frames=null) {
592         let stack = util.stackLines(Error().stack);
593         stack = stack.slice(1, 1 + (frames || stack.length)).join("\n").replace(/^/gm, "    ");
594         util.dump(msg + "\n" + stack + "\n");
595     },
596
597     /**
598      * Escapes quotes, newline and tab characters in *str*. The returned string
599      * is delimited by *delimiter* or " if *delimiter* is not specified.
600      * {@see String#quote}.
601      *
602      * @param {string} str
603      * @param {string} delimiter
604      * @returns {string}
605      */
606     escapeString: function escapeString(str, delimiter) {
607         if (delimiter == undefined)
608             delimiter = '"';
609         return delimiter + str.replace(/([\\'"])/g, "\\$1").replace("\n", "\\n", "g").replace("\t", "\\t", "g") + delimiter;
610     },
611
612     /**
613      * Converts *bytes* to a pretty printed data size string.
614      *
615      * @param {number} bytes The number of bytes.
616      * @param {string} decimalPlaces The number of decimal places to use if
617      *     *humanReadable* is true.
618      * @param {boolean} humanReadable Use byte multiples.
619      * @returns {string}
620      */
621     formatBytes: function formatBytes(bytes, decimalPlaces, humanReadable) {
622         const unitVal = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
623         let unitIndex = 0;
624         let tmpNum = parseInt(bytes, 10) || 0;
625         let strNum = [tmpNum + ""];
626
627         if (humanReadable) {
628             while (tmpNum >= 1024) {
629                 tmpNum /= 1024;
630                 if (++unitIndex > (unitVal.length - 1))
631                     break;
632             }
633
634             let decPower = Math.pow(10, decimalPlaces);
635             strNum = ((Math.round(tmpNum * decPower) / decPower) + "").split(".", 2);
636
637             if (!strNum[1])
638                 strNum[1] = "";
639
640             while (strNum[1].length < decimalPlaces) // pad with "0" to the desired decimalPlaces)
641                 strNum[1] += "0";
642         }
643
644         for (let u = strNum[0].length - 3; u > 0; u -= 3) // make a 10000 a 10,000
645             strNum[0] = strNum[0].substr(0, u) + "," + strNum[0].substr(u);
646
647         if (unitIndex) // decimalPlaces only when > Bytes
648             strNum[0] += "." + strNum[1];
649
650         return strNum[0] + " " + unitVal[unitIndex];
651     },
652
653     /**
654      * Converts *seconds* into a human readable time string.
655      *
656      * @param {number} seconds
657      * @returns {string}
658      */
659     formatSeconds: function formatSeconds(seconds) {
660         function pad(n, val) ("0000000" + val).substr(-Math.max(n, String(val).length));
661         function div(num, denom) [Math.floor(num / denom), Math.round(num % denom)];
662         let days, hours, minutes;
663
664         [minutes, seconds] = div(Math.round(seconds), 60);
665         [hours, minutes]   = div(minutes, 60);
666         [days, hours]      = div(hours,   24);
667         if (days)
668             return /*L*/days + " days " + hours + " hours";
669         if (hours)
670             return /*L*/hours + "h " + minutes + "m";
671         if (minutes)
672             return /*L*/minutes + ":" + pad(2, seconds);
673         return /*L*/seconds + "s";
674     },
675
676     /**
677      * Returns the file which backs a given URL, if available.
678      *
679      * @param {nsIURI} uri The URI for which to find a file.
680      * @returns {File|null}
681      */
682     getFile: function getFile(uri) {
683         try {
684             if (isString(uri))
685                 uri = util.newURI(uri);
686
687             if (uri instanceof Ci.nsIFileURL)
688                 return File(uri.file);
689
690             if (uri instanceof Ci.nsIFile)
691                 return File(uri);
692
693             let channel = services.io.newChannelFromURI(uri);
694             try { channel.cancel(Cr.NS_BINDING_ABORTED); } catch (e) {}
695             if (channel instanceof Ci.nsIFileChannel)
696                 return File(channel.file);
697         }
698         catch (e) {}
699         return null;
700     },
701
702     /**
703      * Returns the host for the given URL, or null if invalid.
704      *
705      * @param {string} url
706      * @returns {string|null}
707      */
708     getHost: function getHost(url) {
709         try {
710             return util.createURI(url).host;
711         }
712         catch (e) {}
713         return null;
714     },
715
716     /**
717      * Sends a synchronous or asynchronous HTTP request to *url* and returns
718      * the XMLHttpRequest object. If *callback* is specified the request is
719      * asynchronous and the *callback* is invoked with the object as its
720      * argument.
721      *
722      * @param {string} url
723      * @param {object} params Optional parameters for this request:
724      *    method: {string} The request method. @default "GET"
725      *
726      *    params: {object} Parameters to append to *url*'s query string.
727      *    data: {*} POST data to send to the server. Ordinary objects
728      *              are converted to FormData objects, with one datum
729      *              for each property/value pair.
730      *
731      *    onload:   {function(XMLHttpRequest, Event)} The request's load event handler.
732      *    onerror:  {function(XMLHttpRequest, Event)} The request's error event handler.
733      *    callback: {function(XMLHttpRequest, Event)} An event handler
734      *              called for either error or load events.
735      *
736      *    background: {boolean} Whether to perform the request in the
737      *                background. @default true
738      *
739      *    mimeType: {string} Override the response mime type with the
740      *              given value.
741      *    responseType: {string} Override the type of the "response"
742      *                  property.
743      *
744      *    headers: {objects} Extra request headers.
745      *
746      *    user: {string} The user name to send via HTTP Authentication.
747      *    pass: {string} The password to send via HTTP Authentication.
748      *
749      *    quiet: {boolean} If true, don't report errors.
750      *
751      * @returns {XMLHttpRequest}
752      */
753     httpGet: function httpGet(url, callback, self) {
754         let params = callback;
755         if (!isObject(params))
756             params = { callback: params && ((...args) => callback.apply(self, args)) };
757
758         try {
759             let xmlhttp = services.Xmlhttp();
760             xmlhttp.mozBackgroundRequest = Set.has(params, "background") ? params.background : true;
761
762             let async = params.callback || params.onload || params.onerror;
763             if (async) {
764                 xmlhttp.addEventListener("load",  function handler(event) { util.trapErrors(params.onload  || params.callback, params, xmlhttp, event); }, false);
765                 xmlhttp.addEventListener("error", function handler(event) { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event); }, false);
766             }
767
768             if (isObject(params.params)) {
769                 let data = [encodeURIComponent(k) + "=" + encodeURIComponent(v)
770                             for ([k, v] in iter(params.params))];
771                 let uri = util.newURI(url);
772                 uri.query += (uri.query ? "&" : "") + data.join("&");
773
774                 url = uri.spec;
775             }
776
777             if (isObject(params.data) && !(params.data instanceof Ci.nsISupports)) {
778                 let data = services.FormData();
779                 for (let [k, v] in iter(params.data))
780                     data.append(k, v);
781                 params.data = data;
782             }
783
784             if (params.mimeType)
785                 xmlhttp.overrideMimeType(params.mimeType);
786
787             let args = [params.method || "GET", url, async];
788             if (params.user != null || params.pass != null)
789                 args.push(params.user);
790             if (params.pass != null)
791                 args.push(prams.pass);
792             xmlhttp.open.apply(xmlhttp, args);
793
794             for (let [header, val] in Iterator(params.headers || {}))
795                 xmlhttp.setRequestHeader(header, val);
796
797             if (params.responseType)
798                 xmlhttp.responseType = params.responseType;
799
800             if (params.notificationCallbacks)
801                 xmlhttp.channel.notificationCallbacks = params.notificationCallbacks;
802
803             xmlhttp.send(params.data);
804             return xmlhttp;
805         }
806         catch (e) {
807             if (!params.quiet)
808                 util.reportError(e);
809             return null;
810         }
811     },
812
813     /**
814      * The identity function.
815      *
816      * @param {Object} k
817      * @returns {Object}
818      */
819     identity: function identity(k) k,
820
821     /**
822      * Returns the intersection of two rectangles.
823      *
824      * @param {Object} r1
825      * @param {Object} r2
826      * @returns {Object}
827      */
828     intersection: function intersection(r1, r2) ({
829         get width()  this.right - this.left,
830         get height() this.bottom - this.top,
831         left: Math.max(r1.left, r2.left),
832         right: Math.min(r1.right, r2.right),
833         top: Math.max(r1.top, r2.top),
834         bottom: Math.min(r1.bottom, r2.bottom)
835     }),
836
837     /**
838      * Returns true if the given stack frame resides in Dactyl code.
839      *
840      * @param {nsIStackFrame} frame
841      * @returns {boolean}
842      */
843     isDactyl: Class.Memoize(function () {
844         let base = util.regexp.escape(Components.stack.filename.replace(/[^\/]+$/, ""));
845         let re = RegExp("^(?:.* -> )?(?:resource://dactyl(?!-content/eval.js)|" + base + ")\\S+$");
846         return function isDactyl(frame) re.test(frame.filename);
847     }),
848
849     /**
850      * Returns true if *url* is in the domain *domain*.
851      *
852      * @param {string} url
853      * @param {string} domain
854      * @returns {boolean}
855      */
856     isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain),
857
858     /**
859      * Returns true if *host* is a subdomain of *domain*.
860      *
861      * @param {string} host The host to check.
862      * @param {string} domain The base domain to check the host against.
863      * @returns {boolean}
864      */
865     isSubdomain: function isSubdomain(host, domain) {
866         if (host == null)
867             return false;
868         let idx = host.lastIndexOf(domain);
869         return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx - 1] == ".");
870     },
871
872     /**
873      * Iterates over all currently open documents, including all
874      * top-level window and sub-frames thereof.
875      */
876     iterDocuments: function iterDocuments(types) {
877         types = types ? types.map(s => "type" + util.capitalize(s))
878                       : ["typeChrome", "typeContent"];
879
880         let windows = services.windowMediator.getXULWindowEnumerator(null);
881         while (windows.hasMoreElements()) {
882             let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
883             for each (let type in types) {
884                 let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type],
885                                                                       Ci.nsIDocShell.ENUMERATE_FORWARDS);
886                 while (docShells.hasMoreElements())
887                     let (viewer = docShells.getNext().QueryInterface(Ci.nsIDocShell).contentViewer) {
888                         if (viewer)
889                             yield viewer.DOMDocument;
890                     };
891             }
892         }
893     },
894
895     // ripped from Firefox; modified
896     unsafeURI: Class.Memoize(() => util.regexp(String.replace(literal(/*
897             [
898                 \s
899                 // Invisible characters (bug 452979)
900                 U001C U001D U001E U001F // file/group/record/unit separator
901                 U00AD // Soft hyphen
902                 UFEFF // BOM
903                 U2060 // Word joiner
904                 U2062 U2063 // Invisible times/separator
905                 U200B UFFFC // Zero-width space/no-break space
906
907                 // Bidi formatting characters. (RFC 3987 sections 3.2 and 4.1 paragraph 6)
908                 U200E U200F U202A U202B U202C U202D U202E
909             ]
910         */), /U/g, "\\u"),
911         "gx")),
912     losslessDecodeURI: function losslessDecodeURI(url) {
913         return url.split("%25").map(function (url) {
914                 // Non-UTF-8 compliant URLs cause "malformed URI sequence" errors.
915                 try {
916                     return decodeURI(url).replace(this.unsafeURI, encodeURIComponent);
917                 }
918                 catch (e) {
919                     return url;
920                 }
921             }, this).join("%25").replace(/[\s.,>)]$/, encodeURIComponent);
922     },
923
924     /**
925      * Creates a DTD fragment from the given object. Each property of
926      * the object is converted to an ENTITY declaration. SGML special
927      * characters other than ' and % are left intact.
928      *
929      * @param {object} obj The object to convert.
930      * @returns {string} The DTD fragment containing entity declaration
931      *      for *obj*.
932      */
933     makeDTD: let (map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" })
934         function makeDTD(obj) {
935             function escape(val) {
936                let isDOM = DOM.isJSONXML(val);
937                return String.replace(val == null ? "null" :
938                                      isDOM       ? DOM.toXML(val)
939                                                  : val,
940                                      isDOM ? /['%]/g
941                                            : /['"%&<>]/g,
942                                      m => map[m]);
943             }
944
945             return iter(obj).map(([k, v]) =>
946                                  ["<!ENTITY ", k, " '", escape(v), "'>"].join(""))
947                             .join("\n");
948         },
949
950     /**
951      * Converts a URI string into a URI object.
952      *
953      * @param {string} uri
954      * @returns {nsIURI}
955      */
956     newURI: function newURI(uri, charset, base) {
957         if (uri instanceof Ci.nsIURI)
958             var res = uri.clone();
959         else {
960             let idx = uri.lastIndexOf(" -> ");
961             if (~idx)
962                 uri = uri.slice(idx + 4);
963
964             res = this.withProperErrors("newURI", services.io, uri, charset, base);
965         }
966         res instanceof Ci.nsIURL;
967         return res;
968     },
969
970     /**
971      * Removes leading garbage prepended to URIs by the subscript
972      * loader.
973      */
974     fixURI: function fixURI(url) String.replace(url, /.* -> /, ""),
975
976     /**
977      * Pretty print a JavaScript object. Use HTML markup to color certain items
978      * if *color* is true.
979      *
980      * @param {Object} object The object to pretty print.
981      * @param {boolean} color Whether the output should be colored.
982      * @returns {string}
983      */
984     objectToString: function objectToString(object, color) {
985         if (object == null)
986             return object + "\n";
987
988         if (!isObject(object))
989             return String(object);
990
991         if (object instanceof Ci.nsIDOMElement) {
992             let elem = object;
993             if (elem.nodeType == elem.TEXT_NODE)
994                 return elem.data;
995
996             return DOM(elem).repr(color);
997         }
998
999         try { // for window.JSON
1000             var obj = String(object);
1001         }
1002         catch (e) {
1003             obj = Object.prototype.toString.call(obj);
1004         }
1005
1006         if (color) {
1007             obj = template.highlightFilter(util.clip(obj, 150), "\n",
1008                                            () => ["span", { highlight: "NonText" },
1009                                                       "^J"]);
1010
1011             var head = ["span", { highlight: "Title Object" }, obj, "::\n"];
1012         }
1013         else
1014             head = util.clip(obj, 150).replace(/\n/g, "^J") + "::\n";
1015
1016         let keys = [];
1017
1018         // window.content often does not want to be queried with "var i in object"
1019         try {
1020             let hasValue = !("__iterator__" in object || isinstance(object, ["Generator", "Iterator"]));
1021             if (object.dactyl && object.modules && object.modules.modules == object.modules) {
1022                 object = Iterator(object);
1023                 hasValue = false;
1024             }
1025             let keyIter = object;
1026             if ("__iterator__" in object && !callable(object.__iterator__))
1027                 keyIter = keys(object);
1028
1029             for (let i in keyIter) {
1030                 let value = Magic("<no value>");
1031                 try {
1032                     value = object[i];
1033                 }
1034                 catch (e) {}
1035                 if (!hasValue) {
1036                     if (isArray(i) && i.length == 2)
1037                         [i, value] = i;
1038                     else {
1039                         var noVal = true;
1040                         value = i;
1041                     }
1042                 }
1043
1044                 let key = i;
1045                 if (!isNaN(i))
1046                     i = parseInt(i);
1047                 else if (/^[A-Z_]+$/.test(i))
1048                     i = "";
1049
1050                 if (color)
1051                     value = template.highlight(value, true, 150, !color);
1052                 else if (value instanceof Magic)
1053                     value = String(value);
1054                 else
1055                     value = util.clip(String(value).replace(/\n/g, "^J"), 150);
1056
1057                 if (noVal)
1058                     var val = value;
1059                 else if (color)
1060                     val = [["span", { highlight: "Key" }, key], ": ", value];
1061                 else
1062                     val = key + ": " + value;
1063
1064                 keys.push([i, val]);
1065             }
1066         }
1067         catch (e) {
1068             util.reportError(e);
1069         }
1070
1071         function compare(a, b) {
1072             if (!isNaN(a[0]) && !isNaN(b[0]))
1073                 return a[0] - b[0];
1074             return String.localeCompare(a[0], b[0]);
1075         }
1076
1077         let vals = template.map(keys.sort(compare), f => f[1],
1078                                 "\n");
1079
1080         if (color) {
1081             return ["div", { style: "white-space: pre-wrap" }, head, vals];
1082         }
1083         return head + vals.join("");
1084     },
1085
1086     prettifyJSON: function prettifyJSON(data, indent, invalidOK) {
1087         const INDENT = indent || "    ";
1088
1089         function rec(data, level, seen) {
1090             if (isObject(data)) {
1091                 if (~seen.indexOf(data))
1092                     throw Error("Recursive object passed");
1093                 seen = seen.concat([data]);
1094             }
1095
1096             let prefix = level + INDENT;
1097
1098             if (data === undefined)
1099                 data = null;
1100
1101             if (~["boolean", "number"].indexOf(typeof data) || data === null)
1102                 res.push(String(data));
1103             else if (isinstance(data, ["String", _]))
1104                 res.push(JSON.stringify(String(data)));
1105             else if (isArray(data)) {
1106                 if (data.length == 0)
1107                     res.push("[]");
1108                 else {
1109                     res.push("[\n");
1110                     for (let [i, val] in Iterator(data)) {
1111                         if (i)
1112                             res.push(",\n");
1113                         res.push(prefix);
1114                         rec(val, prefix, seen);
1115                     }
1116                     res.push("\n", level, "]");
1117                 }
1118             }
1119             else if (isObject(data)) {
1120                 res.push("{\n");
1121
1122                 let i = 0;
1123                 for (let [key, val] in Iterator(data)) {
1124                     if (i++)
1125                         res.push(",\n");
1126                     res.push(prefix, JSON.stringify(key), ": ");
1127                     rec(val, prefix, seen);
1128                 }
1129                 if (i > 0)
1130                     res.push("\n", level, "}");
1131                 else
1132                     res[res.length - 1] = "{}";
1133             }
1134             else if (invalidOK)
1135                 res.push({}.toString.call(data));
1136             else
1137                 throw Error("Invalid JSON object");
1138         }
1139
1140         let res = [];
1141         rec(data, "", []);
1142         return res.join("");
1143     },
1144
1145     observers: {
1146         "dactyl-cleanup-modules": function cleanupModules(subject, reason) {
1147             defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules " + reason);
1148
1149             for (let module in values(defineModule.modules))
1150                 if (module.cleanup) {
1151                     util.dump("cleanup: " + module.constructor.className);
1152                     util.trapErrors(module.cleanup, module, reason);
1153                 }
1154
1155             JSMLoader.cleanup();
1156
1157             if (!this.rehashing)
1158                 services.observer.addObserver(this, "dactyl-rehash", true);
1159         },
1160         "dactyl-rehash": function dactylRehash() {
1161             services.observer.removeObserver(this, "dactyl-rehash");
1162
1163             defineModule.loadLog.push("dactyl: util: observe: dactyl-rehash");
1164             if (!this.rehashing)
1165                 for (let module in values(defineModule.modules)) {
1166                     defineModule.loadLog.push("dactyl: util: init(" + module + ")");
1167                     if (module.reinit)
1168                         module.reinit();
1169                     else
1170                         module.init();
1171                 }
1172         },
1173         "dactyl-purge": function dactylPurge() {
1174             this.rehashing = 1;
1175         },
1176     },
1177
1178     /**
1179      * A generator that returns the values between *start* and *end*, in *step*
1180      * increments.
1181      *
1182      * @param {number} start The interval's start value.
1183      * @param {number} end The interval's end value.
1184      * @param {boolean} step The value to step the range by. May be
1185      *     negative. @default 1
1186      * @returns {Iterator(Object)}
1187      */
1188     range: function range(start, end, step) {
1189         if (!step)
1190             step = 1;
1191         if (step > 0) {
1192             for (; start < end; start += step)
1193                 yield start;
1194         }
1195         else {
1196             while (start > end)
1197                 yield start += step;
1198         }
1199     },
1200
1201     /**
1202      * An interruptible generator that returns all values between *start* and
1203      * *end*. The thread yields every *time* milliseconds.
1204      *
1205      * @param {number} start The interval's start value.
1206      * @param {number} end The interval's end value.
1207      * @param {number} time The time in milliseconds between thread yields.
1208      * @returns {Iterator(Object)}
1209      */
1210     interruptibleRange: function interruptibleRange(start, end, time) {
1211         let endTime = Date.now() + time;
1212         while (start < end) {
1213             if (Date.now() > endTime) {
1214                 util.threadYield(true, true);
1215                 endTime = Date.now() + time;
1216             }
1217             yield start++;
1218         }
1219     },
1220
1221     /**
1222      * Creates a new RegExp object based on the value of expr stripped
1223      * of all white space and interpolated with the values from tokens.
1224      * If tokens, any string in the form of <key> in expr is replaced
1225      * with the value of the property, 'key', from tokens, if that
1226      * property exists. If the property value is itself a RegExp, its
1227      * source is substituted rather than its string value.
1228      *
1229      * Additionally, expr is stripped of all JavaScript comments.
1230      *
1231      * This is similar to Perl's extended regular expression format.
1232      *
1233      * @param {string|XML} expr The expression to compile into a RegExp.
1234      * @param {string} flags Flags to apply to the new RegExp.
1235      * @param {object} tokens The tokens to substitute. @optional
1236      * @returns {RegExp} A custom regexp object.
1237      */
1238     regexp: update(function (expr, flags, tokens) {
1239         flags = flags || [k for ([k, v] in Iterator({ g: "global", i: "ignorecase", m: "multiline", y: "sticky" }))
1240                           if (expr[v])].join("");
1241
1242         if (isinstance(expr, ["RegExp"]))
1243             expr = expr.source;
1244
1245         expr = String.replace(expr, /\\(.)/, function (m, m1) {
1246             if (m1 === "c")
1247                 flags = flags.replace(/i/g, "") + "i";
1248             else if (m1 === "C")
1249                 flags = flags.replace(/i/g, "");
1250             else
1251                 return m;
1252             return "";
1253         });
1254
1255         // Replace replacement <tokens>.
1256         if (tokens)
1257             expr = String.replace(expr, /(\(?P)?<(\w+)>/g,
1258                                   (m, n1, n2) => !n1 && Set.has(tokens, n2) ?    tokens[n2].dactylSource
1259                                                                               || tokens[n2].source
1260                                                                               || tokens[n2]
1261                                                                             : m);
1262
1263         // Strip comments and white space.
1264         if (/x/.test(flags))
1265             expr = String.replace(expr, /(\\.)|\/\/[^\n]*|\/\*[^]*?\*\/|\s+/gm,
1266                                   (m, m1) => m1 || "");
1267
1268         // Replace (?P<named> parameters)
1269         if (/\(\?P</.test(expr)) {
1270             var source = expr;
1271             let groups = ["wholeMatch"];
1272             expr = expr.replace(/((?:[^[(\\]|\\.|\[(?:[^\]]|\\.)*\])*)\((?:\?P<([^>]+)>|(\?))?/gy,
1273                 function (m0, m1, m2, m3) {
1274                     if (!m3)
1275                         groups.push(m2 || "-group-" + groups.length);
1276                     return m1 + "(" + (m3 || "");
1277                 });
1278             var struct = Struct.apply(null, groups);
1279         }
1280
1281         let res = update(RegExp(expr, flags.replace("x", "")), {
1282             closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "closure")),
1283             dactylPropertyNames: ["exec", "match", "test", "toSource", "toString", "global", "ignoreCase", "lastIndex", "multiLine", "source", "sticky"],
1284             iterate: function iterate(str, idx) util.regexp.iterate(this, str, idx)
1285         });
1286
1287         // Return a struct with properties for named parameters if we
1288         // have them.
1289         if (struct)
1290             update(res, {
1291                 exec: function exec() let (match = exec.superapply(this, arguments)) match && struct.fromArray(match),
1292                 dactylSource: source, struct: struct
1293             });
1294         return res;
1295     }, {
1296         /**
1297          * Escapes Regular Expression special characters in *str*.
1298          *
1299          * @param {string} str
1300          * @returns {string}
1301          */
1302         escape: function regexp_escape(str) str.replace(/([\\{}()[\]^$.?*+|])/g, "\\$1"),
1303
1304         /**
1305          * Given a RegExp, returns its source in the form showable to the user.
1306          *
1307          * @param {RegExp} re The regexp showable source of which is to be returned.
1308          * @returns {string}
1309          */
1310         getSource: function regexp_getSource(re) re.source.replace(/\\(.)/g,
1311                                                                    (m0, m1) => m1 === "/" ? m1
1312                                                                                           : m0),
1313
1314         /**
1315          * Iterates over all matches of the given regexp in the given
1316          * string.
1317          *
1318          * @param {RegExp} regexp The regular expression to execute.
1319          * @param {string} string The string to search.
1320          * @param {number} lastIndex The index at which to begin searching. @optional
1321          */
1322         iterate: function iterate(regexp, string, lastIndex) iter(function () {
1323             regexp.lastIndex = lastIndex = lastIndex || 0;
1324             let match;
1325             while (match = regexp.exec(string)) {
1326                 lastIndex = regexp.lastIndex;
1327                 yield match;
1328                 regexp.lastIndex = lastIndex;
1329                 if (match[0].length == 0 || !regexp.global)
1330                     break;
1331             }
1332         }())
1333     }),
1334
1335     /**
1336      * Flushes the startup or jar cache.
1337      */
1338     flushCache: function flushCache(file) {
1339         if (file)
1340             services.observer.notifyObservers(file, "flush-cache-entry", "");
1341         else
1342             services.observer.notifyObservers(null, "startupcache-invalidate", "");
1343     },
1344
1345     /**
1346      * Reloads dactyl in entirety by disabling the add-on and
1347      * re-enabling it.
1348      */
1349     rehash: function rehash(args) {
1350         storage.storeForSession("commandlineArgs", args);
1351         this.timeout(function () {
1352             this.flushCache();
1353             this.rehashing = true;
1354             let addon = config.addon;
1355             addon.userDisabled = true;
1356             addon.userDisabled = false;
1357         });
1358     },
1359
1360     errorCount: 0,
1361     errors: Class.Memoize(() => []),
1362     maxErrors: 15,
1363     /**
1364      * Reports an error to the Error Console and the standard output,
1365      * along with a stack trace and other relevant information. The
1366      * error is appended to {@see #errors}.
1367      */
1368     reportError: function reportError(error) {
1369         if (error.noTrace)
1370             return;
1371
1372         if (isString(error))
1373             error = Error(error);
1374
1375         Cu.reportError(error);
1376
1377         try {
1378             this.errorCount++;
1379
1380             let obj = update({}, error, {
1381                 toString: function () String(error),
1382                 stack: Magic(util.stackLines(String(error.stack || Error().stack)).join("\n").replace(/^/mg, "\t"))
1383             });
1384
1385             services.console.logStringMessage(obj.stack);
1386
1387             this.errors.push([new Date, obj + "\n" + obj.stack]);
1388             this.errors = this.errors.slice(-this.maxErrors);
1389             this.errors.toString = function () [k + "\n" + v for ([k, v] in array.iterValues(this))].join("\n\n");
1390
1391             this.dump(String(error));
1392             this.dump(obj);
1393             this.dump("");
1394         }
1395         catch (e) {
1396             try {
1397                 this.dump(String(error));
1398                 this.dump(util.stackLines(error.stack).join("\n"));
1399             }
1400             catch (e) { dump(e + "\n"); }
1401         }
1402
1403         // ctypes.open("libc.so.6").declare("kill", ctypes.default_abi, ctypes.void_t, ctypes.int, ctypes.int)(
1404         //     ctypes.open("libc.so.6").declare("getpid", ctypes.default_abi, ctypes.int)(), 2)
1405     },
1406
1407     /**
1408      * Given a domain, returns an array of all non-toplevel subdomains
1409      * of that domain.
1410      *
1411      * @param {string} host The host for which to find subdomains.
1412      * @returns {[string]}
1413      */
1414     subdomains: function subdomains(host) {
1415         if (/(^|\.)\d+$|:.*:/.test(host))
1416             // IP address or similar
1417             return [host];
1418
1419         let base = host.replace(/.*\.(.+?\..+?)$/, "$1");
1420         try {
1421             base = services.tld.getBaseDomainFromHost(host);
1422         }
1423         catch (e) {}
1424
1425         let ary = host.split(".");
1426         ary = [ary.slice(i).join(".") for (i in util.range(ary.length, 0, -1))];
1427         return ary.filter(h => h.length >= base.length);
1428     },
1429
1430     /**
1431      * Returns the selection controller for the given window.
1432      *
1433      * @param {Window} window
1434      * @returns {nsISelectionController}
1435      */
1436     selectionController: function selectionController(win)
1437         win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1438            .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
1439            .QueryInterface(Ci.nsISelectionController),
1440
1441     /**
1442      * Escapes a string against shell meta-characters and argument
1443      * separators.
1444      */
1445     shellEscape: function shellEscape(str) '"' + String.replace(str, /[\\"$`]/g, "\\$&") + '"',
1446
1447     /**
1448      * Suspend execution for at least *delay* milliseconds. Functions by
1449      * yielding execution to the next item in the main event queue, and
1450      * so may lead to unexpected call graphs, and long delays if another
1451      * handler yields execution while waiting.
1452      *
1453      * @param {number} delay The time period for which to sleep in milliseconds.
1454      */
1455     sleep: function sleep(delay) {
1456         let mainThread = services.threading.mainThread;
1457
1458         let end = Date.now() + delay;
1459         while (Date.now() < end)
1460             mainThread.processNextEvent(true);
1461         return true;
1462     },
1463
1464     /**
1465      * Behaves like String.split, except that when *limit* is reached,
1466      * the trailing element contains the entire trailing portion of the
1467      * string.
1468      *
1469      *     util.split("a, b, c, d, e", /, /, 3) -> ["a", "b", "c, d, e"]
1470      *
1471      * @param {string} str The string to split.
1472      * @param {RegExp|string} re The regular expression on which to split the string.
1473      * @param {number} limit The maximum number of elements to return.
1474      * @returns {[string]}
1475      */
1476     split: function split(str, re, limit) {
1477         re.lastIndex = 0;
1478         if (!re.global)
1479             re = RegExp(re.source || re, "g");
1480         let match, start = 0, res = [];
1481         while (--limit && (match = re.exec(str)) && match[0].length) {
1482             res.push(str.substring(start, match.index));
1483             start = match.index + match[0].length;
1484         }
1485         res.push(str.substring(start));
1486         return res;
1487     },
1488
1489     /**
1490      * Split a string on literal occurrences of a marker.
1491      *
1492      * Specifically this ignores occurrences preceded by a backslash, or
1493      * contained within 'single' or "double" quotes.
1494      *
1495      * It assumes backslash escaping on strings, and will thus not count quotes
1496      * that are preceded by a backslash or within other quotes as starting or
1497      * ending quoted sections of the string.
1498      *
1499      * @param {string} str
1500      * @param {RegExp} marker
1501      * @returns {[string]}
1502      */
1503     splitLiteral: function splitLiteral(str, marker) {
1504         let results = [];
1505         let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source);
1506         let cont = true;
1507
1508         while (cont) {
1509             cont = false;
1510             str = str.replace(resep, function (match, before) {
1511                 results.push(before);
1512                 cont = match !== "";
1513                 return "";
1514             });
1515         }
1516
1517         results.push(str);
1518         return results;
1519     },
1520
1521     yielders: 0,
1522     /**
1523      * Yields execution to the next event in the current thread's event
1524      * queue. This is a potentially dangerous operation, since any
1525      * yielders higher in the event stack will prevent execution from
1526      * returning to the caller until they have finished their wait. The
1527      * potential for deadlock is high.
1528      *
1529      * @param {boolean} flush If true, flush all events in the event
1530      *      queue before returning. Otherwise, wait for an event to
1531      *      process before proceeding.
1532      * @param {boolean} interruptable If true, this yield may be
1533      *      interrupted by pressing <C-c>, in which case,
1534      *      Error("Interrupted") will be thrown.
1535      */
1536     threadYield: function threadYield(flush, interruptable) {
1537         this.yielders++;
1538         try {
1539             let mainThread = services.threading.mainThread;
1540             /* FIXME */
1541             util.interrupted = false;
1542             do {
1543                 mainThread.processNextEvent(!flush);
1544                 if (util.interrupted)
1545                     throw Error("Interrupted");
1546             }
1547             while (flush === true && mainThread.hasPendingEvents());
1548         }
1549         finally {
1550             this.yielders--;
1551         }
1552     },
1553
1554     /**
1555      * Waits for the function *test* to return true, or *timeout*
1556      * milliseconds to expire.
1557      *
1558      * @param {function} test The predicate on which to wait.
1559      * @param {object} self The 'this' object for *test*.
1560      * @param {Number} timeout The maximum number of milliseconds to
1561      *      wait.
1562      *      @optional
1563      * @param {boolean} interruptable If true, may be interrupted by
1564      *      pressing <C-c>, in which case, Error("Interrupted") will be
1565      *      thrown.
1566      */
1567     waitFor: function waitFor(test, self, timeout, interruptable) {
1568         let end = timeout && Date.now() + timeout, result;
1569
1570         let timer = services.Timer(function () {}, 10, services.Timer.TYPE_REPEATING_SLACK);
1571         try {
1572             while (!(result = test.call(self)) && (!end || Date.now() < end))
1573                 this.threadYield(false, interruptable);
1574         }
1575         finally {
1576             timer.cancel();
1577         }
1578         return result;
1579     },
1580
1581     /**
1582      * Makes the passed function yieldable. Each time the function calls
1583      * yield, execution is suspended for the yielded number of
1584      * milliseconds.
1585      *
1586      * Example:
1587      *      let func = yieldable(function () {
1588      *          util.dump(Date.now()); // 0
1589      *          yield 1500;
1590      *          util.dump(Date.now()); // 1500
1591      *      });
1592      *      func();
1593      *
1594      * @param {function} func The function to mangle.
1595      * @returns {function} A new function which may not execute
1596      *      synchronously.
1597      */
1598     yieldable: function yieldable(func)
1599         function magic() {
1600             let gen = func.apply(this, arguments);
1601             (function next() {
1602                 try {
1603                     util.timeout(next, gen.next());
1604                 }
1605                 catch (e if e instanceof StopIteration) {};
1606             })();
1607         },
1608
1609     /**
1610      * Wraps a callback function such that its errors are not lost. This
1611      * is useful for DOM event listeners, which ordinarily eat errors.
1612      * The passed function has the property *wrapper* set to the new
1613      * wrapper function, while the wrapper has the property *wrapped*
1614      * set to the original callback.
1615      *
1616      * @param {function} callback The callback to wrap.
1617      * @returns {function}
1618      */
1619     wrapCallback: wrapCallback,
1620
1621     /**
1622      * Returns the top-level chrome window for the given window.
1623      *
1624      * @param {Window} win The child window.
1625      * @returns {Window} The top-level parent window.
1626      */
1627     topWindow: function topWindow(win)
1628             win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1629                .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
1630                .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow),
1631
1632     /**
1633      * Traps errors in the called function, possibly reporting them.
1634      *
1635      * @param {function} func The function to call
1636      * @param {object} self The 'this' object for the function.
1637      */
1638     trapErrors: function trapErrors(func, self, ...args) {
1639         try {
1640             if (!callable(func))
1641                 func = self[func];
1642             return func.apply(self || this, args);
1643         }
1644         catch (e) {
1645             this.reportError(e);
1646             return undefined;
1647         }
1648     },
1649
1650     /**
1651      * Returns the file path of a given *url*, for debugging purposes.
1652      * If *url* points to a file (even if indirectly), the native
1653      * filesystem path is returned. Otherwise, the URL itself is
1654      * returned.
1655      *
1656      * @param {string} url The URL to mangle.
1657      * @returns {string} The path to the file.
1658      */
1659     urlPath: function urlPath(url) {
1660         try {
1661             return util.getFile(url).path;
1662         }
1663         catch (e) {
1664             return url;
1665         }
1666     },
1667
1668     /**
1669      * Returns a list of all domains and subdomains of documents in the
1670      * given window and all of its descendant frames.
1671      *
1672      * @param {nsIDOMWindow} win The window for which to find domains.
1673      * @returns {[string]} The visible domains.
1674      */
1675     visibleHosts: function visibleHosts(win) {
1676         let res = [], seen = {};
1677         (function rec(frame) {
1678             try {
1679                 if (frame.location.hostname)
1680                     res = res.concat(util.subdomains(frame.location.hostname));
1681             }
1682             catch (e) {}
1683             Array.forEach(frame.frames, rec);
1684         })(win);
1685         return res.filter(h => !Set.add(seen, h));
1686     },
1687
1688     /**
1689      * Returns a list of URIs of documents in the given window and all
1690      * of its descendant frames.
1691      *
1692      * @param {nsIDOMWindow} win The window for which to find URIs.
1693      * @returns {[nsIURI]} The visible URIs.
1694      */
1695     visibleURIs: function visibleURIs(win) {
1696         let res = [], seen = {};
1697         (function rec(frame) {
1698             try {
1699                 res = res.concat(util.newURI(frame.location.href));
1700             }
1701             catch (e) {}
1702             Array.forEach(frame.frames, rec);
1703         })(win);
1704         return res.filter(h => !Set.add(seen, h.spec));
1705     },
1706
1707     /**
1708      * Like Cu.getWeakReference, but won't crash if you pass null.
1709      */
1710     weakReference: function weakReference(jsval) {
1711         if (jsval == null)
1712             return { get: function get() null };
1713         return Cu.getWeakReference(jsval);
1714     },
1715
1716     /**
1717      * Wraps native exceptions thrown by the called function so that a
1718      * proper stack trace may be retrieved from them.
1719      *
1720      * @param {function|string} meth The method to call.
1721      * @param {object} self The 'this' object of the method.
1722      * @param ... Arguments to pass to *meth*.
1723      */
1724     withProperErrors: function withProperErrors(meth, self, ...args) {
1725         try {
1726             return (callable(meth) ? meth : self[meth]).apply(self, args);
1727         }
1728         catch (e) {
1729             throw e.stack ? e : Error(e);
1730         }
1731     }
1732 }, {
1733     Array: array
1734 });
1735
1736 /**
1737  * Math utility methods.
1738  * @singleton
1739  */
1740 var GlobalMath = Math;
1741 this.Math = update(Object.create(GlobalMath), {
1742     /**
1743      * Returns the specified *value* constrained to the range *min* - *max*.
1744      *
1745      * @param {number} value The value to constrain.
1746      * @param {number} min The minimum constraint.
1747      * @param {number} max The maximum constraint.
1748      * @returns {number}
1749      */
1750     constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max)
1751 });
1752
1753 endModule();
1754
1755 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1756
1757 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: