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