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