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