]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/util.jsm
Import 1.0 supporting Firefox up to 14.*
[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.floor(num / denom), Math.round(num % denom)];
631         let days, hours, minutes;
632
633         [minutes, seconds] = div(Math.round(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      *    headers: {objects} Extra request headers.
714      *
715      *    user: {string} The user name to send via HTTP Authentication.
716      *    pass: {string} The password to send via HTTP Authentication.
717      *
718      *    quiet: {boolean} If true, don't report errors.
719      *
720      * @returns {XMLHttpRequest}
721      */
722     httpGet: function httpGet(url, callback, self) {
723         let params = callback;
724         if (!isObject(params))
725             params = { callback: params && function () callback.apply(self, arguments) };
726
727         try {
728             let xmlhttp = services.Xmlhttp();
729             xmlhttp.mozBackgroundRequest = Set.has(params, "background") ? params.background : true;
730
731             let async = params.callback || params.onload || params.onerror;
732             if (async) {
733                 xmlhttp.addEventListener("load",  function handler(event) { util.trapErrors(params.onload  || params.callback, params, xmlhttp, event) }, false);
734                 xmlhttp.addEventListener("error", function handler(event) { util.trapErrors(params.onerror || params.callback, params, xmlhttp, event) }, false);
735             }
736
737
738             if (isObject(params.params)) {
739                 let data = [encodeURIComponent(k) + "=" + encodeURIComponent(v)
740                             for ([k, v] in iter(params.params))];
741                 let uri = util.newURI(url);
742                 uri.query += (uri.query ? "&" : "") + data.join("&");
743
744                 url = uri.spec;
745             }
746
747             if (isObject(params.data) && !(params.data instanceof Ci.nsISupports)) {
748                 let data = services.FormData();
749                 for (let [k, v] in iter(params.data))
750                     data.append(k, v);
751                 params.data = data;
752             }
753
754             if (params.mimeType)
755                 xmlhttp.overrideMimeType(params.mimeType);
756
757             let args = [params.method || "GET", url, async];
758             if (params.user != null || params.pass != null)
759                 args.push(params.user);
760             if (params.pass != null)
761                 args.push(prams.pass);
762             xmlhttp.open.apply(xmlhttp, args);
763
764             for (let [header, val] in Iterator(params.headers || {}))
765                 xmlhttp.setRequestHeader(header, val);
766
767             if (params.responseType)
768                 xmlhttp.responseType = params.responseType;
769
770             if (params.notificationCallbacks)
771                 xmlhttp.channel.notificationCallbacks = params.notificationCallbacks;
772
773             xmlhttp.send(params.data);
774             return xmlhttp;
775         }
776         catch (e) {
777             if (!params.quiet)
778                 util.reportError(e);
779             return null;
780         }
781     },
782
783     /**
784      * The identity function.
785      *
786      * @param {Object} k
787      * @returns {Object}
788      */
789     identity: function identity(k) k,
790
791     /**
792      * Returns the intersection of two rectangles.
793      *
794      * @param {Object} r1
795      * @param {Object} r2
796      * @returns {Object}
797      */
798     intersection: function (r1, r2) ({
799         get width()  this.right - this.left,
800         get height() this.bottom - this.top,
801         left: Math.max(r1.left, r2.left),
802         right: Math.min(r1.right, r2.right),
803         top: Math.max(r1.top, r2.top),
804         bottom: Math.min(r1.bottom, r2.bottom)
805     }),
806
807     /**
808      * Returns true if the given stack frame resides in Dactyl code.
809      *
810      * @param {nsIStackFrame} frame
811      * @returns {boolean}
812      */
813     isDactyl: Class.Memoize(function () {
814         let base = util.regexp.escape(Components.stack.filename.replace(/[^\/]+$/, ""));
815         let re = RegExp("^(?:.* -> )?(?:resource://dactyl(?!-content/eval.js)|" + base + ")\\S+$");
816         return function isDactyl(frame) re.test(frame.filename);
817     }),
818
819     /**
820      * Returns true if *url* is in the domain *domain*.
821      *
822      * @param {string} url
823      * @param {string} domain
824      * @returns {boolean}
825      */
826     isDomainURL: function isDomainURL(url, domain) util.isSubdomain(util.getHost(url), domain),
827
828     /**
829      * Returns true if *host* is a subdomain of *domain*.
830      *
831      * @param {string} host The host to check.
832      * @param {string} domain The base domain to check the host against.
833      * @returns {boolean}
834      */
835     isSubdomain: function isSubdomain(host, domain) {
836         if (host == null)
837             return false;
838         let idx = host.lastIndexOf(domain);
839         return idx > -1 && idx + domain.length == host.length && (idx == 0 || host[idx - 1] == ".");
840     },
841
842     /**
843      * Iterates over all currently open documents, including all
844      * top-level window and sub-frames thereof.
845      */
846     iterDocuments: function iterDocuments(types) {
847         types = types ? types.map(function (s) "type" + util.capitalize(s))
848                       : ["typeChrome", "typeContent"];
849
850         let windows = services.windowMediator.getXULWindowEnumerator(null);
851         while (windows.hasMoreElements()) {
852             let window = windows.getNext().QueryInterface(Ci.nsIXULWindow);
853             for each (let type in types) {
854                 let docShells = window.docShell.getDocShellEnumerator(Ci.nsIDocShellTreeItem[type],
855                                                                       Ci.nsIDocShell.ENUMERATE_FORWARDS);
856                 while (docShells.hasMoreElements())
857                     let (viewer = docShells.getNext().QueryInterface(Ci.nsIDocShell).contentViewer) {
858                         if (viewer)
859                             yield viewer.DOMDocument;
860                     }
861             }
862         }
863     },
864
865     // ripped from Firefox; modified
866     unsafeURI: Class.Memoize(function () util.regexp(String.replace(<![CDATA[
867             [
868                 \s
869                 // Invisible characters (bug 452979)
870                 U001C U001D U001E U001F // file/group/record/unit separator
871                 U00AD // Soft hyphen
872                 UFEFF // BOM
873                 U2060 // Word joiner
874                 U2062 U2063 // Invisible times/separator
875                 U200B UFFFC // Zero-width space/no-break space
876
877                 // Bidi formatting characters. (RFC 3987 sections 3.2 and 4.1 paragraph 6)
878                 U200E U200F U202A U202B U202C U202D U202E
879             ]
880         ]]>, /U/g, "\\u"),
881         "gx")),
882     losslessDecodeURI: function losslessDecodeURI(url) {
883         return url.split("%25").map(function (url) {
884                 // Non-UTF-8 compliant URLs cause "malformed URI sequence" errors.
885                 try {
886                     return decodeURI(url).replace(this.unsafeURI, encodeURIComponent);
887                 }
888                 catch (e) {
889                     return url;
890                 }
891             }, this).join("%25").replace(/[\s.,>)]$/, encodeURIComponent);
892     },
893
894     /**
895      * Creates a DTD fragment from the given object. Each property of
896      * the object is converted to an ENTITY declaration. SGML special
897      * characters other than ' and % are left intact.
898      *
899      * @param {object} obj The object to convert.
900      * @returns {string} The DTD fragment containing entity declaration
901      *      for *obj*.
902      */
903     makeDTD: let (map = { "'": "&apos;", '"': "&quot;", "%": "&#x25;", "&": "&amp;", "<": "&lt;", ">": "&gt;" })
904         function makeDTD(obj) iter(obj)
905           .map(function ([k, v]) ["<!ENTITY ", k, " '", String.replace(v == null ? "null" : typeof v == "xml" ? v.toXMLString() : v,
906                                                                        typeof v == "xml" ? /['%]/g : /['"%&<>]/g,
907                                                                        function (m) map[m]),
908                                   "'>"].join(""))
909           .join("\n"),
910
911     /**
912      * Converts a URI string into a URI object.
913      *
914      * @param {string} uri
915      * @returns {nsIURI}
916      */
917     newURI: function newURI(uri, charset, base) {
918         if (uri instanceof Ci.nsIURI)
919             var res = uri.clone();
920         else {
921             let idx = uri.lastIndexOf(" -> ");
922             if (~idx)
923                 uri = uri.slice(idx + 4);
924
925             res = this.withProperErrors("newURI", services.io, uri, charset, base);
926         }
927         res instanceof Ci.nsIURL;
928         return res;
929     },
930
931     /**
932      * Removes leading garbage prepended to URIs by the subscript
933      * loader.
934      */
935     fixURI: function fixURI(url) String.replace(url, /.* -> /, ""),
936
937     /**
938      * Pretty print a JavaScript object. Use HTML markup to color certain items
939      * if *color* is true.
940      *
941      * @param {Object} object The object to pretty print.
942      * @param {boolean} color Whether the output should be colored.
943      * @returns {string}
944      */
945     objectToString: function objectToString(object, color) {
946         // Use E4X literals so html is automatically quoted
947         // only when it's asked for. No one wants to see &lt;
948         // on their console or :map :foo in their buffer
949         // when they expect :map <C-f> :foo.
950         XML.prettyPrinting = false;
951         XML.ignoreWhitespace = false;
952
953         if (object == null)
954             return object + "\n";
955
956         if (!isObject(object))
957             return String(object);
958
959         if (object instanceof Ci.nsIDOMElement) {
960             let elem = object;
961             if (elem.nodeType == elem.TEXT_NODE)
962                 return elem.data;
963
964             return DOM(elem).repr(color);
965         }
966
967         try { // for window.JSON
968             var obj = String(object);
969         }
970         catch (e) {
971             obj = Object.prototype.toString.call(obj);
972         }
973         obj = template.highlightFilter(util.clip(obj, 150), "\n", !color ? function () "^J" : function () <span highlight="NonText">^J</span>);
974         let string = <><span highlight="Title Object">{obj}</span>::&#x0a;</>;
975
976         let keys = [];
977
978         // window.content often does not want to be queried with "var i in object"
979         try {
980             let hasValue = !("__iterator__" in object || isinstance(object, ["Generator", "Iterator"]));
981             if (object.dactyl && object.modules && object.modules.modules == object.modules) {
982                 object = Iterator(object);
983                 hasValue = false;
984             }
985             let keyIter = object;
986             if ("__iterator__" in object && !callable(object.__iterator__))
987                 keyIter = keys(object)
988
989             for (let i in keyIter) {
990                 let value = <![CDATA[<no value>]]>;
991                 try {
992                     value = object[i];
993                 }
994                 catch (e) {}
995                 if (!hasValue) {
996                     if (isArray(i) && i.length == 2)
997                         [i, value] = i;
998                     else {
999                         var noVal = true;
1000                         value = i;
1001                     }
1002                 }
1003
1004                 value = template.highlight(value, true, 150, !color);
1005                 let key = <span highlight="Key">{i}</span>;
1006                 if (!isNaN(i))
1007                     i = parseInt(i);
1008                 else if (/^[A-Z_]+$/.test(i))
1009                     i = "";
1010                 keys.push([i, <>{noVal ? value : <>{key}: {value}</>}&#x0a;</>]);
1011             }
1012         }
1013         catch (e) {
1014             util.reportError(e);
1015         }
1016
1017         function compare(a, b) {
1018             if (!isNaN(a[0]) && !isNaN(b[0]))
1019                 return a[0] - b[0];
1020             return String.localeCompare(a[0], b[0]);
1021         }
1022         string += template.map(keys.sort(compare), function (f) f[1]);
1023         return color ? <div style="white-space: pre-wrap;">{string}</div> : [s for each (s in string)].join("");
1024     },
1025
1026     observers: {
1027         "dactyl-cleanup-modules": function (subject, reason) {
1028             defineModule.loadLog.push("dactyl: util: observe: dactyl-cleanup-modules " + reason);
1029
1030             for (let module in values(defineModule.modules))
1031                 if (module.cleanup) {
1032                     util.dump("cleanup: " + module.constructor.className);
1033                     util.trapErrors(module.cleanup, module, reason);
1034                 }
1035
1036             JSMLoader.cleanup();
1037
1038             if (!this.rehashing)
1039                 services.observer.addObserver(this, "dactyl-rehash", true);
1040         },
1041         "dactyl-rehash": function () {
1042             services.observer.removeObserver(this, "dactyl-rehash");
1043
1044             defineModule.loadLog.push("dactyl: util: observe: dactyl-rehash");
1045             if (!this.rehashing)
1046                 for (let module in values(defineModule.modules)) {
1047                     defineModule.loadLog.push("dactyl: util: init(" + module + ")");
1048                     if (module.reinit)
1049                         module.reinit();
1050                     else
1051                         module.init();
1052                 }
1053         },
1054         "dactyl-purge": function () {
1055             this.rehashing = 1;
1056         },
1057     },
1058
1059     /**
1060      * A generator that returns the values between *start* and *end*, in *step*
1061      * increments.
1062      *
1063      * @param {number} start The interval's start value.
1064      * @param {number} end The interval's end value.
1065      * @param {boolean} step The value to step the range by. May be
1066      *     negative. @default 1
1067      * @returns {Iterator(Object)}
1068      */
1069     range: function range(start, end, step) {
1070         if (!step)
1071             step = 1;
1072         if (step > 0) {
1073             for (; start < end; start += step)
1074                 yield start;
1075         }
1076         else {
1077             while (start > end)
1078                 yield start += step;
1079         }
1080     },
1081
1082     /**
1083      * An interruptible generator that returns all values between *start* and
1084      * *end*. The thread yields every *time* milliseconds.
1085      *
1086      * @param {number} start The interval's start value.
1087      * @param {number} end The interval's end value.
1088      * @param {number} time The time in milliseconds between thread yields.
1089      * @returns {Iterator(Object)}
1090      */
1091     interruptibleRange: function interruptibleRange(start, end, time) {
1092         let endTime = Date.now() + time;
1093         while (start < end) {
1094             if (Date.now() > endTime) {
1095                 util.threadYield(true, true);
1096                 endTime = Date.now() + time;
1097             }
1098             yield start++;
1099         }
1100     },
1101
1102     /**
1103      * Creates a new RegExp object based on the value of expr stripped
1104      * of all white space and interpolated with the values from tokens.
1105      * If tokens, any string in the form of <key> in expr is replaced
1106      * with the value of the property, 'key', from tokens, if that
1107      * property exists. If the property value is itself a RegExp, its
1108      * source is substituted rather than its string value.
1109      *
1110      * Additionally, expr is stripped of all JavaScript comments.
1111      *
1112      * This is similar to Perl's extended regular expression format.
1113      *
1114      * @param {string|XML} expr The expression to compile into a RegExp.
1115      * @param {string} flags Flags to apply to the new RegExp.
1116      * @param {object} tokens The tokens to substitute. @optional
1117      * @returns {RegExp} A custom regexp object.
1118      */
1119     regexp: update(function (expr, flags, tokens) {
1120         flags = flags || [k for ([k, v] in Iterator({ g: "global", i: "ignorecase", m: "multiline", y: "sticky" }))
1121                           if (expr[v])].join("");
1122
1123         if (isinstance(expr, ["RegExp"]))
1124             expr = expr.source;
1125
1126         expr = String.replace(expr, /\\(.)/, function (m, m1) {
1127             if (m1 === "c")
1128                 flags = flags.replace(/i/g, "") + "i";
1129             else if (m1 === "C")
1130                 flags = flags.replace(/i/g, "");
1131             else
1132                 return m;
1133             return "";
1134         });
1135
1136         // Replace replacement <tokens>.
1137         if (tokens)
1138             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);
1139
1140         // Strip comments and white space.
1141         if (/x/.test(flags))
1142             expr = String.replace(expr, /(\\.)|\/\/[^\n]*|\/\*[^]*?\*\/|\s+/gm, function (m, m1) m1 || "");
1143
1144         // Replace (?P<named> parameters)
1145         if (/\(\?P</.test(expr)) {
1146             var source = expr;
1147             let groups = ["wholeMatch"];
1148             expr = expr.replace(/((?:[^[(\\]|\\.|\[(?:[^\]]|\\.)*\])*)\((?:\?P<([^>]+)>|(\?))?/gy,
1149                 function (m0, m1, m2, m3) {
1150                     if (!m3)
1151                         groups.push(m2 || "-group-" + groups.length);
1152                     return m1 + "(" + (m3 || "");
1153                 });
1154             var struct = Struct.apply(null, groups);
1155         }
1156
1157         let res = update(RegExp(expr, flags.replace("x", "")), {
1158             closure: Class.Property(Object.getOwnPropertyDescriptor(Class.prototype, "closure")),
1159             dactylPropertyNames: ["exec", "match", "test", "toSource", "toString", "global", "ignoreCase", "lastIndex", "multiLine", "source", "sticky"],
1160             iterate: function (str, idx) util.regexp.iterate(this, str, idx)
1161         });
1162
1163         // Return a struct with properties for named parameters if we
1164         // have them.
1165         if (struct)
1166             update(res, {
1167                 exec: function exec() let (match = exec.superapply(this, arguments)) match && struct.fromArray(match),
1168                 dactylSource: source, struct: struct
1169             });
1170         return res;
1171     }, {
1172         /**
1173          * Escapes Regular Expression special characters in *str*.
1174          *
1175          * @param {string} str
1176          * @returns {string}
1177          */
1178         escape: function regexp_escape(str) str.replace(/([\\{}()[\]^$.?*+|])/g, "\\$1"),
1179
1180         /**
1181          * Given a RegExp, returns its source in the form showable to the user.
1182          *
1183          * @param {RegExp} re The regexp showable source of which is to be returned.
1184          * @returns {string}
1185          */
1186         getSource: function regexp_getSource(re) re.source.replace(/\\(.)/g, function (m0, m1) m1 === "/" ? "/" : m0),
1187
1188         /**
1189          * Iterates over all matches of the given regexp in the given
1190          * string.
1191          *
1192          * @param {RegExp} regexp The regular expression to execute.
1193          * @param {string} string The string to search.
1194          * @param {number} lastIndex The index at which to begin searching. @optional
1195          */
1196         iterate: function iterate(regexp, string, lastIndex) iter(function () {
1197             regexp.lastIndex = lastIndex = lastIndex || 0;
1198             let match;
1199             while (match = regexp.exec(string)) {
1200                 lastIndex = regexp.lastIndex;
1201                 yield match;
1202                 regexp.lastIndex = lastIndex;
1203                 if (match[0].length == 0 || !regexp.global)
1204                     break;
1205             }
1206         }())
1207     }),
1208
1209     /**
1210      * Flushes the startup or jar cache.
1211      */
1212     flushCache: function flushCache(file) {
1213         if (file)
1214             services.observer.notifyObservers(file, "flush-cache-entry", "");
1215         else
1216             services.observer.notifyObservers(null, "startupcache-invalidate", "");
1217     },
1218
1219     /**
1220      * Reloads dactyl in entirety by disabling the add-on and
1221      * re-enabling it.
1222      */
1223     rehash: function (args) {
1224         storage.session.commandlineArgs = args;
1225         this.timeout(function () {
1226             this.flushCache();
1227             this.rehashing = true;
1228             let addon = config.addon;
1229             addon.userDisabled = true;
1230             addon.userDisabled = false;
1231         });
1232     },
1233
1234     errorCount: 0,
1235     errors: Class.Memoize(function () []),
1236     maxErrors: 15,
1237     /**
1238      * Reports an error to the Error Console and the standard output,
1239      * along with a stack trace and other relevant information. The
1240      * error is appended to {@see #errors}.
1241      */
1242     reportError: function reportError(error) {
1243         if (error.noTrace)
1244             return;
1245
1246         if (isString(error))
1247             error = Error(error);
1248
1249         Cu.reportError(error);
1250
1251         try {
1252             this.errorCount++;
1253
1254             let obj = update({}, error, {
1255                 toString: function () String(error),
1256                 stack: <>{util.stackLines(String(error.stack || Error().stack)).join("\n").replace(/^/mg, "\t")}</>
1257             });
1258
1259             services.console.logStringMessage(obj.stack);
1260
1261             this.errors.push([new Date, obj + "\n" + obj.stack]);
1262             this.errors = this.errors.slice(-this.maxErrors);
1263             this.errors.toString = function () [k + "\n" + v for ([k, v] in array.iterValues(this))].join("\n\n");
1264
1265             this.dump(String(error));
1266             this.dump(obj);
1267             this.dump("");
1268         }
1269         catch (e) {
1270             try {
1271                 this.dump(String(error));
1272                 this.dump(util.stackLines(error.stack).join("\n"));
1273             }
1274             catch (e) { dump(e + "\n"); }
1275         }
1276
1277         // ctypes.open("libc.so.6").declare("kill", ctypes.default_abi, ctypes.void_t, ctypes.int, ctypes.int)(
1278         //     ctypes.open("libc.so.6").declare("getpid", ctypes.default_abi, ctypes.int)(), 2)
1279     },
1280
1281     /**
1282      * Given a domain, returns an array of all non-toplevel subdomains
1283      * of that domain.
1284      *
1285      * @param {string} host The host for which to find subdomains.
1286      * @returns {[string]}
1287      */
1288     subdomains: function subdomains(host) {
1289         if (/(^|\.)\d+$|:.*:/.test(host))
1290             // IP address or similar
1291             return [host];
1292
1293         let base = host.replace(/.*\.(.+?\..+?)$/, "$1");
1294         try {
1295             base = services.tld.getBaseDomainFromHost(host);
1296         }
1297         catch (e) {}
1298
1299         let ary = host.split(".");
1300         ary = [ary.slice(i).join(".") for (i in util.range(ary.length, 0, -1))];
1301         return ary.filter(function (h) h.length >= base.length);
1302     },
1303
1304     /**
1305      * Returns the selection controller for the given window.
1306      *
1307      * @param {Window} window
1308      * @returns {nsISelectionController}
1309      */
1310     selectionController: function (win)
1311         win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1312            .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
1313            .QueryInterface(Ci.nsISelectionController),
1314
1315     /**
1316      * Escapes a string against shell meta-characters and argument
1317      * separators.
1318      */
1319     shellEscape: function shellEscape(str) '"' + String.replace(str, /[\\"$`]/g, "\\$&") + '"',
1320
1321     /**
1322      * Suspend execution for at least *delay* milliseconds. Functions by
1323      * yielding execution to the next item in the main event queue, and
1324      * so may lead to unexpected call graphs, and long delays if another
1325      * handler yields execution while waiting.
1326      *
1327      * @param {number} delay The time period for which to sleep in milliseconds.
1328      */
1329     sleep: function (delay) {
1330         let mainThread = services.threading.mainThread;
1331
1332         let end = Date.now() + delay;
1333         while (Date.now() < end)
1334             mainThread.processNextEvent(true);
1335         return true;
1336     },
1337
1338     /**
1339      * Behaves like String.split, except that when *limit* is reached,
1340      * the trailing element contains the entire trailing portion of the
1341      * string.
1342      *
1343      *     util.split("a, b, c, d, e", /, /, 3) -> ["a", "b", "c, d, e"]
1344      *
1345      * @param {string} str The string to split.
1346      * @param {RegExp|string} re The regular expression on which to split the string.
1347      * @param {number} limit The maximum number of elements to return.
1348      * @returns {[string]}
1349      */
1350     split: function (str, re, limit) {
1351         re.lastIndex = 0;
1352         if (!re.global)
1353             re = RegExp(re.source || re, "g");
1354         let match, start = 0, res = [];
1355         while (--limit && (match = re.exec(str)) && match[0].length) {
1356             res.push(str.substring(start, match.index));
1357             start = match.index + match[0].length;
1358         }
1359         res.push(str.substring(start));
1360         return res;
1361     },
1362
1363     /**
1364      * Split a string on literal occurrences of a marker.
1365      *
1366      * Specifically this ignores occurrences preceded by a backslash, or
1367      * contained within 'single' or "double" quotes.
1368      *
1369      * It assumes backslash escaping on strings, and will thus not count quotes
1370      * that are preceded by a backslash or within other quotes as starting or
1371      * ending quoted sections of the string.
1372      *
1373      * @param {string} str
1374      * @param {RegExp} marker
1375      * @returns {[string]}
1376      */
1377     splitLiteral: function splitLiteral(str, marker) {
1378         let results = [];
1379         let resep = RegExp(/^(([^\\'"]|\\.|'([^\\']|\\.)*'|"([^\\"]|\\.)*")*?)/.source + marker.source);
1380         let cont = true;
1381
1382         while (cont) {
1383             cont = false;
1384             str = str.replace(resep, function (match, before) {
1385                 results.push(before);
1386                 cont = match !== "";
1387                 return "";
1388             });
1389         }
1390
1391         results.push(str);
1392         return results;
1393     },
1394
1395     yielders: 0,
1396     /**
1397      * Yields execution to the next event in the current thread's event
1398      * queue. This is a potentially dangerous operation, since any
1399      * yielders higher in the event stack will prevent execution from
1400      * returning to the caller until they have finished their wait. The
1401      * potential for deadlock is high.
1402      *
1403      * @param {boolean} flush If true, flush all events in the event
1404      *      queue before returning. Otherwise, wait for an event to
1405      *      process before proceeding.
1406      * @param {boolean} interruptable If true, this yield may be
1407      *      interrupted by pressing <C-c>, in which case,
1408      *      Error("Interrupted") will be thrown.
1409      */
1410     threadYield: function (flush, interruptable) {
1411         this.yielders++;
1412         try {
1413             let mainThread = services.threading.mainThread;
1414             /* FIXME */
1415             util.interrupted = false;
1416             do {
1417                 mainThread.processNextEvent(!flush);
1418                 if (util.interrupted)
1419                     throw Error("Interrupted");
1420             }
1421             while (flush === true && mainThread.hasPendingEvents());
1422         }
1423         finally {
1424             this.yielders--;
1425         }
1426     },
1427
1428     /**
1429      * Waits for the function *test* to return true, or *timeout*
1430      * milliseconds to expire.
1431      *
1432      * @param {function} test The predicate on which to wait.
1433      * @param {object} self The 'this' object for *test*.
1434      * @param {Number} timeout The maximum number of milliseconds to
1435      *      wait.
1436      *      @optional
1437      * @param {boolean} interruptable If true, may be interrupted by
1438      *      pressing <C-c>, in which case, Error("Interrupted") will be
1439      *      thrown.
1440      */
1441     waitFor: function waitFor(test, self, timeout, interruptable) {
1442         let end = timeout && Date.now() + timeout, result;
1443
1444         let timer = services.Timer(function () {}, 10, services.Timer.TYPE_REPEATING_SLACK);
1445         try {
1446             while (!(result = test.call(self)) && (!end || Date.now() < end))
1447                 this.threadYield(false, interruptable);
1448         }
1449         finally {
1450             timer.cancel();
1451         }
1452         return result;
1453     },
1454
1455     /**
1456      * Makes the passed function yieldable. Each time the function calls
1457      * yield, execution is suspended for the yielded number of
1458      * milliseconds.
1459      *
1460      * Example:
1461      *      let func = yieldable(function () {
1462      *          util.dump(Date.now()); // 0
1463      *          yield 1500;
1464      *          util.dump(Date.now()); // 1500
1465      *      });
1466      *      func();
1467      *
1468      * @param {function} func The function to mangle.
1469      * @returns {function} A new function which may not execute
1470      *      synchronously.
1471      */
1472     yieldable: function yieldable(func)
1473         function magic() {
1474             let gen = func.apply(this, arguments);
1475             (function next() {
1476                 try {
1477                     util.timeout(next, gen.next());
1478                 }
1479                 catch (e if e instanceof StopIteration) {};
1480             })();
1481         },
1482
1483     /**
1484      * Wraps a callback function such that its errors are not lost. This
1485      * is useful for DOM event listeners, which ordinarily eat errors.
1486      * The passed function has the property *wrapper* set to the new
1487      * wrapper function, while the wrapper has the property *wrapped*
1488      * set to the original callback.
1489      *
1490      * @param {function} callback The callback to wrap.
1491      * @returns {function}
1492      */
1493     wrapCallback: wrapCallback,
1494
1495     /**
1496      * Returns the top-level chrome window for the given window.
1497      *
1498      * @param {Window} win The child window.
1499      * @returns {Window} The top-level parent window.
1500      */
1501     topWindow: function topWindow(win)
1502             win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
1503                .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
1504                .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow),
1505
1506     /**
1507      * Traps errors in the called function, possibly reporting them.
1508      *
1509      * @param {function} func The function to call
1510      * @param {object} self The 'this' object for the function.
1511      */
1512     trapErrors: function trapErrors(func, self) {
1513         try {
1514             if (!callable(func))
1515                 func = self[func];
1516             return func.apply(self || this, Array.slice(arguments, 2));
1517         }
1518         catch (e) {
1519             this.reportError(e);
1520             return undefined;
1521         }
1522     },
1523
1524     /**
1525      * Returns the file path of a given *url*, for debugging purposes.
1526      * If *url* points to a file (even if indirectly), the native
1527      * filesystem path is returned. Otherwise, the URL itself is
1528      * returned.
1529      *
1530      * @param {string} url The URL to mangle.
1531      * @returns {string} The path to the file.
1532      */
1533     urlPath: function urlPath(url) {
1534         try {
1535             return util.getFile(url).path;
1536         }
1537         catch (e) {
1538             return url;
1539         }
1540     },
1541
1542     /**
1543      * Returns a list of all domains and subdomains of documents in the
1544      * given window and all of its descendant frames.
1545      *
1546      * @param {nsIDOMWindow} win The window for which to find domains.
1547      * @returns {[string]} The visible domains.
1548      */
1549     visibleHosts: function (win) {
1550         let res = [], seen = {};
1551         (function rec(frame) {
1552             try {
1553                 if (frame.location.hostname)
1554                     res = res.concat(util.subdomains(frame.location.hostname));
1555             }
1556             catch (e) {}
1557             Array.forEach(frame.frames, rec);
1558         })(win);
1559         return res.filter(function (h) !Set.add(seen, h));
1560     },
1561
1562     /**
1563      * Returns a list of URIs of documents in the given window and all
1564      * of its descendant frames.
1565      *
1566      * @param {nsIDOMWindow} win The window for which to find URIs.
1567      * @returns {[nsIURI]} The visible URIs.
1568      */
1569     visibleURIs: function (win) {
1570         let res = [], seen = {};
1571         (function rec(frame) {
1572             try {
1573                 res = res.concat(util.newURI(frame.location.href));
1574             }
1575             catch (e) {}
1576             Array.forEach(frame.frames, rec);
1577         })(win);
1578         return res.filter(function (h) !Set.add(seen, h.spec));
1579     },
1580
1581     /**
1582      * Like Cu.getWeakReference, but won't crash if you pass null.
1583      */
1584     weakReference: function weakReference(jsval) {
1585         if (jsval == null)
1586             return { get: function get() null };
1587         return Cu.getWeakReference(jsval);
1588     },
1589
1590     /**
1591      * Wraps native exceptions thrown by the called function so that a
1592      * proper stack trace may be retrieved from them.
1593      *
1594      * @param {function|string} meth The method to call.
1595      * @param {object} self The 'this' object of the method.
1596      * @param ... Arguments to pass to *meth*.
1597      */
1598     withProperErrors: function withProperErrors(meth, self) {
1599         try {
1600             return (callable(meth) ? meth : self[meth]).apply(self, Array.slice(arguments, withProperErrors.length));
1601         }
1602         catch (e) {
1603             throw e.stack ? e : Error(e);
1604         }
1605     },
1606
1607     xmlToDom: function () DOM.fromXML.apply(DOM, arguments)
1608 }, {
1609     Array: array
1610 });
1611
1612
1613 /**
1614  * Math utility methods.
1615  * @singleton
1616  */
1617 var GlobalMath = Math;
1618 var Math = update(Object.create(GlobalMath), {
1619     /**
1620      * Returns the specified *value* constrained to the range *min* - *max*.
1621      *
1622      * @param {number} value The value to constrain.
1623      * @param {number} min The minimum constraint.
1624      * @param {number} max The maximum constraint.
1625      * @returns {number}
1626      */
1627     constrain: function constrain(value, min, max) Math.min(Math.max(min, value), max)
1628 });
1629
1630 endModule();
1631
1632 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1633
1634 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: